feat(configs): add HDBSCAN benchmark configs - #225
Alexandr-Solovev wants to merge 3 commits into
Conversation
Add sklearnex-vs-sklearn HDBSCAN CPU benchmarks: * configs/common/hdbscan.json - reusable parameter sets (methods, metrics, density/cluster-selection/store_centers sweeps, dtypes, data formats), modelled on the DBSCAN common config * configs/regular/hdbscan.json - tree- and brute-friendly datasets * configs/experiments/hdbscan_parameters.json - parameter space sweeps * configs/experiments/hdbscan_scaling.json - thread/NUMA core scaling plus n_samples and n_features sweeps Two harness fixes are needed to make these runnable: * enrich_result: normalize a `*.preview.*` library to its base library so preview-namespace estimators (sklearnex.preview.cluster.HDBSCAN) are comparable with the stock implementation in the report * box_filter: fall back to the whole series when the 20-80% box is empty, which happens for 2 measurements - a `time_limit` early stop can leave exactly 2 and the aggregated time became NaN
|
@Alexandr-Solovev Please shorten the amount of benchmarks for the regular config. You can put them all into a separate one not under |
Review feedback: the regular scope was too heavy and the sweep parameter sets only ever served the experiment configs. * configs/regular/hdbscan.json: 36 -> 8 cases. One tree method (kd_tree) on two datasets and brute force on two, which is enough to track performance changes. * configs/weekly/hdbscan.json: new, 30 cases. Takes over the high-load part of what regular used to run: road_network/covtype/cifar/sensit and the n_features blob sweeps over both tree methods. * configs/common/hdbscan.json: keeps only the sets shared across scopes (implementations, common/sklearn parameters, method selection). The metric, cluster-selection, density, store_centers, dtype and data format sweeps moved into configs/experiments/hdbscan_parameters.json, their only consumer. * configs/experiments/README.md: describe the two HDBSCAN experiments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@david-cortes-intel Done in f2a8edd — thanks, that was the right call. Config scope after the change
Short report from these configs Ran the whole set on a 2×56-core Xeon (224 threads), Fit-time speedup (sklearn / sklearnex):
No regressions — the slowest paired case is still 2.9x. 34 of 45 are ≥10x. By dataset: road_network 187.7x, make_blobs 76.9x, skin_segmentation 72.0x, covtype 57.3x, then the high-dimensional brute sets — sensit 3.3x, mnist 3.2x, cifar 2.9x. Representative cases:
Quality. Cluster count, Davies-Bouldin, homogeneity and completeness match sklearn to 3-4 digits on 43 of the 45 pairs. The two that differ are both tie-degenerate inputs rather than accuracy problems:
Both are worth keeping in the configs precisely because they exercise tie handling. One thing the configs surfaced is that brute is the weak method — it was the only group under 10x. Profiling the 25k×1024 and cifar cases pointed at the dense backend's MST stage, which is now being addressed upstream in oneDAL (Boruvka over a materialized N² mutual-reachability matrix does ~log₂N passes over the whole matrix where Prim's needs one). I'll follow up here with re-measured brute numbers once that lands so the config baseline reflects it. |
| { | ||
| "dataset": "skin_segmentation", | ||
| "split_kwargs": { "train_size": 100000 }, | ||
| "preprocessing_kwargs": { "normalize": "standard" } | ||
| }, | ||
| { | ||
| "source": "make_blobs", | ||
| "generation_kwargs": { | ||
| "centers": 10, | ||
| "n_samples": 100000, | ||
| "n_features": 16, | ||
| "cluster_std": 2.0 | ||
| }, |
There was a problem hiding this comment.
Both cases are very similar. One idea with the benchmark is to track performance under different types of shapes and data situations. Please choose the examples carefully so as to maximize coverage.
Same for the two brute-force cases which are also similar.
There was a problem hiding this comment.
Done in 68ba517. Both pairs were picked for size, which is exactly the problem you point at, so each of the four now covers a different regime rather than a different row count:
| method | dataset | regime it covers |
|---|---|---|
kd_tree |
skin_segmentation 100000 x 3 |
real, discrete, duplicate-heavy RGB: low dimension, thousands of small clusters, a tie-dense mutual reachability graph |
kd_tree |
make_blobs 100000 x 8, 100 centers (was 16 features / 10 centers) |
many well-separated clusters, so the cost moves into the condensed tree and the cluster selection instead of the neighbors search |
brute |
mnist 20000 x 784 |
high dimension: the pairwise distance computation dominates |
brute |
make_blobs 25000 x 4 (was 256) |
the opposite brute-force regime: the distance computation is negligible and the passes over the n^2 matrix dominate |
Still 8 cases. Two notes on the choices:
- The old tree-side blobs case (10 centers, 4/16/64 features) is already swept by
configs/weekly/hdbscan.json, so it was a strict subset of the weekly coverage. 100 centers is not. - The two brute cases now sit at opposite ends of the same code path, which is the axis that actually decides where its time goes -- see the numbers below.
Verified end to end on the new config against stock scikit-learn (1 run each, 2 x Xeon 8480L, 224 threads):
| method | dataset | shape | sklearn | oneDAL | speedup | clusters sk/oneDAL | Davies-Bouldin sk/oneDAL |
|---|---|---|---|---|---|---|---|
| kd_tree | skin_segmentation | 100000 x 3 | 46942 ms | 136 ms | 345.8x | 4847 / 4977 | 1.203 / 1.213 |
| kd_tree | make_blobs | 100000 x 8 | 57754 ms | 327 ms | 176.7x | 98 / 98 | 0.836 / 0.836 |
| brute | mnist | 20000 x 784 | 3870 ms | 748 ms | 5.2x | 11 / 13 | 2.472 / 2.274 |
| brute | make_blobs | 25000 x 4 | 5478 ms | 960 ms | 5.7x | 10 / 10 | 0.945 / 0.945 |
The four datasets in the regular config were picked for size, which left two near-duplicate pairs. Each now covers a different regime: * kd_tree, skin_segmentation 100000 x 3 -- real, discrete, duplicate-heavy, so low dimension with thousands of small clusters and a tie-dense mutual reachability graph. * kd_tree, make_blobs 100000 x 8 with 100 centers (was 16 features, 10 centers) -- many well-separated clusters, which moves the cost into the condensed tree and the cluster selection instead of the neighbors search. Also no longer a subset of the weekly sweep, which covers 10 centers at 4 / 16 / 64 features. * brute, mnist 20000 x 784 -- high dimension, where the pairwise distance computation dominates. * brute, make_blobs 25000 x 4 (was 256 features) -- the opposite brute-force regime: the distance computation is negligible and the passes over the n^2 matrix dominate. Still 8 cases. Verified end to end against stock scikit-learn: 345.8x, 176.7x, 5.2x and 5.7x respectively, with matching cluster counts and Davies-Bouldin scores. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What these configs measuredSince the point of the configs is to be used, here is the short report from the first full run of them, so the numbers are on the record next to the configs that produce them. Host: 2 x Intel Xeon Platinum 8480L (224 threads), scikit-learn 1.9.0, 90 cases run across
No case is slower than scikit-learn and 34 of 45 are at least 10x faster. Cluster counts and Davies-Bouldin scores agree throughout; where they differ it is in the last ~1% of the cluster count with matching quality metrics. The configs found a real bottleneckThe brute-force column above was the outlier, and the
That last inversion can only come from work euclidean does and the others do not, which turned out to be a serial sweep over the whole n x n matrix in oneDAL's shared Euclidean distance primitive -- ~0.6 s on one thread at 25000 rows while 223 threads idled. Fixed in uxlfoundation/oneDAL#3750:
The three non-euclidean rows are the control: they never enter the code that changed. Cluster counts and Davies-Bouldin scores are bit-identical before and after in all ten cases. Restricted to euclidean, the brute-force geomean against scikit-learn moves from 3.67x to 6.70x. The configs also flagged one case worth knowing about when reading these results: Full write-up, including the correctness findings and the two optimizations that were measured and rejected, is in |
Description
Adds HDBSCAN benchmark configs (sklearn vs.
sklearnex.preview.cluster.HDBSCAN, CPU) plus the two harness fixes needed to make them runnable and reportable.Configs
configs/common/hdbscan.jsonbrute/kd_tree/ball_tree), metrics, density / cluster-selection /store_centerssweeps, dtypes, data formats. Modelled on the existing DBSCAN common config.configs/regular/hdbscan.jsonskin_segmentation,road_network,covtype, low/mid-dimensional blobs) and brute-friendly ones (mnist,cifar,sensit, high-dimensional blobs).configs/experiments/hdbscan_parameters.jsonconfigs/experiments/hdbscan_scaling.jsonn_samplesandn_featuressweeps.The dataset split between tree- and brute-friendly is deliberate: HDBSCAN's tree methods degrade with dimensionality, so
mnist/cifar/sensitand the wide blobs are benchmarked underbruteonly, and the 100k-row sets under the tree methods.Harness fixes
1.
enrich_result: normalize a*.preview.*library to its base library (sklbench/benchmarks/common.py)HDBSCAN is only available from
sklearnex.preview.cluster. Without this, the reported library string issklearnex.preview.cluster, which does not matchsklearn's row in the report, so the stock and accelerated results never get paired and no speedup column is produced. The existing.replace("sklbench.emulators.", "")/.replace(".utils", "")normalization is the same idea; this extends it to the preview namespace.2.
box_filter: fall back to the whole series when the 20–80% box is empty (sklbench/utils/measurement.py)For a 2-element series,
lower = array[0]andupper = array[1], and the filter keeps only items strictly between them — so the result is empty and the aggregated time becomesNaN. Two measurements happen in practice whenever atime_limitearly stop triggers after the second run, which is easy to hit with HDBSCAN on stock scikit-learn. The existingsize == 1guard covers the 1-element case; this covers the empty-box case the same way.Validation
Run on 2x Intel Xeon (112 threads), oneDAL
main, scikit-learn 1.9.0,configs/regular/hdbscan.jsonwith-p bench:time_limit=600. All 18 sklearn/sklearnex pairs produced and paired correctly in the report:Clustering quality is reported alongside and agrees closely — Davies-Bouldin matches to three decimals on every case, homogeneity/completeness match, and cluster counts agree to within ~0.8% (e.g.
covtype133 vs 133,skin_segmentation4847 vs 4886).Note that HDBSCAN picks up the existing clustering quality metrics for free:
sklearn_estimator.pygates them on"DBSCAN" in str(estimator_instance), whichHDBSCANalso satisfies, so cluster count, Davies-Bouldin, homogeneity and completeness are all populated with no further change.No regression to existing behaviour:
configs/sklearn_example.json(one of the configs CI runs) still reports libraries assklearn/sklearnexacross all 44 cases with noNaNtimings.🤖 Generated with Claude Code