Skip to content

Commit 85e4d64

Browse files
Merge pull request #354 from KernelTuner/constrained_optimization_tunable
Optimized searchspace operations
2 parents c44a397 + 4078ca1 commit 85e4d64

2 files changed

Lines changed: 63 additions & 34 deletions

File tree

kernel_tuner/searchspace.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def __init__(
103103
self.param_names = list(self.tune_params.keys())
104104
self.params_values = tuple(tuple(param_vals) for param_vals in self.tune_params.values())
105105
self.params_values_indices = None
106+
self._alloc_diff = None
107+
self._alloc_sum_of_index_differences = None
106108
self.build_neighbors_index = build_neighbors_index
107109
self.solver_method = solver_method
108110
self.tune_param_is_numeric = { param_name: all(isinstance(val, (int, float)) for val in param_values) and not any(isinstance(val, bool) for val in param_values) for (param_name, param_values) in tune_params.items() }
@@ -715,21 +717,46 @@ def get_list_param_indices_numpy(self) -> np.ndarray:
715717
the NumPy array.
716718
"""
717719
if self.__list_param_indices is None:
720+
721+
# compute the lookups
718722
tune_params_to_index_lookup = list()
719723
tune_params_from_index_lookup = list()
724+
all_values_integer_nonnegative = True
720725
for param_name, param_values in self.tune_params.items():
721726
tune_params_to_index_lookup.append({ value: index for index, value in enumerate(param_values) })
722727
tune_params_from_index_lookup.append({ index: value for index, value in enumerate(param_values) })
723-
728+
if (all_values_integer_nonnegative and
729+
not all(isinstance(v, int) and 0 < v < 2**15 for v in param_values)
730+
):
731+
all_values_integer_nonnegative = False
732+
724733
# build the list
725-
list_param_indices = list()
726-
for param_config in self.list:
727-
list_param_indices.append([tune_params_to_index_lookup[index][val] for index, val in enumerate(param_config)])
734+
if all_values_integer_nonnegative:
735+
# optimized case for integer non-negative values
736+
configs = np.asarray(self.list)
737+
index_arrays = []
738+
for values in self.tune_params.values():
739+
arr = np.full(max(values) + 1, -1, dtype=np.int16)
740+
for i, v in enumerate(values):
741+
arr[v] = i
742+
index_arrays.append(arr)
743+
# use advanced indexing to build the list of parameter indices
744+
list_param_indices = np.column_stack([
745+
index_arrays[i][configs[:, i]]
746+
for i in range(configs.shape[1])
747+
])
748+
else:
749+
# general case for any type of values
750+
list_param_indices = list()
751+
for param_config in self.list:
752+
list_param_indices.append([tune_params_to_index_lookup[index][val] for index, val in enumerate(param_config)])
753+
list_param_indices = np.array(list_param_indices)
728754

729755
# register the computed results
730756
self.__tune_params_to_index_lookup = tune_params_to_index_lookup
731757
self.__tune_params_from_index_lookup = tune_params_from_index_lookup
732-
self.__list_param_indices = np.array(list_param_indices)
758+
self.__list_param_indices = list_param_indices
759+
733760
assert self.__list_param_indices.shape == (self.size, self.num_params), f"Expected shape {(self.size, self.num_params)}, got {self.__list_param_indices.shape}"
734761

735762
# calculate the actual minimum and maximum index for each parameter after restrictions
@@ -962,6 +989,8 @@ def __prepare_neighbors_index(self):
962989
"""Prepare by calculating the indices for the individual parameters."""
963990
if self.params_values_indices is None:
964991
self.params_values_indices = self.get_list_param_indices_numpy()
992+
self._alloc_diff = np.empty_like(self.params_values_indices, dtype=self.params_values_indices.dtype)
993+
self._alloc_sum_of_index_differences = np.empty((self.params_values_indices.shape[0],), dtype=self.params_values_indices.dtype)
965994

966995
def __get_neighbor_indices_closest_param_indices(self, param_config: tuple, param_index: int = None, return_one=False) -> List[int]:
967996
"""Get the neighbors closest in parameter indices difference from the parameter configuration. Always returns at least 1 neighbor."""
@@ -972,19 +1001,19 @@ def __get_neighbor_indices_closest_param_indices(self, param_config: tuple, para
9721001
self.__prepare_neighbors_index()
9731002

9741003
# calculate the absolute difference between the parameter value indices
975-
abs_index_difference = np.abs(self.params_values_indices - np.array(param_indices), dtype=self.params_values_indices.dtype)
976-
# calculate the sum of the absolute differences for each parameter configuration
977-
sum_of_index_differences = np.sum(abs_index_difference, axis=1)
1004+
self.__calc_sum_of_index_differences(np.array(param_indices))
9781005
if param_index is not None:
9791006
# set the sum of index differences to infinity for the parameter index to avoid returning the same parameter configuration
980-
sum_of_index_differences[param_index] = self.get_list_param_indices_numpy_max()
1007+
self._alloc_sum_of_index_differences[param_index] = self.get_list_param_indices_numpy_max()
1008+
1009+
# return the indices of the closest parameter configurations
9811010
if return_one:
9821011
# if return_one is True, return the index of the closest parameter configuration (faster than finding all)
983-
get_partial_neighbors_indices = [np.argmin(sum_of_index_differences)]
1012+
matching_indices = [np.argmin(self._alloc_sum_of_index_differences).item()]
9841013
else:
9851014
# find the param config indices where the difference is the smallest
986-
min_difference = np.min(sum_of_index_differences)
987-
matching_indices = (sum_of_index_differences == min_difference).nonzero()[0]
1015+
min_difference = np.min(self._alloc_sum_of_index_differences)
1016+
matching_indices = (self._alloc_sum_of_index_differences == min_difference).nonzero()[0]
9881017
return matching_indices
9891018

9901019
def __get_neighbors_indices_hamming(self, param_config: tuple) -> List[int]:
@@ -1073,15 +1102,17 @@ def __get_neighbors_indices_strictlyadjacent(
10731102
"""Get the neighbors using strictly adjacent distance from the parameter configuration (parameter index absolute difference == 1)."""
10741103
if self.params_values_indices is None:
10751104
self.__prepare_neighbors_index()
1076-
param_config_value_indices = (
1105+
param_config_value_indices = np.array(
10771106
self.get_param_indices(param_config)
10781107
if param_config_index is None
10791108
else self.params_values_indices[param_config_index]
10801109
)
1110+
10811111
# calculate the absolute difference between the parameter value indices
10821112
abs_index_difference = np.abs(self.params_values_indices - param_config_value_indices, dtype=self.params_values_indices.dtype)
10831113
# get the param config indices where the difference is one or less for each position
10841114
matching_indices = (np.max(abs_index_difference, axis=1) <= 1).nonzero()[0]
1115+
10851116
# as the selected param config does not differ anywhere, remove it from the matches
10861117
if param_config_index is not None:
10871118
matching_indices = np.setdiff1d(matching_indices, [param_config_index], assume_unique=True)
@@ -1145,12 +1176,18 @@ def __build_neighbors_index(self, neighbor_method) -> List[List[int]]:
11451176
)
11461177
if neighbor_method == "closest-param-indices":
11471178
return list(
1148-
self.__get_neighbor_indices_closest_param_indices(param_config, param_config_index)
1179+
self.__get_neighbor_indices_closest_param_indices(param_config, param_config_index, return_one=False)
11491180
for param_config_index, param_config in enumerate(self.list)
11501181
)
11511182

11521183
raise NotImplementedError(f"The neighbor method {neighbor_method} is not implemented")
11531184

1185+
def __calc_sum_of_index_differences(self, target_param_config_indices: np.ndarray):
1186+
"""Calculates the absolute difference between the parameter value indices and `target_param_config_indices` into `self._alloc_sum_of_index_differences`."""
1187+
np.subtract(self.params_values_indices, target_param_config_indices, out=self._alloc_diff)
1188+
np.abs(self._alloc_diff, out=self._alloc_diff)
1189+
np.einsum('ij->i', self._alloc_diff, out=self._alloc_sum_of_index_differences)
1190+
11541191
def get_random_sample_indices(self, num_samples: int) -> np.ndarray:
11551192
"""Get the list indices for a random, non-conflicting sample."""
11561193
if num_samples > self.size:
@@ -1169,7 +1206,7 @@ def get_random_sample(self, num_samples: int) -> List[tuple]:
11691206
return self.get_param_configs_at_indices(self.get_random_sample_indices(num_samples))
11701207

11711208
def get_distributed_random_sample_indices(self, num_samples: int, sampling_factor=10) -> List[int]:
1172-
"""Get a distributed random sample of parameter configuration indices. Note: `get_LHS_random_sample_indices` is likely faster and better distributed."""
1209+
"""Get a distributed random sample of parameter configuration indices. Note: `get_LHS_sample_indices` is likely faster and better distributed."""
11731210
if num_samples > self.size:
11741211
warn(
11751212
f"Too many samples requested ({num_samples}), reducing the number of samples to half of the searchspace size ({self.size})"
@@ -1219,16 +1256,12 @@ def get_next_sample(lower: tuple, upper: tuple) -> tuple:
12191256
self.__prepare_neighbors_index()
12201257
target_sample_indices = list()
12211258
for target_sample_param_config_indices in target_samples_param_indices:
1222-
# calculate the absolute difference between the parameter value indices
1223-
abs_index_difference = np.abs(self.params_values_indices - target_sample_param_config_indices, dtype=self.params_values_indices.dtype)
1224-
# find the param config index where the difference is the smallest
1225-
sum_of_index_differences = np.sum(abs_index_difference, axis=1)
12261259
param_index = self.get_param_config_index(self.get_param_config_from_param_indices(target_sample_param_config_indices))
12271260
if param_index is not None:
1228-
# set the sum of index differences to infinity for the parameter index to avoid returning the same parameter configuration
1229-
sum_of_index_differences[param_index] = self.get_list_param_indices_numpy_max()
1230-
min_index_difference_index = np.argmin(sum_of_index_differences)
1231-
target_sample_indices.append(min_index_difference_index.item())
1261+
target_sample_indices.append(param_index)
1262+
else:
1263+
self.__calc_sum_of_index_differences(target_sample_param_config_indices)
1264+
target_sample_indices.append(np.argmin(self._alloc_sum_of_index_differences).item())
12321265

12331266
# filter out duplicate samples and replace with random ones
12341267
target_sample_indices = list(set(target_sample_indices))
@@ -1267,16 +1300,12 @@ def get_LHS_sample_indices(self, num_samples: int) -> List[int]:
12671300
# for each of the target sample indices, calculate which parameter configuration is closest
12681301
target_sample_indices = list()
12691302
for target_sample_param_config_indices in target_samples_param_indices:
1270-
# calculate the absolute difference between the parameter value indices
1271-
abs_index_difference = np.abs(self.params_values_indices - target_sample_param_config_indices, dtype=self.params_values_indices.dtype)
1272-
# find the param config index where the difference is the smallest
1273-
sum_of_index_differences = np.sum(abs_index_difference, axis=1)
12741303
param_index = self.get_param_config_index(self.get_param_config_from_param_indices(target_sample_param_config_indices))
12751304
if param_index is not None:
1276-
# set the sum of index differences to infinity for the parameter index to avoid returning the same parameter configuration
1277-
sum_of_index_differences[param_index] = self.get_list_param_indices_numpy_max()
1278-
min_index_difference_index = np.argmin(sum_of_index_differences)
1279-
target_sample_indices.append(min_index_difference_index.item())
1305+
target_sample_indices.append(param_index)
1306+
else:
1307+
self.__calc_sum_of_index_differences(target_sample_param_config_indices)
1308+
target_sample_indices.append(np.argmin(self._alloc_sum_of_index_differences).item())
12801309

12811310
# filter out duplicate samples and replace with random ones
12821311
target_sample_indices = list(set(target_sample_indices))

kernel_tuner/strategies/diff_evo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import numpy as np
55

66
from kernel_tuner.util import StopCriterionReached
7-
from scipy.stats.qmc import LatinHypercube
87
from kernel_tuner.searchspace import Searchspace
98
from kernel_tuner.strategies import common
109
from kernel_tuner.strategies.common import CostFunc
@@ -387,12 +386,13 @@ def repair(trial_vector, searchspace):
387386
"""
388387
Attempts to repair trial_vector if trial_vector is invalid
389388
"""
390-
if not searchspace.is_param_config_valid(tuple(trial_vector)):
389+
trial_tuple = tuple(trial_vector)
390+
if not searchspace.is_param_config_valid(trial_tuple):
391391
# search for valid configurations neighboring trial_vector
392392
for neighbor_method in ["closest-param-indices"]:
393393
# start from strictly-adjacent to increasingly allowing more neighbors
394394
# for neighbor_method in ["strictly-adjacent", "adjacent", "Hamming"]:
395-
new_trial_vector = searchspace.get_random_neighbor(tuple(trial_vector), neighbor_method=neighbor_method)
395+
new_trial_vector = searchspace.get_random_neighbor(trial_tuple, neighbor_method=neighbor_method)
396396
if new_trial_vector is not None:
397397
# print(f"Differential evolution resulted in invalid config {trial_vector=}, repaired to {new_trial_vector=}")
398398
return list(new_trial_vector)

0 commit comments

Comments
 (0)