Coverage for mlprodict/onnxrt/validate/validate.py: 98%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2@file
3@brief Validates runtime for many :scikit-learn: operators.
4The submodule relies on :epkg:`onnxconverter_common`,
5:epkg:`sklearn-onnx`.
6"""
7import pprint
8from inspect import signature
9import numpy
10from numpy.linalg import LinAlgError
11import sklearn
12from sklearn import __all__ as sklearn__all__, __version__ as sklearn_version
13from sklearn.exceptions import ConvergenceWarning
14from sklearn.utils._testing import ignore_warnings
15from ... import (
16 __version__ as ort_version,
17 __max_supported_opset__, get_ir_version,
18 __max_supported_opsets__)
19from ...onnx_conv import to_onnx, register_converters, register_rewritten_operators
20from ...tools.model_info import analyze_model, set_random_state
21from ..onnx_inference import OnnxInference
22from ...onnx_tools.optim.sklearn_helper import inspect_sklearn_model, set_n_jobs
23from ...onnx_tools.optim.onnx_helper import onnx_statistics
24from ...onnx_tools.optim import onnx_optimisations
25from .validate_problems import find_suitable_problem
26from .validate_scenarios import _extra_parameters
27from .validate_difference import measure_relative_difference
28from .validate_helper import (
29 _dispsimple, sklearn_operators,
30 _measure_time, _shape_exc, dump_into_folder,
31 default_time_kwargs, RuntimeBadResultsError,
32 _dictionary2str, _merge_options, _multiply_time_kwargs,
33 _get_problem_data)
34from .validate_benchmark import benchmark_fct
37@ignore_warnings(category=(UserWarning, ConvergenceWarning))
38def _dofit_model(dofit, obs, inst, X_train, y_train, X_test, y_test,
39 Xort_test, init_types, store_models,
40 debug, verbose, fLOG):
41 if dofit:
42 if verbose >= 2 and fLOG is not None:
43 fLOG("[enumerate_compatible_opset] fit, type: '{}' dtype: {}".format(
44 type(X_train), getattr(X_train, 'dtype', '-')))
45 try:
46 set_random_state(inst)
47 if y_train is None:
48 t4 = _measure_time(lambda: inst.fit(X_train))[1]
49 else:
50 t4 = _measure_time(
51 lambda: inst.fit(X_train, y_train))[1]
52 except (AttributeError, TypeError, ValueError,
53 IndexError, NotImplementedError, MemoryError,
54 LinAlgError, StopIteration) as e:
55 if debug:
56 raise # pragma: no cover
57 obs["_1training_time_exc"] = str(e)
58 return False
60 obs["training_time"] = t4
61 try:
62 skl_st = inspect_sklearn_model(inst)
63 except NotImplementedError:
64 skl_st = {}
65 obs.update({'skl_' + k: v for k, v in skl_st.items()})
67 if store_models:
68 obs['MODEL'] = inst
69 obs['X_test'] = X_test
70 obs['Xort_test'] = Xort_test
71 obs['init_types'] = init_types
72 else:
73 obs["training_time"] = 0.
74 if store_models:
75 obs['MODEL'] = inst
76 obs['init_types'] = init_types
78 return True
81def _run_skl_prediction(obs, check_runtime, assume_finite, inst,
82 method_name, predict_kwargs, X_test,
83 benchmark, debug, verbose, time_kwargs,
84 skip_long_test, time_kwargs_fact, fLOG):
85 if not check_runtime:
86 return None # pragma: no cover
87 if verbose >= 2 and fLOG is not None:
88 fLOG("[enumerate_compatible_opset] check_runtime SKL {}-{}-{}-{}-{}".format(
89 id(inst), method_name, predict_kwargs, time_kwargs,
90 time_kwargs_fact))
91 with sklearn.config_context(assume_finite=assume_finite):
92 # compute sklearn prediction
93 obs['ort_version'] = ort_version
94 try:
95 meth = getattr(inst, method_name)
96 except AttributeError as e: # pragma: no cover
97 if debug:
98 raise # pragma: no cover
99 obs['_2skl_meth_exc'] = str(e)
100 return e
101 try:
102 ypred, t4, ___ = _measure_time(
103 lambda: meth(X_test, **predict_kwargs))
104 obs['lambda-skl'] = (lambda xo: meth(xo, **predict_kwargs), X_test)
105 except (ValueError, AttributeError, # pragma: no cover
106 TypeError, MemoryError, IndexError) as e:
107 if debug:
108 raise # pragma: no cover
109 obs['_3prediction_exc'] = str(e)
110 return e
111 obs['prediction_time'] = t4
112 obs['assume_finite'] = assume_finite
113 if benchmark and 'lambda-skl' in obs:
114 obs['bench-skl'] = benchmark_fct(
115 *obs['lambda-skl'], obs=obs,
116 time_kwargs=_multiply_time_kwargs(
117 time_kwargs, time_kwargs_fact, inst),
118 skip_long_test=skip_long_test)
119 if verbose >= 3 and fLOG is not None:
120 fLOG("[enumerate_compatible_opset] scikit-learn prediction")
121 _dispsimple(ypred, fLOG)
122 if verbose >= 2 and fLOG is not None:
123 fLOG("[enumerate_compatible_opset] predictions stored")
124 return ypred
127def _retrieve_problems_extra(model, verbose, fLOG, extended_list):
128 """
129 Use by @see fn enumerate_compatible_opset.
130 """
131 extras = None
132 if extended_list:
133 from ...onnx_conv.validate_scenarios import find_suitable_problem as fsp_extended
134 problems = fsp_extended(model)
135 if problems is not None:
136 from ...onnx_conv.validate_scenarios import build_custom_scenarios as fsp_scenarios
137 extra_parameters = fsp_scenarios()
139 if verbose >= 2 and fLOG is not None:
140 fLOG(
141 "[enumerate_compatible_opset] found custom for model={}".format(model))
142 extras = extra_parameters.get(model, None)
143 if extras is not None:
144 fLOG(
145 "[enumerate_compatible_opset] found custom scenarios={}".format(extras))
146 else:
147 problems = None
149 if problems is None:
150 # scikit-learn
151 extra_parameters = _extra_parameters
152 try:
153 problems = find_suitable_problem(model)
154 except RuntimeError as e: # pragma: no cover
155 return {'name': model.__name__, 'skl_version': sklearn_version,
156 '_0problem_exc': e}, extras
157 extras = extra_parameters.get(model, [('default', {})])
159 # checks existence of random_state
160 sig = signature(model.__init__)
161 if 'random_state' in sig.parameters:
162 new_extras = []
163 for extra in extras:
164 if 'random_state' not in extra[1]:
165 ps = extra[1].copy()
166 ps['random_state'] = 42
167 if len(extra) == 2:
168 extra = (extra[0], ps)
169 else:
170 extra = (extra[0], ps) + extra[2:]
171 new_extras.append(extra)
172 extras = new_extras
174 return problems, extras
177def enumerate_compatible_opset(model, opset_min=-1, opset_max=-1, # pylint: disable=R0914
178 check_runtime=True, debug=False,
179 runtime='python', dump_folder=None,
180 store_models=False, benchmark=False,
181 assume_finite=True, node_time=False,
182 fLOG=print, filter_exp=None,
183 verbose=0, time_kwargs=None,
184 extended_list=False, dump_all=False,
185 n_features=None, skip_long_test=True,
186 filter_scenario=None, time_kwargs_fact=None,
187 time_limit=4, n_jobs=None):
188 """
189 Lists all compatible opsets for a specific model.
191 @param model operator class
192 @param opset_min starts with this opset
193 @param opset_max ends with this opset (None to use
194 current onnx opset)
195 @param check_runtime checks that runtime can consume the
196 model and compute predictions
197 @param debug catch exception (True) or not (False)
198 @param runtime test a specific runtime, by default ``'python'``
199 @param dump_folder dump information to replicate in case of mismatch
200 @param dump_all dump all models not only the one which fail
201 @param store_models if True, the function
202 also stores the fitted model and its conversion
203 into :epkg:`ONNX`
204 @param benchmark if True, measures the time taken by each function
205 to predict for different number of rows
206 @param fLOG logging function
207 @param filter_exp function which tells if the experiment must be run,
208 None to run all, takes *model, problem* as an input
209 @param filter_scenario second function which tells if the experiment must be run,
210 None to run all, takes *model, problem, scenario, extra, options*
211 as an input
212 @param node_time collect time for each node in the :epkg:`ONNX` graph
213 @param assume_finite See `config_context
214 <https://scikit-learn.org/stable/modules/generated/
215 sklearn.config_context.html>`_, If True, validation for finiteness
216 will be skipped, saving time, but leading to potential crashes.
217 If False, validation for finiteness will be performed, avoiding error.
218 @param verbose verbosity
219 @param extended_list extends the list to custom converters
220 and problems
221 @param time_kwargs to define a more precise way to measure a model
222 @param n_features modifies the shorts datasets used to train the models
223 to use exactly this number of features, it can also
224 be a list to test multiple datasets
225 @param skip_long_test skips tests for high values of N if they seem too long
226 @param time_kwargs_fact see :func:`_multiply_time_kwargs <mlprodict.onnxrt.validate.validate_helper._multiply_time_kwargs>`
227 @param time_limit to stop benchmarking after this amount of time was spent
228 @param n_jobs *n_jobs* is set to the number of CPU by default unless this
229 value is changed
230 @return dictionaries, each row has the following
231 keys: opset, exception if any, conversion time,
232 problem chosen to test the conversion...
234 The function requires :epkg:`sklearn-onnx`.
235 The outcome can be seen at pages references
236 by :ref:`l-onnx-availability`.
237 The parameter *time_kwargs* is a dictionary which defines the
238 number of times to repeat the same predictions in order
239 to give more precise figures. The default value (if None) is returned
240 by the following code:
242 .. runpython::
243 :showcode:
244 :warningout: DeprecationWarning
246 from mlprodict.onnxrt.validate.validate_helper import default_time_kwargs
247 import pprint
248 pprint.pprint(default_time_kwargs())
250 Parameter *time_kwargs_fact* multiples these values for some
251 specific models. ``'lin'`` multiplies by 10 when the model
252 is linear.
253 """
254 if opset_min == -1:
255 opset_min = __max_supported_opset__ # pragma: no cover
256 if opset_max == -1:
257 opset_max = __max_supported_opset__ # pragma: no cover
258 if verbose > 0 and fLOG is not None:
259 fLOG("[enumerate_compatible_opset] opset in [{}, {}].".format(
260 opset_min, opset_max))
261 if verbose > 1 and fLOG:
262 fLOG("[enumerate_compatible_opset] validate class '{}'.".format(
263 model.__name__))
264 if verbose > 2:
265 fLOG(model)
267 if time_kwargs is None:
268 time_kwargs = default_time_kwargs()
269 problems, extras = _retrieve_problems_extra(
270 model, verbose, fLOG, extended_list)
271 if isinstance(problems, dict):
272 yield problems # pragma: no cover
273 problems = [] # pragma: no cover
275 if opset_max is None:
276 opset_max = __max_supported_opset__ # pragma: no cover
277 opsets = list(range(opset_min, opset_max + 1)) # pragma: no cover
278 opsets.append(None) # pragma: no cover
279 else:
280 opsets = list(range(opset_min, opset_max + 1))
282 if extras is None:
283 problems = []
284 yield {'name': model.__name__, 'skl_version': sklearn_version,
285 '_0problem_exc': 'SKIPPED'}
287 if not isinstance(n_features, list):
288 n_features = [n_features]
290 for prob in problems:
291 if filter_exp is not None and not filter_exp(model, prob):
292 continue
293 for n_feature in n_features:
294 if verbose >= 2 and fLOG is not None:
295 fLOG("[enumerate_compatible_opset] problem={} n_feature={}".format(
296 prob, n_feature))
298 (X_train, X_test, y_train,
299 y_test, Xort_test,
300 init_types, conv_options, method_name,
301 output_index, dofit, predict_kwargs) = _get_problem_data(prob, n_feature)
303 for scenario_extra in extras:
304 subset_problems = None
305 optimisations = None
306 new_conv_options = None
307 if len(scenario_extra) > 2:
308 options = scenario_extra[2]
309 if isinstance(options, dict):
310 subset_problems = options.get('subset_problems', None)
311 optimisations = options.get('optim', None)
312 new_conv_options = options.get('conv_options', None)
313 else:
314 subset_problems = options
316 if subset_problems and isinstance(subset_problems, (list, set)):
317 if prob not in subset_problems:
318 # Skips unrelated problem for a specific configuration.
319 continue
320 elif subset_problems is not None:
321 raise RuntimeError( # pragma: no cover
322 "subset_problems must be a set or a list not {}.".format(
323 subset_problems))
325 try:
326 scenario, extra = scenario_extra[:2]
327 except TypeError as e: # pragma: no cover
328 raise TypeError(
329 "Unable to interpret 'scenario_extra'\n{}".format(
330 scenario_extra)) from e
331 if optimisations is None:
332 optimisations = [None]
333 if new_conv_options is None:
334 new_conv_options = [{}]
336 if (filter_scenario is not None and
337 not filter_scenario(model, prob, scenario,
338 extra, new_conv_options)):
339 continue
341 if verbose >= 2 and fLOG is not None:
342 fLOG("[enumerate_compatible_opset] ##############################")
343 fLOG("[enumerate_compatible_opset] scenario={} optim={} extra={} dofit={} (problem={})".format(
344 scenario, optimisations, extra, dofit, prob))
346 # training
347 obs = {'scenario': scenario, 'name': model.__name__,
348 'skl_version': sklearn_version, 'problem': prob,
349 'method_name': method_name, 'output_index': output_index,
350 'fit': dofit, 'conv_options': conv_options,
351 'idtype': Xort_test.dtype, 'predict_kwargs': predict_kwargs,
352 'init_types': init_types, 'inst': extra if extra else None,
353 'n_features': X_train.shape[1] if len(X_train.shape) == 2 else 1}
354 inst = None
355 extra = set_n_jobs(model, extra, n_jobs=n_jobs)
356 try:
357 inst = model(**extra)
358 except TypeError as e: # pragma: no cover
359 if debug: # pragma: no cover
360 raise
361 if "__init__() missing" not in str(e):
362 raise RuntimeError(
363 "Unable to instantiate model '{}'.\nextra=\n{}".format(
364 model.__name__, pprint.pformat(extra))) from e
365 yield obs.copy()
366 continue
368 if not _dofit_model(dofit, obs, inst, X_train, y_train, X_test, y_test,
369 Xort_test, init_types, store_models,
370 debug, verbose, fLOG):
371 yield obs.copy()
372 continue
374 # statistics about the trained model
375 skl_infos = analyze_model(inst)
376 for k, v in skl_infos.items():
377 obs['fit_' + k] = v
379 # runtime
380 ypred = _run_skl_prediction(
381 obs, check_runtime, assume_finite, inst,
382 method_name, predict_kwargs, X_test,
383 benchmark, debug, verbose, time_kwargs,
384 skip_long_test, time_kwargs_fact, fLOG)
385 if isinstance(ypred, Exception):
386 yield obs.copy()
387 continue
389 for run_obs in _call_conv_runtime_opset(
390 obs=obs.copy(), opsets=opsets, debug=debug,
391 new_conv_options=new_conv_options,
392 model=model, prob=prob, scenario=scenario,
393 extra=extra, extras=extras, conv_options=conv_options,
394 init_types=init_types, inst=inst,
395 optimisations=optimisations, verbose=verbose,
396 benchmark=benchmark, runtime=runtime,
397 filter_scenario=filter_scenario,
398 X_test=X_test, y_test=y_test, ypred=ypred,
399 Xort_test=Xort_test, method_name=method_name,
400 check_runtime=check_runtime,
401 output_index=output_index,
402 kwargs=dict(
403 dump_all=dump_all,
404 dump_folder=dump_folder,
405 node_time=node_time,
406 skip_long_test=skip_long_test,
407 store_models=store_models,
408 time_kwargs=_multiply_time_kwargs(
409 time_kwargs, time_kwargs_fact, inst)
410 ),
411 time_limit=time_limit,
412 fLOG=fLOG):
413 yield run_obs
416def _check_run_benchmark(benchmark, stat_onnx, bench_memo, runtime):
417 unique = set(stat_onnx.items())
418 unique.add(runtime)
419 run_benchmark = benchmark and all(
420 map(lambda u: unique != u, bench_memo))
421 if run_benchmark:
422 bench_memo.append(unique)
423 return run_benchmark
426def _call_conv_runtime_opset(
427 obs, opsets, debug, new_conv_options,
428 model, prob, scenario, extra, extras, conv_options,
429 init_types, inst, optimisations, verbose,
430 benchmark, runtime, filter_scenario,
431 check_runtime, X_test, y_test, ypred, Xort_test,
432 method_name, output_index,
433 kwargs, time_limit, fLOG):
434 # Calls the conversion and runtime for different opets
435 if None in opsets:
436 set_opsets = [None] + list(sorted((_ for _ in opsets if _ is not None),
437 reverse=True))
438 else:
439 set_opsets = list(sorted(opsets, reverse=True))
440 bench_memo = []
442 for opset in set_opsets:
443 if verbose >= 2 and fLOG is not None:
444 fLOG("[enumerate_compatible_opset] opset={} init_types={}".format(
445 opset, init_types))
446 obs_op = obs.copy()
447 if opset is not None:
448 obs_op['opset'] = opset
450 if len(init_types) != 1:
451 raise NotImplementedError( # pragma: no cover
452 "Multiple types are is not implemented: "
453 "{}.".format(init_types))
455 if not isinstance(runtime, list):
456 runtime = [runtime]
458 obs_op_0c = obs_op.copy()
459 for aoptions in new_conv_options:
460 obs_op = obs_op_0c.copy()
461 all_conv_options = {} if conv_options is None else conv_options.copy()
462 all_conv_options = _merge_options(
463 all_conv_options, aoptions)
464 obs_op['conv_options'] = all_conv_options
466 if (filter_scenario is not None and
467 not filter_scenario(model, prob, scenario,
468 extra, all_conv_options)):
469 continue
471 for rt in runtime:
472 def fct_conv(itt=inst, it=init_types[0][1], ops=opset,
473 options=all_conv_options):
474 if isinstance(ops, int):
475 ops_dict = __max_supported_opsets__.copy()
476 ops_dict[''] = ops
477 else:
478 ops_dict = ops
479 return to_onnx(itt, it, target_opset=ops_dict, options=options,
480 rewrite_ops=rt in ('', None, 'python',
481 'python_compiled'))
483 if verbose >= 2 and fLOG is not None:
484 fLOG(
485 "[enumerate_compatible_opset] conversion to onnx: {}".format(all_conv_options))
486 try:
487 conv, t4 = _measure_time(fct_conv)[:2]
488 obs_op["convert_time"] = t4
489 except (RuntimeError, IndexError, AttributeError, TypeError,
490 ValueError, NameError, NotImplementedError) as e:
491 if debug:
492 fLOG(pprint.pformat(obs_op)) # pragma: no cover
493 raise # pragma: no cover
494 obs_op["_4convert_exc"] = e
495 yield obs_op.copy()
496 continue
498 if verbose >= 6 and fLOG is not None:
499 fLOG( # pragma: no cover
500 "[enumerate_compatible_opset] ONNX:\n{}".format(conv))
502 if all_conv_options.get('optim', '') == 'cdist': # pragma: no cover
503 check_cdist = [_ for _ in str(conv).split('\n')
504 if 'CDist' in _]
505 check_scan = [_ for _ in str(conv).split('\n')
506 if 'Scan' in _]
507 if len(check_cdist) == 0 and len(check_scan) > 0:
508 raise RuntimeError(
509 "Operator CDist was not used in\n{}"
510 "".format(conv))
512 obs_op0 = obs_op.copy()
513 for optimisation in optimisations:
514 obs_op = obs_op0.copy()
515 if optimisation is not None:
516 if optimisation == 'onnx':
517 obs_op['optim'] = optimisation
518 if len(aoptions) != 0:
519 obs_op['optim'] += '/' + \
520 _dictionary2str(aoptions)
521 conv = onnx_optimisations(conv)
522 else:
523 raise ValueError( # pragma: no cover
524 "Unknown optimisation option '{}' (extra={})"
525 "".format(optimisation, extras))
526 else:
527 obs_op['optim'] = _dictionary2str(aoptions)
529 if verbose >= 3 and fLOG is not None:
530 fLOG("[enumerate_compatible_opset] optim='{}' optimisation={} all_conv_options={}".format(
531 obs_op['optim'], optimisation, all_conv_options))
532 if kwargs['store_models']:
533 obs_op['ONNX'] = conv
534 if verbose >= 2 and fLOG is not None:
535 fLOG( # pragma: no cover
536 "[enumerate_compatible_opset] onnx nodes: {}".format(
537 len(conv.graph.node)))
538 stat_onnx = onnx_statistics(conv)
539 obs_op.update(
540 {'onx_' + k: v for k, v in stat_onnx.items()})
542 # opset_domain
543 for op_imp in list(conv.opset_import):
544 obs_op['domain_opset_%s' %
545 op_imp.domain] = op_imp.version
547 run_benchmark = _check_run_benchmark(
548 benchmark, stat_onnx, bench_memo, rt)
550 # prediction
551 if check_runtime:
552 yield _call_runtime(obs_op=obs_op.copy(), conv=conv,
553 opset=opset, debug=debug,
554 runtime=rt, inst=inst,
555 X_test=X_test, y_test=y_test,
556 init_types=init_types,
557 method_name=method_name,
558 output_index=output_index,
559 ypred=ypred, Xort_test=Xort_test,
560 model=model,
561 dump_folder=kwargs['dump_folder'],
562 benchmark=run_benchmark,
563 node_time=kwargs['node_time'],
564 time_kwargs=kwargs['time_kwargs'],
565 fLOG=fLOG, verbose=verbose,
566 store_models=kwargs['store_models'],
567 dump_all=kwargs['dump_all'],
568 skip_long_test=kwargs['skip_long_test'],
569 time_limit=time_limit)
570 else:
571 yield obs_op.copy() # pragma: no cover
574def _call_runtime(obs_op, conv, opset, debug, inst, runtime,
575 X_test, y_test, init_types, method_name, output_index,
576 ypred, Xort_test, model, dump_folder,
577 benchmark, node_time, fLOG,
578 verbose, store_models, time_kwargs,
579 dump_all, skip_long_test, time_limit):
580 """
581 Private.
582 """
583 if 'onnxruntime' in runtime:
584 old = conv.ir_version
585 conv.ir_version = get_ir_version(opset)
586 else:
587 old = None
589 ser, t5, ___ = _measure_time(lambda: conv.SerializeToString())
590 obs_op['tostring_time'] = t5
591 obs_op['runtime'] = runtime
593 if old is not None:
594 conv.ir_version = old
596 # load
597 if verbose >= 2 and fLOG is not None:
598 fLOG("[enumerate_compatible_opset-R] load onnx")
599 try:
600 sess, t5, ___ = _measure_time(
601 lambda: OnnxInference(
602 ser, runtime=runtime, runtime_options=dict(
603 log_severity_level=3)))
604 obs_op['tostring_time'] = t5
605 except (RuntimeError, ValueError, KeyError, IndexError, TypeError) as e:
606 if debug:
607 raise # pragma: no cover
608 obs_op['_5ort_load_exc'] = e
609 return obs_op
611 # compute batch
612 if store_models:
613 obs_op['OINF'] = sess
614 if verbose >= 2 and fLOG is not None:
615 fLOG("[enumerate_compatible_opset-R] compute batch with runtime "
616 "'{}'".format(runtime))
618 def fct_batch(se=sess, xo=Xort_test, it=init_types): # pylint: disable=W0102
619 return se.run({it[0][0]: xo},
620 verbose=max(verbose - 1, 1) if debug else 0, fLOG=fLOG)
622 try:
623 opred, t5, ___ = _measure_time(fct_batch)
624 obs_op['ort_run_time_batch'] = t5
625 obs_op['lambda-batch'] = (lambda xo: sess.run(
626 {init_types[0][0]: xo}, node_time=node_time), Xort_test)
627 except (RuntimeError, TypeError, ValueError, KeyError, IndexError) as e:
628 if debug:
629 raise RuntimeError("Issue with {}.".format(
630 obs_op)) from e # pragma: no cover
631 obs_op['_6ort_run_batch_exc'] = e
632 if (benchmark or node_time) and 'lambda-batch' in obs_op:
633 try:
634 benres = benchmark_fct(*obs_op['lambda-batch'], obs=obs_op,
635 node_time=node_time, time_kwargs=time_kwargs,
636 skip_long_test=skip_long_test,
637 time_limit=time_limit)
638 obs_op['bench-batch'] = benres
639 except (RuntimeError, TypeError, ValueError) as e: # pragma: no cover
640 if debug:
641 raise e # pragma: no cover
642 obs_op['_6ort_run_batch_exc'] = e
643 obs_op['_6ort_run_batch_bench_exc'] = e
645 # difference
646 debug_exc = []
647 if verbose >= 2 and fLOG is not None:
648 fLOG("[enumerate_compatible_opset-R] differences")
649 if '_6ort_run_batch_exc' not in obs_op:
650 if isinstance(opred, dict):
651 ch = [(k, v) for k, v in opred.items()]
652 opred = [_[1] for _ in ch]
654 if output_index != 'all':
655 try:
656 opred = opred[output_index]
657 except IndexError as e: # pragma: no cover
658 if debug:
659 raise IndexError(
660 "Issue with output_index={}/{}".format(
661 output_index, len(opred))) from e
662 obs_op['_8max_rel_diff_batch_exc'] = (
663 "Unable to fetch output {}/{} for model '{}'"
664 "".format(output_index, len(opred),
665 model.__name__))
666 opred = None
668 if opred is not None:
669 if store_models:
670 obs_op['skl_outputs'] = ypred
671 obs_op['ort_outputs'] = opred
672 if verbose >= 3 and fLOG is not None:
673 fLOG("[_call_runtime] runtime prediction")
674 _dispsimple(opred, fLOG)
676 if (method_name == "decision_function" and hasattr(opred, 'shape') and
677 hasattr(ypred, 'shape') and len(opred.shape) == 2 and
678 opred.shape[1] == 2 and len(ypred.shape) == 1):
679 # decision_function, for binary classification,
680 # raw score is a distance
681 try:
682 max_rel_diff = measure_relative_difference(
683 ypred, opred[:, 1])
684 except AttributeError: # pragma: no cover
685 max_rel_diff = numpy.nan
686 else:
687 try:
688 max_rel_diff = measure_relative_difference(
689 ypred, opred)
690 except AttributeError: # pragma: no cover
691 max_rel_diff = numpy.nan
693 if max_rel_diff >= 1e9 and debug: # pragma: no cover
694 _shape = lambda o: o.shape if hasattr(
695 o, 'shape') else 'no shape'
696 raise RuntimeError(
697 "Big difference (opset={}, runtime='{}' p='{}' s='{}')"
698 ":\n-------\n{}-{}\n{}\n--------\n{}-{}\n{}".format(
699 opset, runtime, obs_op['problem'], obs_op['scenario'],
700 type(ypred), _shape(ypred), ypred,
701 type(opred), _shape(opred), opred))
703 if numpy.isnan(max_rel_diff):
704 obs_op['_8max_rel_diff_batch_exc'] = ( # pragma: no cover
705 "Unable to compute differences between"
706 " {}-{}\n{}\n--------\n{}".format(
707 _shape_exc(
708 ypred), _shape_exc(opred),
709 ypred, opred))
710 if debug: # pragma: no cover
711 debug_exc.append(RuntimeError(
712 obs_op['_8max_rel_diff_batch_exc']))
713 else:
714 obs_op['max_rel_diff_batch'] = max_rel_diff
715 if dump_folder and max_rel_diff > 1e-5:
716 dump_into_folder(dump_folder, kind='batch', obs_op=obs_op,
717 X_test=X_test, y_test=y_test, Xort_test=Xort_test)
718 if debug and max_rel_diff >= 0.1: # pragma: no cover
719 raise RuntimeError("Two big differences {}\n{}\n{}\n{}".format(
720 max_rel_diff, inst, conv, pprint.pformat(obs_op)))
722 if debug and len(debug_exc) == 2:
723 raise debug_exc[0] # pragma: no cover
724 if debug and verbose >= 2: # pragma: no cover
725 if verbose >= 3:
726 fLOG(pprint.pformat(obs_op))
727 else:
728 obs_op_log = {k: v for k,
729 v in obs_op.items() if 'lambda-' not in k}
730 fLOG(pprint.pformat(obs_op_log))
731 if verbose >= 2 and fLOG is not None:
732 fLOG("[enumerate_compatible_opset-R] next...")
733 if dump_all:
734 dump = dump_into_folder(dump_folder, kind='batch', obs_op=obs_op,
735 X_test=X_test, y_test=y_test, Xort_test=Xort_test,
736 is_error=len(debug_exc) > 1,
737 onnx_bytes=conv.SerializeToString(),
738 skl_model=inst, ypred=ypred)
739 obs_op['dumped'] = dump
740 return obs_op
743def _enumerate_validated_operator_opsets_ops(extended_list, models, skip_models):
744 ops = [_ for _ in sklearn_operators(extended=extended_list)]
746 if models is not None:
747 if not all(map(lambda m: isinstance(m, str), models)):
748 raise ValueError( # pragma: no cover
749 "models must be a set of strings.")
750 ops_ = [_ for _ in ops if _['name'] in models]
751 if len(ops) == 0:
752 raise ValueError( # pragma: no cover
753 "Parameter models is wrong: {}\n{}".format(
754 models, ops[0]))
755 ops = ops_
756 if skip_models is not None:
757 ops = [m for m in ops if m['name'] not in skip_models]
758 return ops
761def _enumerate_validated_operator_opsets_version(runtime):
762 from numpy import __version__ as numpy_version # delayed
763 from onnx import __version__ as onnx_version # delayed
764 from scipy import __version__ as scipy_version # delayed
765 from skl2onnx import __version__ as skl2onnx_version # delayed
766 from onnxruntime import __version__ as onnxrt_version # delayed
767 add_versions = {'v_numpy': numpy_version, 'v_onnx': onnx_version,
768 'v_scipy': scipy_version, 'v_skl2onnx': skl2onnx_version,
769 'v_sklearn': sklearn_version, 'v_onnxruntime': ort_version}
770 if "onnxruntime" in runtime:
771 add_versions['v_onnxruntime'] = onnxrt_version
772 return add_versions
775def enumerate_validated_operator_opsets(verbose=0, opset_min=-1, opset_max=-1,
776 check_runtime=True, debug=False, runtime='python',
777 models=None, dump_folder=None, store_models=False,
778 benchmark=False, skip_models=None,
779 assume_finite=True, node_time=False,
780 fLOG=print, filter_exp=None,
781 versions=False, extended_list=False,
782 time_kwargs=None, dump_all=False,
783 n_features=None, skip_long_test=True,
784 fail_bad_results=False,
785 filter_scenario=None,
786 time_kwargs_fact=None,
787 time_limit=4, n_jobs=None):
788 """
789 Tests all possible configurations for all possible
790 operators and returns the results.
792 :param verbose: integer 0, 1, 2
793 :param opset_min: checks conversion starting from the opset, -1
794 to get the last one
795 :param opset_max: checks conversion up to this opset,
796 None means `__max_supported_opset__`
797 :param check_runtime: checks the python runtime
798 :param models: only process a small list of operators,
799 set of model names
800 :param debug: stops whenever an exception
801 is raised
802 :param runtime: test a specific runtime, by default ``'python'``
803 :param dump_folder: dump information to replicate in case of mismatch
804 :param dump_all: dump all models not only the one which fail
805 :param store_models: if True, the function
806 also stores the fitted model and its conversion
807 into :epkg:`ONNX`
808 :param benchmark: if True, measures the time taken by each function
809 to predict for different number of rows
810 :param filter_exp: function which tells if the experiment must be run,
811 None to run all, takes *model, problem* as an input
812 :param filter_scenario: second function which tells if the experiment must be run,
813 None to run all, takes *model, problem, scenario, extra, options*
814 as an input
815 :param skip_models: models to skip
816 :param assume_finite: See `config_context
817 <https://scikit-learn.org/stable/modules/generated/
818 sklearn.config_context.html>`_, If True, validation for finiteness
819 will be skipped, saving time, but leading to potential crashes.
820 If False, validation for finiteness will be performed, avoiding error.
821 :param node_time: measure time execution for every node in the graph
822 :param versions: add columns with versions of used packages,
823 :epkg:`numpy`, :epkg:`scikit-learn`, :epkg:`onnx`,
824 :epkg:`onnxruntime`, :epkg:`sklearn-onnx`
825 :param extended_list: also check models this module implements a converter for
826 :param time_kwargs: to define a more precise way to measure a model
827 :param n_features: modifies the shorts datasets used to train the models
828 to use exactly this number of features, it can also
829 be a list to test multiple datasets
830 :param skip_long_test: skips tests for high values of N if they seem too long
831 :param fail_bad_results: fails if the results are aligned with :epkg:`scikit-learn`
832 :param time_kwargs_fact: see :func:`_multiply_time_kwargs
833 <mlprodict.onnxrt.validate.validate_helper._multiply_time_kwargs>`
834 :param time_limit: to skip the rest of the test after this limit (in second)
835 :param n_jobs: *n_jobs* is set to the number of CPU by default unless this
836 value is changed
837 :param fLOG: logging function
838 :return: list of dictionaries
840 The function is available through command line
841 :ref:`validate_runtime <l-cmd-validate_runtime>`.
842 The default for *time_kwargs* is the following:
844 .. runpython::
845 :showcode:
846 :warningout: DeprecationWarning
848 from mlprodict.onnxrt.validate.validate_helper import default_time_kwargs
849 import pprint
850 pprint.pprint(default_time_kwargs())
851 """
852 register_converters()
853 register_rewritten_operators()
854 ops = _enumerate_validated_operator_opsets_ops(
855 extended_list, models, skip_models)
857 if verbose > 0:
859 def iterate():
860 for i, row in enumerate(ops): # pragma: no cover
861 fLOG("{}/{} - {}".format(i + 1, len(ops), row))
862 yield row
864 if verbose >= 11:
865 verbose -= 10 # pragma: no cover
866 loop = iterate() # pragma: no cover
867 else:
868 try:
869 from tqdm import trange
871 def iterate_tqdm():
872 with trange(len(ops)) as t:
873 for i in t:
874 row = ops[i]
875 disp = row['name'] + " " * (28 - len(row['name']))
876 t.set_description("%s" % disp)
877 yield row
879 loop = iterate_tqdm()
881 except ImportError: # pragma: no cover
882 loop = iterate()
883 else:
884 loop = ops
886 if versions:
887 add_versions = _enumerate_validated_operator_opsets_version(runtime)
888 else:
889 add_versions = {}
891 current_opset = __max_supported_opset__
892 if opset_min == -1:
893 opset_min = __max_supported_opset__
894 if opset_max == -1:
895 opset_max = __max_supported_opset__
896 if verbose > 0 and fLOG is not None:
897 fLOG("[enumerate_validated_operator_opsets] opset in [{}, {}].".format(
898 opset_min, opset_max))
899 for row in loop:
901 model = row['cl']
902 if verbose > 1:
903 fLOG("[enumerate_validated_operator_opsets] - model='{}'".format(model))
905 for obs in enumerate_compatible_opset(
906 model, opset_min=opset_min, opset_max=opset_max,
907 check_runtime=check_runtime, runtime=runtime,
908 debug=debug, dump_folder=dump_folder,
909 store_models=store_models, benchmark=benchmark,
910 fLOG=fLOG, filter_exp=filter_exp,
911 assume_finite=assume_finite, node_time=node_time,
912 verbose=verbose, extended_list=extended_list,
913 time_kwargs=time_kwargs, dump_all=dump_all,
914 n_features=n_features, skip_long_test=skip_long_test,
915 filter_scenario=filter_scenario,
916 time_kwargs_fact=time_kwargs_fact,
917 time_limit=time_limit, n_jobs=n_jobs):
919 for mandkey in ('inst', 'method_name', 'problem',
920 'scenario'):
921 if '_0problem_exc' in obs:
922 continue
923 if mandkey not in obs:
924 raise ValueError("Missing key '{}' in\n{}".format(
925 mandkey, pprint.pformat(obs))) # pragma: no cover
926 if verbose > 1:
927 fLOG('[enumerate_validated_operator_opsets] - OBS')
928 if verbose > 2:
929 fLOG(" ", obs)
930 else:
931 obs_log = {k: v for k,
932 v in obs.items() if 'lambda-' not in k}
933 fLOG(" ", obs_log)
934 elif verbose > 0 and "_0problem_exc" in obs:
935 fLOG(" ???", obs) # pragma: no cover
937 diff = obs.get('max_rel_diff_batch', None)
938 batch = 'max_rel_diff_batch' in obs and diff is not None
939 op1 = obs.get('domain_opset_', '')
940 op2 = obs.get('domain_opset_ai.onnx.ml', '')
941 op = '{}/{}'.format(op1, op2)
943 obs['available'] = "?"
944 if diff is not None:
945 if diff < 1e-5:
946 obs['available'] = 'OK'
947 elif diff < 0.0001:
948 obs['available'] = 'e<0.0001' # pragma: no cover
949 elif diff < 0.001:
950 obs['available'] = 'e<0.001'
951 elif diff < 0.01:
952 obs['available'] = 'e<0.01' # pragma: no cover
953 elif diff < 0.1:
954 obs['available'] = 'e<0.1'
955 else:
956 obs['available'] = "ERROR->=%1.1f" % diff
957 obs['available'] += '-' + op
958 if not batch:
959 obs['available'] += "-NOBATCH" # pragma: no cover
960 if fail_bad_results and 'e<' in obs['available']:
961 raise RuntimeBadResultsError(
962 "Wrong results '{}'.".format(obs['available']), obs) # pragma: no cover
964 excs = []
965 for k, v in sorted(obs.items()):
966 if k.endswith('_exc'):
967 excs.append((k, v))
968 break
969 if 'opset' not in obs:
970 # It fails before the conversion happens.
971 obs['opset'] = current_opset
972 if obs['opset'] == current_opset and len(excs) > 0:
973 k, v = excs[0]
974 obs['available'] = 'ERROR-%s' % k
975 obs['available-ERROR'] = v
977 if 'bench-skl' in obs:
978 b1 = obs['bench-skl']
979 if 'bench-batch' in obs:
980 b2 = obs['bench-batch']
981 else:
982 b2 = None
983 if b1 is not None and b2 is not None:
984 for k in b1:
985 if k in b2 and b2[k] is not None and b1[k] is not None:
986 key = 'time-ratio-N=%d' % k
987 obs[key] = b2[k]['average'] / b1[k]['average']
988 key = 'time-ratio-N=%d-min' % k
989 obs[key] = b2[k]['min_exec'] / b1[k]['max_exec']
990 key = 'time-ratio-N=%d-max' % k
991 obs[key] = b2[k]['max_exec'] / b1[k]['min_exec']
993 obs.update(row)
994 obs.update(add_versions)
995 yield obs.copy()