forkrun 0.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forkrun-0.16.0/LICENSE +21 -0
- forkrun-0.16.0/MANIFEST.in +27 -0
- forkrun-0.16.0/Makefile.substrate +163 -0
- forkrun-0.16.0/PKG-INFO +401 -0
- forkrun-0.16.0/README.md +163 -0
- forkrun-0.16.0/forkrun_ring.c +10374 -0
- forkrun-0.16.0/forkrun_substrate.h +122 -0
- forkrun-0.16.0/pyproject.toml +11 -0
- forkrun-0.16.0/python/README.md +357 -0
- forkrun-0.16.0/python/forkrun/__init__.py +77 -0
- forkrun-0.16.0/python/forkrun/_api.py +247 -0
- forkrun-0.16.0/python/forkrun/_batch.py +148 -0
- forkrun-0.16.0/python/forkrun/_bindings.py +468 -0
- forkrun-0.16.0/python/forkrun/_checkpoint.py +333 -0
- forkrun-0.16.0/python/forkrun/_cuda_guard.py +122 -0
- forkrun-0.16.0/python/forkrun/_executor_core.py +359 -0
- forkrun-0.16.0/python/forkrun/_fd_scrub.py +103 -0
- forkrun-0.16.0/python/forkrun/_numa.py +217 -0
- forkrun-0.16.0/python/forkrun/_pipes.py +71 -0
- forkrun-0.16.0/python/forkrun/_plugin.py +194 -0
- forkrun-0.16.0/python/forkrun/_reactor.py +1342 -0
- forkrun-0.16.0/python/forkrun/_reassembly.py +166 -0
- forkrun-0.16.0/python/forkrun/_resume.py +478 -0
- forkrun-0.16.0/python/forkrun/_shim.c +2840 -0
- forkrun-0.16.0/python/forkrun/_signals.py +224 -0
- forkrun-0.16.0/python/forkrun/_spawn.py +118 -0
- forkrun-0.16.0/python/forkrun/_sweep.py +93 -0
- forkrun-0.16.0/python/forkrun/_worker.py +1048 -0
- forkrun-0.16.0/python/forkrun/exceptions.py +120 -0
- forkrun-0.16.0/python/forkrun/libforkrun_python.so +0 -0
- forkrun-0.16.0/python/forkrun/py.typed +0 -0
- forkrun-0.16.0/python/forkrun/run.py +7479 -0
- forkrun-0.16.0/python/forkrun.egg-info/PKG-INFO +401 -0
- forkrun-0.16.0/python/forkrun.egg-info/SOURCES.txt +38 -0
- forkrun-0.16.0/python/forkrun.egg-info/dependency_links.txt +1 -0
- forkrun-0.16.0/python/forkrun.egg-info/top_level.txt +1 -0
- forkrun-0.16.0/ring_loadables/forkrun_plugin.h +76 -0
- forkrun-0.16.0/setup.cfg +4 -0
- forkrun-0.16.0/setup.py +228 -0
- forkrun-0.16.0/substratestubs.c +42 -0
forkrun-0.16.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 jkool702
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# MANIFEST.in — sdist contents for forkrun (W-PY23).
|
|
2
|
+
#
|
|
3
|
+
# The wheel carries a prebuilt .so, but the sdist must let anyone
|
|
4
|
+
# rebuild it: setup.py's build_py step runs Makefile.substrate, which
|
|
5
|
+
# needs the engine TU, the shim, the stubs, the plugin ABI header,
|
|
6
|
+
# and the makefile itself (all paths relative to the sdist root,
|
|
7
|
+
# mirroring the repo layout).
|
|
8
|
+
|
|
9
|
+
# C sources needed for building the substrate
|
|
10
|
+
include forkrun_ring.c
|
|
11
|
+
include forkrun_substrate.h
|
|
12
|
+
include substratestubs.c
|
|
13
|
+
include Makefile.substrate
|
|
14
|
+
include ring_loadables/forkrun_plugin.h
|
|
15
|
+
|
|
16
|
+
# Python package (modules + shim source + type marker)
|
|
17
|
+
recursive-include python/forkrun *.py *.c
|
|
18
|
+
include python/forkrun/py.typed
|
|
19
|
+
|
|
20
|
+
# Documentation and license (PyPI page + legal)
|
|
21
|
+
include python/README.md
|
|
22
|
+
include LICENSE
|
|
23
|
+
|
|
24
|
+
# Build metadata
|
|
25
|
+
include pyproject.toml
|
|
26
|
+
include setup.py
|
|
27
|
+
include MANIFEST.in
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Stage 1 canary build (v1.3 §2.1): link-checks the substrate boundary.
|
|
2
|
+
# libforkrun.so must carry ZERO bash linkage — enforced by
|
|
3
|
+
# -Wl,--no-undefined plus an explicit allowed-dependency set.
|
|
4
|
+
# The canary compiles the engine TU against stub bash symbols and fails
|
|
5
|
+
# the build if any new bash-internal reference is introduced.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# make -f Makefile.substrate canary # link check only
|
|
9
|
+
# make -f Makefile.substrate check # canary + Stage-0 python tests
|
|
10
|
+
# make -f Makefile.substrate canary CC=aarch64-linux-gnu-gcc OUT=libforkrun.aarch64.so
|
|
11
|
+
# make -f Makefile.substrate python-substrate # libforkrun_python.so for ctypes
|
|
12
|
+
#
|
|
13
|
+
# FALSE-GREEN GUARD: make tracks timestamps, not command lines, so switching CC
|
|
14
|
+
# between invocations used to leave the previous arch's objects AND output in
|
|
15
|
+
# place while this target (whose recipe is only an echo) still printed
|
|
16
|
+
# "canary OK" — reporting success for a stale artifact of the wrong arch. Two
|
|
17
|
+
# defenses: the CCSTAMP prerequisite forces a full rebuild whenever the
|
|
18
|
+
# compiler identity changes, and the arch check below refuses to report OK
|
|
19
|
+
# when the artifact's ELF machine doesn't match the requested compiler.
|
|
20
|
+
CC ?= gcc
|
|
21
|
+
CFLAGS ?= -O1 -fPIC -Wall -Wextra
|
|
22
|
+
INCLUDES = -I/usr/include/bash -I/usr/include/bash/include -I/usr/include/bash/builtins
|
|
23
|
+
|
|
24
|
+
# W-PY23 reproducible builds: same source + same compiler must yield
|
|
25
|
+
# byte-identical outputs. Three inputs defeat that by default:
|
|
26
|
+
# - __FILE__ embeds the compile-time path (varies with the checkout
|
|
27
|
+
# directory) -> -ffile-prefix-map pins it to ".".
|
|
28
|
+
# - __DATE__/__TIME__ in forkrun_ring.c (version/debug output, engine
|
|
29
|
+
# frozen — cannot remove) embed the wall clock -> predefined-macro
|
|
30
|
+
# override pins them (standard reproducible-builds practice; no
|
|
31
|
+
# source change, the strings are informational only).
|
|
32
|
+
# - the GNU build-id hashes link inputs incl. the above -> none.
|
|
33
|
+
# These append (never replace) so a CC/CFLAGS override keeps working;
|
|
34
|
+
# setup.py's fallback gcc line mirrors them (kept in sync by
|
|
35
|
+
# inspection, same rule as the INCLUDES above).
|
|
36
|
+
CFLAGS += -ffile-prefix-map=$(CURDIR)=.
|
|
37
|
+
CFLAGS += -Wno-builtin-macro-redefined '-D__DATE__="Sep 1 2026"' '-D__TIME__="00:00:00"'
|
|
38
|
+
LDFLAGS_REPRO = -Wl,--build-id=none
|
|
39
|
+
|
|
40
|
+
OUT ?= libforkrun.so
|
|
41
|
+
CCSTAMP = .canary-cc
|
|
42
|
+
|
|
43
|
+
# Allowed shared deps for the standalone substrate (explicit set).
|
|
44
|
+
ALLOWED_DEPS = -ldl -lrt
|
|
45
|
+
|
|
46
|
+
CANARY_STUBS = substratestubs.o
|
|
47
|
+
|
|
48
|
+
# W-PY1 Python frontend substrate (Stage 4 Phase 1): python/forkrun/_shim.c
|
|
49
|
+
# textually includes forkrun_ring.c (same TU, so the engine's statics and
|
|
50
|
+
# TLS are visible) and ADDS non-static fr_py_* entry points for ctypes.
|
|
51
|
+
# forkrun_ring.c itself is unmodified. Same flags/hygiene as the canary:
|
|
52
|
+
# no -D_GNU_SOURCE on the CLI (the TU sets its own FTMs first).
|
|
53
|
+
PY_OUT ?= python/forkrun/libforkrun_python.so
|
|
54
|
+
PY_SHIM_OBJ = forkrun_python_shim.o
|
|
55
|
+
|
|
56
|
+
all: canary
|
|
57
|
+
|
|
58
|
+
.PHONY: all canary canary-versions check clean python-substrate FORCE bench bench-small bench-large bench-csv reproducibility-check clean-python
|
|
59
|
+
|
|
60
|
+
# Rewritten only when the compiler identity/options actually change, so its
|
|
61
|
+
# mtime bumps exactly when dependent objects must rebuild.
|
|
62
|
+
$(CCSTAMP): FORCE
|
|
63
|
+
@printf '%s\n' '$(CC) :: $(CFLAGS)' | cmp -s - $@ || \
|
|
64
|
+
printf '%s\n' '$(CC) :: $(CFLAGS)' > $@
|
|
65
|
+
|
|
66
|
+
FORCE:
|
|
67
|
+
|
|
68
|
+
substratestubs.o: substratestubs.c forkrun_substrate.h $(CCSTAMP)
|
|
69
|
+
$(CC) $(CFLAGS) -c substratestubs.c -o $@
|
|
70
|
+
|
|
71
|
+
forkrun_ring_canary.o: forkrun_ring.c forkrun_substrate.h $(CCSTAMP)
|
|
72
|
+
# NOTE: no -D_GNU_SOURCE here — the TU must set its own FTMs (before any
|
|
73
|
+
# libc include). A CLI define would mask exactly the include-order bug
|
|
74
|
+
# class the canary exists to catch.
|
|
75
|
+
$(CC) $(CFLAGS) -DSHELL -DHAVE_CONFIG_H $(INCLUDES) -c forkrun_ring.c -o $@
|
|
76
|
+
|
|
77
|
+
$(OUT): forkrun_ring_canary.o $(CANARY_STUBS)
|
|
78
|
+
$(CC) -shared -o $@ forkrun_ring_canary.o $(CANARY_STUBS) \
|
|
79
|
+
-Wl,--no-undefined $(ALLOWED_DEPS) $(LDFLAGS_REPRO)
|
|
80
|
+
|
|
81
|
+
forkrun_python_shim.o: python/forkrun/_shim.c forkrun_ring.c forkrun_substrate.h ring_loadables/forkrun_plugin.h $(CCSTAMP)
|
|
82
|
+
# NOTE: same FTM hygiene as the canary — no -D_GNU_SOURCE on the CLI.
|
|
83
|
+
# _shim.c's first directive includes forkrun_ring.c, so the engine's
|
|
84
|
+
# FTM block still precedes the first libc include in this TU.
|
|
85
|
+
$(CC) $(CFLAGS) -DSHELL -DHAVE_CONFIG_H $(INCLUDES) -c python/forkrun/_shim.c -o $@
|
|
86
|
+
|
|
87
|
+
$(PY_OUT): $(PY_SHIM_OBJ) $(CANARY_STUBS)
|
|
88
|
+
$(CC) -shared -o $@ $(PY_SHIM_OBJ) $(CANARY_STUBS) \
|
|
89
|
+
-Wl,--no-undefined $(ALLOWED_DEPS) $(LDFLAGS_REPRO)
|
|
90
|
+
|
|
91
|
+
python-substrate: $(PY_OUT)
|
|
92
|
+
@file $(PY_OUT) | sed 's/^/python substrate artifact: /'
|
|
93
|
+
@echo "python-substrate OK: $(PY_OUT)"
|
|
94
|
+
|
|
95
|
+
# W-PY23: two consecutive builds must be byte-identical (same
|
|
96
|
+
# source + same compiler). Touches the shim so both builds
|
|
97
|
+
# recompile+relink fully (make would otherwise no-op the second).
|
|
98
|
+
reproducibility-check: python-substrate
|
|
99
|
+
@echo "=== Reproducibility check ($(PY_OUT)) ==="
|
|
100
|
+
@sha256sum $(PY_OUT) > /tmp/fr_build1.sha
|
|
101
|
+
@touch python/forkrun/_shim.c
|
|
102
|
+
@$(MAKE) -f Makefile.substrate python-substrate > /dev/null
|
|
103
|
+
@sha256sum $(PY_OUT) > /tmp/fr_build2.sha
|
|
104
|
+
@if diff /tmp/fr_build1.sha /tmp/fr_build2.sha > /dev/null 2>&1; then \
|
|
105
|
+
echo "REPRODUCIBLE: consecutive $(PY_OUT) builds are byte-identical"; \
|
|
106
|
+
else \
|
|
107
|
+
echo "NOT REPRODUCIBLE: builds differ:"; \
|
|
108
|
+
cat /tmp/fr_build1.sha /tmp/fr_build2.sha; \
|
|
109
|
+
exit 1; \
|
|
110
|
+
fi
|
|
111
|
+
@rm -f /tmp/fr_build1.sha /tmp/fr_build2.sha
|
|
112
|
+
|
|
113
|
+
clean-python:
|
|
114
|
+
rm -f $(PY_SHIM_OBJ) $(PY_OUT) $(CCSTAMP)
|
|
115
|
+
|
|
116
|
+
bench: python-substrate
|
|
117
|
+
python3 python/benchmarks/run_all.py --scale medium --trials 3
|
|
118
|
+
|
|
119
|
+
bench-small: python-substrate
|
|
120
|
+
python3 python/benchmarks/run_all.py --scale small --trials 2
|
|
121
|
+
|
|
122
|
+
bench-large: python-substrate
|
|
123
|
+
python3 python/benchmarks/run_all.py --scale large --trials 5
|
|
124
|
+
|
|
125
|
+
bench-csv: python-substrate
|
|
126
|
+
python3 python/benchmarks/run_all.py --scale medium --trials 5 \
|
|
127
|
+
--csv python/benchmarks/results/pybench.csv
|
|
128
|
+
|
|
129
|
+
canary: $(OUT)
|
|
130
|
+
@file $(OUT) | sed 's/^/canary artifact: /'
|
|
131
|
+
# ARCH ASSERTION uses the compiler's own TARGET TRIPLE, not its NAME:
|
|
132
|
+
# pattern-matching on $(CC) false-REDs on a native aarch64 host running
|
|
133
|
+
# plain `gcc` (the old *gcc* arm grepped for x86-64 and failed a correct
|
|
134
|
+
# artifact). `$(CC) -dumpmachine` is the correct identity source.
|
|
135
|
+
# Arches with no mapping below (ppc64le / s390x / riscv64) get the link
|
|
136
|
+
# check only: on those the canary's job is -Wl,--no-undefined, not an
|
|
137
|
+
# ELF-machine assertion.
|
|
138
|
+
@trip="$$($(CC) -dumpmachine 2>/dev/null)"; \
|
|
139
|
+
case "$$trip" in \
|
|
140
|
+
*aarch64*) want=aarch64 ;; \
|
|
141
|
+
*x86_64*) want=x86-64 ;; \
|
|
142
|
+
*) want="" ;; \
|
|
143
|
+
esac; \
|
|
144
|
+
echo "canary compiler: $(CC) -> $${trip:-unknown} (expecting ELF machine: $${want:-<link-check only>})"; \
|
|
145
|
+
if [ -n "$$want" ] && ! file $(OUT) | grep -q "$$want"; then \
|
|
146
|
+
echo "canary FAIL: $(CC) targets '$$trip' but $(OUT) is not '$$want' (stale/wrong-arch artifact)" >&2; \
|
|
147
|
+
exit 1; \
|
|
148
|
+
fi
|
|
149
|
+
@echo "canary OK: $(OUT) links with no undefined symbols"
|
|
150
|
+
|
|
151
|
+
# W-BASHCOMPAT-BC5: version-floor gate over the canary closure. Every
|
|
152
|
+
# bash-internal stub carries its first-export version (or an explicit
|
|
153
|
+
# unexported marker); the checker fails on missing annotations,
|
|
154
|
+
# post-floor versions, and stub-matched undefined symbols above the
|
|
155
|
+
# floor in the shipped prebuilts. Run wherever the canary runs.
|
|
156
|
+
canary-versions:
|
|
157
|
+
python3 tools/check_canary_versions.py
|
|
158
|
+
|
|
159
|
+
check: canary
|
|
160
|
+
python3 -m unittest discover -s python/tests
|
|
161
|
+
|
|
162
|
+
clean:
|
|
163
|
+
rm -f *.o *.so $(CCSTAMP) $(PY_SHIM_OBJ) $(PY_OUT)
|
forkrun-0.16.0/PKG-INFO
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forkrun
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: NUMA-aware contention-free streaming parallelization for Python
|
|
5
|
+
Home-page: https://github.com/jkool702/forkrun
|
|
6
|
+
Author: forkrun contributors
|
|
7
|
+
Author-email:
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/jkool702/forkrun
|
|
10
|
+
Project-URL: Documentation, https://github.com/jkool702/forkrun/tree/main/DOCS
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/jkool702/forkrun/issues
|
|
12
|
+
Project-URL: Benchmarks, https://github.com/jkool702/forkrun/tree/main/python/benchmarks
|
|
13
|
+
Keywords: parallel,parallelization,streaming,NUMA,high-performance,data-processing,fork,multiprocessing,concurrent,pipeline
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: C
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
27
|
+
Classifier: Topic :: System
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering
|
|
29
|
+
Classifier: Topic :: Utilities
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: project-url
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# forkrun Python frontend — v0.16.0 (W-PY23 release prep)
|
|
46
|
+
|
|
47
|
+
> **New here?** Start with [`docs/QUICKSTART.md`](docs/QUICKSTART.md)
|
|
48
|
+
> (5 minutes to your first parallel job). Full user guides:
|
|
49
|
+
> [`docs/API.md`](docs/API.md),
|
|
50
|
+
> [`docs/MODES.md`](docs/MODES.md),
|
|
51
|
+
> [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md),
|
|
52
|
+
> [`docs/FAULT_TOLERANCE.md`](docs/FAULT_TOLERANCE.md),
|
|
53
|
+
> [`docs/NUMA.md`](docs/NUMA.md),
|
|
54
|
+
> [`docs/STREAMING.md`](docs/STREAMING.md),
|
|
55
|
+
> [`docs/EXAMPLES.md`](docs/EXAMPLES.md),
|
|
56
|
+
> [`docs/PLUGINS.md`](docs/PLUGINS.md),
|
|
57
|
+
> [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md),
|
|
58
|
+
> [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md),
|
|
59
|
+
> [`docs/MIGRATION.md`](docs/MIGRATION.md),
|
|
60
|
+
> [`docs/COMPARISON.md`](docs/COMPARISON.md).
|
|
61
|
+
> What follows is the developer/contract reference.
|
|
62
|
+
|
|
63
|
+
Minimum viable `forkrun.run()` over the C substrate via ctypes. No bash in
|
|
64
|
+
the path: Python drives the engine (claim → payload → ack) directly.
|
|
65
|
+
|
|
66
|
+
`forkrun.__version__` is `"0.16.0"`; `forkrun.__engine_version__` reports the
|
|
67
|
+
substrate build (e.g. `"v3.5.2"`, `"unknown"` when the `.so` isn't built).
|
|
68
|
+
|
|
69
|
+
## Build
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
make -f Makefile.substrate python-substrate
|
|
73
|
+
# Output: python/forkrun/libforkrun_python.so (gitignored)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `.so` is built from `python/forkrun/_shim.c`, which textually includes
|
|
77
|
+
`forkrun_ring.c` (same TU, so the engine's statics/TLS are visible) and adds
|
|
78
|
+
non-static `fr_py_*` entry points. `forkrun_ring.c` itself is unmodified.
|
|
79
|
+
|
|
80
|
+
## Install (W-PY10)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install . # builds the substrate, installs the wheel
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- No `src/` move: `package_dir={"": "python"}` maps `python/forkrun/`
|
|
87
|
+
to the wheel top-level; tests never ship (no `__init__.py` under
|
|
88
|
+
`python/tests/`). Version single-sources from `__version__`.
|
|
89
|
+
- System build deps: `gcc make bash-devel` (Fedora; same rule as the
|
|
90
|
+
canary — Debian has no bash-headers package). `setup.py` drives
|
|
91
|
+
`Makefile.substrate` (single source of flags) with a documented
|
|
92
|
+
gcc fallback.
|
|
93
|
+
- Linux-only: import raises `ImportError` off Linux (plan §4).
|
|
94
|
+
- No PyPI upload in this work order — local wheel (`pip wheel .`)
|
|
95
|
+
only; reproducible-builds/signing gate the first PyPI release.
|
|
96
|
+
|
|
97
|
+
## Use
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import forkrun
|
|
101
|
+
|
|
102
|
+
def upper(batch): # batch: Batch (borrowed memoryview)
|
|
103
|
+
return bytes(batch.data).upper()
|
|
104
|
+
|
|
105
|
+
forkrun.run(upper, "inputs.txt", workers=4) # fire-and-forget
|
|
106
|
+
forkrun.run(upper, "inputs.txt", sink=on_batch) # payload-side sink
|
|
107
|
+
results = forkrun.map(upper, "inputs.txt", # batch-granular,
|
|
108
|
+
workers=4, order="index") # ordered by batch_index
|
|
109
|
+
for r in forkrun.stream(upper, "inputs.txt"): ... # TRUE v1 streaming:
|
|
110
|
+
# yields while workers run
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `payload`: `"pkg.mod:func"` (imported post-fork in the worker — keeps the
|
|
114
|
+
parent virgin of native imports) or a callable (fork-inherited, never
|
|
115
|
+
pickled). Receives a `Batch`, returns `bytes`/`memoryview`/`str`/`None`.
|
|
116
|
+
- `source`: path | fd | pipe/socket object. v0.5 **materializes** the input
|
|
117
|
+
into an ingress memfd and mmaps it whole: bounded inputs only.
|
|
118
|
+
- `Batch.data`: borrowed `memoryview` (zero-copy MAP_SHARED window). Valid
|
|
119
|
+
during the payload call only; `batch.copy()` persists. `line_count` is
|
|
120
|
+
`None` in byte mode. `offsets` are lazy absolute plane coordinates.
|
|
121
|
+
- `on_error`: `"retry"` (escrow retry then poison-skip, bash `-E` analogue),
|
|
122
|
+
`"skip"`, `"fail-fast"` (global abort, workers exit non-zero).
|
|
123
|
+
- `lines` / `bytes` (mutually exclusive): batching controls. Default
|
|
124
|
+
adaptive already batches in the sweet spot (~4k lines); see below.
|
|
125
|
+
- `streaming`: `None` (default; fifo/socket sources stream, files
|
|
126
|
+
materialize), `True` (bounded-ingress streaming ingest, TB-scale),
|
|
127
|
+
`False` (force materialized).
|
|
128
|
+
- Modes `spawn`/`plugin`, multi-node, ordered emitter pipe, resume: not in
|
|
129
|
+
v0 (`NotImplementedError`).
|
|
130
|
+
- Workers are single-threaded by contract: payloads must not spawn threads
|
|
131
|
+
(documented, not policed at runtime since W-PY21-B — a thread that
|
|
132
|
+
outlives its batch is UB-by-contract); return bytes and
|
|
133
|
+
parallelize in the parent instead.
|
|
134
|
+
|
|
135
|
+
## Upgrade path (v0 → v1)
|
|
136
|
+
|
|
137
|
+
- **Ordered streaming:** `stream()` yields completion order, or
|
|
138
|
+
`batch_index` sequence with `order="index"` (parent-side reassembly;
|
|
139
|
+
landed W-PY7).
|
|
140
|
+
- **Spawn/plugin v1 fast paths (W-PY13, landed):** C-level dispatch with
|
|
141
|
+
v0 fallback — see Modes. Remaining: NUMA multi-node.
|
|
142
|
+
- **Resume UX, halt, TUI:** Stage 6 (demand-pulled).
|
|
143
|
+
|
|
144
|
+
## Modes
|
|
145
|
+
|
|
146
|
+
- `mode="python"` (default): payload is `"pkg.mod:func"` (imported
|
|
147
|
+
post-fork in the worker) or a callable (fork-inherited, never pickled).
|
|
148
|
+
Receives a `Batch`, returns `bytes`/`memoryview`/`str`/`None`.
|
|
149
|
+
Output crosses via `fr_py_emit` (W-PY14): one C call writes the
|
|
150
|
+
header + body with `writev` (zero-copy for `bytes` returns) and the
|
|
151
|
+
signal; `None` emits no record, `b""` emits an empty one. Falls back
|
|
152
|
+
to Python writes under `FORKRUN_NO_V1=1`.
|
|
153
|
+
- `mode="spawn"` (W-PY8, v1 in W-PY13): payload is a COMMAND (`str`
|
|
154
|
+
split on whitespace, or `list` argv — use list form for anything
|
|
155
|
+
quoting would be needed for). Each batch is spliced zero-copy from the
|
|
156
|
+
ingress memfd to the command's stdin; stdout captured as the result.
|
|
157
|
+
Non-zero exit / spawn failure → `SpawnError` → escrow → retry → poison
|
|
158
|
+
(bash `-E` semantics); missing command exits 127 (shell convention,
|
|
159
|
+
retryable — v0 parity). v1 dispatches in C (`fr_py_exec_spawn`:
|
|
160
|
+
`posix_spawnp` + concurrent poll pump, ~10µs overhead vs v0's ~350µs
|
|
161
|
+
`subprocess`); v0 remains as fallback (pre-v1 `.so`,
|
|
162
|
+
`FORKRUN_NO_V1=1`, `sink=` present, or discard mode). Measured
|
|
163
|
+
(i9-7940X): 2.1× at small batches (1.05M vs 0.49M lines/s,
|
|
164
|
+
`lines=100`); parity at adaptive batching (~24M both — command-bound).
|
|
165
|
+
v1 has no per-batch timeout (waits like bash `-X`; v0 keeps its 30s).
|
|
166
|
+
- `mode="plugin"`: payload is `"path:function"` (C `.so` entry point).
|
|
167
|
+
v1 (W-PY13) dispatches in C (`fr_py_plugin_call`) through the FROZEN
|
|
168
|
+
engine ABI (`forkrun_ctx`, 128B, dialect negotiated from the plugin's
|
|
169
|
+
`forkrun_use_ctx` exactly like `ring_call`): the plugin reads the
|
|
170
|
+
input window zero-copy (RAW borrowed window or `pread` on `fd_in`)
|
|
171
|
+
and writes stdout, which the shim stages and frames. **A bash `-C`
|
|
172
|
+
plugin works from Python unchanged and vice versa** (ABI unification).
|
|
173
|
+
v0 (72B `fr_py_plugin_ctx` ctypes convention) remains for plugins
|
|
174
|
+
WITHOUT a `forkrun_use_ctx` export — selected by parent-side probe,
|
|
175
|
+
never guessed. Measured: transform throughput ≈ v0 (0.8–1× on the
|
|
176
|
+
uppercase micro-bench — the capture staging costs what ctypes saves);
|
|
177
|
+
v1's wins are unification + zero-copy input + no per-batch input
|
|
178
|
+
allocation. Non-zero return → escrow → retry → poison, like all
|
|
179
|
+
payload errors.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
forkrun.map("./myplugin.so:process", "data.txt", mode="plugin")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
forkrun.map("gzip -c", "logs.txt", mode="spawn")
|
|
187
|
+
forkrun.map(["sed", "s/old/new/"], "data.txt", mode="spawn")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `mode="splice"` (W-PY18): kernel passthrough, zero Python per batch.
|
|
191
|
+
Payload must be `None`; byte batching only (`bytes=N`, default
|
|
192
|
+
512KB — `lines=` rejected, boundaries never detected); `run()`
|
|
193
|
+
rejected (passthrough produces output — use `map()`/`stream()`).
|
|
194
|
+
Workers run `fr_py_worker_splice_loop` (claim → sendfile → signal →
|
|
195
|
+
ack; output failure rides escrow-retry, signal failure is fatal).
|
|
196
|
+
Measured ≈ Python passthrough (both parent-bound: spill/scan/parse;
|
|
197
|
+
the 2B target needs ~26GB/s end to end — beyond this box's
|
|
198
|
+
tmpfs+Python shape). Full analysis:
|
|
199
|
+
`python/benchmarks/results/splice_study.md`.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
forkrun.map(None, "input.bin", mode="splice", bytes=512*1024)
|
|
203
|
+
for chunk in forkrun.stream(None, "input.bin", mode="splice"):
|
|
204
|
+
process(chunk) # raw byte-windows as they arrive
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- **Zero-copy ingest (W-PY18 addendum):** `_spill_to_memfd` copies
|
|
208
|
+
via `fr_py_copy_range` (copy_file_range, then sendfile, explicit
|
|
209
|
+
offsets both sides — never touches fd positions) with a silent
|
|
210
|
+
pread/pwrite fallback for exotic pairs (pipes that reject kernel
|
|
211
|
+
copy take the original sequential loop verbatim). Measured ~20%
|
|
212
|
+
faster spill (4.5 vs 3.7 GB/s); end-to-end effect is small (spill
|
|
213
|
+
is ~1/5 of map time).
|
|
214
|
+
- **`fr_py_get_raw_window`:** borrowed MAP_SHARED pointer into the
|
|
215
|
+
ingress memfd (the engine's TLS-cached mapping — the same mechanism
|
|
216
|
+
FLAG_RAW delivery uses internally). For future C consumers;
|
|
217
|
+
materialized workers map once (stable), streaming workers may remap
|
|
218
|
+
on growth.
|
|
219
|
+
|
|
220
|
+
## CUDA Policy (v0, W-PY5)
|
|
221
|
+
|
|
222
|
+
Python workers are **CPU-only by default**. forkrun refuses to fork if a
|
|
223
|
+
live CUDA context exists in the parent (fork would corrupt driver state).
|
|
224
|
+
|
|
225
|
+
- **Importing torch is fine** — the guard (`dlopen(RTLD_NOLOAD)` +
|
|
226
|
+
`cuCtxGetCurrent`, `/proc/self/maps` fallback only when inconclusive)
|
|
227
|
+
fires on live contexts, not loaded libraries, so CUDA-virgin scripts
|
|
228
|
+
pass untaxed.
|
|
229
|
+
- **Initializing CUDA before `run()` is refused** with an actionable
|
|
230
|
+
error naming the fix (spawn workers first, then init CUDA; GPU work
|
|
231
|
+
belongs in the parent/consumer).
|
|
232
|
+
- Over-refusal is the safe direction; under-refusal causes UB. An
|
|
233
|
+
early-spawn escape hatch is demand-pulled Stage 6+, not a v0 feature.
|
|
234
|
+
|
|
235
|
+
## Robustness (W-PY4 characterization)
|
|
236
|
+
|
|
237
|
+
- **Faults:** Python exceptions → escrow retry → poison-skip, pipeline
|
|
238
|
+
continues. True process death (segfault) is recovered via respawn
|
|
239
|
+
on the default path (transient deaths complete byte-exact; a batch
|
|
240
|
+
that kills every attempt is poison-skipped, all-death trips the
|
|
241
|
+
bounded respawn cap and raises `RuntimeError`). `orchestrator=False`
|
|
242
|
+
selects legacy fail-fast (any worker death raises `RuntimeError`,
|
|
243
|
+
no zombies). `KeyboardInterrupt` inside a
|
|
244
|
+
payload is a payload error (retry path), not a global abort.
|
|
245
|
+
- **Reuse:** sequential and thread-concurrent `run()` calls in one process
|
|
246
|
+
are correct (a process-wide lock serializes engine access; fd counts
|
|
247
|
+
stable across 10 runs).
|
|
248
|
+
- **Layer 3 (numpy past invalidation):** measured intact for the run's
|
|
249
|
+
duration (worker mmap held, no fallow) — an observation, not a contract.
|
|
250
|
+
v1 fallow/windowing may zero it; never depend on intactness.
|
|
251
|
+
- **RSS:** parent peak is flat across 4x stream growth with no output
|
|
252
|
+
collection, and output-sized under `map` (collect-all); constant-memory
|
|
253
|
+
PIPE drain is v1.
|
|
254
|
+
- **Daemon payloads:** double-forked daemons survive without blocking
|
|
255
|
+
`waitpid`, but inherit the full worker fd set in v0 (no scrubbing yet —
|
|
256
|
+
recorded baseline for v1).
|
|
257
|
+
|
|
258
|
+
## Result crossing (emitter, §3.9b v0.5 → v1)
|
|
259
|
+
|
|
260
|
+
- The parent creates one **output memfd per worker pre-fork** (a memfd
|
|
261
|
+
created post-fork would exist only in the child's fd table); workers
|
|
262
|
+
inherit them and write keyed records
|
|
263
|
+
(`batch_idx u64 LE, length u64 LE, payload bytes`) via copy-on-return.
|
|
264
|
+
- The emitter **transports, it does not order**: `map(order="index")`
|
|
265
|
+
reassembles parent-side over `batch_index` keys. No Python pipe/queue
|
|
266
|
+
ever carries payload bytes; no pickle.
|
|
267
|
+
- **v1 streaming** (`stream()`): after each record the worker emits a
|
|
268
|
+
16-byte `(wid, batch_idx)` signal (indices only) down a pipe; the parent
|
|
269
|
+
`select()`s, `pread()`s the new bytes incrementally, and yields while
|
|
270
|
+
workers run. A slow consumer fills the pipe → workers block in the
|
|
271
|
+
signal write holding unacked batches → claims stop (backpressure).
|
|
272
|
+
`map()` stays on the v0.5 post-completion drain; `run()` (discard /
|
|
273
|
+
worker-side sink) needs no drain at all.
|
|
274
|
+
- **Pipe capacities (W-PY15):** the streaming signal pipe is 1MB
|
|
275
|
+
(65536 outstanding 16B signals vs 4096 at the 64KB default), and the
|
|
276
|
+
spawn stdin/stdout pipes are 1MB (batch-sized transfers without
|
|
277
|
+
intermediate blocking) — all best-effort via `F_SETPIPE_SZ` with
|
|
278
|
+
silent fallback (`forkrun/_pipes.py`). The engine ack pipe stays 4KB
|
|
279
|
+
by design (H3 backpressure invariant); escrow/death pipes untouched.
|
|
280
|
+
- **Ordered streaming** (`stream(order="index")`): parent-side reassembly
|
|
281
|
+
over `batch_idx` keys (bounded out-of-order buffer; poisoned-batch holes
|
|
282
|
+
flush sorted at EOF — brief head-of-line blocking behind a hole is
|
|
283
|
+
inherent). `order="none"` (default) yields completion order.
|
|
284
|
+
- **C-level emit (W-PY14):** `fr_py_emit` replaces header pack + two
|
|
285
|
+
writes + signal pack/write with one call (`writev`, zero-copy `bytes`).
|
|
286
|
+
Measured ≈ v0 (±noise) — the remaining cost is payload-side copies
|
|
287
|
+
(`bytes(data).upper()`), not output syscalls; `map()`/`run()` already
|
|
288
|
+
skipped signals since W-PY7, so the only real saving is one syscall
|
|
289
|
+
per batch. Kept as permanent infra (fewer syscalls, exact v0
|
|
290
|
+
semantics incl. `None`-vs-`b""`).
|
|
291
|
+
- **Streaming ingest (W-PY16):** `streaming=True` (or auto for
|
|
292
|
+
fifo/socket sources) spills the source in 1MB chunks while a forked
|
|
293
|
+
scanner publishes concurrently and a forked reaper
|
|
294
|
+
(`fr_py_fallow_loop` → the engine's own `ring_fallow_main`) punches
|
|
295
|
+
holes behind the contiguous acked prefix. Workers fork on first DATA
|
|
296
|
+
publish (never during pre-flight) and grow their MAP_SHARED view as
|
|
297
|
+
claims advance (zero-copy kept); acks carry the fallow write end.
|
|
298
|
+
Measured: 1GB via pipe with 0.2MB parent RSS growth (fallow-bounded);
|
|
299
|
+
byte-exact vs materialized across python/spawn/plugin × map/stream.
|
|
300
|
+
Slower per-byte than materialized at medium scale (~2-3× CPU, bimodal
|
|
301
|
+
— under investigation; the TB-scale value is capability, not speed).
|
|
302
|
+
Every forked child scrubs host fds (`_fd_scrub`, engine fds kept), so
|
|
303
|
+
forkrun runs inside event-loop hosts (opencode/Jupyter/asyncio).
|
|
304
|
+
- **Batch sizes (W-PY17):** per-batch cost is ~10ns/line at every size
|
|
305
|
+
(it scales with bytes — there is no fixed overhead to amortize), so
|
|
306
|
+
forced large batches peak at ~96M lines/s (8 workers) / 116M
|
|
307
|
+
(1 worker), not 1B+. Sweet spot is lines=1k–10k (adaptive already
|
|
308
|
+
chooses inside it): lines=100 loses ~40%, lines=50k+ loses ~35%
|
|
309
|
+
(starvation: 20 batches ÷ 8 workers). JSONL regresses at 10k
|
|
310
|
+
(payload-bound — tune the payload, not `lines`). Memory flat across
|
|
311
|
+
the 100× range. Full tables:
|
|
312
|
+
`python/benchmarks/results/batch_size_study.md`.
|
|
313
|
+
- **Reactor orchestration (W-PY19, the default since W-REL1/R1):**
|
|
314
|
+
`run`/`map`/`stream` supervise workers with per-worker death pipes
|
|
315
|
+
(kernel-observable exit), bounded respawn (cap 3/slot), trap-ACK
|
|
316
|
+
confirmation (3s grace), and the C orderer for `order="index"`.
|
|
317
|
+
A segfaulted worker is respawned and a transient death completes
|
|
318
|
+
byte-exact (best-effort per batch: a batch that kills every attempt
|
|
319
|
+
is poison-skipped; cap-reaching all-death raises `RuntimeError`),
|
|
320
|
+
with a stderr recovery note; unconfirmed
|
|
321
|
+
death raises `RuntimeError` after the grace. `orchestrator=False`
|
|
322
|
+
selects the legacy fail-fast fork-and-wait path (healthy-path results
|
|
323
|
+
byte-identical either way).
|
|
324
|
+
- **Parameter sweeps (W-PY20):** `forkrun.sweep(payload,
|
|
325
|
+
args=[["a", "b"], ["x", "y"]])` runs the payload once per
|
|
326
|
+
combination (Cartesian; `link=True` zips, `args_from=[files]`
|
|
327
|
+
loads dimensions from files). Each batch's `.metadata` carries
|
|
328
|
+
the sweep tuple; results return in combination order. With
|
|
329
|
+
`source=`, every combination processes the full source.
|
|
330
|
+
- **NUMA multi-node (W-PY21):** `nodes="@N"` (or real topologies
|
|
331
|
+
via `nodes="auto"`/N/`"0,1"`) runs the born-local pipeline
|
|
332
|
+
(per-node indexers/scanners, MPOL_BIND ingest, physical fallow,
|
|
333
|
+
CPU pinning, C orderer with the packed major/minor key).
|
|
334
|
+
Per-node rings batch independently, so parity with UMA is over
|
|
335
|
+
byte content (ordered mode reconstructs the input byte-exact),
|
|
336
|
+
never batch counts. `nodes=1` forces the unchanged UMA path.
|
|
337
|
+
- **C drain (W-PY21-A, opt-in `c_drain=True`, default False):** a
|
|
338
|
+
forked C loop (`fr_py_drain_loop`) moves signal consume + memfd
|
|
339
|
+
pread into a results memfd (map/run) or 1MB pipe (stream,
|
|
340
|
+
backpressure preserved). Framing/parsing untouched —
|
|
341
|
+
byte-identical to the legacy drain. **Measured 0.7-1.0x of
|
|
342
|
+
legacy, so it stays opt-in:** the parent must parse every
|
|
343
|
+
record either way, Python signal reads are already batched,
|
|
344
|
+
and the drain adds a full extra transit of the output bytes.
|
|
345
|
+
No threads anywhere (fork-before-threads intact); the reactor
|
|
346
|
+
is unchanged.
|
|
347
|
+
- **Resume/checkpoint (W-PY22, C-orderer paths only):**
|
|
348
|
+
`map()`/`stream()` with `order="index"` (reactor is the default)
|
|
349
|
+
accept `resume=<ckpt>` (resume FROM) and
|
|
350
|
+
`checkpoint_file=<path>` (publish TO on abort). Anything else
|
|
351
|
+
(`run()`, unordered, non-reactor, splice, multi-node NUMA)
|
|
352
|
+
raises `RuntimeError` — a checkpoint without a tracker would be
|
|
353
|
+
silent loss, so it is refused instead. Checkpoints are BYTE
|
|
354
|
+
coordinates (`FORKRUN_RESUME_HORIZON=<offset>`,
|
|
355
|
+
`FORKRUN_RESUME_JAGGED=("s:e" ...)`), parsed strictly
|
|
356
|
+
(fail-closed) and published atomically (tmp → fsync → chmod
|
|
357
|
+
600 → rename). On abort the parent quiesces (bounded reap),
|
|
358
|
+
drains the C orderer, snapshots the engine ledger
|
|
359
|
+
(`fr_py_resume_snapshot`, same seqlock reader as
|
|
360
|
+
`ring_dump_resume`), and publishes — before teardown destroys
|
|
361
|
+
the engine. `map()` additionally preserves the aborted run's
|
|
362
|
+
committed output in a `<ckpt>.coll` sidecar and prepends it on
|
|
363
|
+
a successful resume (abort+resume is byte-identical to an
|
|
364
|
+
uninterrupted run); `stream()` has no sidecar (the consumer
|
|
365
|
+
owns everything yielded live).
|
|
366
|
+
**Engine commit is exactly-once; Python consumption is not:**
|
|
367
|
+
a crash between engine commit and Python observation means the
|
|
368
|
+
checkpoint skips bytes the caller never saw — persist consumed
|
|
369
|
+
results yourself for end-to-end exactly-once.
|
|
370
|
+
|
|
371
|
+
## Layout
|
|
372
|
+
|
|
373
|
+
- `forkrun/__init__.py` — `run() / map() / stream()` (§3.0) + `Batch`.
|
|
374
|
+
- `forkrun/_api.py` — signature validation incl. source/sink plumbing.
|
|
375
|
+
- `forkrun/_batch.py` — `Batch` (§3.5) with claim→invalidate→ack lifetime.
|
|
376
|
+
- `forkrun/_bindings.py` — ctypes loader + `FrPyBatch` + `fr_py_*` signatures.
|
|
377
|
+
- `forkrun/_shim.c` — Python-facing C entry points (new file; engine frozen).
|
|
378
|
+
- `forkrun/_fd_scrub.py` — post-fork fd hygiene (keep engine + job fds).
|
|
379
|
+
- `forkrun/_pipes.py` — pipe capacity utilities (1MB signal/spawn pipes).
|
|
380
|
+
- `forkrun/_reactor.py` — reactor supervision (death pipes, respawn,
|
|
381
|
+
trap-ACK, C-orderer spawn, scanner death-pipe helpers).
|
|
382
|
+
- `forkrun/_sweep.py` — sweep combination generator (Cartesian/zip,
|
|
383
|
+
args/args_from).
|
|
384
|
+
- `forkrun/_numa.py` — NUMA topology, pinning, worker distribution.
|
|
385
|
+
- `forkrun/run.py` — parent orchestration (init/spill/scan/fork/wait).
|
|
386
|
+
- `forkrun/_worker.py` — forked claim/payload/ack loop (`os._exit` only).
|
|
387
|
+
- `tests/test_api_surface.py` — engine-free validation tests.
|
|
388
|
+
- `tests/test_v0.py` — v0 engine tests (need the built `.so`).
|
|
389
|
+
- `tests/test_reactor.py` — reactor tests (need the built `.so`).
|
|
390
|
+
- `tests/test_sweep.py` — sweep tests (need the built `.so`).
|
|
391
|
+
- `tests/test_numa.py` — NUMA tests (need the built `.so`).
|
|
392
|
+
- `tests/test_c_drain.py` — C drain tests (need the built `.so`).
|
|
393
|
+
|
|
394
|
+
## Key contracts (v1.3)
|
|
395
|
+
|
|
396
|
+
- `source ∈ {path, fd, pipe/socket}` — iterables/generators rejected;
|
|
397
|
+
Python is never an input pump.
|
|
398
|
+
- `sink=None` → discard (v0); callable → payload-side sink in the worker.
|
|
399
|
+
- `mode="python"`, `order ∈ {none, index}` (`map` reorders parent-side).
|
|
400
|
+
- Fork before threads; `os._exit()` in workers; bytes/memoryview default;
|
|
401
|
+
never pickle; no bash subprocess.
|