perf(tpe): compute KDE bandwidths once per iteration, not per call (383K -> 188K instr)
Each TPE iteration drew `candidate_samples` candidates; every candidate triggered three scott_bandwidths calls (one in sample_from_kde, two in log_kde_density) -- each an O(support) two-pass scan plus a powf(-0.2). But the good / bad supports are fixed for the whole iteration, so only two distinct bandwidth vectors exist. Deriving them once and threading them through cuts ~34 of every 36 scott_bandwidths calls. Also hoists the constant (2*pi).sqrt() out of the inner density loop. tpe_short: 382_518 -> 187_898 (-51%, 2.04x). scott_bandwidths is deterministic in its inputs, so the once-vs-many results are identical -- output bit-identical, all 606 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
-48
@@ -143,31 +143,19 @@ where
|
|||||||
// Split into good vs bad observations.
|
// Split into good vs bad observations.
|
||||||
let (good_idx, bad_idx) = split_good_bad(&targets, self.config.good_fraction);
|
let (good_idx, bad_idx) = split_good_bad(&targets, self.config.good_fraction);
|
||||||
|
|
||||||
|
// The good / bad supports are fixed for this iteration, so their
|
||||||
|
// Scott's-rule bandwidths are too — derive them once instead of
|
||||||
|
// recomputing inside every sample / density call.
|
||||||
|
let good_bw = scott_bandwidths(&decisions, &good_idx, self.config.bandwidth_factor);
|
||||||
|
let bad_bw = scott_bandwidths(&decisions, &bad_idx, self.config.bandwidth_factor);
|
||||||
|
|
||||||
// Sample candidates from the good KDE.
|
// Sample candidates from the good KDE.
|
||||||
let mut best_x: Option<Vec<f64>> = None;
|
let mut best_x: Option<Vec<f64>> = None;
|
||||||
let mut best_ratio = f64::NEG_INFINITY;
|
let mut best_ratio = f64::NEG_INFINITY;
|
||||||
for _ in 0..self.config.candidate_samples {
|
for _ in 0..self.config.candidate_samples {
|
||||||
let cand = sample_from_kde(
|
let cand = sample_from_kde(&decisions, &good_idx, &self.bounds, &good_bw, &mut rng);
|
||||||
&decisions,
|
let l = log_kde_density(&cand, &decisions, &good_idx, &self.bounds, &good_bw);
|
||||||
&good_idx,
|
let g = log_kde_density(&cand, &decisions, &bad_idx, &self.bounds, &bad_bw);
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
&mut rng,
|
|
||||||
);
|
|
||||||
let l = log_kde_density(
|
|
||||||
&cand,
|
|
||||||
&decisions,
|
|
||||||
&good_idx,
|
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
);
|
|
||||||
let g = log_kde_density(
|
|
||||||
&cand,
|
|
||||||
&decisions,
|
|
||||||
&bad_idx,
|
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
);
|
|
||||||
let ratio = l - g;
|
let ratio = l - g;
|
||||||
if ratio > best_ratio {
|
if ratio > best_ratio {
|
||||||
best_ratio = ratio;
|
best_ratio = ratio;
|
||||||
@@ -271,14 +259,13 @@ fn sample_from_kde(
|
|||||||
decisions: &[Vec<f64>],
|
decisions: &[Vec<f64>],
|
||||||
support: &[usize],
|
support: &[usize],
|
||||||
bounds: &RealBounds,
|
bounds: &RealBounds,
|
||||||
bandwidth_factor: f64,
|
bandwidths: &[f64],
|
||||||
rng: &mut Rng,
|
rng: &mut Rng,
|
||||||
) -> Vec<f64> {
|
) -> Vec<f64> {
|
||||||
if support.is_empty() {
|
if support.is_empty() {
|
||||||
return sample_uniform_in_bounds(bounds, rng);
|
return sample_uniform_in_bounds(bounds, rng);
|
||||||
}
|
}
|
||||||
let dim = bounds.bounds.len();
|
let dim = bounds.bounds.len();
|
||||||
let bandwidths = scott_bandwidths(decisions, support, bandwidth_factor);
|
|
||||||
|
|
||||||
let pick = support[rng.random_range(0..support.len())];
|
let pick = support[rng.random_range(0..support.len())];
|
||||||
let center = &decisions[pick];
|
let center = &decisions[pick];
|
||||||
@@ -292,19 +279,20 @@ fn sample_from_kde(
|
|||||||
x
|
x
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-axis log-density at `x` of the KDE built on `support`.
|
/// Per-axis log-density at `x` of the KDE built on `support`, given the
|
||||||
|
/// precomputed per-axis `bandwidths`.
|
||||||
fn log_kde_density(
|
fn log_kde_density(
|
||||||
x: &[f64],
|
x: &[f64],
|
||||||
decisions: &[Vec<f64>],
|
decisions: &[Vec<f64>],
|
||||||
support: &[usize],
|
support: &[usize],
|
||||||
bounds: &RealBounds,
|
bounds: &RealBounds,
|
||||||
bandwidth_factor: f64,
|
bandwidths: &[f64],
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
if support.is_empty() {
|
if support.is_empty() {
|
||||||
return f64::NEG_INFINITY;
|
return f64::NEG_INFINITY;
|
||||||
}
|
}
|
||||||
let dim = bounds.bounds.len();
|
let dim = bounds.bounds.len();
|
||||||
let bandwidths = scott_bandwidths(decisions, support, bandwidth_factor);
|
let sqrt_2pi = (2.0 * std::f64::consts::PI).sqrt();
|
||||||
|
|
||||||
// Sum of per-axis log-densities, with the kernel a product of 1-D
|
// Sum of per-axis log-densities, with the kernel a product of 1-D
|
||||||
// Gaussians. Using log-sum-exp for numerical stability would be more
|
// Gaussians. Using log-sum-exp for numerical stability would be more
|
||||||
@@ -314,10 +302,11 @@ fn log_kde_density(
|
|||||||
let mut total = 0.0;
|
let mut total = 0.0;
|
||||||
for j in 0..dim {
|
for j in 0..dim {
|
||||||
let h = bandwidths[j].max(1e-12);
|
let h = bandwidths[j].max(1e-12);
|
||||||
|
let norm = h * sqrt_2pi;
|
||||||
let mut s = 0.0;
|
let mut s = 0.0;
|
||||||
for &i in support {
|
for &i in support {
|
||||||
let z = (x[j] - decisions[i][j]) / h;
|
let z = (x[j] - decisions[i][j]) / h;
|
||||||
s += (-0.5 * z * z).exp() / (h * (2.0 * std::f64::consts::PI).sqrt());
|
s += (-0.5 * z * z).exp() / norm;
|
||||||
}
|
}
|
||||||
let mean_density = s / support.len() as f64;
|
let mean_density = s / support.len() as f64;
|
||||||
total += mean_density.max(1e-300).ln();
|
total += mean_density.max(1e-300).ln();
|
||||||
@@ -417,30 +406,17 @@ impl Tpe {
|
|||||||
for _ in 0..self.config.iterations {
|
for _ in 0..self.config.iterations {
|
||||||
let (good_idx, bad_idx) = split_good_bad(&targets, self.config.good_fraction);
|
let (good_idx, bad_idx) = split_good_bad(&targets, self.config.good_fraction);
|
||||||
|
|
||||||
|
// Bandwidths depend only on the (fixed-for-this-iteration)
|
||||||
|
// supports — compute once, not once per sample / density call.
|
||||||
|
let good_bw = scott_bandwidths(&decisions, &good_idx, self.config.bandwidth_factor);
|
||||||
|
let bad_bw = scott_bandwidths(&decisions, &bad_idx, self.config.bandwidth_factor);
|
||||||
|
|
||||||
let mut best_x: Option<Vec<f64>> = None;
|
let mut best_x: Option<Vec<f64>> = None;
|
||||||
let mut best_ratio = f64::NEG_INFINITY;
|
let mut best_ratio = f64::NEG_INFINITY;
|
||||||
for _ in 0..self.config.candidate_samples {
|
for _ in 0..self.config.candidate_samples {
|
||||||
let cand = sample_from_kde(
|
let cand = sample_from_kde(&decisions, &good_idx, &self.bounds, &good_bw, &mut rng);
|
||||||
&decisions,
|
let l = log_kde_density(&cand, &decisions, &good_idx, &self.bounds, &good_bw);
|
||||||
&good_idx,
|
let g = log_kde_density(&cand, &decisions, &bad_idx, &self.bounds, &bad_bw);
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
&mut rng,
|
|
||||||
);
|
|
||||||
let l = log_kde_density(
|
|
||||||
&cand,
|
|
||||||
&decisions,
|
|
||||||
&good_idx,
|
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
);
|
|
||||||
let g = log_kde_density(
|
|
||||||
&cand,
|
|
||||||
&decisions,
|
|
||||||
&bad_idx,
|
|
||||||
&self.bounds,
|
|
||||||
self.config.bandwidth_factor,
|
|
||||||
);
|
|
||||||
let ratio = l - g;
|
let ratio = l - g;
|
||||||
if ratio > best_ratio {
|
if ratio > best_ratio {
|
||||||
best_ratio = ratio;
|
best_ratio = ratio;
|
||||||
|
|||||||
Reference in New Issue
Block a user