Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion kernel_tuner/searchspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
get_interval,
)

supported_neighbor_methods = ["strictly-adjacent", "adjacent", "Hamming", "closest-param-indices"]

supported_neighbor_methods = ["strictly-adjacent", "adjacent", "Hamming", "Hamming-adjacent", "closest-param-indices"]


class Searchspace:
Expand All @@ -65,6 +66,7 @@ def __init__(
strictly-adjacent: differs +1 or -1 parameter index value for each parameter
adjacent: picks closest parameter value in both directions for each parameter
Hamming: any parameter config with 1 different parameter value is a neighbor
Hamming-adjacent: differs +1 or -1 parameter index value for exactly 1 parameter.
Optionally sort the searchspace by the order in which the parameter values were specified. By default, sort goes from first to last parameter, to reverse this use sort_last_param_first.
Optionally an imported cache can be used instead with `from_cache`, in which case the `tune_params`, `restrictions` and `max_threads` arguments can be set to None, and construction is skipped.
Optionally construction can be deffered to a later time by setting `defer_construction` to True, in which case the searchspace is not built on instantiation (experimental).
Expand Down Expand Up @@ -1009,6 +1011,44 @@ def __get_random_neighbor_hamming(self, param_config: tuple) -> tuple:
return self.get_param_configs_at_indices([i])[0]
return None

def __get_neighbors_indices_hammingadjacent(self, param_config_index: int = None, param_config: tuple = None) -> List[int]:
"""Get the neighbors using adjacent distance from the parameter configuration (parameter index absolute difference >= 1)."""
param_config_value_indices = (
self.get_param_indices(param_config)
if param_config_index is None
else self.params_values_indices[param_config_index]
)

# compute boolean mask for all configuration that differ at exactly one parameter (Hamming distance == 1)
hamming_mask = np.count_nonzero(self.params_values_indices != param_config_value_indices, axis=1) == 1

# get the configuration indices of the hamming neighbors
hamming_indices, = np.nonzero(hamming_mask)

# for the hamming neighbors, calculate the difference between parameter value indices
hamming_values_indices = self.params_values_indices[hamming_mask]

# for each parameter get the closest upper and lower parameter (absolute index difference >= 1)
upper_bound = np.min(
hamming_values_indices,
initial=self.get_list_param_indices_numpy_max(),
axis=0,
where=hamming_values_indices > param_config_value_indices,
)

lower_bound = np.max(
hamming_values_indices,
initial=self.get_list_param_indices_numpy_min(),
axis=0,
where=hamming_values_indices < param_config_value_indices,
)

# return mask for adjacent neighbors (each parameter is within bounds)
adjacent_mask = np.all((lower_bound <= hamming_values_indices) & (hamming_values_indices <= upper_bound), axis=1)

# return hamming neighbors that are also adjacent
return hamming_indices[adjacent_mask]

def __get_random_neighbor_adjacent(self, param_config: tuple) -> tuple:
"""Get an approximately random adjacent neighbor of the parameter configuration."""
# NOTE: this is not truly random as we only progressively increase the allowed index difference if no neighbors are found, but much faster than generating all neighbors
Expand All @@ -1022,6 +1062,7 @@ def __get_random_neighbor_adjacent(self, param_config: tuple) -> tuple:
if param_config_index is None
else self.params_values_indices[param_config_index]
)

max_index_difference_per_param = [max(len(self.params_values[p]) - 1 - i, i) for p, i in enumerate(param_config_value_indices)]

# calculate the absolute difference between the parameter value indices
Expand Down Expand Up @@ -1050,6 +1091,7 @@ def __get_random_neighbor_adjacent(self, param_config: tuple) -> tuple:
allowed_index_difference += 1
return None


def __add_to_neighbor_partial_cache(self, param_config: tuple, neighbor_indices: List[int], neighbor_method: str, full_neighbors = False):
"""Add the neighbor indices to the partial cache using the given parameter configuration."""
param_config_index = self.get_param_config_index(param_config)
Expand Down Expand Up @@ -1133,6 +1175,13 @@ def __build_neighbors_index(self, neighbor_method) -> List[List[int]]:
# for each parameter configuration, find the neighboring parameter configurations
if self.params_values_indices is None:
self.__prepare_neighbors_index()

if neighbor_method == "Hamming-adjacent":
return list(
self.__get_neighbors_indices_hammingadjacent(param_config_index, param_config)
for param_config_index, param_config in enumerate(self.list)
)

if neighbor_method == "strictly-adjacent":
return list(
self.__get_neighbors_indices_strictlyadjacent(param_config_index, param_config)
Expand Down Expand Up @@ -1319,6 +1368,8 @@ def get_neighbors_indices_no_cache(self, param_config: tuple, neighbor_method=No
self.__prepare_neighbors_index()

# if the passed param_config is fictious, we can not use the pre-calculated neighbors index
if neighbor_method == "Hamming-adjacent":
return self.__get_neighbors_indices_hammingadjacent(param_config_index, param_config)
if neighbor_method == "strictly-adjacent":
return self.__get_neighbors_indices_strictlyadjacent(param_config_index, param_config)
if neighbor_method == "adjacent":
Expand Down
2 changes: 1 addition & 1 deletion kernel_tuner/strategies/hillclimbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def base_hillclimb(base_sol: tuple, neighbor_method: str, max_fevals: int, searc
child[index] = val

# get score for this position
score = cost_func(child, check_restrictions=False)
score = cost_func(child)

# generalize this to other tuning objectives
if score < best_score:
Expand Down
19 changes: 19 additions & 0 deletions test/test_searchspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,16 @@ def test_neighbors_hamming():
assert random_neighbor != test_config


def test_neighbors_hammingadjacent():
"""Test whether the Hamming-adjacent neighbors are as expected."""
test_config = tuple([1, 4, "string_1"])
expected_neighbors = [
(1.5, 4, 'string_1'),
]

__test_neighbors(test_config, expected_neighbors, "Hamming-adjacent")


def test_neighbors_strictlyadjacent():
"""Test whether the strictly adjacent neighbors are as expected."""
test_config = tuple([1, 4, "string_1"])
Expand Down Expand Up @@ -320,11 +330,19 @@ def test_neighbors_closest_param_indices():
def test_neighbors_fictious():
"""Test whether the neighbors are as expected for a fictious parameter configuration (i.e. not existing in the search space due to restrictions)."""
test_config = tuple([1.5, 4, "string_1"])

expected_neighbors_hamming = [
(1.5, 4, 'string_2'),
(1.5, 5.5, 'string_1'),
(3, 4, 'string_1'),
]

expected_neighbors_hammingadjacent = [
(1.5, 4, 'string_2'),
(1.5, 5.5, 'string_1'),
(3, 4, 'string_1'),
]

expected_neighbors_strictlyadjacent = [
(1.5, 5.5, 'string_2'),
(1.5, 5.5, 'string_1'),
Expand All @@ -340,6 +358,7 @@ def test_neighbors_fictious():
]

__test_neighbors_direct(test_config, expected_neighbors_hamming, "Hamming")
__test_neighbors_direct(test_config, expected_neighbors_hammingadjacent, "Hamming-adjacent")
__test_neighbors_direct(test_config, expected_neighbors_strictlyadjacent, "strictly-adjacent")
__test_neighbors_direct(test_config, expected_neighbors_adjacent, "adjacent")

Expand Down