KMeans recorded benchmark study
Inspect the measured native KMeans workloads, preserved legacy baseline, corrected arithmetic, worker scaling and separate memory evidence.
Development APIsThe matching development runtime improves numerical accumulation and adds explicit local CPU execution. Published Aner 0.0.5 retains its legacy serial KMeans implementation. Updated serial and parallel results agree bitwise under the recorded binary/floating-point contract; updated arithmetic can differ from legacy results. Clustering quality on an external scientific problem was not measured here.
On the primary Batch workload, four reused workers reduce fit time from 5.209583 ms updated serial to 1.810625 ms: 2.88×, or 1.71× relative to legacy serial. On Wide, fit falls from 54.920042 to 15.143375 ms: 3.63× relative to updated serial, or 3.16× relative to legacy. Prediction reaches 3.65× and 3.53× relative to updated serial for Batch/Wide respectively.
The updated serial implementation is slower than legacy in these observations. Fitting changes numerical accumulation and adds checked execution infrastructure; prediction has no exact-mean/SSE reduction but adds numerical environment setup, workspace/cancellation/error checks and changed loop structure. This capture does not isolate those individual costs. Tiny calls regress under pools, and skewed fitting scales less well than the primary Wide case. The tables retain these outcomes.
Raw observations and source/binary identities · KMeans contract · implementation note
The figure uses zero origin axes, both serial baselines and reused pools. Bars/whiskers show median ± median absolute deviation (MAD); MAD describes observed spread, not a confidence interval. Tables below retain more displayed precision and every workload. Download the standalone SVG or inspect its reproduction script.
Machine and measurement scope
Recorded on an Apple M2 Max, macOS 26.7.2 arm64, with 12 reported logical CPUs and admitted contexts with 1, 2 and 4 workers. AppleClang 21, C++20 Release -O3 -DNDEBUG, ThinLTO, -mcpu=apple-m1, -fno-fast-math and -ffp-contract=off were used. The apple-m1 compiler target is an instruction baseline; the recorded physical CPU is M2 Max. Worker counts bound pool concurrency and do not establish core affinity, dedicated cores or CPU utilization.
Each mode retains seven observations after two excluded warmups. All configurations ran sequentially after builds and validation finished. Inputs were already materialized CPU Float64. Fit timing includes initialization, all Lloyd updates, final labels/inertia and workspace allocation. Prediction timing excludes its prerequisite fit. Parsing, fixture creation and independent correctness checks are outside the timers. Every updated timed result is checked afterward against its own updated serial reference.
Legacy and updated kernels use the same native harness and matched build defaults. The preserved legacy source is from revision 32e34b92ef103156c9919212999b97b020e65aba, SHA-256 12f9ef7d5175f0d3c29c7aa1c6b814980541d00c4bd3b322e88931ddc12b0e64. Its unchanged Tensor/NN dependencies and current executor library are shared; legacy KMeans invokes no executor. The updated kernel SHA-256 is 771881f0f79758509a2bf2e4b604591e067297d737367eba64bc0d1e64325686. Source/binary hashes and complete raw samples remain in the JSON.
Workloads and completed updates
N is fitting rows, Q query rows, D features, k clusters and M the requested update cap. All use initialization seed 42. The final three cases examine overlap, skew and a fixed one update budget. A capped fit is a valid result with converged=false.
| Workload | N | Q | D | k | M | Legacy/new updates | Legacy/new converged |
|---|---|---|---|---|---|---|---|
| Tiny | 32 | 1 | 4 | 2 | 20 | 3 / 3 | true / true |
| Batch | 4,096 | 32,768 | 8 | 4 | 20 | 4 / 4 | true / true |
| Wide | 2,048 | 4,096 | 32 | 8 | 20 | 9 / 9 | true / true |
| Larger | 8,192 | 32,768 | 16 | 16 | 20 | 20 / 20 | false / false |
| Many clusters | 4,096 | 8,192 | 8 | 32 | 20 | 13 / 13 | true / true |
| Every row a center | 128 | 4,096 | 4 | 128 | 10 | 1 / 1 | true / true |
| Overlapping batch | 4,096 | 32,768 | 8 | 4 | 20 | 9 / 9 | true / true |
| Skewed batch | 4,096 | 32,768 | 8 | 4 | 20 | 20 / 20 | false / false |
| One update batch | 4,096 | 32,768 | 8 | 4 | 1 | 1 / 1 | false / false |
The completed counts happen to match in these nine cases. Improved numerical means can change stopping on other inputs, so before/after elapsed time alone does not prove equal per update work. The one update case fixes that count explicitly.
Latency: all nine workloads
Every cell is median ± MAD in milliseconds. Pools are reused; Pool 1 is distinct from a serial call. Legacy has only a serial baseline. Values are rounded for display; the JSON retains raw seconds.
Fitting
| Workload | Legacy serial | New serial | Pool 1 | Pool 2 | Pool 4 |
|---|---|---|---|---|---|
| Tiny | 0.004750 ± 0.000084 | 0.014416 ± 0.000417 | 0.064041 ± 0.004459 | 0.079000 ± 0.017125 | 0.162333 ± 0.011958 |
| Batch | 3.096917 ± 0.013000 | 5.209583 ± 0.026251 | 5.457750 ± 0.018959 | 2.962667 ± 0.058334 | 1.810625 ± 0.033833 |
| Wide | 47.914291 ± 0.092793 | 54.920042 ± 0.104125 | 55.302250 ± 0.374375 | 28.308000 ± 0.135500 | 15.143375 ± 0.117375 |
| Larger | 286.268042 ± 1.005542 | 339.641625 ± 0.760333 | 341.580042 ± 0.338416 | 172.620500 ± 0.235833 | 93.825000 ± 1.239625 |
| Many clusters | 65.372833 ± 0.194208 | 92.460541 ± 0.133792 | 93.637791 ± 0.139834 | 47.480125 ± 0.100542 | 28.646125 ± 1.498125 |
| Every row a center | 0.416041 ± 0.003293 | 0.682167 ± 0.000876 | 0.742458 ± 0.002959 | 0.390625 ± 0.003125 | 0.308916 ± 0.009459 |
| Overlapping batch | 6.257459 ± 0.054042 | 11.457458 ± 0.049375 | 11.820917 ± 0.034041 | 6.412625 ± 0.157375 | 4.038583 ± 0.156833 |
| Skewed batch | 13.077208 ± 0.081999 | 21.116666 ± 0.057751 | 21.654750 ± 0.040250 | 11.839875 ± 0.236542 | 7.903791 ± 0.613832 |
| One update batch | 1.209542 ± 0.011167 | 2.215542 ± 0.016749 | 2.263500 ± 0.040375 | 1.204042 ± 0.006375 | 0.795125 ± 0.023500 |
Prediction
| Workload | Legacy serial | New serial | Pool 1 | Pool 2 | Pool 4 |
|---|---|---|---|---|---|
| Tiny | 0.000125 ± 0.000000 | 0.000209 ± 0.000001 | 0.007875 ± 0.000541 | 0.006291 ± 0.001043 | 0.012750 ± 0.006083 |
| Batch | 4.912459 ± 0.047125 | 7.750041 ± 0.112918 | 7.472000 ± 0.091000 | 3.991458 ± 0.062375 | 2.124916 ± 0.032582 |
| Wide | 9.600208 ± 0.045041 | 10.663916 ± 0.098584 | 10.569167 ± 0.115708 | 5.319709 ± 0.020249 | 3.021708 ± 0.071792 |
| Larger | 53.502375 ± 0.347083 | 63.008583 ± 0.183666 | 62.750541 ± 0.157916 | 31.805750 ± 0.095209 | 16.070375 ± 0.048916 |
| Many clusters | 9.317916 ± 0.007334 | 12.851875 ± 0.032833 | 12.828416 ± 0.063625 | 6.527959 ± 0.051250 | 3.642084 ± 0.082417 |
| Every row a center | 6.595541 ± 0.008667 | 8.344625 ± 0.066875 | 8.300875 ± 0.013125 | 4.197875 ± 0.030500 | 2.307958 ± 0.090167 |
| Overlapping batch | 4.853625 ± 0.021875 | 7.618417 ± 0.019709 | 7.623000 ± 0.060792 | 3.905792 ± 0.006583 | 2.474709 ± 0.121126 |
| Skewed batch | 4.841333 ± 0.011083 | 7.143125 ± 0.073709 | 7.095125 ± 0.012291 | 3.657292 ± 0.053208 | 1.975292 ± 0.023667 |
| One update batch | 4.825083 ± 0.012375 | 7.999250 ± 0.048250 | 8.177917 ± 0.146124 | 4.219708 ± 0.011874 | 2.362916 ± 0.073792 |
Speedups and their denominators
Each cell lists Pool 1 / Pool 2 / Pool 4, in that order, as ratios:
- Vs new serial: updated serial median divided by the shown reused pool median. This compares execution modes with the same updated arithmetic.
- Vs legacy serial: legacy median divided by the shown pool median. This includes numerical/kernel changes as well as parallel execution.
- Vs Pool 1: reused one worker median divided by the shown pool median. This compares scaling within the pool path.
A value below 1× means slower execution. These are ratios of medians; no uncertainty interval for a ratio is inferred from MAD.
Fitting speedups
| Workload | Vs new serial: 1 / 2 / 4 | Vs legacy serial: 1 / 2 / 4 | Vs Pool 1: 1 / 2 / 4 |
|---|---|---|---|
| Tiny | 0.225× / 0.182× / 0.089× | 0.074× / 0.060× / 0.029× | 1.000× / 0.811× / 0.395× |
| Batch | 0.955× / 1.758× / 2.877× | 0.567× / 1.045× / 1.710× | 1.000× / 1.842× / 3.014× |
| Wide | 0.993× / 1.940× / 3.627× | 0.866× / 1.693× / 3.164× | 1.000× / 1.954× / 3.652× |
| Larger | 0.994× / 1.968× / 3.620× | 0.838× / 1.658× / 3.051× | 1.000× / 1.979× / 3.641× |
| Many clusters | 0.987× / 1.947× / 3.228× | 0.698× / 1.377× / 2.282× | 1.000× / 1.972× / 3.269× |
| Every row a center | 0.919× / 1.746× / 2.208× | 0.560× / 1.065× / 1.347× | 1.000× / 1.901× / 2.403× |
| Overlapping batch | 0.969× / 1.787× / 2.837× | 0.529× / 0.976× / 1.549× | 1.000× / 1.843× / 2.927× |
| Skewed batch | 0.975× / 1.784× / 2.672× | 0.604× / 1.105× / 1.655× | 1.000× / 1.829× / 2.740× |
| One update batch | 0.979× / 1.840× / 2.786× | 0.534× / 1.005× / 1.521× | 1.000× / 1.880× / 2.847× |
Prediction speedups
| Workload | Vs new serial: 1 / 2 / 4 | Vs legacy serial: 1 / 2 / 4 | Vs Pool 1: 1 / 2 / 4 |
|---|---|---|---|
| Tiny | 0.027× / 0.033× / 0.016× | 0.016× / 0.020× / 0.010× | 1.000× / 1.252× / 0.618× |
| Batch | 1.037× / 1.942× / 3.647× | 0.657× / 1.231× / 2.312× | 1.000× / 1.872× / 3.516× |
| Wide | 1.009× / 2.005× / 3.529× | 0.908× / 1.805× / 3.177× | 1.000× / 1.987× / 3.498× |
| Larger | 1.004× / 1.981× / 3.921× | 0.853× / 1.682× / 3.329× | 1.000× / 1.973× / 3.905× |
| Many clusters | 1.002× / 1.969× / 3.529× | 0.726× / 1.427× / 2.558× | 1.000× / 1.965× / 3.522× |
| Every row a center | 1.005× / 1.988× / 3.616× | 0.795× / 1.571× / 2.858× | 1.000× / 1.977× / 3.597× |
| Overlapping batch | 0.999× / 1.951× / 3.079× | 0.637× / 1.243× / 1.961× | 1.000× / 1.952× / 3.080× |
| Skewed batch | 1.007× / 1.953× / 3.616× | 0.682× / 1.324× / 2.451× | 1.000× / 1.940× / 3.592× |
| One update batch | 0.978× / 1.896× / 3.385× | 0.590× / 1.143× / 2.042× | 1.000× / 1.938× / 3.461× |
Tiny fitting takes 0.014416 ms updated serial and 0.162333 ms with four reused workers; tiny prediction takes 0.000209 versus 0.012750 ms. Its one query supplies one prediction lane regardless of a larger pool. These timings are especially sensitive to clock/scheduling overhead; small MAD or zero displayed MAD is not a precision guarantee.
Skewed fitting achieves 2.672× relative to updated serial with four workers, versus 3.627× for primary Wide. The phase structure and membership distribution limit scaling. These are local observations, not GPU/cluster, cross platform or universal acceleration claims.
Cold pool calls
Cells are median ± MAD in milliseconds, including pool creation and destruction. Compare them with the reused rows above; their difference is not an isolated measurement of thread startup. All nine workloads and both operations are retained.
| Workload / operation | Cold 1 worker | Cold 2 workers | Cold 4 workers |
|---|---|---|---|
| Tiny / fit | 0.077291 ± 0.008916 | 0.144833 ± 0.025417 | 0.230667 ± 0.047333 |
| Tiny / predict | 0.039583 ± 0.003084 | 0.067459 ± 0.005876 | 0.092250 ± 0.005542 |
| Batch / fit | 5.549000 ± 0.047625 | 3.072375 ± 0.039000 | 1.915375 ± 0.130375 |
| Batch / predict | 7.676625 ± 0.097000 | 4.050333 ± 0.037084 | 2.247625 ± 0.020167 |
| Wide / fit | 55.273083 ± 0.065167 | 28.453708 ± 0.107417 | 15.798583 ± 0.295083 |
| Wide / predict | 10.589333 ± 0.027041 | 5.425125 ± 0.034667 | 3.016708 ± 0.064916 |
| Larger / fit | 341.974625 ± 1.080000 | 173.210084 ± 0.037583 | 104.796625 ± 0.918167 |
| Larger / predict | 63.017667 ± 0.088625 | 31.463708 ± 0.044250 | 16.664833 ± 0.204792 |
| Many clusters / fit | 93.500334 ± 0.326250 | 49.386084 ± 0.883208 | 27.673041 ± 0.150625 |
| Many clusters / predict | 13.021125 ± 0.083958 | 6.510791 ± 0.021958 | 3.561084 ± 0.094418 |
| Every row a center / fit | 0.753917 ± 0.006249 | 0.448709 ± 0.006541 | 0.369500 ± 0.003042 |
| Every row a center / predict | 8.400042 ± 0.015001 | 4.303250 ± 0.050583 | 2.385917 ± 0.074417 |
| Overlapping batch / fit | 11.977125 ± 0.177000 | 6.365500 ± 0.056917 | 4.336084 ± 0.355959 |
| Overlapping batch / predict | 7.727000 ± 0.071708 | 4.560167 ± 0.472083 | 2.510250 ± 0.120083 |
| Skewed batch / fit | 21.749333 ± 0.012958 | 13.039834 ± 0.689291 | 7.962208 ± 0.320041 |
| Skewed batch / predict | 7.139208 ± 0.033209 | 3.760875 ± 0.016625 | 2.056667 ± 0.058167 |
| One update batch / fit | 2.282583 ± 0.010750 | 1.273042 ± 0.011167 | 0.931125 ± 0.007708 |
| One update batch / predict | 8.025833 ± 0.265000 | 4.295958 ± 0.047458 | 2.481208 ± 0.115125 |
Charged workspace admission
Memory probes cover Batch and Wide only, separately for fit and prediction. Each reported byte budget is the minimum accepted budget, verified to accept exactly that value and reject one byte less. All 16 boundaries passed. Cells show bytes (KiB), using 1 KiB = 1,024 bytes.
| Workload / operation | Serial | Pool 1 | Pool 2 | Pool 4 |
|---|---|---|---|---|
| Batch / fit | 67,532 (65.949) | 67,532 (65.949) | 68,368 (66.766) | 70,040 (68.398) |
| Batch / predict | 262,528 (256.375) | 262,528 (256.375) | 262,544 (256.391) | 262,576 (256.422) |
| Wide / fit | 38,412 (37.512) | 38,412 (37.512) | 39,248 (38.328) | 40,920 (39.961) |
| Wide / predict | 33,152 (32.375) | 33,152 (32.375) | 33,168 (32.391) | 33,200 (32.422) |
The default 64 MiB (67,108,864 bytes) is a cap, not a fixed allocation. New fitting charges returned centers/labels, vector capacities, CSR/center buffers, lane accumulator variants, bounded controls and the explicit mean finalization temporary. Prediction charges its output/lane controls and excludes its already fitted model. This recorded macOS arm64 ABI uses 8 byte Float64 values and size indices. Accumulator payloads are 544 bytes for exact means or 528 bytes for exact squares, plus variant/lane metadata; each active mean lane additionally charges 272 bytes. These probes add 836 admitted bytes per additional fit lane and 16 bytes per additional prediction lane, with both workloads exposing the requested lanes.
Iteration storage uses two N index buffers and old/next k×D centers, with k+1 offsets and k cursors. A fit has no N×k distance matrix or P×k×D accumulator array. Obsolete iteration capacities are released before final labels are allocated. The same retained numerical model payload is 8(N+kD) bytes in old/new implementations: 33,024 bytes (32.25 KiB) for Batch and 18,432 bytes (18 KiB) for Wide, before fitted-state/ownership metadata.
The preserved legacy logical fit peak payload formula on this 64 bit ABI is 24N + 40kD + 8k bytes, excluding vector/owner controls and allocator/input storage. It describes a different accounting scope from the updated charged boundary; comparing the two as measured RSS or as identical allocation measurements would be invalid.
Charged workspace excludes input/host-conversion caches, existing executor/session internals and thread stacks, observer/context payloads and strings, opaque shared ownership bookkeeping, allocator overhead, ordinary call stack frames and process RSS. The bounded mean temporary is explicitly included despite local storage duration. Other workloads have timings but no additional workspace boundary/RSS measurement here.
Fresh process peak RSS
This is a separate full process lifetime measurement, using getrusage, outside timing processes. Each cell is the maximum of three independent fresh process lifetime peaks, in MiB (1 MiB = 1,048,576 bytes). The JSON retains all three raw peaks. macOS returns bytes directly; the harness converts Linux KiB when applicable.
| Workload / operation | Legacy serial | New serial | Pool 1 | Pool 2 | Pool 4 |
|---|---|---|---|---|---|
| Batch / fit | 8.000000 | 7.937500 | 7.937500 | 7.968750 | 7.984375 |
| Batch / predict | 8.281250 | 8.187500 | 8.203125 | 8.218750 | 8.250000 |
| Wide / fit | 7.234375 | 7.156250 | 7.203125 | 7.218750 | 7.250000 |
| Wide / predict | 7.250000 | 7.187500 | 7.187500 | 7.203125 | 7.234375 |
These peaks include runtime startup, input/query Tensors, executor threads and the operation. Prediction RSS includes its prerequisite serial fit. RSS only processes omit oracle model copies and timing repetitions. No subtraction converts these peaks into a per call allocation estimate; close process peaks do not establish general memory savings or zero thread stack cost. Keep RSS and charged workspace as distinct observations.
Correctness and development boundaries
The recorded validation reports 136,733 native KMeans checks, plus the language/session and existing ML/KNN/tree/executor regressions. Independent exact rational arithmetic checked 14,584 primitive cases and 38 full fit fixtures across serial/1/2/4 workers: 152 observations. Exact centroid means and exact sums of squared supplied/computed binary64 distances matched the independent once rounded reference. Sanitizer runs were kept separate from measurement.
The numerical contract uses exact sums of stored Float64 coordinates divided once for means, and exact squares of computed Float64 hypot distances summed before one final rounding for inertia. Distance computation itself remains checked, coordinate ordered and rounded. The integration report contains a fixture where exact coordinate SSE differs from squared computed distance SSE, demonstrating why these claims must remain separate.
Updated serial/pool results agree bitwise for every recorded mode and all fitted readouts/metadata; legacy arithmetic is deliberately a distinct comparison. This does not establish clustering quality, a global optimum, or cross math library bitwise portability. Seed/row selection, lowest cluster distance ties, empty cluster retention, absolute tolerance and final readouts against returned centers retain their API policies. Native cancellation/observer failures join before any successful output publication; full fit session admission and memory details are in the implementation note.
Reproduce the study
Use a source checkout containing the preserved legacy source and the matching development toolchain. On the recorded macOS platform, both benchmark targets inherit the same project Release, CPU, LTO and strict floating point defaults. ANER_KMEANS_BASELINE selects only the preserved legacy kernel. The exact compiler/OS/hardware identities affect reproduced timings.
cmake -S . -B build/kmeans-study -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON \
-DANER_USE_MPS=OFF -DANER_USE_ACCELERATE=ON \
-DANER_ENABLE_LTO=ON -DANER_APPLE_CPU=apple-m1
cmake --build build/kmeans-study --target \
aner_kmeans_legacy_benchmark aner_kmeans_benchmark -j3
cmake --build build/kmeans-study --target \
aner_ml_exact_tests aner_kmeans_oracle_probe aner_kmeans_parallel_tests -j3
python3 tools/verify_kmeans_exact.py \
--binary build/kmeans-study/aner_ml_exact_tests \
--output build/kmeans-study/exact-oracle.json
python3 tools/verify_kmeans_numeric.py \
--binary build/kmeans-study/aner_kmeans_oracle_probe \
--build-root build/kmeans-study \
--output build/kmeans-study/integration-oracle.json
build/kmeans-study/aner_kmeans_parallel_tests > build/kmeans-study/native-checks.log
KMEANS_NATIVE_CHECKS="$(python3 -c 'import pathlib,re; print(re.search(r"(\d+) checks passed", pathlib.Path("build/kmeans-study/native-checks.log").read_text()).group(1))')"
python3 tools/kmeans_benchmark.py \
--baseline build/kmeans-study/aner_kmeans_legacy_benchmark \
--corrected build/kmeans-study/aner_kmeans_benchmark \
--source-root . \
--baseline-source docs/benchmarks/kmeans_legacy_ml_32e.cpp \
--oracle-report build/kmeans-study/exact-oracle.json \
--integration-report build/kmeans-study/integration-oracle.json \
--native-checks "$KMEANS_NATIVE_CHECKS" \
--repeats 7 --rss-repeats 3 \
--output build/kmeans-study/new-observations.json
python3 tools/plot_kmeans_benchmark.py \
--input build/kmeans-study/new-observations.json \
--output build/kmeans-study/new-observations.svgThe driver requires admission for four workers, preserves an existing output file rather than overwriting it, and runs configurations sequentially. Use another supported CPU profile/provider configuration on other platforms and retain those differences in the evidence. Finish builds, tests and other substantial CPU work before timing.
The figure tool recomputes median/MAD from raw samples and checks the stored summaries. It uses an optional plotting backend when available and a standards based dependency free SVG renderer otherwise; the committed figure uses the SVG renderer. To reproduce that figure from the committed evidence without running timings:
python3 tools/plot_kmeans_benchmark.py --renderer svgBenchmark harness, measurement driver, exact arithmetic oracle, full fit numerical oracle, and runnable local example are available for inspection.
See the Machine learning module reference for the shared API.