Skip to content

Commit d06251f

Browse files
authored
feat: add free-threading example (#50)
1 parent 6a8fa9d commit d06251f

File tree

11 files changed

+205
-3
lines changed

11 files changed

+205
-3
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ jobs:
4040
- uses: wntrblm/[email protected]
4141
- run: nox -s ${{ matrix.session }}
4242

43+
free-threading:
44+
runs-on: ${{ matrix.runs-on }}
45+
strategy:
46+
fail-fast: false
47+
matrix:
48+
runs-on: [ubuntu-latest, windows-latest]
49+
name: cibw on ${{ matrix.runs-on }}
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: pypa/[email protected]
53+
with:
54+
package-dir: projects/hello-free-threading
55+
env:
56+
CIBW_PRERELEASE_PYTHONS: True
57+
4358
pass:
4459
if: always()
4560
needs: [checks]

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ docs/_build
5656
# build output (testing)
5757
skbuild/cmake_test_compile/*
5858
*.env
59+
wheelhouse
60+
dist/*
5961

6062
# Generated manifests
6163
MANIFEST

noxfile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"core-pybind11-hello",
1616
"hatchling-pybind11-hello",
1717
]
18+
if sys.version_info >= (3, 13):
19+
hello_list.append("hello-free-threading")
1820

1921

2022
@nox.session
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
cmake_minimum_required(VERSION 3.15...3.29)
2+
3+
project(FreeComputePi LANGUAGES CXX)
4+
5+
set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to use")
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
set(CMAKE_CXX_EXTENSIONS OFF)
8+
9+
find_package(
10+
Python
11+
COMPONENTS Interpreter Development.Module
12+
REQUIRED)
13+
14+
python_add_library(_core MODULE src/freecomputepi/_core.cpp WITH_SOABI)
15+
16+
install(TARGETS _core DESTINATION freecomputepi)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[build-system]
2+
requires = ["scikit-build-core >=0.9.5"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "freecomputepi"
7+
version = "0.0.1"
8+
9+
[tool.cibuildwheel]
10+
build = "cp313*"
11+
free-threaded-support = true
12+
test-command = "pytest {package} --durations=0"
13+
test-requires = ["pytest"]

projects/hello-free-threading/src/freecomputepi/__init__.py

Whitespace-only changes.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#define PY_SSIZE_T_CLEAN
2+
#include <Python.h>
3+
4+
#include <random>
5+
6+
7+
static PyObject * pi(PyObject *self, PyObject *arg) {
8+
int n = PyLong_AsLong(arg);
9+
if (n == -1 && PyErr_Occurred()) {
10+
return NULL;
11+
}
12+
double sum = 0.0;
13+
14+
std::random_device r;
15+
std::default_random_engine e1(r());
16+
std::uniform_real_distribution<double> uniform_dist(-1, 1);
17+
18+
for (int i = 0; i < n; i++) {
19+
double x = uniform_dist(e1);
20+
double y = uniform_dist(e1);
21+
if (x * x + y * y <= 1.0) {
22+
sum += 1.0;
23+
}
24+
}
25+
26+
return Py_BuildValue("d", 4.0 * sum / n);
27+
}
28+
29+
extern "C" {
30+
31+
static PyMethodDef core_methods[] = {
32+
{"pi", pi, METH_O, "Compute pi"},
33+
{NULL, NULL, 0, NULL} /* Sentinel */
34+
};
35+
36+
static int _core_exec(PyObject *m) {
37+
if (PyModule_AddFunctions(m, core_methods) < 0)
38+
return -1;
39+
return 0;
40+
}
41+
42+
static PyModuleDef_Slot module_slots[] = {
43+
{Py_mod_exec, (void*) _core_exec},
44+
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
45+
#ifdef Py_GIL_DISABLED
46+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
47+
#endif
48+
{0, NULL},
49+
};
50+
51+
static struct PyModuleDef coremodule = {
52+
PyModuleDef_HEAD_INIT,
53+
"_core",
54+
NULL,
55+
0,
56+
core_methods,
57+
module_slots,
58+
NULL,
59+
NULL,
60+
NULL,
61+
};
62+
63+
64+
PyMODINIT_FUNC PyInit__core(void) {
65+
return PyModuleDef_Init(&coremodule);
66+
}
67+
68+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import argparse
2+
import statistics
3+
import time
4+
from concurrent.futures import ThreadPoolExecutor
5+
6+
from ._core import pi
7+
8+
9+
def pi_in_threads(threads: int, trials: int) -> float:
10+
if threads == 0:
11+
return pi(trials)
12+
with ThreadPoolExecutor(max_workers=threads) as executor:
13+
return statistics.mean(executor.map(pi, [trials // threads] * threads))
14+
15+
16+
def main() -> None:
17+
parser = argparse.ArgumentParser()
18+
parser.add_argument("--threads", type=int, default=0)
19+
parser.add_argument("--trials", type=int, default=100_000_000)
20+
args = parser.parse_args()
21+
22+
start = time.monotonic()
23+
π = pi_in_threads(args.threads, args.trials)
24+
stop = time.monotonic()
25+
26+
print(f"{args.trials} trials, {π = }, {stop - start:.4} s")
27+
28+
29+
if __name__ == "__main__":
30+
main()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import argparse
2+
import random
3+
import statistics
4+
import time
5+
from concurrent.futures import ThreadPoolExecutor
6+
7+
8+
def pi(trials: int) -> float:
9+
Ncirc = 0
10+
ran = random.Random()
11+
12+
for _ in range(trials):
13+
x = ran.uniform(-1, 1)
14+
y = ran.uniform(-1, 1)
15+
16+
test = x * x + y * y
17+
if test <= 1:
18+
Ncirc += 1
19+
20+
return 4.0 * (Ncirc / trials)
21+
22+
23+
def pi_in_threads(threads: int, trials: int) -> float:
24+
if threads == 0:
25+
return pi(trials)
26+
with ThreadPoolExecutor(max_workers=threads) as executor:
27+
return statistics.mean(executor.map(pi, [trials // threads] * threads))
28+
29+
30+
def main() -> None:
31+
parser = argparse.ArgumentParser()
32+
parser.add_argument("--threads", type=int, default=0)
33+
parser.add_argument("--trials", type=int, default=10_000_000)
34+
args = parser.parse_args()
35+
36+
start = time.monotonic()
37+
π = pi_in_threads(args.threads, args.trials)
38+
stop = time.monotonic()
39+
40+
print(f"{args.trials} trials, {π = }, {stop - start:.4} s")
41+
42+
43+
if __name__ == "__main__":
44+
main()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import freecomputepi.comp
2+
import freecomputepi.pure
3+
import pytest
4+
5+
6+
@pytest.mark.parametrize("threads", [0, 1, 2, 4])
7+
def test_pure(threads):
8+
π = freecomputepi.pure.pi_in_threads(threads, 10_000_000)
9+
assert π == pytest.approx(3.1415926535, rel=0.01)
10+
11+
12+
@pytest.mark.parametrize("threads", [0, 1, 2, 4])
13+
def test_comp(threads):
14+
π = freecomputepi.comp.pi_in_threads(threads, 100_000_000)
15+
assert π == pytest.approx(3.1415926535, rel=0.01)

0 commit comments

Comments
 (0)