Skip to content

Commit

Permalink
feat: add free-threading example (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
henryiii authored Jun 1, 2024
1 parent 6a8fa9d commit d06251f
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 3 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ jobs:
- uses: wntrblm/[email protected]
- run: nox -s ${{ matrix.session }}

free-threading:
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
runs-on: [ubuntu-latest, windows-latest]
name: cibw on ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- uses: pypa/[email protected]
with:
package-dir: projects/hello-free-threading
env:
CIBW_PRERELEASE_PYTHONS: True

pass:
if: always()
needs: [checks]
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ docs/_build
# build output (testing)
skbuild/cmake_test_compile/*
*.env
wheelhouse
dist/*

# Generated manifests
MANIFEST
2 changes: 2 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"core-pybind11-hello",
"hatchling-pybind11-hello",
]
if sys.version_info >= (3, 13):
hello_list.append("hello-free-threading")


@nox.session
Expand Down
16 changes: 16 additions & 0 deletions projects/hello-free-threading/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.15...3.29)

project(FreeComputePi LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to use")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)

python_add_library(_core MODULE src/freecomputepi/_core.cpp WITH_SOABI)

install(TARGETS _core DESTINATION freecomputepi)
13 changes: 13 additions & 0 deletions projects/hello-free-threading/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[build-system]
requires = ["scikit-build-core >=0.9.5"]
build-backend = "scikit_build_core.build"

[project]
name = "freecomputepi"
version = "0.0.1"

[tool.cibuildwheel]
build = "cp313*"
free-threaded-support = true
test-command = "pytest {package} --durations=0"
test-requires = ["pytest"]
Empty file.
68 changes: 68 additions & 0 deletions projects/hello-free-threading/src/freecomputepi/_core.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <random>


static PyObject * pi(PyObject *self, PyObject *arg) {
int n = PyLong_AsLong(arg);
if (n == -1 && PyErr_Occurred()) {
return NULL;
}
double sum = 0.0;

std::random_device r;
std::default_random_engine e1(r());
std::uniform_real_distribution<double> uniform_dist(-1, 1);

for (int i = 0; i < n; i++) {
double x = uniform_dist(e1);
double y = uniform_dist(e1);
if (x * x + y * y <= 1.0) {
sum += 1.0;
}
}

return Py_BuildValue("d", 4.0 * sum / n);
}

extern "C" {

static PyMethodDef core_methods[] = {
{"pi", pi, METH_O, "Compute pi"},
{NULL, NULL, 0, NULL} /* Sentinel */
};

static int _core_exec(PyObject *m) {
if (PyModule_AddFunctions(m, core_methods) < 0)
return -1;
return 0;
}

static PyModuleDef_Slot module_slots[] = {
{Py_mod_exec, (void*) _core_exec},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL},
};

static struct PyModuleDef coremodule = {
PyModuleDef_HEAD_INIT,
"_core",
NULL,
0,
core_methods,
module_slots,
NULL,
NULL,
NULL,
};


PyMODINIT_FUNC PyInit__core(void) {
return PyModuleDef_Init(&coremodule);
}

};
30 changes: 30 additions & 0 deletions projects/hello-free-threading/src/freecomputepi/comp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import argparse
import statistics
import time
from concurrent.futures import ThreadPoolExecutor

from ._core import pi


def pi_in_threads(threads: int, trials: int) -> float:
if threads == 0:
return pi(trials)
with ThreadPoolExecutor(max_workers=threads) as executor:
return statistics.mean(executor.map(pi, [trials // threads] * threads))


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--threads", type=int, default=0)
parser.add_argument("--trials", type=int, default=100_000_000)
args = parser.parse_args()

start = time.monotonic()
π = pi_in_threads(args.threads, args.trials)
stop = time.monotonic()

print(f"{args.trials} trials, {π = }, {stop - start:.4} s")


if __name__ == "__main__":
main()
44 changes: 44 additions & 0 deletions projects/hello-free-threading/src/freecomputepi/pure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import argparse
import random
import statistics
import time
from concurrent.futures import ThreadPoolExecutor


def pi(trials: int) -> float:
Ncirc = 0
ran = random.Random()

for _ in range(trials):
x = ran.uniform(-1, 1)
y = ran.uniform(-1, 1)

test = x * x + y * y
if test <= 1:
Ncirc += 1

return 4.0 * (Ncirc / trials)


def pi_in_threads(threads: int, trials: int) -> float:
if threads == 0:
return pi(trials)
with ThreadPoolExecutor(max_workers=threads) as executor:
return statistics.mean(executor.map(pi, [trials // threads] * threads))


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--threads", type=int, default=0)
parser.add_argument("--trials", type=int, default=10_000_000)
args = parser.parse_args()

start = time.monotonic()
π = pi_in_threads(args.threads, args.trials)
stop = time.monotonic()

print(f"{args.trials} trials, {π = }, {stop - start:.4} s")


if __name__ == "__main__":
main()
15 changes: 15 additions & 0 deletions projects/hello-free-threading/tests/test_thread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import freecomputepi.comp
import freecomputepi.pure
import pytest


@pytest.mark.parametrize("threads", [0, 1, 2, 4])
def test_pure(threads):
π = freecomputepi.pure.pi_in_threads(threads, 10_000_000)
assert π == pytest.approx(3.1415926535, rel=0.01)


@pytest.mark.parametrize("threads", [0, 1, 2, 4])
def test_comp(threads):
π = freecomputepi.comp.pi_in_threads(threads, 100_000_000)
assert π == pytest.approx(3.1415926535, rel=0.01)
3 changes: 0 additions & 3 deletions pytest.ini

This file was deleted.

0 comments on commit d06251f

Please sign in to comment.