-
Notifications
You must be signed in to change notification settings - Fork 64
Implemented multi-fidelity bayesian search for the auto-tuner #1019
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
base: main
Are you sure you want to change the base?
Changes from all commits
e6e9f4f
2b93ed0
68f33aa
f1b34a1
c7d496f
5fe0486
39e96e4
5d0e010
34c92d4
e13b1cc
7f32dc6
0f42fe0
748a466
9f6546e
1fddddc
99c12f1
6a364e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import numpy as np | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use torch to avoid adding a numpy dependency. |
||
| from scipy.stats import norm | ||
|
|
||
| if TYPE_CHECKING: | ||
| from numpy.typing import NDArray | ||
|
|
||
|
|
||
| def expected_improvement( | ||
| mu: NDArray[np.float64], | ||
| sigma: NDArray[np.float64], | ||
| best_so_far: float, | ||
| xi: float = 0.01, | ||
| ) -> NDArray[np.float64]: | ||
| """ | ||
| Expected Improvement acquisition function. | ||
| Balances exploration (high uncertainty) and exploitation (low predicted value). | ||
| Args: | ||
| mu: GP mean predictions (N,). | ||
| sigma: GP uncertainty (standard deviation) (N,). | ||
| best_so_far: Current best (minimum) performance observed. | ||
| xi: Exploration parameter (higher = more exploration). | ||
| Returns: | ||
| Expected improvement scores (higher = more valuable to evaluate). | ||
| """ | ||
| # Avoid division by zero | ||
| sigma = np.maximum(sigma, 1e-9) | ||
|
|
||
| # We're minimizing, so improvement is best_so_far - mu | ||
| improvement = best_so_far - mu - xi | ||
| Z = improvement / sigma | ||
|
|
||
| # Expected improvement formula | ||
| ei = improvement * norm.cdf(Z) + sigma * norm.pdf(Z) | ||
|
|
||
| # If sigma is very small, just use the improvement | ||
| return np.where(sigma > 1e-9, ei, np.maximum(improvement, 0.0)) | ||
|
|
||
|
|
||
| def upper_confidence_bound( | ||
| mu: NDArray[np.float64], | ||
| sigma: NDArray[np.float64], | ||
| beta: float = 2.0, | ||
| ) -> NDArray[np.float64]: | ||
| """ | ||
| Upper Confidence Bound acquisition function. | ||
| For minimization, we use Lower Confidence Bound (LCB). | ||
| Args: | ||
| mu: GP mean predictions (N,). | ||
| sigma: GP uncertainty (standard deviation) (N,). | ||
| beta: Exploration parameter (higher = more exploration). | ||
| Returns: | ||
| UCB scores (lower = more valuable for minimization). | ||
| """ | ||
| # For minimization, we want lower confidence bound | ||
| return mu - beta * sigma | ||
|
|
||
|
|
||
| def probability_of_improvement( | ||
| mu: NDArray[np.float64], | ||
| sigma: NDArray[np.float64], | ||
| best_so_far: float, | ||
| xi: float = 0.01, | ||
| ) -> NDArray[np.float64]: | ||
| """ | ||
| Probability of Improvement acquisition function. | ||
| Args: | ||
| mu: GP mean predictions (N,). | ||
| sigma: GP uncertainty (standard deviation) (N,). | ||
| best_so_far: Current best (minimum) performance observed. | ||
| xi: Exploration parameter. | ||
| Returns: | ||
| Probability of improvement scores. | ||
| """ | ||
| sigma = np.maximum(sigma, 1e-9) | ||
| improvement = best_so_far - mu - xi | ||
| Z = improvement / sigma | ||
| return norm.cdf(Z) | ||
|
|
||
|
|
||
| def cost_aware_ei( | ||
| mu: NDArray[np.float64], | ||
| sigma: NDArray[np.float64], | ||
| best_so_far: float, | ||
| cost: float = 1.0, | ||
| xi: float = 0.01, | ||
| ) -> NDArray[np.float64]: | ||
| """ | ||
| Cost-aware Expected Improvement. | ||
| Normalizes EI by evaluation cost, useful for multi-fidelity optimization. | ||
| Args: | ||
| mu: GP mean predictions (N,). | ||
| sigma: GP uncertainty (standard deviation) (N,). | ||
| best_so_far: Current best (minimum) performance observed. | ||
| cost: Cost of evaluation at this fidelity. | ||
| xi: Exploration parameter. | ||
| Returns: | ||
| Cost-normalized expected improvement scores. | ||
| """ | ||
| ei = expected_improvement(mu, sigma, best_so_far, xi) | ||
| return ei / np.sqrt(cost) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -295,14 +295,17 @@ def benchmark(self, config: Config) -> tuple[Callable[..., object], float]: | |
| return fn, self.benchmark_function(config, fn) | ||
| return fn, inf | ||
|
|
||
| def benchmark_function(self, config: Config, fn: CompiledConfig) -> float: | ||
| def benchmark_function( | ||
| self, config: Config, fn: CompiledConfig, *, fidelity: int = 50 | ||
| ) -> float: | ||
| """ | ||
| Benchmark a compiled function. This function is called by the autotuner to measure the | ||
| performance of a specific configuration. | ||
|
|
||
| Args: | ||
| config: The configuration to benchmark. | ||
| fn: A precompiled version of config. | ||
| fidelity: Number of repetitions for benchmarking (default: 50). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's rename to the repeat or samples. |
||
|
|
||
| Returns: | ||
| The performance of the configuration in ms. | ||
|
|
@@ -329,7 +332,7 @@ def benchmark_function(self, config: Config, fn: CompiledConfig) -> float: | |
| functools.partial(fn, *self.args), | ||
| return_mode="median", | ||
| warmup=1, # we are already warmed up above | ||
| rep=50, | ||
| rep=fidelity, | ||
| ) | ||
| t2 = time.perf_counter() | ||
| assert isinstance(res, float) | ||
|
|
@@ -591,18 +594,25 @@ class PopulationMember: | |
| perfs (list[float]): The performance of the configuration, accumulated over multiple benchmarks. | ||
| flat_values (FlatConfig): The flat representation of the configuration values. | ||
| config (Config): The full configuration object. | ||
| fidelities (list[int]): The fidelity levels used for each benchmark. | ||
| """ | ||
|
|
||
| fn: Callable[..., object] | ||
| perfs: list[float] | ||
| flat_values: FlatConfig | ||
| config: Config | ||
| status: Literal["ok", "error", "timeout", "unknown"] = "unknown" | ||
| fidelities: list[int] = dataclasses.field(default_factory=list) | ||
|
|
||
| @property | ||
| def perf(self) -> float: | ||
| return self.perfs[-1] | ||
|
|
||
| @property | ||
| def fidelity(self) -> int: | ||
| """Get the fidelity of the latest benchmark.""" | ||
| return self.fidelities[-1] if self.fidelities else 50 | ||
|
|
||
|
|
||
| def performance(member: PopulationMember) -> float: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,135 @@ | ||||||||
| from __future__ import annotations | ||||||||
|
|
||||||||
| import math | ||||||||
| from typing import TYPE_CHECKING | ||||||||
|
|
||||||||
| import numpy as np | ||||||||
|
|
||||||||
| from .config_fragment import Category | ||||||||
|
|
||||||||
| if TYPE_CHECKING: | ||||||||
| from .config_generation import ConfigGeneration | ||||||||
| from .config_generation import FlatConfig | ||||||||
|
|
||||||||
|
|
||||||||
| class ConfigEncoder: | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this share more code with ConfigGeneration? |
||||||||
| """ | ||||||||
| Encodes Helion configurations into numerical vectors for Gaussian Process models. | ||||||||
| Handles various config types: | ||||||||
| - Power-of-2 values: log2 encoding | ||||||||
| - Integers: direct encoding with normalization | ||||||||
| - Booleans: 0/1 encoding | ||||||||
| - Enums: one-hot encoding | ||||||||
| - Permutations: inversion count encoding | ||||||||
| """ | ||||||||
|
|
||||||||
| def __init__(self, config_gen: ConfigGeneration) -> None: | ||||||||
| """ | ||||||||
| Initialize the encoder with a configuration generator. | ||||||||
| Args: | ||||||||
| config_gen: The configuration generator containing the flat spec. | ||||||||
| """ | ||||||||
| self.config_gen = config_gen | ||||||||
| self.flat_spec = config_gen.flat_spec | ||||||||
| self._compute_encoding_metadata() | ||||||||
|
|
||||||||
| def _compute_encoding_metadata(self) -> None: | ||||||||
| """Precompute metadata for encoding to determine output dimensionality.""" | ||||||||
| self.encoded_dim = 0 | ||||||||
| self.encoding_map: list[tuple[int, int, str]] = [] # (start_idx, end_idx, type) | ||||||||
|
|
||||||||
| for spec in self.flat_spec: | ||||||||
| category = spec.category() | ||||||||
| start_idx = self.encoded_dim | ||||||||
|
|
||||||||
| if category in { | ||||||||
| Category.BLOCK_SIZE, | ||||||||
| Category.NUM_WARPS, | ||||||||
| }: | ||||||||
|
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better if you used the ConfigFragement directly (add a method for this type of encoding) rather than switching based on category. |
||||||||
| # Single numerical value | ||||||||
| self.encoded_dim += 1 | ||||||||
| self.encoding_map.append((start_idx, self.encoded_dim, "numerical")) | ||||||||
| elif hasattr(spec, "choices"): | ||||||||
| # Enum - one-hot encoding | ||||||||
| num_choices = len(spec.choices) # type: ignore[no-untyped-call] | ||||||||
| self.encoded_dim += num_choices | ||||||||
| self.encoding_map.append((start_idx, self.encoded_dim, "enum")) | ||||||||
| else: | ||||||||
| # Boolean or other single value | ||||||||
| self.encoded_dim += 1 | ||||||||
| self.encoding_map.append((start_idx, self.encoded_dim, "numerical")) | ||||||||
|
|
||||||||
| def encode(self, flat_config: FlatConfig) -> np.ndarray: | ||||||||
| """ | ||||||||
| Convert a flat configuration to a numerical vector. | ||||||||
| Args: | ||||||||
| flat_config: The flat configuration values. | ||||||||
| Returns: | ||||||||
| A numpy array suitable for GP training. | ||||||||
| """ | ||||||||
| encoded = np.zeros(self.encoded_dim, dtype=np.float64) | ||||||||
|
|
||||||||
| for flat_idx, spec in enumerate(self.flat_spec): | ||||||||
| value = flat_config[flat_idx] | ||||||||
| category = spec.category() | ||||||||
| enc_start, enc_end, enc_type = self.encoding_map[flat_idx] | ||||||||
|
|
||||||||
| if enc_type == "numerical": | ||||||||
| if category in {Category.BLOCK_SIZE, Category.NUM_WARPS}: | ||||||||
| # Power-of-2: use log2 encoding | ||||||||
| if isinstance(value, (int, float)) and value > 0: | ||||||||
| encoded[enc_start] = math.log2(float(value)) | ||||||||
| else: | ||||||||
| encoded[enc_start] = 0.0 | ||||||||
| else: | ||||||||
| # Other numerical: direct encoding | ||||||||
| encoded[enc_start] = ( | ||||||||
| float(value) if isinstance(value, (int, float)) else 0.0 | ||||||||
| ) | ||||||||
| elif enc_type == "enum": | ||||||||
| # One-hot encoding | ||||||||
| if hasattr(spec, "choices"): | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When is this false? |
||||||||
| choices = spec.choices # type: ignore[attr-defined] | ||||||||
| try: | ||||||||
| choice_idx = choices.index(value) | ||||||||
| encoded[enc_start + choice_idx] = 1.0 | ||||||||
| except (ValueError, IndexError): | ||||||||
| # Default to first choice if value not found | ||||||||
| encoded[enc_start] = 1.0 | ||||||||
|
|
||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| return encoded | ||||||||
|
|
||||||||
| def get_bounds(self) -> list[tuple[float, float]]: | ||||||||
| """ | ||||||||
| Get bounds for each encoded dimension. | ||||||||
| Returns: | ||||||||
| List of (min, max) tuples for each dimension. | ||||||||
| """ | ||||||||
| bounds: list[tuple[float, float]] = [] | ||||||||
|
|
||||||||
| for flat_idx, spec in enumerate(self.flat_spec): | ||||||||
| category = spec.category() | ||||||||
| enc_start, enc_end, enc_type = self.encoding_map[flat_idx] | ||||||||
|
|
||||||||
| if enc_type == "numerical": | ||||||||
| if category in {Category.BLOCK_SIZE, Category.NUM_WARPS}: | ||||||||
| # Power-of-2: log2 bounds | ||||||||
| min_val = math.log2(float(spec.low)) # type: ignore[attr-defined] | ||||||||
| max_val = math.log2(float(spec.high)) # type: ignore[attr-defined] | ||||||||
| bounds.append((min_val, max_val)) | ||||||||
| else: | ||||||||
| # Other numerical bounds | ||||||||
| bounds.append( | ||||||||
| (float(spec.low), float(spec.high)) # type: ignore[attr-defined] | ||||||||
| ) | ||||||||
| elif enc_type == "enum": | ||||||||
| # One-hot: each dimension is 0 or 1 | ||||||||
| num_choices = enc_end - enc_start | ||||||||
| bounds.extend([(0.0, 1.0)] * num_choices) | ||||||||
|
Comment on lines
+106
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ConfigSpecFragment already has methods for this. |
||||||||
|
|
||||||||
| return bounds | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks algorithm specific, let's move all the related files to a subfolder.