⚡️ Speed up function _build_parzen_estimator by 19%
#168
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
📄 19% (0.19x) speedup for
_build_parzen_estimatorinoptuna/importance/_ped_anova/scott_parzen_estimator.py⏱️ Runtime :
1.24 milliseconds→1.04 milliseconds(best of34runs)📝 Explanation and details
The optimized code achieves an 18% speedup by replacing inefficient counting operations with NumPy's optimized
bincountfunction and eliminating unnecessary intermediate operations.Key Optimizations:
Replaced
np.uniquewithnp.bincount: In both counting functions, the original code usednp.unique(array, return_counts=True)followed by manual index assignment. The optimized version usesnp.bincountdirectly, which is specifically designed for counting integer indices and is significantly faster - reducing execution time from ~296μs to ~34μs in_count_numerical_param_in_grid.Pre-allocated NumPy array for categorical indices: Instead of using a Python list comprehension
[int(dist.to_internal_repr(t.params[param_name])) for t in trials], the optimized version pre-allocates a NumPy array withnp.empty(len(trials), dtype=np.intp)and fills it in a loop. This reduces Python object overhead and improves memory locality, cutting execution time from ~60μs to ~16μs in_count_categorical_param_in_grid.Eliminated redundant operations: The original code created zero arrays and then used fancy indexing to populate counts. The optimized version leverages
bincount'sminlengthparameter to ensure proper array sizing in a single operation.Why these optimizations work:
np.bincountis highly optimized C code that directly counts integer indices without needing to sort or identify unique values firstminlengthparameter eliminates the need for separate zero array creation and fancy indexingPerformance characteristics:
Based on the test results, the optimizations provide consistent speedups across different input sizes and distribution types, with particularly strong performance improvements for numerical parameter counting scenarios that are likely common in hyperparameter optimization workloads.
✅ Correctness verification report:
⚙️ Existing Unit Tests and Runtime
importance_tests/pedanova_tests/test_scott_parzen_estimator.py::test_assert_in_build_parzen_estimatorimportance_tests/pedanova_tests/test_scott_parzen_estimator.py::test_build_parzen_estimator🌀 Generated Regression Tests and Runtime
from types import SimpleNamespace
import numpy as np
imports
import pytest
from optuna.importance._ped_anova.scott_parzen_estimator import
_build_parzen_estimator
Minimal stubs for Optuna classes to allow testing
class BaseDistribution:
pass
class FloatDistribution(BaseDistribution):
def init(self, low, high, log=False, step=None):
self.low = low
self.high = high
self.log = log
self.step = step
class IntDistribution(BaseDistribution):
def init(self, low, high, log=False, step=None):
self.low = low
self.high = high
self.log = log
self.step = step
class CategoricalDistribution(BaseDistribution):
def init(self, choices):
if not choices:
raise ValueError("The
choicesmust contain one or more elements.")self.choices = tuple(choices)
def to_internal_repr(self, val):
return self.choices.index(val)
class FrozenTrial:
def init(self, params):
self.params = params
The estimator class returned by _build_parzen_estimator
class _ScottParzenEstimator:
def init(self, param_name, dist, weights, prior_weight):
self.param_name = param_name
self.dist = dist
self.weights = weights
self.prior_weight = prior_weight
from optuna.importance._ped_anova.scott_parzen_estimator import
_build_parzen_estimator
------------------ UNIT TESTS ------------------
1. BASIC TEST CASES
def test_invalid_distribution_type():
# Edge: Passing an unknown distribution type should assert
class DummyDist(BaseDistribution): pass
dist = DummyDist()
trials = [FrozenTrial({'x': 1})]
with pytest.raises(AssertionError):
_build_parzen_estimator('x', dist, trials, n_steps=2, prior_weight=0.0) # 11.6μs -> 11.2μs (3.32% faster)
def test_int_distribution_with_step_and_log():
# Edge: IntDistribution with both step and log True should assert
dist = IntDistribution(1, 10, log=True, step=1)
trials = [FrozenTrial({'x': 1})]
with pytest.raises(AssertionError):
_build_parzen_estimator('x', dist, trials, n_steps=5, prior_weight=0.0) # 3.05μs -> 3.24μs (5.80% slower)
#------------------------------------------------
import math
imports
import pytest
from optuna.importance._ped_anova.scott_parzen_estimator import
_build_parzen_estimator
Mocks and minimal implementations needed for testing
class BaseDistribution:
pass
class IntDistribution(BaseDistribution):
def init(self, low, high, log=False, step=None):
self.low = low
self.high = high
self.log = log
self.step = step
class FloatDistribution(BaseDistribution):
def init(self, low, high, log=False, step=None):
self.low = low
self.high = high
self.log = log
self.step = step
class CategoricalDistribution(BaseDistribution):
def init(self, choices):
if not choices:
raise ValueError("The
choicesmust contain one or more elements.")self.choices = tuple(choices)
def to_internal_repr(self, param_value_in_external_repr):
return self.choices.index(param_value_in_external_repr)
class FrozenTrial:
def init(self, params):
self.params = params
class _ScottParzenEstimator:
def init(self, param_name, dist, weights, prior_weight):
self.param_name = param_name
self.dist = dist
self.weights = weights
self.prior_weight = prior_weight
from optuna.importance._ped_anova.scott_parzen_estimator import
_build_parzen_estimator
Unit tests
1. Basic Test Cases
def test_invalid_distribution_type():
# Edge: Unknown distribution type
class DummyDist(BaseDistribution): pass
dist = DummyDist()
with pytest.raises(AssertionError):
_build_parzen_estimator('x', dist, [], 2, 1.0) # 10.8μs -> 11.1μs (2.64% slower)
#------------------------------------------------
from optuna.distributions import IntUniformDistribution
from optuna.importance._ped_anova.scott_parzen_estimator import _build_parzen_estimator
import pytest
def test__build_parzen_estimator():
with pytest.raises(IndexError, match='index\ \-1\ is\ out\ of\ bounds\ for\ axis\ 0\ with\ size\ 0'):
_build_parzen_estimator('', IntUniformDistribution(0, 3, step=1), [], 2, 0.0)
🔎 Concolic Coverage Tests and Runtime
codeflash_concolic_gbwq510t/tmp91__y82g/test_concolic_coverage.py::test__build_parzen_estimatorTo edit these changes
git checkout codeflash/optimize-_build_parzen_estimator-mhtrcx2tand push.