Skip to content

Commit a2607d9

Browse files
Merge pull request #358 from maric-a-b/multiobjective_optimization
Multiobjective optimization
2 parents 8756e6d + 4153fc2 commit a2607d9

23 files changed

Lines changed: 663 additions & 99 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ temp_*.*
3131
.DS_Store
3232
.python-version
3333
.nox
34+
.venv
3435

3536
### Visual Studio Code ###
3637
!.vscode/settings.json

kernel_tuner/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from importlib.metadata import version
22

3-
from kernel_tuner.interface import run_kernel, tune_kernel, tune_kernel_T1
3+
from kernel_tuner.interface import run_kernel, tune_kernel, tune_kernel_T1, tune_cache
44

55
__version__ = version(__package__)
66

@@ -9,6 +9,7 @@
99
"run_kernel",
1010
"store_results",
1111
"tune_kernel",
12+
"tune_cache",
1213
"tune_kernel_T1",
1314
"__version__",
1415
]

kernel_tuner/core.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,11 +506,14 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett
506506
print(
507507
f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch"
508508
)
509-
result[objective] = util.RuntimeFailedConfig()
509+
result['__error__'] = util.RuntimeFailedConfig()
510510
else:
511511
logging.debug("benchmark encountered runtime failure: " + str(e))
512512
print("Error while benchmarking:", instance.name)
513513
raise e
514+
515+
assert util.check_result_type(result), "The error in a result MUST be an actual error."
516+
514517
return result
515518

516519
def check_kernel_output(
@@ -600,7 +603,7 @@ def compile_and_benchmark(self, kernel_source, gpu_args, params, kernel_options,
600603

601604
instance = self.create_kernel_instance(kernel_source, kernel_options, params, verbose)
602605
if isinstance(instance, util.ErrorConfig):
603-
result[to.objective] = util.InvalidConfig()
606+
result['__error__'] = util.InvalidConfig()
604607
else:
605608
# Preprocess the argument list. This is required to deal with `MixedPrecisionArray`s
606609
gpu_args = _preprocess_gpu_arguments(gpu_args, params)
@@ -610,7 +613,7 @@ def compile_and_benchmark(self, kernel_source, gpu_args, params, kernel_options,
610613
start_compilation = time.perf_counter()
611614
func = self.compile_kernel(instance, verbose)
612615
if not func:
613-
result[to.objective] = util.CompilationFailedConfig()
616+
result['__error__'] = util.CompilationFailedConfig()
614617
else:
615618
# add shared memory arguments to compiled module
616619
if kernel_options.smem_args is not None:
@@ -655,6 +658,8 @@ def compile_and_benchmark(self, kernel_source, gpu_args, params, kernel_options,
655658
result["verification_time"] = last_verification_time or 0
656659
result["benchmark_time"] = last_benchmark_time or 0
657660

661+
assert util.check_result_type(result), "The error in a result MUST be an actual error."
662+
658663
return result
659664

660665
def compile_kernel(self, instance, verbose):

kernel_tuner/file_utils.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def input_file_schema():
2020
2121
:returns: the current version of the T1 schemas and the JSON string of the schema
2222
:rtype: string, string
23-
"""
23+
"""
2424
current_version = "1.0.0"
2525
input_file = schema_dir.joinpath(f"T1/{current_version}/input-schema.json")
2626
with input_file.open() as fh:
@@ -30,9 +30,9 @@ def input_file_schema():
3030
def get_input_file(filepath: Path, validate=True) -> dict[str, any]:
3131
"""Load the T1 input file from the given path, validates it and returns contents if valid.
3232
33-
:param filepath: Path to the input file to load.
34-
:returns: the contents of the file if valid.
35-
"""
33+
:param filepath: Path to the input file to load.
34+
:returns: the contents of the file if valid.
35+
"""
3636
with filepath.open() as fp:
3737
input_file = json.load(fp)
3838
if validate:
@@ -57,20 +57,38 @@ def output_file_schema(target):
5757
return current_version, json_string
5858

5959

60-
def get_configuration_validity(objective) -> str:
60+
def get_configuration_validity(error) -> str:
6161
"""Convert internal Kernel Tuner error to string."""
6262
errorstring: str
63-
if not isinstance(objective, util.ErrorConfig):
63+
if not isinstance(error, util.ErrorConfig):
6464
errorstring = "correct"
6565
else:
66-
if isinstance(objective, util.CompilationFailedConfig):
66+
if isinstance(error, util.CompilationFailedConfig):
6767
errorstring = "compile"
68-
elif isinstance(objective, util.RuntimeFailedConfig):
68+
elif isinstance(error, util.RuntimeFailedConfig):
6969
errorstring = "runtime"
70-
elif isinstance(objective, util.InvalidConfig):
70+
elif isinstance(error, util.InvalidConfig):
7171
errorstring = "constraints"
7272
else:
73-
raise ValueError(f"Unkown objective type {type(objective)}, value {objective}")
73+
raise ValueError(f"Unkown error type {type(error)}, value {error}")
74+
return errorstring
75+
76+
77+
def get_configuration_validity2(result) -> str:
78+
"""Convert internal Kernel Tuner error to string."""
79+
errorstring: str
80+
if "__error__" not in result:
81+
errorstring = "correct"
82+
else:
83+
error = result["__error__"]
84+
if isinstance(error, util.CompilationFailedConfig):
85+
errorstring = "compile"
86+
elif isinstance(error, util.RuntimeFailedConfig):
87+
errorstring = "runtime"
88+
elif isinstance(error, util.InvalidConfig):
89+
errorstring = "constraints"
90+
else:
91+
raise ValueError(f"Unkown error type {type(error)}, value {error}")
7492
return errorstring
7593

7694

@@ -103,6 +121,11 @@ def get_t4_results(results, tune_params, objective="time"):
103121
:type objective: string
104122
105123
"""
124+
assert not isinstance(objective, (list, tuple))
125+
126+
if isinstance(objective, (list, tuple)) and len(objective) > 1:
127+
raise ValueError("The T4 format does not support multiple objectives.")
128+
106129
timing_keys = ["compile_time", "benchmark_time", "framework_time", "strategy_time", "verification_time"]
107130
not_measurement_keys = list(tune_params.keys()) + timing_keys + ["timestamp"] + ["times"]
108131

@@ -129,7 +152,8 @@ def get_t4_results(results, tune_params, objective="time"):
129152
out["times"] = timings
130153

131154
# encode the validity of the configuration
132-
out["invalidity"] = get_configuration_validity(result[objective])
155+
# out["invalidity"] = get_configuration_validity(result[objective])
156+
out["invalidity"] = get_configuration_validity2(result)
133157

134158
# Kernel Tuner does not support producing results of configs that fail the correctness check
135159
# therefore correctness is always 1
@@ -143,10 +167,9 @@ def get_t4_results(results, tune_params, objective="time"):
143167
out["measurements"] = measurements
144168

145169
# objectives
146-
# In Kernel Tuner we currently support only one objective at a time, this can be a user-defined
147-
# metric that combines scores from multiple different quantities into a single value to support
148-
# multi-objective tuning however.
149-
out["objectives"] = [objective]
170+
objectives = [objective] if isinstance(objective, str) else list(objective)
171+
assert isinstance(objectives, list)
172+
out["objectives"] = objectives
150173

151174
# append to output
152175
output_data.append(out)
@@ -310,7 +333,7 @@ def load_module(module_name):
310333
spec = spec_from_file_location(module_name, file_path)
311334
if spec is None:
312335
raise ImportError(f"Could not load spec from {file_path}")
313-
336+
314337
# create a module from the spec and execute it
315338
module = module_from_spec(spec)
316339
spec.loader.exec_module(module)
@@ -322,6 +345,6 @@ def load_module(module_name):
322345
module = load_module(file_path.stem)
323346
except ImportError:
324347
module = load_module(f"{file_path.parent.stem}.{file_path.stem}")
325-
348+
326349
# return the class from the module
327350
return getattr(module, class_name)

kernel_tuner/interface.py

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
"pyatf_strategies": "kernel_tuner.strategies.pyatf_strategies",
7575
"hybrid_vndx": "kernel_tuner.strategies.gen_hybrid_vndx",
7676
"adaptive_tabu_greywolf": "kernel_tuner.strategies.gen_adaptive_tabu_greywolf",
77+
"nsga2": "kernel_tuner.strategies.pymoo_minimize",
78+
"nsga3": "kernel_tuner.strategies.pymoo_minimize",
7779
}
7880

7981
_STRATEGY_PARALLEL = ["brute_force", "random_sample", "diff_evo", "genetic_algorithm", "pso", "firefly_algorithm"]
@@ -460,15 +462,15 @@ def __deepcopy__(self, _):
460462
"""Optimization objective to sort results on, consisting of a string
461463
that also occurs in results as a metric or observed quantity, default 'time'.
462464
Please see :ref:`objectives`.""",
463-
"string",
465+
"str | list[str]",
464466
),
465467
),
466468
(
467469
"objective_higher_is_better",
468470
(
469471
"""boolean that specifies whether the objective should
470472
be maximized (True) or minimized (False), default False.""",
471-
"bool",
473+
"bool | list[bool]",
472474
),
473475
),
474476
(
@@ -498,6 +500,7 @@ def __deepcopy__(self, _):
498500
),
499501
("metrics", ("specifies user-defined metrics, please see :ref:`metrics`.", "dict")),
500502
("simulation_mode", ("Simulate an auto-tuning search from an existing cachefile", "bool")),
503+
("seed", ("""The random seed.""", "int")),
501504
("parallel", ("Set to `True` or an integer to enable parallel tuning. If set to an integer, this will be the number of parallel workers.", "int|bool")),
502505
(
503506
"observers",
@@ -621,6 +624,8 @@ def tune_kernel(
621624
observers=None,
622625
objective=None,
623626
objective_higher_is_better=None,
627+
objectives=None,
628+
seed=None,
624629
):
625630
start_overhead_time = perf_counter()
626631
if log:
@@ -630,8 +635,22 @@ def tune_kernel(
630635

631636
_check_user_input(kernel_name, kernelsource, arguments, block_size_names)
632637

633-
# default objective if none is specified
634-
objective, objective_higher_is_better = get_objective_defaults(objective, objective_higher_is_better)
638+
if objectives:
639+
if isinstance(objectives, dict):
640+
objective = list(objectives.keys())
641+
objective_higher_is_better = list(objectives.values())
642+
else:
643+
raise ValueError("objectives should be a dict of (objective, higher_is_better) pairs")
644+
else:
645+
objective, objective_higher_is_better = get_objective_defaults(objective, objective_higher_is_better)
646+
if isinstance(objective, str):
647+
objective = [objective]
648+
if isinstance(objective_higher_is_better, bool):
649+
objective_higher_is_better = [objective_higher_is_better]
650+
651+
assert isinstance(objective, list)
652+
assert isinstance(objective_higher_is_better, list)
653+
assert len(objective) == len(objective_higher_is_better)
635654

636655
# check for forbidden names in tune parameters
637656
util.check_tune_params_list(tune_params, observers, simulation_mode=simulation_mode)
@@ -663,7 +682,6 @@ def tune_kernel(
663682
if "searchspace_construction_options" in strategy_options:
664683
searchspace_construction_options = strategy_options["searchspace_construction_options"]
665684

666-
667685
# log the user inputs
668686
logging.debug("tune_kernel called")
669687
logging.debug("kernel_options: %s", util.get_config_string(kernel_options))
@@ -776,13 +794,35 @@ def preprocess_cache(filepath):
776794

777795
# finished iterating over search space
778796
if results: # checks if results is not empty
779-
best_config = util.get_best_config(results, objective, objective_higher_is_better)
780-
# add the best configuration to env
781-
env["best_config"] = best_config
782-
if not device_options.quiet:
783-
units = getattr(runner, "units", None)
784-
print("best performing configuration:")
785-
util.print_config_output(tune_params, best_config, device_options.quiet, metrics, units)
797+
if len(objective) == 1:
798+
objective = objective[0]
799+
objective_higher_is_better = objective_higher_is_better[0]
800+
best_config = util.get_best_config(results, objective, objective_higher_is_better)
801+
# add the best configuration to env
802+
env['best_config'] = best_config
803+
if not device_options.quiet:
804+
units = getattr(runner, "units", None)
805+
keys = list(tune_params.keys())
806+
keys += [objective]
807+
if metrics:
808+
keys += list(metrics.keys())
809+
if not quiet:
810+
print(f"\nBEST PERFORMING CONFIGURATION FOR OBJECTIVE {objective}:")
811+
print(util.get_config_string(best_config, keys, units))
812+
else:
813+
pareto_front = util.get_pareto_results(results, objective, objective_higher_is_better)
814+
# add the best configuration to env
815+
env['best_config'] = pareto_front
816+
if not device_options.quiet:
817+
units = getattr(runner, "units", None)
818+
keys = list(tune_params.keys())
819+
keys += list(objective)
820+
if metrics:
821+
keys += list(metrics.keys())
822+
if not quiet:
823+
print(f"\nBEST PERFORMING CONFIGURATIONS FOR OBJECTIVES: {objective}:")
824+
for best_config in pareto_front:
825+
print(util.get_config_string(best_config, keys, units))
786826
elif not device_options.quiet:
787827
print("no results to report")
788828

@@ -797,6 +837,28 @@ def preprocess_cache(filepath):
797837

798838
tune_kernel.__doc__ = _tune_kernel_docstring
799839

840+
841+
def tune_cache(
842+
cache_path,
843+
restrictions = None,
844+
**kwargs,
845+
):
846+
cache = util.read_cache(cache_path, open_cache=False)
847+
tune_args = util.infer_args_from_cache(cache)
848+
_restrictions = [util.infer_restrictions_from_cache(cache)]
849+
850+
# Add the user provided restrictions
851+
if restrictions:
852+
if isinstance(restrictions, list):
853+
_restrictions.extend(restrictions)
854+
else:
855+
raise ValueError("The restrictions must be a list()")
856+
857+
tune_args.update(kwargs)
858+
859+
return tune_kernel(**tune_args, cache=cache_path, restrictions=_restrictions, simulation_mode=True)
860+
861+
800862
_run_kernel_docstring = """Compile and run a single kernel
801863
802864
Compiles and runs a single kernel once, given a specific instance of the kernels tuning parameters.

kernel_tuner/runners/sequential.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from kernel_tuner.core import DeviceInterface
77
from kernel_tuner.runners.runner import Runner
8-
from kernel_tuner.util import ErrorConfig, Timer, print_config_output, process_metrics, store_cache, copy_without_benchmark_timings
8+
from kernel_tuner.util import ErrorConfig, Timer, print_config_output, process_metrics, store_cache, copy_without_benchmark_timings, check_result_type
99

1010

1111
class SequentialRunner(Runner):
@@ -41,10 +41,12 @@ def __init__(self, kernel_source, kernel_options, device_options, iterations, ob
4141
self.gpu_args = self.dev.ready_argument_list(kernel_options.arguments)
4242

4343
def get_device_info(self):
44+
""" Return the backend used by this runner. """
4445
return self.dev
4546

4647
def get_environment(self, tuning_options):
47-
return self.dev.get_environment()
48+
env = self.dev.get_environment()
49+
return env
4850

4951
def run(self, parameter_space, tuning_options):
5052
"""Iterate through the entire parameter space using a single Python process.
@@ -101,13 +103,15 @@ def run(self, parameter_space, tuning_options):
101103
result["compile_time"] + result["verification_time"] + result["benchmark_time"]
102104
) / 1000
103105

106+
assert check_result_type(result)
107+
104108
params.update(result)
105109

106-
if isinstance(result.get(tuning_options.objective), ErrorConfig):
110+
if '__error__' in result:
107111
logging.debug("kernel configuration was skipped silently due to compile or runtime failure")
108112

109113
# only compute metrics on configs that have not errored
110-
if not isinstance(params.get(tuning_options.objective), ErrorConfig):
114+
if tuning_options.metrics and '__error__' not in params:
111115
params = process_metrics(params, tuning_options.metrics)
112116

113117
params["timestamp"] = str(datetime.now(timezone.utc))
@@ -119,6 +123,8 @@ def run(self, parameter_space, tuning_options):
119123
# add configuration to cache
120124
store_cache(x_int, params, tuning_options.cachefile, tuning_options.cache)
121125

126+
assert check_result_type(params)
127+
122128
# all visited configurations are added to results to provide a trace for optimization strategies
123129
results.append(params)
124130

0 commit comments

Comments
 (0)