A compiled classical machine-learning library for Flow, inspired by scikit-learn's estimator ecosystem while exploring native deployment, whole-estimator compilation and a smaller runtime.
Status: experimental, actively developed, and not affiliated with or endorsed by the scikit-learn project.
Documentation & benchmarks · Execution architecture · Flow language · Examples · Tests
flow-scikit asks a direct systems question: how much of the practical scikit-learn experience can be reproduced in a statically typed compiled language without carrying a Python runtime into deployment?
The goal is not a line-for-line port. The project implements familiar estimators, preprocessing, metrics, model selection and composition in Flow, then uses that implementation to test native deployment, explicit memory/layout choices and whole-estimator optimization against a mature reference ecosystem.
A useful practitioner snapshot appears in the r/datascience discussion "Do people not use sci-kit learn / other traditional libraries anymore?". It is anecdotal rather than a survey, but the recurring sentiment is clear: regression, trees, SVMs, clustering and other conventional methods remain routine production tools, especially for tabular and business workloads where larger neural models are unnecessary.
The other recurring point is that scikit-learn's value is larger than any individual estimator. Its common estimator interface makes preprocessing, fitting, evaluation, tuning and composition unusually coherent. flow-scikit is interested in preserving that practical model while testing a different runtime boundary.
The thesis is simple:
Use the least complicated model that solves the problem, and make that model available in the least complicated runtime that satisfies the deployment requirements.
That makes classical ML particularly interesting for native applications, embedded systems, mobile deployment, low-latency services, command-line tools and constrained environments.
Performance claims in this repository are generated from committed benchmark artifacts rather than selected examples.
The current canonical v2 result is benchmarks/headline_result_v2.json: 19 of 19 rows are parity-eligible and measurement-resolved. In that committed run, Flow wins 19 of 19 end-to-end fit + predict comparisons and scikit-learn wins 0 of 19. There are no parity-unresolved or measurement-unresolved rows.
Canonical v2 uses explicit TIMING_UNIT|ms markers, persisted identical train/test fixtures, repeated timing aggregation and estimator-specific numerical parity gates. Unsupervised rows are not forced into classifier-style metrics: KMeans uses adjusted Rand index and inertia, while PCA additionally checks explained variance, singular values, reconstruction error and sign-aligned components.
Digits KMeans is classified as approximately equivalent, under the same declared tolerances as every other clustering row. Flow's seeded k-means++ consumes an MT19937 stream matching NumPy's RandomState and picks each centre as the best of 2 + int(log(k)) candidates, so both implementations start from the same initial centres and reach the same partition. The semantic audit records the differences that remain after that: the convergence statistic, the point at which inertia is reported, empty-cluster relocation and the n_init selection rule. None of them moves a canonical row, and all of them stay in the disparity artifact.
The separate deterministic algorithm suite currently records 16/16 output-parity checks passing. The repository also records a historical ~1.4 MB native executable footprint and ~65× cold-start advantage in a separate deployment comparison. Those deployment figures are not mixed into the canonical estimator timing denominator.
See the full canonical benchmark report.
"Python versus compiled" is too crude a performance model for scikit-learn. Its public API is Python, but estimator hot paths may execute in Python orchestration, NumPy/SciPy, BLAS/LAPACK, sklearn-owned Cython/native code or external native libraries such as liblinear and libsvm.
flow-scikit now maintains a generated execution map rather than inferring opportunity from file extensions. The current committed evidence contains:
- 491 estimator-operation inventory rows across the pinned sklearn public estimator surface
- 32 dynamic runtime-attribution rows
- 491 generated optimization-roadmap rows
- 146 compiled/mixed native-hotspot dispositions
- 8 whole-estimator experiments
- substrate and speedup joins for all 19 canonical benchmark rows
The win count is machine-dependent and the committed artifact says which machine it came from. This matters enough to have changed a verdict once: an earlier run of this work won all 19 rows on an Apple M4 Max with Accelerate while CI, on an Intel Xeon with OpenBLAS, won 18 and lost LogisticRegression on digits at 0.94x. That row was 1.18x on the Mac, close enough to 1x for a different BLAS and core count to take it the other way. After the convergence fix it has been measured winning on four machines: 3.18x on the M4 Max, 2.87x on an Intel Xeon Platinum 8370C, and 23x on two AMD EPYC 7763 runners.
Do not read that 23x as a property of the library. scikit-learn's own fit of that row takes about 25 ms on the Intel runner and about 181 ms on the AMD one, for the same code and the same data, so the AMD figure is measuring an OpenBLAS path that suits that machine badly rather than anything Flow does well. Flow's own time on the two runners is 8.9 ms and 7.5 ms. The Intel ratio is the honest one to quote, and a row whose margin sits near 1x can still land either way. The parity contract gates on correctness and measurement resolution rather than on the win count.
The current grouped headline evidence is descriptive rather than causal: Flow wins every row in all three substrate groups, at a mean of 31.14x on Python-bound rows, 7.51x on mixed rows and 2.94x on external-native-bound rows in the committed architecture map.
Two earlier readings of this table were wrong, and both were artifacts of how Flow was built rather than of the substrate. While the Flow side was compiled unoptimized, external-native-bound rows all lost, which read as sklearn-owned compiled code being out of reach. The grouping is a guide to where the Python boundary costs most. It is not a ceiling.
One row is not a like-for-like comparison, and the disparity report records it. Flow fits a forest's trees concurrently; scikit-learn's default is one worker, and the benchmark leaves it at its default. Both RandomForest rows therefore carry a declared n_jobs difference. Single-threaded, RandomForest on digits runs at 1.82x rather than 5.01x, so the row wins either way. Asking scikit-learn for all cores does not close the gap on this workload: at n_jobs=-1 its own fit measured slower than at n_jobs=1, because joblib's pool costs more than ten small trees save.
The 19 canonical rows sit on iris, digits and diabetes, none of which exceeds 1797 samples. A separate matrix runs five estimators at 100, 1000 and 10000 rows against 8 and 32 features, which is where an implementation that only suits small inputs would show it. CI measures it on every run and reports it without gating the build.
One run of that matrix does not settle a row. benchmarks/scaled_ci_history.json holds five consecutive runs. Flow wins 30 of the 34 rows in all five. The other four dipped below 1x in at least one run, and all four are at 1000 samples, where a fit finishes in well under a millisecond and the runner moves the measurement by more than the difference being measured: Lasso at 1000 rows and 32 features was recorded at 3.83x and at 0.92x on code that differs in nothing touching Lasso.
Both KernelSVC_RBF rows at 1000 samples are in that group for a different reason. They lost consistently, at 0.68x and 0.88x, until a fitted one-vs-one model stopped keeping the whole training set and started keeping its support vectors. They have won every run since, at 1.16x to 1.36x. That is a step change that tracks the commit rather than the runner.
The ten rows at 10000 samples, the only sizes in the matrix large enough to measure without fighting the noise, win in all five runs, from a median 1.15x on RandomForest at 32 features to 8.06x on GaussianNB at 8.
benchmarks/scaled_flow_baseline.json is a separate Flow-against-itself gate that does fail the build. Refresh it from a CI artifact rather than from a developer machine, or the gate will read a fast laptop as the standard and fail every CI run.
The 19 canonical rows race twelve estimators. lib/scikit exports 203, and a
claim about Flow against scikit-learn covers six percent of the library while
the rest go unmeasured. A registry maps every exported fit to its
scikit-learn counterpart, times both sides on the same data, and a CI job
fails the build when a ranked row is slower.
Flow is faster on 197 of the 197 ranked rows. Eleven rows carry no ratio.
Six of them have no scikit-learn counterpart at all. The other five are the
older entry points of estimators that are raced through a second one: a
voting estimator that takes models already fitted is half of what
VotingClassifier does, and voting_classifier_full_fit beside it is the row
that races the class.
Nothing is excluded for being a stand-in any more. Four implementations used to say in their own comments that they were simplified, and each runs the algorithm its counterpart runs now.
These rows carry no parity contract and no declared tolerances. Each library runs its own defaults over the same data, which answers whether an implementation is in the same performance league and says nothing about whether it computes the same thing. The canonical rows above are where numerical equivalence is established.
Detailed artifacts:
benchmarks/SKLEARN_EXECUTION_INVENTORY.mdbenchmarks/ARCHITECTURE_PERFORMANCE_MAP.mdbenchmarks/OPTIMIZATION_ROADMAP.mdbenchmarks/NATIVE_HOTSPOT_AUDIT.md
Flow is a statically typed compiled language with a C backend, algebraic effects and autodiff. For this project that enables native binaries, explicit memory/layout decisions, cross-compilation and no mandatory Python interpreter.
flow-scikit is therefore both a library experiment and a systems experiment: it tests which parts of the scikit-learn programming model are fundamental and which parts are consequences of Python, NumPy, native-library boundaries and historical compatibility constraints.
The implementation under lib/scikit/ includes preprocessing, linear models, neighbors, SVMs, trees and ensembles, clustering, decomposition, model selection, metrics, feature selection, Gaussian processes, mixture models, neural networks, covariance, imputation, manifold learning, kernel approximation, multi-output learning and related utilities.
The source tree is the authoritative API inventory; coverage changes quickly while the project is under active development.
git clone https://github.com/flooooooooooow/flow.git
git clone https://github.com/godofecht/flow-scikit.git
cd flow-scikit
# BLAS linkage is required. Without FLOW_LDFLAGS the build fails at the
# link step with undefined cblas_* symbols.
export FLOW_HOST=python
export FLOW_OPT_LEVEL=0
export FLOW_LDFLAGS="-framework Accelerate lib/scikit/flow_time.c lib/scikit/flow_parallel.c" # macOS
# export FLOW_LDFLAGS="-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c" # Linux, matching CI
python tools/run_all.pyBenchmarks are compiled at -O3 and link the timing shim:
export FLOW_OPT_LEVEL=3
export FLOW_LDFLAGS="-framework Accelerate lib/scikit/flow_time.c lib/scikit/flow_parallel.c" # macOS
# export FLOW_LDFLAGS="-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c" # Linux, matching CI
python benchmarks/run_headline.py --repeats 7scikit-learn is measured as a released wheel, which ships optimized. Building
the Flow side at -O0 measured the two at different optimization levels and
understated every row; the canonical contract now compiles both sides
optimized. lib/scikit/flow_time.c supplies the monotonic clock every timing
harness uses, and must be linked for the tests as well as the benchmarks.
tools/run_all.py is what the full CI suite exercises. A single file runs with
flow run tests/test_new_features.flow under the same environment.
CI performs syntax/import validation and runs the Flow test/example suite. The benchmark pipeline additionally verifies JSON/binary split-fixture identity, validates the 19-row estimator contract, reruns repeated Flow/sklearn measurements, gates competitive timing on parity, detects sklearn estimator-surface drift and regenerates the architecture evidence and optimization roadmap.
Benchmark claims should be treated as reproducible measurements rather than constants: compiler revisions, optimization settings, BLAS implementations, hardware and estimator implementations can all move the numbers.
BLAS implementations differ in more than speed. Results verified bit-identical under one BLAS have been observed to differ in the last float32 digit under another, which matters wherever a threshold turns that difference into a different answer. Parity work should be verified on the platform CI runs on.
See AGENTS.md for the full build, measurement and concurrency notes, including why timings taken on a loaded machine are not usable and why BLAS substitutions need a small-input work threshold.
flow-scikit is an independent project. It takes substantial inspiration from scikit-learn's estimator vocabulary and public API discussions, but it is not an official port, fork or subproject.
Where the two libraries differ deliberately in API design, the reasoning is recorded in the design rationale (source), along with the gaps that remain open.
During development, an automated coding agent that was intended to submit work to the upstream Flow repository mis-targeted submissions to a scikit-related upstream repository. Those submissions were unintended automation output. The incident led to stronger repository allow-lists, publication checks, bounded automation and explicit human-visible CI/review gates in this project.
The technical comparisons here are intended to stand or fall on reproducible code and measurements rather than project politics.
Contributions are welcome, particularly when they improve correctness, parity, benchmarks, tests, documentation or reproducibility. Performance changes should include a correctness check and measurements against an appropriate baseline.
Automated contributions are welcome only when they have been reviewed for repository scope and can pass the same validation expected of human-authored changes.
See LICENSE for the repository's license terms.