Skip to content

Add new Hamming-adjacent neighborhood method #313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
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
51 changes: 50 additions & 1 deletion kernel_tuner/searchspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from kernel_tuner.util import check_restrictions as check_instance_restrictions
from kernel_tuner.util import compile_restrictions, default_block_size_names

supported_neighbor_methods = ["strictly-adjacent", "adjacent", "Hamming"]
supported_neighbor_methods = ["strictly-adjacent", "adjacent", "Hamming", "Hamming-adjacent"]


class Searchspace:
Expand All @@ -44,6 +44,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.
"""
# set the object attributes using the arguments
Expand Down Expand Up @@ -552,6 +553,45 @@ def __get_neighbors_indices_hamming(self, param_config: tuple) -> List[int]:
matching_indices = (num_matching_params == self.num_params - 1).nonzero()[0]
return matching_indices

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)
# np.PINF has been replaced by 1e12 here, as on some systems np.PINF becomes np.NINF
upper_bound = np.min(
hamming_values_indices,
initial=1e12,
axis=0,
where=hamming_values_indices > param_config_value_indices,
)

lower_bound = np.max(
hamming_values_indices,
initial=-1e12,
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_neighbors_indices_strictlyadjacent(
self, param_config_index: int = None, param_config: tuple = None
) -> List[int]:
Expand Down Expand Up @@ -615,6 +655,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 @@ -667,6 +714,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 @@ -80,7 +80,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 @@ -245,6 +245,16 @@ def test_neighbors_hamming():
__test_neighbors(test_config, expected_neighbors, "Hamming")


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 @@ -274,11 +284,19 @@ def test_neighbors_adjacent():
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 @@ -294,6 +312,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