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.
Files changed (40) hide show
  1. forkrun-0.16.0/LICENSE +21 -0
  2. forkrun-0.16.0/MANIFEST.in +27 -0
  3. forkrun-0.16.0/Makefile.substrate +163 -0
  4. forkrun-0.16.0/PKG-INFO +401 -0
  5. forkrun-0.16.0/README.md +163 -0
  6. forkrun-0.16.0/forkrun_ring.c +10374 -0
  7. forkrun-0.16.0/forkrun_substrate.h +122 -0
  8. forkrun-0.16.0/pyproject.toml +11 -0
  9. forkrun-0.16.0/python/README.md +357 -0
  10. forkrun-0.16.0/python/forkrun/__init__.py +77 -0
  11. forkrun-0.16.0/python/forkrun/_api.py +247 -0
  12. forkrun-0.16.0/python/forkrun/_batch.py +148 -0
  13. forkrun-0.16.0/python/forkrun/_bindings.py +468 -0
  14. forkrun-0.16.0/python/forkrun/_checkpoint.py +333 -0
  15. forkrun-0.16.0/python/forkrun/_cuda_guard.py +122 -0
  16. forkrun-0.16.0/python/forkrun/_executor_core.py +359 -0
  17. forkrun-0.16.0/python/forkrun/_fd_scrub.py +103 -0
  18. forkrun-0.16.0/python/forkrun/_numa.py +217 -0
  19. forkrun-0.16.0/python/forkrun/_pipes.py +71 -0
  20. forkrun-0.16.0/python/forkrun/_plugin.py +194 -0
  21. forkrun-0.16.0/python/forkrun/_reactor.py +1342 -0
  22. forkrun-0.16.0/python/forkrun/_reassembly.py +166 -0
  23. forkrun-0.16.0/python/forkrun/_resume.py +478 -0
  24. forkrun-0.16.0/python/forkrun/_shim.c +2840 -0
  25. forkrun-0.16.0/python/forkrun/_signals.py +224 -0
  26. forkrun-0.16.0/python/forkrun/_spawn.py +118 -0
  27. forkrun-0.16.0/python/forkrun/_sweep.py +93 -0
  28. forkrun-0.16.0/python/forkrun/_worker.py +1048 -0
  29. forkrun-0.16.0/python/forkrun/exceptions.py +120 -0
  30. forkrun-0.16.0/python/forkrun/libforkrun_python.so +0 -0
  31. forkrun-0.16.0/python/forkrun/py.typed +0 -0
  32. forkrun-0.16.0/python/forkrun/run.py +7479 -0
  33. forkrun-0.16.0/python/forkrun.egg-info/PKG-INFO +401 -0
  34. forkrun-0.16.0/python/forkrun.egg-info/SOURCES.txt +38 -0
  35. forkrun-0.16.0/python/forkrun.egg-info/dependency_links.txt +1 -0
  36. forkrun-0.16.0/python/forkrun.egg-info/top_level.txt +1 -0
  37. forkrun-0.16.0/ring_loadables/forkrun_plugin.h +76 -0
  38. forkrun-0.16.0/setup.cfg +4 -0
  39. forkrun-0.16.0/setup.py +228 -0
  40. 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)
@@ -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.