Browse documentation
aner.ml · Reference

KMeans recorded benchmark study

Inspect the measured native KMeans workloads, preserved legacy baseline, corrected arithmetic, worker scaling and separate memory evidence.

Development APIs

The 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

Recorded KMeans fit and prediction latency for Batch and Wide; median plus or minus MAD in milliseconds

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.

WorkloadNQDkMLegacy/new updatesLegacy/new converged
Tiny32142203 / 3true / true
Batch4,09632,76884204 / 4true / true
Wide2,0484,096328209 / 9true / true
Larger8,19232,76816162020 / 20false / false
Many clusters4,0968,1928322013 / 13true / true
Every row a center1284,0964128101 / 1true / true
Overlapping batch4,09632,76884209 / 9true / true
Skewed batch4,09632,768842020 / 20false / false
One update batch4,09632,7688411 / 1false / 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

WorkloadLegacy serialNew serialPool 1Pool 2Pool 4
Tiny0.004750 ± 0.0000840.014416 ± 0.0004170.064041 ± 0.0044590.079000 ± 0.0171250.162333 ± 0.011958
Batch3.096917 ± 0.0130005.209583 ± 0.0262515.457750 ± 0.0189592.962667 ± 0.0583341.810625 ± 0.033833
Wide47.914291 ± 0.09279354.920042 ± 0.10412555.302250 ± 0.37437528.308000 ± 0.13550015.143375 ± 0.117375
Larger286.268042 ± 1.005542339.641625 ± 0.760333341.580042 ± 0.338416172.620500 ± 0.23583393.825000 ± 1.239625
Many clusters65.372833 ± 0.19420892.460541 ± 0.13379293.637791 ± 0.13983447.480125 ± 0.10054228.646125 ± 1.498125
Every row a center0.416041 ± 0.0032930.682167 ± 0.0008760.742458 ± 0.0029590.390625 ± 0.0031250.308916 ± 0.009459
Overlapping batch6.257459 ± 0.05404211.457458 ± 0.04937511.820917 ± 0.0340416.412625 ± 0.1573754.038583 ± 0.156833
Skewed batch13.077208 ± 0.08199921.116666 ± 0.05775121.654750 ± 0.04025011.839875 ± 0.2365427.903791 ± 0.613832
One update batch1.209542 ± 0.0111672.215542 ± 0.0167492.263500 ± 0.0403751.204042 ± 0.0063750.795125 ± 0.023500

Prediction

WorkloadLegacy serialNew serialPool 1Pool 2Pool 4
Tiny0.000125 ± 0.0000000.000209 ± 0.0000010.007875 ± 0.0005410.006291 ± 0.0010430.012750 ± 0.006083
Batch4.912459 ± 0.0471257.750041 ± 0.1129187.472000 ± 0.0910003.991458 ± 0.0623752.124916 ± 0.032582
Wide9.600208 ± 0.04504110.663916 ± 0.09858410.569167 ± 0.1157085.319709 ± 0.0202493.021708 ± 0.071792
Larger53.502375 ± 0.34708363.008583 ± 0.18366662.750541 ± 0.15791631.805750 ± 0.09520916.070375 ± 0.048916
Many clusters9.317916 ± 0.00733412.851875 ± 0.03283312.828416 ± 0.0636256.527959 ± 0.0512503.642084 ± 0.082417
Every row a center6.595541 ± 0.0086678.344625 ± 0.0668758.300875 ± 0.0131254.197875 ± 0.0305002.307958 ± 0.090167
Overlapping batch4.853625 ± 0.0218757.618417 ± 0.0197097.623000 ± 0.0607923.905792 ± 0.0065832.474709 ± 0.121126
Skewed batch4.841333 ± 0.0110837.143125 ± 0.0737097.095125 ± 0.0122913.657292 ± 0.0532081.975292 ± 0.023667
One update batch4.825083 ± 0.0123757.999250 ± 0.0482508.177917 ± 0.1461244.219708 ± 0.0118742.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

WorkloadVs new serial: 1 / 2 / 4Vs legacy serial: 1 / 2 / 4Vs Pool 1: 1 / 2 / 4
Tiny0.225× / 0.182× / 0.089×0.074× / 0.060× / 0.029×1.000× / 0.811× / 0.395×
Batch0.955× / 1.758× / 2.877×0.567× / 1.045× / 1.710×1.000× / 1.842× / 3.014×
Wide0.993× / 1.940× / 3.627×0.866× / 1.693× / 3.164×1.000× / 1.954× / 3.652×
Larger0.994× / 1.968× / 3.620×0.838× / 1.658× / 3.051×1.000× / 1.979× / 3.641×
Many clusters0.987× / 1.947× / 3.228×0.698× / 1.377× / 2.282×1.000× / 1.972× / 3.269×
Every row a center0.919× / 1.746× / 2.208×0.560× / 1.065× / 1.347×1.000× / 1.901× / 2.403×
Overlapping batch0.969× / 1.787× / 2.837×0.529× / 0.976× / 1.549×1.000× / 1.843× / 2.927×
Skewed batch0.975× / 1.784× / 2.672×0.604× / 1.105× / 1.655×1.000× / 1.829× / 2.740×
One update batch0.979× / 1.840× / 2.786×0.534× / 1.005× / 1.521×1.000× / 1.880× / 2.847×

Prediction speedups

WorkloadVs new serial: 1 / 2 / 4Vs legacy serial: 1 / 2 / 4Vs Pool 1: 1 / 2 / 4
Tiny0.027× / 0.033× / 0.016×0.016× / 0.020× / 0.010×1.000× / 1.252× / 0.618×
Batch1.037× / 1.942× / 3.647×0.657× / 1.231× / 2.312×1.000× / 1.872× / 3.516×
Wide1.009× / 2.005× / 3.529×0.908× / 1.805× / 3.177×1.000× / 1.987× / 3.498×
Larger1.004× / 1.981× / 3.921×0.853× / 1.682× / 3.329×1.000× / 1.973× / 3.905×
Many clusters1.002× / 1.969× / 3.529×0.726× / 1.427× / 2.558×1.000× / 1.965× / 3.522×
Every row a center1.005× / 1.988× / 3.616×0.795× / 1.571× / 2.858×1.000× / 1.977× / 3.597×
Overlapping batch0.999× / 1.951× / 3.079×0.637× / 1.243× / 1.961×1.000× / 1.952× / 3.080×
Skewed batch1.007× / 1.953× / 3.616×0.682× / 1.324× / 2.451×1.000× / 1.940× / 3.592×
One update batch0.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 / operationCold 1 workerCold 2 workersCold 4 workers
Tiny / fit0.077291 ± 0.0089160.144833 ± 0.0254170.230667 ± 0.047333
Tiny / predict0.039583 ± 0.0030840.067459 ± 0.0058760.092250 ± 0.005542
Batch / fit5.549000 ± 0.0476253.072375 ± 0.0390001.915375 ± 0.130375
Batch / predict7.676625 ± 0.0970004.050333 ± 0.0370842.247625 ± 0.020167
Wide / fit55.273083 ± 0.06516728.453708 ± 0.10741715.798583 ± 0.295083
Wide / predict10.589333 ± 0.0270415.425125 ± 0.0346673.016708 ± 0.064916
Larger / fit341.974625 ± 1.080000173.210084 ± 0.037583104.796625 ± 0.918167
Larger / predict63.017667 ± 0.08862531.463708 ± 0.04425016.664833 ± 0.204792
Many clusters / fit93.500334 ± 0.32625049.386084 ± 0.88320827.673041 ± 0.150625
Many clusters / predict13.021125 ± 0.0839586.510791 ± 0.0219583.561084 ± 0.094418
Every row a center / fit0.753917 ± 0.0062490.448709 ± 0.0065410.369500 ± 0.003042
Every row a center / predict8.400042 ± 0.0150014.303250 ± 0.0505832.385917 ± 0.074417
Overlapping batch / fit11.977125 ± 0.1770006.365500 ± 0.0569174.336084 ± 0.355959
Overlapping batch / predict7.727000 ± 0.0717084.560167 ± 0.4720832.510250 ± 0.120083
Skewed batch / fit21.749333 ± 0.01295813.039834 ± 0.6892917.962208 ± 0.320041
Skewed batch / predict7.139208 ± 0.0332093.760875 ± 0.0166252.056667 ± 0.058167
One update batch / fit2.282583 ± 0.0107501.273042 ± 0.0111670.931125 ± 0.007708
One update batch / predict8.025833 ± 0.2650004.295958 ± 0.0474582.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 / operationSerialPool 1Pool 2Pool 4
Batch / fit67,532 (65.949)67,532 (65.949)68,368 (66.766)70,040 (68.398)
Batch / predict262,528 (256.375)262,528 (256.375)262,544 (256.391)262,576 (256.422)
Wide / fit38,412 (37.512)38,412 (37.512)39,248 (38.328)40,920 (39.961)
Wide / predict33,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 / operationLegacy serialNew serialPool 1Pool 2Pool 4
Batch / fit8.0000007.9375007.9375007.9687507.984375
Batch / predict8.2812508.1875008.2031258.2187508.250000
Wide / fit7.2343757.1562507.2031257.2187507.250000
Wide / predict7.2500007.1875007.1875007.2031257.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.

sh
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.svg

The 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:

sh
python3 tools/plot_kmeans_benchmark.py --renderer svg

Benchmark 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.

Aner documentation · Reference and guides