Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
._*

*.pyc
*.nbc
*.nbi
*.egg-info
build
dist
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ dependencies = [
"threadpoolctl>=2.1.0",
"tqdm>=4.64.1",
]

[project.optional-dependencies]
numba = [
"numba>=0.59",
"scipy>=1.8.0",
]

[project.urls]
Homepage = "https://github.com/KamitaniLab/PyFastL2LiR"
Repository = "https://github.com/KamitaniLab/PyFastL2LiR"
Expand Down
2 changes: 2 additions & 0 deletions speedtests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
local.sh
*.csv
86 changes: 86 additions & 0 deletions speedtests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Speed Tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we split speedtests/ out into its own PR rather than landing it here? It is ~900 lines across several files and inflates this PR's review scope well beyond the ~157-line core numba change, and it is a separate concern (benchmark tooling) from the solver feature itself. Keeping them apart makes both easier to review and gets the solver work merged sooner.

To be clear, this is worth keeping, not dropping. A reproducible benchmark harness that records the BLAS backend, thread settings, and prediction-difference checks is genuinely valuable for a library whose main claim is speed. I would like it back as a dedicated PR. When it returns, a few portability issues are worth fixing so that others can actually run it:

  • speedrun_legacy.sh runs conda activate base and then executes ${LEGACY_CONDA}/bin/python. This assumes the user has conda installed and that the legacy fastl2lir lives specifically in conda's base environment. Anyone who keeps the legacy package in a named env, or who does not use conda at all, cannot run this. The environment and interpreter should be configurable rather than hardcoded to base.

  • The legacy and MKL runners default to the local paths .venv-legacy and .venv-mkl (in local.sh.example, speedrun_legacy.sh, and speedrun_mkl.sh). Those directories will not exist on another machine, so those two runners only work for whoever happens to have environments at exactly those paths. That is fine as a personal convenience, but it should be documented as required local setup rather than presented as a runner that works out of the box.

  • speedrun_mkl.sh defaults MKL_BENCH_RUNNERS="numpy,numba", so it tries to run the numba solver. But the numba solver raises RuntimeError when it detects an MKL BLAS backend, which is exactly the environment this script targets. As written, the MKL speed run would crash on the numba runner. Either the MKL run should exclude numba, or the MKL rejection policy needs revisiting.

A clear home such as a benchmarks/ directory would also help.

For this PR, it would be enough to run the benchmark on these changes and share a simple per-environment speed comparison (a small table of numpy vs numba median times / speedups across the environments you tested) in the PR description or a comment.


This directory contains speed-run scripts for measuring PyFastL2LiR fitting
runtime. These scripts are not the package test suite; correctness tests live
under `tests/`.

The speed runs generate synthetic fMRI-like regression data, fit FastL2LiR on
several target shapes, compare predictions between runners when more than one
runner is used, print timing summaries, and save CSV summaries in this
directory.

## Files

- `speedrun.sh`: Run the current working-tree package with the project Python
environment. It compares `solver="numpy"` and `solver="numba"`.
- `speedrun.py`: Python entry point used by `speedrun.sh`.
- `speedrun_legacy.sh`: Run the legacy installed `fastl2lir` package from a
configured legacy Python environment.
- `speedrun_legacy.py`: Python entry point used by `speedrun_legacy.sh`.
- `speedrun_mkl.sh`: Run the current working-tree package from a configured
MKL-oriented Python environment.
- `speedtest_common.py`: Shared benchmark setup, timing, prediction checks,
summaries, and CSV writing.
- `local.sh.example`: Template for machine-local environment paths.

## Local Setup

Copy `local.sh.example` to `local.sh` and edit paths for the machine:

```bash
cp speedtests/local.sh.example speedtests/local.sh
```

`local.sh` is ignored by git. Relative paths are resolved from the repository
root.

## Running

Current environment:

```bash
./speedtests/speedrun.sh
```

Legacy environment:

```bash
./speedtests/speedrun_legacy.sh
```

MKL-oriented environment:

```bash
./speedtests/speedrun_mkl.sh
```

For a quick smoke run, restrict the synthetic data size and case list:

```bash
BENCH_N=24 BENCH_P=12 BENCH_K=4 BENCH_REPEATS=1 BENCH_CASES=fc8 ./speedtests/speedrun.sh
```

## Controls

- `BENCH_N`: Number of samples. Default: `1000`.
- `BENCH_P`: Number of input features or voxels. Default: `15000`.
- `BENCH_K`: Number of selected features. Default: `500`.
- `BENCH_REPEATS`: Repeats per benchmark case. Default: `3`.
- `BENCH_CASES`: Comma-separated subset of `fc8`, `fc6`, `conv5_chunk5`, and
`conv4_chunk5`.
- `BENCH_RUNNERS`: Comma-separated subset of available runner labels. The
current speed run supports `numpy` and `numba`; the legacy speed run supports
`legacy`.
- `BENCH_SERVER`: Label written to the terminal output and CSV. Defaults to the
hostname.

## Outputs

CSV files are written under `speedtests/`:

- `speedtest_<server>.csv` for the current and MKL-oriented speed runs.
- `speedtest_legacy_<server>.csv` for the legacy speed run.

The CSV columns include the server label, benchmark case, matrix dimensions,
repeat count, median runtime columns, speedup columns when multiple runners are
present, and the maximum prediction difference on a small validation slice.
12 changes: 12 additions & 0 deletions speedtests/local.sh.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copy this file to speedtests/local.sh and edit paths for your machine.
# speedtests/local.sh is intentionally ignored by git.

# Python environment used by speedtests/speedrun.sh. Relative paths are resolved from the
# repository root.
VENV_PATH=.venv

# Legacy environment used by speedtests/speedrun_legacy.sh.
LEGACY_CONDA=.venv-legacy

# MKL-oriented environment used by speedtests/speedrun_mkl.sh.
MKL_CONDA=.venv-mkl
35 changes: 35 additions & 0 deletions speedtests/speedrun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Current-environment speed test for PyFastL2LiR implementations.

This script compares L2-regularized linear regression fits on synthetic
fMRI-like matrices in the active project Python environment. The mathematical
objects are a Gaussian input matrix X, Gaussian target arrays Y with dense and
convolutional feature shapes, ridge weights W, bias b, and predictions on a
small validation slice. The execution stages are environment reporting,
synthetic data generation, repeated fitting for the package numpy solver, the
package numba solver, prediction-difference checks, median runtime summaries,
and CSV writing. The saved output is speedtests/speedtest_<server>.csv.

Run directly with:
speedtests/speedrun.sh
"""

import os
import socket
from pathlib import Path

import fastl2lir

from speedtest_common import run_speedtest

server_name = os.environ.get("BENCH_SERVER") or socket.gethostname()

run_speedtest(
fastl2lir_module=fastl2lir,
runners=[
{"label": "numpy", "fit_kwargs": {"solver": "numpy"}},
{"label": "numba", "fit_kwargs": {"solver": "numba"}},
],
output_csv=Path("speedtests") / f"speedtest_{server_name}.csv",
comparison="current FastL2LiR.fit numpy and numba solver comparison",
module_names=("numba",),
)
73 changes: 73 additions & 0 deletions speedtests/speedrun.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
local_config="${script_dir}/local.sh"

if [[ -f "$local_config" ]]; then
# shellcheck source=/dev/null
source "$local_config"
fi

resolve_path() {
local path_value="$1"

if [[ "$path_value" = /* ]]; then
printf "%s\n" "$path_value"
else
printf "%s/%s\n" "$repo_root" "$path_value"
fi
}

cd "$repo_root"

# Use the project Python environment only. Thread handling is intentionally
# left to fastl2lir and the runtime environment under test.
export PYTHONUNBUFFERED="${PYTHONUNBUFFERED:-1}"
export BENCH_SERVER="${BENCH_SERVER:-$(hostname)}"

VENV_PATH="${VENV_PATH:-.venv}"
python_bin="$(resolve_path "$VENV_PATH")/bin/python"

if [[ ! -x "$python_bin" ]]; then
if [[ "$VENV_PATH" != ".venv" ]]; then
echo "ERROR: Python is not executable: $python_bin" >&2
echo "Create that environment or update VENV_PATH in speedtests/local.sh." >&2
exit 1
fi
if ! command -v uv >/dev/null 2>&1; then
echo "ERROR: .venv is missing and uv is not available." >&2
echo "Install uv or set VENV_PATH in speedtests/local.sh." >&2
exit 1
fi
uv sync --extra numba
python_bin="$(resolve_path "$VENV_PATH")/bin/python"
fi

if ! "$python_bin" - <<'PY'
import sys

if sys.version_info < (3, 10):
raise SystemExit("Python >= 3.10 is required")

import fastl2lir # noqa: F401
import numba # noqa: F401
import numpy # noqa: F401
import scipy # noqa: F401
PY
then
if [[ "$VENV_PATH" != ".venv" ]]; then
echo "ERROR: Python environment is incomplete: $python_bin" >&2
echo "Install fastl2lir, numpy, scipy, and numba there, or update VENV_PATH." >&2
exit 1
fi
if ! command -v uv >/dev/null 2>&1; then
echo "ERROR: Python environment is incomplete and uv is not available." >&2
echo "Set VENV_PATH in speedtests/local.sh to a complete environment." >&2
exit 1
fi
uv sync --extra numba
fi

exec "$python_bin" -u speedtests/speedrun.py "$@"
34 changes: 34 additions & 0 deletions speedtests/speedrun_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Legacy-environment speed test for PyFastL2LiR.

This script measures L2-regularized linear regression runtime for the legacy
FastL2LiR implementation in the configured legacy Python environment. The
mathematical objects are synthetic Gaussian input matrices X, synthetic target
arrays Y, ridge weights W, bias b, and validation predictions used to check
repeat consistency. The execution stages are environment reporting, benchmark
case construction, repeated legacy fitting, median runtime summarization, and
CSV writing. The saved output is speedtests/speedtest_legacy_<server>.csv.

Run directly with:
speedtests/speedrun_legacy.sh
"""

import os
import socket
from pathlib import Path

import fastl2lir

from speedtest_common import run_speedtest


server_name = os.environ.get("BENCH_SERVER") or socket.gethostname()

run_speedtest(
fastl2lir_module=fastl2lir,
runners=[
{"label": "legacy", "fit_kwargs": {}},
],
output_csv=Path("speedtests") / f"speedtest_legacy_{server_name}.csv",
comparison="legacy FastL2LiR.fit runtime",
module_names=("scipy",),
)
56 changes: 56 additions & 0 deletions speedtests/speedrun_legacy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
local_config="${script_dir}/local.sh"

if [[ -f "$local_config" ]]; then
# shellcheck source=/dev/null
source "$local_config"
fi

resolve_path() {
local path_value="$1"

if [[ "$path_value" = /* ]]; then
printf "%s\n" "$path_value"
else
printf "%s/%s\n" "$repo_root" "$path_value"
fi
}

cd "$repo_root"

export PYTHONUNBUFFERED="${PYTHONUNBUFFERED:-1}"
export BENCH_SERVER="${BENCH_SERVER:-$(hostname)}"

LEGACY_CONDA="${LEGACY_CONDA:-.venv-legacy}"
conda_root="$(resolve_path "$LEGACY_CONDA")"
python_bin="${conda_root}/bin/python"

if [[ ! -x "$python_bin" ]]; then
echo "ERROR: legacy Python is not executable: $python_bin" >&2
echo "Set LEGACY_CONDA in speedtests/local.sh." >&2
exit 1
fi

if [[ -f "${conda_root}/etc/profile.d/conda.sh" ]]; then
source "${conda_root}/etc/profile.d/conda.sh"
conda activate base
else
export PATH="${conda_root}/bin:${PATH}"
fi

"$python_bin" - <<'PY'
import sys

import fastl2lir # noqa: F401
import numpy # noqa: F401
import scipy # noqa: F401

if sys.version_info[:2] != (3, 8):
print(f"WARNING: expected Python 3.8, got {sys.version.split()[0]}")
PY

exec "$python_bin" -u speedtests/speedrun_legacy.py "$@"
Loading