Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9c3e56a
Adding UCBPatternSearch autotuner and fragment_encoder
Nov 11, 2025
8035c8e
add tests and import
Nov 11, 2025
e24b529
moved config encoding to config_fragment
Nov 13, 2025
e5be414
Merge remote-tracking branch 'upstream/main' into ucb_pattern_search
Nov 14, 2025
140b3b2
adapt ucb_pattern_search to new encoder
Nov 14, 2025
ca99252
merged new config fragment
Nov 14, 2025
899d30d
imports
Nov 14, 2025
c6fc4cc
Merge branch 'main' into ucb_pattern_search
ethche Nov 14, 2025
13ea3b4
remove encode_scalar
Nov 14, 2025
9cf7dbe
Merge branch 'ucb_pattern_search' of https://github.com/ethche/helion…
Nov 14, 2025
3b739f5
fix imports
Nov 15, 2025
62aaf77
early stopping helper for pattern search
Nov 15, 2025
97963f5
fix tests
Nov 16, 2025
a0ef224
fix dim
Nov 16, 2025
018a626
ucb fix lints and better hyperparams
Nov 16, 2025
2a65701
revert linter changes
Nov 16, 2025
79c0fa3
name change
Nov 16, 2025
c2a578f
revert unrelated changes in config_generation
Nov 16, 2025
fc2929d
revert unrelated changes in config_generation
Nov 16, 2025
44c925d
save gp state
Nov 16, 2025
70a46fe
better ucb docstring
Nov 16, 2025
f837953
combined dependencies
Nov 16, 2025
2c2eec9
fix pyproject
Nov 16, 2025
74b0754
reverting unrelated changes to comments
Nov 16, 2025
d9cce1e
no need for encode for integer fragment, inherit from base integer
Nov 16, 2025
3437347
Merge branch 'main' into ucb_pattern_search
ethche Nov 16, 2025
4a791a9
optimize batch UCB function, simplify batch selection
Nov 17, 2025
913b330
Merge branch 'ucb_pattern_search' of https://github.com/ethche/helion…
Nov 17, 2025
047509d
batch optimization by default
Nov 17, 2025
eb430ce
LFBO instead of ucb_pattern_search
Nov 18, 2025
33803df
LFBO tests
Nov 18, 2025
b6c24cc
LFBO better docstring
Nov 18, 2025
e3106a8
LFBO remove patience feature
Nov 18, 2025
1c810f0
LFBO imports
Nov 18, 2025
c25362d
Fix comments
Nov 18, 2025
b5a65be
Fix test names
Nov 18, 2025
e30bdad
Merge branch 'main' into ucb_pattern_search
ethche Nov 18, 2025
062be0c
remove comma
Nov 18, 2025
fcb070d
Fix comments
Nov 18, 2025
1572172
Fix comments
Nov 18, 2025
b6b191e
better lfbo hyperparams
Nov 19, 2025
4301669
rename to surrogate
Nov 20, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
- name: Install Helion
run: |
source .venv/bin/activate
SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0" uv pip install -e .'[dev,de-surrogate]'
SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0" uv pip install -e .'[dev,surrogate]'
python -c "import helion; print(helion.__name__)"

- name: Install Benchmark Requirements
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ jobs:
run: |
source .venv/bin/activate
uv pip install setuptools ninja
SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0" uv pip install -e .'[dev,de-surrogate]'
SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0" uv pip install -e .'[dev,surrogate]'
python -c "import helion; print(helion.__name__)"

- name: Run Tests
Expand Down
2 changes: 2 additions & 0 deletions helion/autotuner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
from .local_cache import StrictLocalAutotuneCache as StrictLocalAutotuneCache
from .pattern_search import PatternSearch as PatternSearch
from .random_search import RandomSearch as RandomSearch
from .surrogate_pattern_search import LFBOPatternSearch

search_algorithms = {
"DESurrogateHybrid": DESurrogateHybrid,
"LFBOPatternSearch": LFBOPatternSearch,
"DifferentialEvolutionSearch": DifferentialEvolutionSearch,
"FiniteSearch": FiniteSearch,
"PatternSearch": PatternSearch,
Expand Down
85 changes: 63 additions & 22 deletions helion/autotuner/config_fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@ def differential_mutation(self, a: object, b: object, c: object) -> object:
def is_block_size(self) -> bool:
return False

def get_minimum(self) -> int:
def is_categorical(self) -> bool:
return True

def dim(self) -> int:
"""
Return the minimum allowed value for this fragment.
Returns the dimension of the output of encode
"""
raise NotImplementedError

def encode_scalar(self, value: object) -> float:
def encode(self, value: object) -> list[float]:
"""
Encode a configuration value into a float for ML models.
Encode a configuration value into a list of floats for ML models.

This is used by surrogate-assisted algorithms to convert configurations
into numerical vectors for prediction models.
Expand All @@ -68,14 +71,15 @@ def encode_scalar(self, value: object) -> float:
value: The configuration value to encode.

Returns:
A float representing the encoded value.
A list of floats representing the encoded value.
"""
# Default: convert to float if possible
if not isinstance(value, (int, float, bool)):
raise TypeError(
f"Cannot encode {type(value).__name__} value {value!r} for ML"
)
return float(value)
raise NotImplementedError

def get_minimum(self) -> int:
"""
Return the minimum allowed value for this fragment.
"""
raise NotImplementedError


@dataclasses.dataclass
Expand Down Expand Up @@ -106,6 +110,17 @@ def pattern_neighbors(self, current: object) -> list[object]:
neighbors.append(swapped)
return neighbors

def dim(self) -> int:
return self.length

def encode(self, value: object) -> list[float]:
assert isinstance(value, list)
encoded = []
for val in value:
assert isinstance(val, int)
encoded.append(float(val))
return value


@dataclasses.dataclass
class BaseIntegerFragment(ConfigSpecFragment):
Expand All @@ -126,9 +141,15 @@ def default(self) -> int:
def clamp(self, val: int) -> int:
return max(min(val, self.high), self.low)

def is_categorical(self) -> bool:
return False

def get_minimum(self) -> int:
return self.low

def dim(self) -> int:
return 1

def pattern_neighbors(self, current: object) -> list[object]:
if type(current) is not int: # bool is not allowed
raise TypeError(f"Expected int, got {type(current).__name__}")
Expand All @@ -141,13 +162,9 @@ def pattern_neighbors(self, current: object) -> list[object]:
neighbors.append(upper)
return neighbors

def encode_scalar(self, value: object) -> float:
"""Encode integer values directly as floats."""
if not isinstance(value, (int, float)):
raise TypeError(
f"Expected int/float for BaseIntegerFragment, got {type(value).__name__}: {value!r}"
)
return float(value)
def encode(self, value: object) -> list[float]:
assert isinstance(value, int)
return [float(value)]


class PowerOfTwoFragment(BaseIntegerFragment):
Expand Down Expand Up @@ -180,7 +197,7 @@ def differential_mutation(self, a: object, b: object, c: object) -> int:
return self.clamp(ai * 2)
return ai

def encode_scalar(self, value: object) -> float:
def encode(self, value: object) -> list[float]:
"""Encode power-of-2 values using log2 transformation."""
import math

Expand All @@ -192,7 +209,7 @@ def encode_scalar(self, value: object) -> float:
raise ValueError(
f"Expected positive value for PowerOfTwoFragment, got {value}"
)
return math.log2(float(value))
return [math.log2(float(value))]


class IntegerFragment(BaseIntegerFragment):
Expand Down Expand Up @@ -235,7 +252,10 @@ def differential_mutation(self, a: object, b: object, c: object) -> object:
choices.remove(a)
return random.choice(choices)

def encode_scalar(self, value: object) -> float:
def dim(self) -> int:
return len(self.choices)

def encode(self, value: object) -> list[float]:
"""Encode enum values as their index."""
try:
choice_idx = self.choices.index(value)
Expand All @@ -244,7 +264,7 @@ def encode_scalar(self, value: object) -> float:
f"Invalid enum value {value!r} for EnumFragment. "
f"Valid choices: {self.choices}"
) from None
return float(choice_idx)
return [1.0 if i == choice_idx else 0.0 for i in range(len(self.choices))]


class BooleanFragment(ConfigSpecFragment):
Expand All @@ -265,6 +285,14 @@ def differential_mutation(self, a: object, b: object, c: object) -> bool:
return a
return not a

def dim(self) -> int:
return 1

def encode(self, value: object) -> list[float]:
"""Encode enum values as their index."""
assert isinstance(value, bool)
return [1.0] if value else [0.0]


class BlockSizeFragment(PowerOfTwoFragment):
def category(self) -> Category:
Expand Down Expand Up @@ -296,6 +324,9 @@ def random(self) -> list[object]:
"""Return a list of random values."""
return [self.inner.random() for _ in range(self.length)]

def is_categorical(self) -> bool:
return self.inner.is_categorical()

def pattern_neighbors(self, current: object) -> list[object]:
"""Return neighbors by changing one element at a time."""
if not isinstance(current, list) or len(current) != self.length:
Expand All @@ -320,3 +351,13 @@ def differential_mutation(self, a: object, b: object, c: object) -> list[object]
self.inner.differential_mutation(a[i], b[i], c[i])
for i in range(self.length)
]

def dim(self) -> int:
return self.length * self.inner.dim()

def encode(self, value: object) -> list[float]:
assert isinstance(value, list)
encoded = []
for v in value:
encoded.extend(self.inner.encode(v))
return encoded
2 changes: 1 addition & 1 deletion helion/autotuner/config_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,6 @@ def encode_config(self, flat_config: FlatConfig) -> list[float]:

for flat_idx, spec in enumerate(self.flat_spec):
value = flat_config[flat_idx]
encoded.append(spec.encode_scalar(value))
encoded.extend(spec.encode(value))

return encoded
2 changes: 1 addition & 1 deletion helion/autotuner/de_surrogate_hybrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(
if not HAS_ML_DEPS:
raise ImportError(
"DESurrogateHybrid requires numpy and scikit-learn. "
"Install them with: pip install helion[de-surrogate]"
"Install them with: pip install helion[surrogate]"
)

# Initialize parent with early stopping parameters
Expand Down
35 changes: 25 additions & 10 deletions helion/autotuner/pattern_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,35 @@ def _pattern_search_from(
if len(candidates) <= 1:
return # no new candidates, stop searching
yield candidates # yield new population to benchmark in parallel
# update search copy and check early stopping criteria
best = min(candidates, key=performance)
if best is current:
return # no improvement, stop searching
# Stop if the relative improvement is smaller than a user-specified delta
if (
self.min_improvement_delta > 0.0
and math.isfinite(best.perf)
and math.isfinite(current.perf)
and current.perf != 0.0
and abs(best.perf / current.perf - 1.0) < self.min_improvement_delta
):
if self._check_early_stopping(best, current):
return
current = best

def _check_early_stopping(
self, best: PopulationMember, current: PopulationMember
) -> bool:
"""
Check if early stopping criteria are met for the search copy

Early stops if either the best config has not changed or if
the relative improvement is smaller than a user-specified delta

Returns:
True the search copy is terminated, False otherwise.
"""
if best is current:
return True # no improvement, stop searching
# Stop if the relative improvement is smaller than a user-specified delta
return bool(
self.min_improvement_delta > 0.0
and math.isfinite(best.perf)
and math.isfinite(current.perf)
and current.perf != 0.0
and abs(best.perf / current.perf - 1.0) < self.min_improvement_delta
)

def _generate_neighbors(self, base: FlatConfig) -> list[FlatConfig]:
"""
Generate neighboring configurations by changing one or two parameters at a time.
Expand Down
Loading