Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .github/workflows/flow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain

- uses: actions/setup-python@v5
Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain

- uses: actions/setup-python@v5
Expand All @@ -109,7 +109,7 @@ jobs:
FLOW_BIN: ${{ github.workspace }}/.flow-toolchain/flow
FLOW_HOST: python
FLOW_OPT_LEVEL: "0"
FLOW_LDFLAGS: "-lm -lopenblas"
FLOW_LDFLAGS: "-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
run: python tools/run_all.py

benchmark-contract:
Expand All @@ -130,7 +130,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain

- uses: actions/setup-python@v5
Expand All @@ -146,8 +146,8 @@ jobs:
- name: Run repeated canonical benchmark and parity gate
env:
FLOW_HOST: python
FLOW_OPT_LEVEL: "0"
FLOW_LDFLAGS: "-lm -lopenblas"
FLOW_OPT_LEVEL: "3"
FLOW_LDFLAGS: "-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
FLOW_HEADLINE_COMMAND: ${{ github.workspace }}/.flow-toolchain/flow run benchmarks/bench_flow_v2.flow
run: python benchmarks/run_headline.py --repeats 5

Expand Down Expand Up @@ -176,7 +176,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain

- uses: actions/setup-python@v5
Expand All @@ -195,8 +195,8 @@ jobs:
- name: Run repeated scaled Flow matrix through 10k rows
env:
FLOW_HOST: python
FLOW_OPT_LEVEL: "0"
FLOW_LDFLAGS: "-lm -lopenblas"
FLOW_OPT_LEVEL: "3"
FLOW_LDFLAGS: "-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
run: |
rm -f benchmarks/scaled_flow_samples.txt
for repeat in 1 2 3; do
Expand Down Expand Up @@ -232,7 +232,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain

- uses: actions/setup-python@v5
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/remaining-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: flooooooooooow/flow
ref: 3f6031813912e8f77bcebe18c5b25dc35861216b
ref: 88aac5095488309813c7173d268c1f8260421c6e
path: .flow-toolchain
- uses: actions/setup-python@v5
with:
Expand All @@ -53,7 +53,7 @@ jobs:
FLOW_HEADLINE_COMMAND: ${{ github.workspace }}/.flow-toolchain/flow run benchmarks/bench_flow_v2.flow
FLOW_HOST: python
FLOW_OPT_LEVEL: "0"
FLOW_LDFLAGS: "-lm -lopenblas"
FLOW_LDFLAGS: "-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
# Both sides call into OpenBLAS, and its thread count was left to
# runtime detection. Pinned to 1 on a trial run, LogisticRegression on
# digits lands at -1.88 log2 instead of the usual +1.08, which is the
Expand Down
38 changes: 36 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Filed issues so far:
code generation for regression and clustering. Ridge R2 jumps from 0.33
to 0.61, KMeans iris drops from 0.80 to 0.47. Workaround: import prng.flow
directly from cluster.flow and ensemble.flow instead of through scikit.flow.
- #843: FIXED in Flow, merged as `88aac509` (PR #846). A function name used as a value
emitted the bare Flow name in the generated C, which does not exist, so a
Flow callback could not be handed to a C dispatcher and
`lib/scikit/threading.flow` had no caller. RandomForest now fits its trees
through `flow_parallel_for`. KMeans n_init restarts are still sequential
and are the obvious next use of it.
- #547: RETRACTED, closed as invalid. This was reported as dead code in an
uncalled module deciding whether an unrelated program corrupts its heap.
It was not a compiler bug. `examples/regression_demo.flow` hardcoded
Expand Down Expand Up @@ -70,6 +76,18 @@ Filed issues so far:
- Flow structs are passed by value. Mutating functions must return the struct.
- Use generous allocation sizes (128+ bytes per struct) on arm64.

## Toolchain requirement

RandomForest fits its trees concurrently, which needs a Flow compiler that can
take a function's address. Before Flow PR #846 a function named as a value
emitted the source-level name and the generated C failed with
`use of undeclared identifier`. That was Flow issue #843.

`.github/workflows/flow.yml` and `remaining-issues.yml` pin the toolchain by
commit. The pin has to name a commit containing that fix or
`lib/scikit/ensemble.flow` will not compile. It names `88aac509`, the commit
that merged #846 into Flow `main`.

## Build and test

BLAS linkage is required. Without `FLOW_LDFLAGS` the build fails at the
Expand All @@ -79,15 +97,15 @@ macOS:
```
export FLOW_HOST=python
export FLOW_OPT_LEVEL=0
export FLOW_LDFLAGS="-framework Accelerate"
export FLOW_LDFLAGS="-framework Accelerate lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
flow run tests/test_new_features.flow
```

Linux, matching CI:
```
export FLOW_HOST=python
export FLOW_OPT_LEVEL=0
export FLOW_LDFLAGS="-lm -lopenblas"
export FLOW_LDFLAGS="-lm -lopenblas lib/scikit/flow_time.c lib/scikit/flow_parallel.c"
flow run tests/test_new_features.flow
```

Expand All @@ -96,6 +114,22 @@ Run everything the way CI does:
python tools/run_all.py
```

Benchmarks are compiled at `-O3` and link the timing shim:

```bash
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 7
```

scikit-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` passes a file purely on its process exit code. A test
that prints `FAIL` and returns 0 is invisible. New tests must count
failures and `return 1` from `main`; see `tests/test_preprocessing.flow`.
Expand Down
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The goal is not a line-for-line port. The project implements familiar estimators

## Why traditional machine learning still matters

A useful practitioner snapshot appears in the r/datascience discussion [Do people not use sci-kit learn / other traditional libraries anymore?](https://www.reddit.com/r/datascience/comments/16lu9ni/do_people_not_use_scikit_learn_other_traditional/). 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.
A useful practitioner snapshot appears in the r/datascience discussion ["Do people not use sci-kit learn / other traditional libraries anymore?"](https://www.reddit.com/r/datascience/comments/16lu9ni/do_people_not_use_scikit_learn_other_traditional/). 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.

Expand All @@ -28,7 +28,7 @@ That makes classical ML particularly interesting for native applications, embedd

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`](benchmarks/headline_result_v2.json): **19 of 19 rows are parity-eligible and measurement-resolved**. In that committed run, **Flow wins 11 of 19 end-to-end fit + predict comparisons and scikit-learn wins 7 of 19**. There are no parity-unresolved or measurement-unresolved rows.
The current canonical v2 result is [`benchmarks/headline_result_v2.json`](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.

Expand All @@ -40,7 +40,7 @@ See the [full canonical benchmark report](https://godofecht.github.io/flow-sciki

## What sklearn actually executes

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.
"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:

Expand All @@ -51,7 +51,15 @@ flow-scikit now maintains a generated execution map rather than inferring opport
- **8 whole-estimator experiments**
- substrate and speedup joins for **all 19 canonical benchmark rows**

The current grouped headline evidence is descriptive rather than causal: Flow wins **75% of Python-bound rows**, about **45% of mixed rows**, and **0% of external-native-bound rows** in the committed architecture map. That pattern is useful enough to guide engineering: optimize Python/boundary-heavy paths aggressively, treat sklearn-owned compiled code as a direct implementation contest, and retain mature BLAS/LAPACK/liblinear/libsvm kernels unless measurements justify replacement.
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 20.99x on Python-bound rows, 6.84x on mixed rows and 3.96x 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.

Detailed artifacts:

Expand Down Expand Up @@ -83,12 +91,28 @@ cd flow-scikit
# link step with undefined cblas_* symbols.
export FLOW_HOST=python
export FLOW_OPT_LEVEL=0
export FLOW_LDFLAGS="-framework Accelerate" # macOS
# export FLOW_LDFLAGS="-lm -lopenblas" # Linux, matching CI
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.py
```

Benchmarks are compiled at `-O3` and link the timing shim:

```bash
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 7
```

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

Expand Down
44 changes: 22 additions & 22 deletions benchmarks/ARCHITECTURE_PERFORMANCE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@ This report is generated from the committed inventory, mixed-stack profiles, par

| Algorithm | sklearn estimator | Dataset | Substrate | Parity | Flow/sklearn speedup | Python self share |
|---|---|---|---|---|---:|---:|
| `LogisticRegression` | `LogisticRegression` | iris | mixed | approximately equivalent | 14.60× | 76.4% |
| `LinearSVC` | `LinearSVC` | iris | external-native-bound | approximately equivalent | 0.67× | |
| `KernelSVC_RBF` | `SVC` | iris | external-native-bound | approximately equivalent | 1.02× | 88.4% |
| `DecisionTree` | `DecisionTreeClassifier` | iris | mixed | approximately equivalent | 6.81× | |
| `RandomForest` | `RandomForestClassifier` | iris | mixed | approximately equivalent | 10.51× | |
| `GaussianNB` | `GaussianNB` | iris | python-bound | parity verified | 53.92× | 77.4% |
| `KMeans` | `KMeans` | iris | mixed | approximately equivalent | 8.47× | 83.1% |
| `PCA` | `PCA` | iris | python-bound | parity verified | 11.83× | 76.0% |
| `LogisticRegression` | `LogisticRegression` | digits | mixed | approximately equivalent | 2.64× | 76.4% |
| `LinearSVC` | `LinearSVC` | digits | external-native-bound | approximately equivalent | 0.23× | |
| `KernelSVC_RBF` | `SVC` | digits | external-native-bound | approximately equivalent | 0.63× | 88.4% |
| `DecisionTree` | `DecisionTreeClassifier` | digits | mixed | approximately equivalent | 0.47× | |
| `RandomForest` | `RandomForestClassifier` | digits | mixed | approximately equivalent | 0.31× | |
| `GaussianNB` | `GaussianNB` | digits | python-bound | parity verified | 1.00× | 77.4% |
| `KMeans` | `KMeans` | digits | mixed | approximately equivalent | 0.19× | 83.1% |
| `Ridge` | `Ridge` | diabetes | python-bound | approximately equivalent | 7.31× | |
| `Lasso` | `Lasso` | diabetes | mixed | approximately equivalent | 1.09× | |
| `LinearRegression` | `LinearRegression` | diabetes | mixed | parity verified | 8.52× | 76.1% |
| `KernelRidge_RBF` | `KernelRidge` | diabetes | mixed | parity verified | 0.11× | |
| `LogisticRegression` | `LogisticRegression` | iris | mixed | approximately equivalent | 10.42× | 76.4% |
| `LinearSVC` | `LinearSVC` | iris | external-native-bound | approximately equivalent | 5.70× | |
| `KernelSVC_RBF` | `SVC` | iris | external-native-bound | approximately equivalent | 2.14× | 88.4% |
| `DecisionTree` | `DecisionTreeClassifier` | iris | mixed | approximately equivalent | 8.98× | |
| `RandomForest` | `RandomForestClassifier` | iris | mixed | approximately equivalent | 17.45× | |
| `GaussianNB` | `GaussianNB` | iris | python-bound | parity verified | 48.15× | 77.4% |
| `KMeans` | `KMeans` | iris | mixed | approximately equivalent | 11.31× | 83.1% |
| `PCA` | `PCA` | iris | python-bound | parity verified | 16.68× | 76.0% |
| `LogisticRegression` | `LogisticRegression` | digits | mixed | approximately equivalent | 3.18× | 76.4% |
| `LinearSVC` | `LinearSVC` | digits | external-native-bound | approximately equivalent | 4.31× | |
| `KernelSVC_RBF` | `SVC` | digits | external-native-bound | approximately equivalent | 3.20× | 88.4% |
| `DecisionTree` | `DecisionTreeClassifier` | digits | mixed | approximately equivalent | 1.88× | |
| `RandomForest` | `RandomForestClassifier` | digits | mixed | approximately equivalent | 5.24× | |
| `GaussianNB` | `GaussianNB` | digits | python-bound | parity verified | 4.52× | 77.4% |
| `KMeans` | `KMeans` | digits | mixed | approximately equivalent | 1.41× | 83.1% |
| `Ridge` | `Ridge` | diabetes | python-bound | approximately equivalent | 12.55× | |
| `Lasso` | `Lasso` | diabetes | mixed | approximately equivalent | 2.69× | |
| `LinearRegression` | `LinearRegression` | diabetes | mixed | parity verified | 10.32× | 76.1% |
| `KernelRidge_RBF` | `KernelRidge` | diabetes | mixed | parity verified | 2.11× | |

## Speedup grouped by execution substrate

| Substrate | Rows | Mean speedup | Flow win fraction |
|---|---:|---:|---:|
| external-native-bound | 4 | 0.64× | 25% |
| mixed | 11 | 4.88× | 64% |
| python-bound | 4 | 18.51× | 75% |
| external-native-bound | 4 | 3.84× | 100% |
| mixed | 11 | 6.82× | 100% |
| python-bound | 4 | 20.47× | 100% |
Loading
Loading