Skip to content
FikokoPublic

Repository files navigation

GPTPS — General Purpose Task Processing System

An embeddable C99 task-processing library with resource-aware scheduling. Link one library, register a task, submit work. GPTPS runs it on a worker pool under declared resource budgets, with retries / timeouts / dead-letter, and gives you the result back — plus an optional live terminal dashboard to watch and steer it. No server, no broker, no mandatory dependency. Runs on Linux, macOS, and Windows, and can even run single-threaded with no libc heap for embedded / bare-metal targets — and, in bounded mode, allocate nothing at all once work starts.

Long-running services are supervised (restart-on-exit, stopped cleanly at shutdown), and scale is opt-in and by composition — shard across engines, route across worker processes, or swap the scheduler — never baked into the mechanism-only core.


Contents

Quick start

#include "gptps.h"
#include <stdio.h>
#include <string.h>

/* a task: sum the payload bytes, return the sum */
static gptps_status sum(gptps_ctx *ctx, void *ud) {
    size_t n, i; const unsigned char *p = gptps_payload(ctx, &n);
    unsigned long s = 0; (void)ud;
    for (i = 0; i < n; ++i) s += p[i];
    return gptps_result_set(ctx, &s, sizeof s);
}

static void on_event(const gptps_event *ev, void *ud) {
    (void)ud;   /* ev->result may be NULL: a task need not set one */
    if (ev->kind == GPTPS_EV_FINISHED && ev->result_len == sizeof(unsigned long)) {
        unsigned long s;
        memcpy(&s, ev->result, sizeof s);   /* copy: valid only for this call */
        printf("task %s done: %lu\n", ev->task_name, s);
    }
}

int main(void) {
    gptps *e;
    gptps_task_def d = {0};
    gptps_handle h;

    gptps_open(NULL, &e);                       /* auto-tunes to the machine  */
    gptps_set_event_cb(e, on_event, NULL);

    d.struct_size = sizeof d; d.name = "sum"; d.run = sum; d.exec = GPTPS_EXEC_INPROC;
    d.default_cost.struct_size = sizeof d.default_cost; d.default_cost.mem_bytes = 4096;
    d.default_policy.struct_size = sizeof d.default_policy; d.default_policy.timeout_seconds = 5;
    gptps_register_task(e, &d);

    gptps_submit(e, "sum", "hello", 5, &h);     /* runs on the pool           */
    gptps_shutdown(e);                          /* drains (bounded), returns  */
    return 0;
}

Getting started

Build → run → embed — the whole path from zero to your own program. You need a C99 compiler (gcc/clang) and CMake ≥ 3.13 — nothing else on Linux/macOS (Windows uses MSVC or mingw-w64; the build auto-selects the Win32 backend).

1. Build.

cmake -S . -B build          # configure (once)
cmake --build build -j       # libgptps.a + the add-on libraries + examples + the test suite

Scaling is never in the core — shard across engines, route across worker processes, or swap the scheduler, all as add-ons you link (see Scaling). Build knobs, all optional:

Knob Default What it does
-DGPTPS_ADDONS= all which add-ons to build: all, none, or a ;-list. A typo is a hard error, not an empty selection
-DGPTPS_BUILD_ADDONS= ON at top level build the add-on libraries at all
-DGPTPS_BUILD_TESTS= / _EXAMPLES= ON at top level the suite and the examples
-DGPTPS_HAL_FAST=ON OFF platform-optimized HAL (adaptive mutexes on glibc); OFF keeps the portable pthread path

All four default OFF when GPTPS is add_subdirectory'd or FetchContent'd, so a consumer gets libgptps.a and nothing else — no CTest targets, no add-on builds.

Building on Windows with mingw-w64. The core is pure C99 and the Win32 backend (hal_win.c + exec_win.c) is selected automatically; mingw-w64 gcc is a first-class, CI-tested toolchain (native PE binaries, full <windows.h>, __atomic support). Using MSYS2 with the UCRT64 environment:

# in the MSYS2 UCRT64 shell
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug   # gcc + ninja is auto-detected
cmake --build build
ctest --test-dir build --output-on-failure              # full suite (should be 100%)

The single-file amalgamation builds the same way (gnu99 for the <windows.h> GNU extensions): sh tools/amalgamate.sh && cc -std=gnu99 -I amalgamation amalgamation/gptps.c myapp.c -o myapp.exe — no -lpthread/-ldl needed (the Win32 HAL uses the OS threads and LoadLibrary directly). The native MSVC (cl.exe) toolchain is also supported — open the folder in Visual Studio or cmake -S . -B build with the default VS generator.

2. See it run — the live dashboard. Run it in a real terminal (it needs an interactive TTY; with no TTY it just prints a line and exits, which is how CI runs it headless):

./build/example_dashboard    # keys: w/f submit · t tasks · l dead-letter · s settings · m KPI · p pause · ? help · q quit

See Live terminal dashboard for what it shows and how to drive it.

3. Run the other examples + the tests.

./build/gptps_demo           # in-process tasks + events (prints results)
./build/example_config       # tune from a TOML file
./build/example_embedded     # no threads + a static memory pool (manual mode)
ctest --test-dir build --output-on-failure   # full suite (should be 100%)

More in examples/: external_program (run any binary as a task), wasm_program (run a .wasm module via a wasm runtime CLI — see WebAssembly below), and edge_ai/ (AI jobs sharing one board's memory, as on a Jetson, each started only when it fits).

4. Embed it in your own program. Generate the amalgamation — two files for the core, plus a self-contained .c/.h pair for each add-on you ask for, and no build system — drop the Quick start program into myapp.c, and link:

sh tools/amalgamate.sh out   # writes out/gptps.c and out/gptps.h
cc -std=c99 myapp.c out/gptps.c -Iout -lpthread -ldl   # Linux
cc -std=c99 myapp.c out/gptps.c -Iout -lpthread        # macOS (dlopen is in libSystem)
./myapp

To embed the dashboard too, ask for it when you amalgamate — no repo checkout needed:

sh tools/amalgamate.sh out --addons tui     # also writes out/gptps_tui.c + .h
sh tools/amalgamate.sh --list               # everything available
cc -std=c99 myapp.c out/gptps.c out/gptps_tui.c -Iout -lpthread -ldl

(examples/demo.c is a fuller starting template.)

Install / consume (optional). cmake --install build --prefix <dir> installs the header, the static library, a CMake package config and a pkg-config file — plus each add-on as its own library (gptps::pool, …) with its header under include/gptps/ and its own .pc, and the gptps_conformance tool in bin/. Downstream projects use either

find_package(gptps 1.0 REQUIRED COMPONENTS durable_queue pool)   # COMPONENTS is optional
target_link_libraries(myapp PRIVATE gptps::durable_queue gptps::pool)

or pkg-config --cflags --libs gptps (add --static if you link statically — the core calls dlopen, so it needs -ldl). Pick which add-ons get built with -DGPTPS_ADDONS="pool;durable_queue" (default all, or none). Full detail in docs/PACKAGING.md.

API at a glance

Call Purpose
gptps_open(path, &e) / gptps_open_ex(cfg, &e) create an engine (auto-tunes workers + memory budget)
gptps_register_task(e, &def) register a task type (in-process fn or external program)
gptps_set_task_priority(e, name, prio) set a task type's scheduling priority (higher runs first)
gptps_define_resource(e, name, budget) · gptps_set_task_resource_cost(e, name, res, n) declare a named admission budget (GPU/IO/seats/quota) and a task's per-item cost against it
gptps_submit(e, name, payload, len, &handle) / gptps_submit_ex(…, &opts, …) enqueue work (_ex: per-submit priority / policy / deadline)
gptps_cancel(e, handle) cancel one queued / in-flight item
gptps_task_count/get_info/exists(e, …) enumerate / introspect registered task types
gptps_set_task_enabled(e, name, on) pause / resume a task type (reversible)
gptps_clone_task(e, src, dst) duplicate a task type under a new name
gptps_unregister_task(e, name, flags) remove a task type (reject-if-busy / drain / cancel)
gptps_define_global/define_task_setting(e, …) declare a custom typed global / per-task setting
gptps_task_setting_int/str(ctx, key, …) read this task's per-task setting from inside run()
gptps_set_event_cb(e, cb, ud) observe lifecycle events (results arrive on FINISHED)
gptps_register_constraint(e, fn, ud) gate admission (rate limit, quota, time window)
gptps_set_scheduler(e, fn, ud) swap the admission ordering (deadline-first, fair-share, …) over the fixed mechanism
gptps_register_observer(e, cb, ud) extra event sink (e.g. analytics)
gptps_dead_letter_count(e) / gptps_dead_letter_drain(e, cb, ud) inspect / reprocess retained failures
gptps_settings_get/set(e, key, …) · gptps_settings_count/get_info(e, …) read / change / introspect any setting at runtime
gptps_settings_save/reload(e, path) persist settings to / from a TOML file
gptps_register_setting(e, &def) add a custom setting (also a host-table routine for add-ons)
gptps_load_addon(e, path) load a binary plug-in over the stable ABI
gptps_addon_count/get_info(e, …) list what is loaded: name, namespace, path, enabled
gptps_addon_disable(e, ns_or_name) ask a plug-in to stop participating (it stays mapped — there is no unload)
gptps_set_scheduler_ex(e, fn, ud, owner, flags) · gptps_scheduler_owner(e) take the ordering seam only if free, and see who holds it
gptps_step(e, &ran) MANUAL mode: pump the engine on the calling thread (no worker threads)
gptps_set_allocator(&a) redirect all core allocation to a custom malloc/realloc/free
gptps_shutdown(e) drain in-flight + queued work (bounded by limits.shutdown_grace_ms), then free

Inside a task you get a gptps_ctx *: gptps_payload(), gptps_is_cancelled() (poll it for cooperative timeout), gptps_result_set() / gptps_result_set_nocopy().

Common tasks

Short recipes for the things you'll actually do — each links to the full details below.

1. Run work and get the result back. Register a task with a run function, set an event callback, and read the result on the FINISHED event — that's the Quick start above.

2. Give a task a timeout, retries, and a failure policy. Set the policy on the task def before registering it:

d.default_policy.timeout_seconds       = 5;
d.default_policy.max_retries           = 3;
d.default_policy.retry_backoff_seconds = 1;
d.default_policy.on_failure            = GPTPS_ON_FAILURE_DEAD_LETTER;  /* or _DROP / _REQUEUE */

In-process tasks must poll gptps_is_cancelled() to honor the timeout. Tasks that exhaust their retries are kept in the dead-letter list — reprocess them with gptps_dead_letter_drain().

3. Cap how much runs at once (the budget). Open with explicit limits (or a config file, so you can re-tune without recompiling):

gptps_config cfg = { .struct_size = sizeof cfg };
cfg.limits.struct_size = sizeof cfg.limits;
cfg.limits.max_concurrent_tasks = 4;          /* 0 => online logical CPUs    */
cfg.limits.max_memory_bytes     = 512u << 20; /* admission budget (declared) */
gptps_open_ex(&cfg, &e);

4. Run heavy or crash-prone work isolated and killable. Set the executor kind on the task: d.exec = GPTPS_EXEC_OOP (forked child, memory-capped, hard-killed on timeout) or GPTPS_EXEC_PROGRAM to run any external binary — see Executor kinds.

5. Watch it live. Install the dashboard add-on and run your program in a terminal — see Live terminal dashboard.

6. Change a setting at runtime and persist it.

gptps_settings_set(e, "tasks.resize.timeout_seconds", "60");  /* validated + applied live */
gptps_settings_save(e, "gptps.toml");                          /* atomic round-trip       */

See Settings.

7. Run with no background threads (embedded / bare-metal). Open with cfg.mode = GPTPS_RUN_MANUAL and drive it yourself with gptps_step() — see Embedded / single-threaded mode.

8. Run a supervised background service. Flag the task GPTPS_TASK_SERVICE: its run() loops until told to stop and is restarted if it exits — see Long-running services.

9. Scale beyond one engine. Shard across engines behind a router, ship work to worker processes, or swap the scheduler — all opt-in, all on the public API, no core change — see Scaling.

Configuration file (optional)

gptps_open("gptps.toml", &e) tunes the engine from a config file — no recompile to re-tune for a new machine or change a task's failure policy. Pass NULL to skip it and auto-tune. gptps_open_ex reads the same file when cfg.config_path is set, and an explicit value in cfg.limits, cfg.max_dead_letters or cfg.shutdown_grace_ms wins over the file's. A subset of TOML is supported (tables, int/float/bool/"string" and single-line string arrays, "quoted" parts in keys and table names such as [tasks."resize v2"], # comments). docs/CONFIG.md lists every key with its type, range, default and when a change applies; it is generated from the code, and a test fails when the two disagree.

# top level: binary plug-ins to dlopen at open, by explicit path. There is no search
# path, deliberately - see docs/SECURITY.md: whoever can write this file can run code
# in your process, so GPTPS makes you name the file rather than scanning a directory.
# A plug-in that declares a namespace puts its own settings under it (below).
addons = ["./libmytasks.so", "/usr/local/lib/gptps/gpu_quota_plugin.so"]

[tasks.render]
"gpuq.units" = 2               # a namespaced plug-in's per-task knob

[limits]
max_concurrent_tasks = 8       # 0 / omitted => online logical CPUs
max_memory_gb        = 4.0     # or max_memory_bytes = 4294967296

[scheduler]
reserve_after_skips = 8        # starvation guard (0 => strict priority, no backfill)

[resources]                    # named resources: the names and budgets are yours
gpu = 2

# [bounded]                    # optional: no allocation once work starts (docs/BOUNDED.md)
# max_items         = 4096
# max_payload_bytes = 256
# max_result_bytes  = 256

[task_defaults]                # applied to every task...
max_retries = 2
on_failure  = "dead_letter"    # dead_letter | drop | requeue
priority    = 0                # higher => admitted first

[tasks.resize]                 # ...then overridden per task name
timeout_seconds = 60
max_retries     = 1
on_failure      = "drop"
mem_bytes       = 268435456
priority        = 10

[tasks.resize.resources]       # what one run holds of each named resource
gpu = 1

Precedence for a task's policy: compiled-in def defaults → [task_defaults] → [tasks.<name>] (most specific wins). Explicit [limits] values win over auto-tune. See gptps.example.toml.

Two limits have a 0 of their own: limits.max_dead_letters (0 keeps every dead letter) and limits.shutdown_grace_ms (0 waits forever). Neither default is 0, and a 0 in cfg.limits means "not set", so they are fields of their own at the end of gptps_config (ABI 2.4). There, too, 0 means not set: the default, or the file's value. GPTPS_LIMIT_NONE means no limit, and any other value is the limit:

cfg.max_dead_letters  = GPTPS_LIMIT_NONE;  /* keep every dead letter (default 1024)        */
cfg.shutdown_grace_ms = 5000;              /* cancel running work after 5 s (default 30 s) */

0 means something different from key to key in the file - auto, no limit, none, wait forever - while in gptps_config it always means not set. docs/CONFIG.md has a table of every key whose 0 is special.

The file is checked as it is read. gptps_open fails with GPTPS_E_CONFIG on a line that does not parse, a value out of range or of the wrong type (TOML's types: "4" in quotes is a string, not a number), a key the engine's own tables ([limits], [scheduler], [bounded], [stats], [measure]) do not have, a table name a letter or two from one of the engine's when the key under it is that table's ([limit] max_concurrent_tasks), an add-on that does not load, and a plug-in's own key that its plug-in refuses. One attempt reports every problem, not just the first: every line that does not parse, or, once the file parses, every value and key that is wrong. Each comes with its line and key, and a suggestion when it looks like a typo:

config gptps.toml:2: limits.max_concurrent_tasks: 70000 must be a whole number between 0 and 65536
config gptps.toml:9: tasks.resize.on_failure: retry must be one of: dead_letter, requeue, drop
config gptps.toml:3: limits.max_memroy_bytes: [limits] has no such key (did you mean limits.max_memory_bytes?)
config gptps.toml:6: schedular.reserve_after_skips: there is no [schedular] table (did you mean [scheduler]?)
config gptps.toml: 4 errors - the engine was not opened

A key that someone defines later waits for them: a [tasks.<name>] table until a task of that name registers, a plug-in's or the host's own setting until it is defined — a host's [status] table too, though it is a letter from [stats]. Each is checked and applied at that moment. Call gptps_config_check(e) once setup is done: it logs every key nothing has used (tasks.resise.max_retries: no task named resise is registered (did you mean resize?)) and returns GPTPS_E_CONFIG if there is one. Without the call, the first submit logs the same keys once, as warnings.

Configure at deploy time; tune live only if you want to. The file can carry everything that shapes an engine: its limits, bounded mode, named resources and what each task costs of them, and every task's policy. It is read once, at open, and again only when you call gptps_settings_reload; nothing in it costs anything per task. The live tools — the settings API and the dashboard — are there when you want them: an engine that never installs the tui add-on runs no dashboard and no observer for it.

Reload and save. gptps_settings_reload(e, NULL) re-reads the file with the same checks. A file that does not parse changes nothing; otherwise every valid value is applied, each problem is logged, and the result is GPTPS_E_CONFIG if there was one. The keys that size the engine (max_concurrent_tasks, [bounded], addons) take effect at the next start. gptps_settings_save(e, NULL) writes the values changed live back into the file, in place: each on its own line, keeping that line's comment; a changed setting the file lacks goes next to its siblings; every other line stays exactly as written, so a 0 = auto stays auto. A file with an error in it is not overwritten: save logs why and returns GPTPS_E_CONFIG. Saving to a new path writes a copy of the loaded file — add-ons, [task_defaults], comments and all — with the live changes made in it, and the engine's own value wherever it refused the file's. Whatever save writes reads back as it was: a setting takes only numbers a file can hold (no nan, inf or hex), and a string is written with every control character escaped. Memory that runs out is not a mistake in the file: open, reload and save return GPTPS_E_NOMEM then, and the same call made again reports whatever else there is. A file that cannot be read at all — missing, a directory, not permitted — is GPTPS_E_CONFIG, and the log says why and that the file cannot be read.

Settings (runtime, introspectable, persistable)

The same knobs are also a live, typed settings registry — one API over core, per-task, and add-on settings (dotted keys like scheduler.reserve_after_skips, tasks.resize.timeout_seconds, tui.kpi, gpu_quota.total_units):

char v[256];
gptps_settings_get(e, "scheduler.reserve_after_skips", v, sizeof v);  /* read current */
gptps_settings_set(e, "tasks.resize.timeout_seconds", "60");          /* validated + applied live */
gptps_settings_save(e, "gptps.toml");                                 /* persist (atomic) */
size_t n = gptps_settings_count(e);                                   /* enumerate for a UI */
  • Typed + validated: set() parses and range/enum-checks before applying (so a bad on_failure or out-of-range value is rejected with GPTPS_E_CONFIG, not silently dropped). gptps_settings_set_ex also says why, for a UI to show: 70000 must be a whole number between 0 and 65536. A file's values go through the same check.
  • Hot vs restart: most settings apply immediately; a few (e.g. the worker-pool size) are flagged effective-on-restart. gptps_settings_get_info exposes type, default, range, and the hot flag for building a UI.
  • Round-trip: gptps_settings_save writes the values changed live into the file in place, atomically, keeping its comments; gptps_settings_reload re-applies it. See Reload and save.
  • Extensible: add-ons register their own settings (via gptps_register_setting or the host-table routine), so they show up in the registry, TOML, and editor uniformly.
  • Generic, no glue: declare your own typed knobs at runtime — gptps_define_global(e, "app.max_upload_mb", GPTPS_SETTING_UINT, "10", "0..4096", 0) for a global, or gptps_define_task_setting(e, "quality", GPTPS_SETTING_UINT, "75", "0..100", 0) to materialize tasks.<name>.quality on every task. A run() reads its own value with gptps_task_setting_int(ctx, "quality", &q). The engine stores and validates them; both round-trip through TOML and appear in the editor.
  • Editor: the tui add-on includes a live Settings pane (s) to browse, edit and save. It explains the selected setting — what it does, what it takes, its default, and whether a change applies at once or at the next start — and says why when it refuses a value.

Manage tasks at runtime (control plane)

The registry is itself live and mutable — enumerate, pause, clone, and remove task types without recompiling or restarting:

size_t n = gptps_task_count(e);                       /* enumerate for a UI ... */
gptps_task_info ti = { .struct_size = sizeof ti };
gptps_task_get_info(e, 0, &ti);                       /* name, exec, prio, queued/running/dead */
uint64_t fl; gptps_task_flags(e, "resize", &fl);      /* GPTPS_TASK_* by name, race-free */

gptps_set_task_enabled(e, "resize", 0);               /* pause: reject new submits, reversibly */
gptps_clone_task(e, "resize", "resize_hi");           /* duplicate, then retune the copy */
gptps_unregister_task(e, "resize", GPTPS_REMOVE_DRAIN);   /* finish in-flight work, then remove */
  • Removal policy (the flags): GPTPS_REMOVE_REJECT_IF_BUSY (default — refuse with GPTPS_E_BUSY while work is outstanding), GPTPS_REMOVE_DRAIN (stop new submits, let queued + in-flight finish, then free), or GPTPS_REMOVE_CANCEL (drop queued, cancel in-flight, then free). A removed name is free to re-register; its own tasks.<name>.* settings are torn down (not a sibling's such as tasks.<name>.big.*); retained dead-letter items survive and stay drainable.
  • Behavior still arrives in code. These calls own configuration and lifecycle. New in-process logic comes from a run fn (code or an add-on); a GPTPS_EXEC_PROGRAM task, though, is fully creatable at runtime (and from the TUI) since its behavior is an external argv. Add-ons get the same control plane via the host-table ABI.

Long-running services

Some work isn't a one-shot task — it's a resident loop (a poller, a listener, a metrics collector) that should run until the process stops and come back if it crashes. Flag the task type GPTPS_TASK_SERVICE and submit it like any task; the engine supervises it:

static gptps_status poller(gptps_ctx *ctx, void *ud) {
    (void)ud;
    while (!gptps_is_cancelled(ctx)) { poll_once(); }   /* loop until told to stop */
    return GPTPS_OK;
}

gptps_task_def d = { .struct_size = sizeof d, .name = "metrics",
                     .run = poller, .exec = GPTPS_EXEC_INPROC,
                     .flags = GPTPS_TASK_SERVICE };
d.default_cost.struct_size = sizeof d.default_cost;
d.default_policy.struct_size = sizeof d.default_policy;
d.default_policy.retry_backoff_seconds = 2;             /* restart delay after a crash */
gptps_register_task(e, &d);

gptps_handle h;
gptps_submit(e, "metrics", NULL, 0, &h);   /* start one instance (submit N for a pool) */
  • Supervised restart. When the loop exits, the engine restarts the instance after retry_backoff_seconds — crash-restart supervision, for free — unless run() returned GPTPS_E_CANCELLED, which ends it like a stop. The failure policy is normalized (restart-on-exit, no timeout) so a config or settings edit can't un-service it.
  • Stop it. The submit handle stays valid across restarts, so gptps_cancel(e, h) stops that one instance for good; gptps_unregister_task stops the whole type; and gptps_shutdown stops running services (raising their cooperative cancel flag) so a resident service never hangs teardown. Non-service in-flight work still drains gracefully.
  • Restart-always vs. on-failure. By default a service is "always up" (even a clean GPTPS_OK return restarts it). Add GPTPS_TASK_RETIRE_ON_OK for the Restart=on-failure semantic: a clean return retires the instance; only a failure other than GPTPS_E_CANCELLED restarts it.
  • v1 services are GPTPS_EXEC_INPROC, GPTPS_RUN_THREADED, and have no timeout.

Scaling (opt-in, by composition)

The core is a single-writer engine — one lock, one dispatcher — which is simple, correct, and the per-node throughput ceiling. Rather than complicate the core, GPTPS scales by composing modules on top of it: you pay for scale only when you reach for it, and the default stays small and portable. None of the below touches the mechanism-only core.

Scale up — shard across engines (gptps_pool). Run N independent engines (each its own lock + dispatcher + worker pool) behind a router:

gptps_pool *pool = gptps_pool_open(4, &cfg);          /* 4 independent shards */
gptps_pool_register_task(pool, &def);                 /* same task type on every shard */
gptps_pool_submit(pool, "resize", buf, len, &h);      /* round-robin */
gptps_pool_submit_keyed(pool, tenant_id, "resize", buf, len, &h);  /* per-key affinity */
gptps_pool_cancel(pool, h);
gptps_pool_close(pool);

The router's only shared state is a round-robin cursor (keyed routing is lock-free); each shard is a full engine. examples/bench_pool measures it. On a 32-thread desktop (i9-14900K), 400k no-op tasks from 8 producers ran at ~270k/s on 1 shard, ~520k/s on 2, ~910k/s on 4 and ~2.09M/s on 8 (7.7×): the single-writer ceiling, then composition breaking past it. One producer reaches ~520k/s on a single engine, about 2 µs a task end to end. Use a large item count if you rerun it: the CI-quick default of 40k finishes in ~20ms and is noise-dominated.

Scale out — worker processes (gptps_xport). Fork N persistent worker processes, each running its own engine, and ship each request to one over a multiplexed IPC link. Work runs in a separate address space (crash-isolated), and the worker's pool, budgets, retries, timeouts, dead-letter and seams all apply there — scaling out keeps everything the engine does. POSIX only, like EXEC_OOP (Windows has no fork):

gptps_xport_config xc = { .struct_size = sizeof xc, .nworkers = 4,
                          .engine_cfg = &per_worker_limits, .tasks = table, .ntasks = n };
gptps_xport *xp = gptps_xport_open_ex(&xc);                 /* 4 workers, an engine in each */
gptps_xport_submit(xp, "resize", buf, len, &res, &rlen, &task_status);        /* blocks */
gptps_xport_submit_async(xp, "resize", buf, len, on_reply, ud, NULL);         /* callback */
gptps_xport_close(xp);                                      /* graceful drain */

gptps_xport_open(n, handler, ud) still gives you the bare-handler transport (no engine in the worker) for plain crash-isolated RPC. Watch either with gptps_stats: install it from the child_init hook and each worker keeps its own counters.

Going cross-machine is not a socket swap, and addons/gptps_remote says why. It is the wire codec — versioned header, fixed big-endian byte order, a request id, stable status codes, a 1 MiB cap — written down because xport's framing is native-endian (silent corruption between a little- and a big-endian host), its gptps_status values are positional (so they would become wire-visible), and its 256 MiB cap is a one-packet DoS from a peer you did not fork. There is deliberately no transport yet and the codec is not published as a release artifact: shipping it is what would create a permanent version-1 peer. Build a transport on it when you have a reason to, with docs/SECURITY.md read first — the task field of a request is a dispatch key chosen by the peer.

Swap the scheduling discipline (gptps_set_scheduler). The admission mechanism (skip-to-fit, budget, starvation guard) is fixed; the ordering is a hook. Return a score per item and the dispatcher admits the highest that fits — so deadline-first, per-tenant fair-share, cost-aware, or aging disciplines compose without a core fork (default is priority/FIFO). The hook sees the task name, priority, attempt, queue age and payload; a deadline is yours to supply, here as the payload's first 8 bytes:

static int64_t earliest_deadline_first(const gptps_sched_input *in, void *ud) {
    int64_t deadline = INT64_MAX;                     /* no deadline: last */
    (void)ud;
    if (in->payload_len >= sizeof deadline) memcpy(&deadline, in->payload, sizeof deadline);
    return deadline == INT64_MIN ? INT64_MAX : -deadline;   /* sooner = higher score */
}
gptps_set_scheduler(e, earliest_deadline_first, NULL);

Faster locks (-DGPTPS_HAL_FAST). An opt-in build knob swaps in adaptive (spin-then-block) mutexes on glibc — a latency knob for the engine's contended critical sections; OFF by default keeps the portable pthread HAL. The HAL is a module boundary, so a downstream can drop in its own platform-optimized backend — and ctest -R hal_conformance holds that backend to the same contract as the shipped ones (docs/HAL.md).

Balance batches of mixed-size work (gptps_balance). Round-robin is the wrong router for a heavy-tailed batch: one shard draws three long items while its neighbours idle, and nothing can move them once they are inside an engine's queue. gptps_balance keeps the queue in the router and hands each shard work only as it frees up (join-shortest-queue with late binding — work-stealing in effect), adapting to any task size without being told sizes. Measured on 4 shards: 27–31% shorter makespan for 200–1,000-item batches, and no difference on a 20,000-item stream, where round-robin is already balanced by the law of large numbers. examples/bench_balance.c reproduces both.

gptps_pool, gptps_balance and gptps_xport are add-ons (addons/) built entirely on the public API — the proof that scaling here needs no core change.

Live terminal dashboard

GPTPS ships an optional, dependency-free terminal dashboard (addons/gptps_tui.c) — link it, point it at your engine, and watch tasks flow in real time. Pure ANSI/VT (no ncurses), auto-enabled on a TTY, with the Windows console put into VT mode automatically.

GPTPS · live demo   up 0.1s   28.6 done/s
queued 18  started 8  in-flight 4  [##########################] peak 4
finished 4  failed 0  retried 0  dead 0  dropped 0
kpi:full mode:realtime refresh:250ms

TASKS
  label              run    ok  fail  dead  drop   ok%   avg ms  key
  Resize               8     4     0     0     0  100%     81.0  [r]
  Thumbnail            0     0     0     0     0    --       --  [t]

RECENT
     0.1 FINISHED resize         #4
     0.1 FINISHED resize         #1
     0.1 STARTED  resize         #7
     0.1 STARTED  resize         #8

keys: [r] Resize  [t] Thumbnail   ·  ? help  s settings  t tasks  l dead-letter  m kpi  p pause  q quit
  • Live metrics: throughput, an in-flight gauge, cumulative counts, and a per-task table (runs / ok / fail / dead / drop / success-rate / average latency).
  • Interactive: hotkeys submit tasks; k/j scroll the event log; m dials the dashboard's own CPU/RAM cost (minimal/normal/full) live; p pauses; s opens the live settings editor, which explains the selected setting, says why it refuses a value, and saves your changes into the config file in place (w); ? shows a help overlay of every key.
  • Task control plane: t opens a task manager — list every type with live queued/running/dead counts, inspect one to edit its settings inline, pause/resume (a), clone (c), create a GPTPS_EXEC_PROGRAM task from a typed name + argv (n), or delete with a confirm dialog (d) that shows the outstanding count and offers drain or cancel-force. l opens a dead-letter view to bulk re-submit or discard retained failures.
  • Friendly: adapts to the terminal size, redraws flicker-free, confirms actions with a toast, and on a real terminal adds a framed title bar, a Unicode block gauge, and color (with an ASCII fallback shown above). Built on a pure render-to-string core, so it is fully testable headlessly.

Run it live in a terminal: ./build/example_dashboard (source: examples/dashboard.c).

Executor kinds (per task, via def.exec)

Kind Runs as Enforcement If the task crashes Platforms
GPTPS_EXEC_INPROC your C function, in-process cooperative cancel (advisory) kills your whole process — the engine, the dispatcher, and every other in-flight item all
GPTPS_EXEC_OOP the same C function in a forked child memory cap + hard-kill on timeout or cancel kills only the child; reported as GPTPS_E_TASK (E_NOMEM if it blew the memory cap) POSIX only (needs fork)
GPTPS_EXEC_PROGRAM an external program (def.argv); payload→stdin, stdout→result memory cap + hard-kill on timeout or cancel kills only the child; reported as GPTPS_E_TASK (E_NOMEM if it blew the memory cap) all (POSIX fork+exec; Windows CreateProcess + Job Object)

Read the blast-radius column before you pick. GPTPS_EXEC_INPROC is 0, so it is what a memset-zeroed gptps_task_def gives you — the fast path, and the right default for code you trust. For anything that can segfault, leak, or run away, OOP/PROGRAM buy you a real fault boundary: a memory cap the OS enforces, a guaranteed kill, and a child_setup hook to drop privileges or install seccomp before the work starts.

The out-of-process executors are fully cancellable: gptps_cancel (and gptps_unregister_task(…, CANCEL)) hard-kill a running child in bounded time — even one with no timeout, which used to run unstoppably. The POSIX program executor pumps the payload to stdin and reads stdout concurrently (a single poll loop), so a large payload through a streaming child never deadlocks.

On POSIX the out-of-process executors enforce the per-task memory cap accurately with cgroup v2 (memory.max, exceeding it ⇒ GPTPS_E_NOMEM) when GPTPS_CGROUP_PARENT points at a memory-delegated cgroup (e.g. a systemd Delegate=yes scope), else a coarse RLIMIT_AS cap; on Windows the program executor uses a Job Object (memory limit + kill-on-close). Either way it's real, killable enforcement the in-process path can't give.

What a job actually used. Admission counts what a task declares; the out-of-process executors also report what each attempt used, because they collect its process when it ends. Every FINISHED and FAILED of an OOP or PROGRAM attempt carries its measurements — peak memory, CPU time, I/O, and in cgroup mode whether it hit its cap — each with the method that measured it, so a host can print "declared 24 GB, peaked at 19.3 GB" and tune its declarations from evidence:

const gptps_measure *m = gptps_event_measure(ev, GPTPS_M_MEM_PEAK);
if (m) printf("%s: declared %llu, peaked at %llu (%s)\n", ev->task_name,
              (unsigned long long)ev->mem_bytes, (unsigned long long)m->value, m->method);

GPTPS reports these and never acts on them. Set measure.sample_ms to sample a running job's memory as well. gptps_stats aggregates measurements per task, the dashboard shows the peak, and docs/MEASUREMENTS.md is the design record: the rules, the vocabulary, and what each platform measures and how.

WebAssembly. A .wasm module is portable, sandboxed task code — and a wasm runtime CLI is just a program, so you can run one through GPTPS_EXEC_PROGRAM with no new code: def.argv = {"wasmtime", "run", "module.wasm", NULL} (argv[0] is PATH-resolved). The payload flows to the module's stdin and its stdout comes back as the result, under the usual budget / timeout / retry. See examples/wasm_program.c. For a tighter, in-process binding, the wasm_exec add-on takes a pluggable runtime hook instead.

Embedded and single-threaded mode

GPTPS runs in two execution modes, chosen at gptps_open_ex via cfg.mode:

  • GPTPS_RUN_THREADED (default) — a dispatcher + worker pool; the engine runs itself. The headline hosted path.
  • GPTPS_RUN_MANUAL — no threads. You drive the engine cooperatively on your own thread with gptps_step(e, &ran): one call completes finished work, promotes backoff-ready retries, admits within budget, and runs the admitted tasks to completion inline. Drain with while (gptps_step(e,&n)==GPTPS_OK && n);, or call it from your existing main loop / RTOS tick. Same admission, priority, retry, and dead-letter semantics as threaded mode (one shared engine_pass).
gptps_config cfg = { .struct_size = sizeof cfg, .mode = GPTPS_RUN_MANUAL };
cfg.limits.struct_size = sizeof cfg.limits;
gptps_open_ex(&cfg, &e);
/* ... register + submit ... */
size_t n; do { gptps_step(e, &n); } while (n);   /* runs on THIS thread */

Pair it with gptps_set_allocator() to take GPTPS off the libc heap entirely — point it at a static pool (SQLite-style; covers all core allocation). Together, MANUAL mode + a custom allocator are the bare-metal shape: zero threads, fixed RAM. The only thing a real MCU/RTOS port adds is a HAL backend (hal_<target>.c) for the mutex and clock primitives — MANUAL mode never calls gptps_thread_start or cond_wait. docs/HAL.md is the contract a backend keeps, and ctest -R hal_conformance checks it. Worked end-to-end in examples/embedded.c.

Bounded mode goes one step further: set cfg.max_items (and the payload and result maxima), and the first submit allocates everything the engine will ever need. After that the work path neither allocates nor frees, every operation's cost is bounded by the maxima, and a full engine answers GPTPS_E_FULL instead of growing. It runs in-process tasks only, and setup must come before the first submit. A counting allocator in tests/test_bounded.c holds it to that, in both modes; docs/BOUNDED.md has the details, sizing and speed.

Caveat: a MANUAL task runs to completion on your thread, so a wall-clock timeout can't preempt it — cooperative tasks should poll gptps_is_cancelled() / gptps_deadline_ms(). For hard kill/timeout, use an out-of-process executor.

Resource budgets, failures, add-ons

  • Admission: each task type declares a rough cost (mem_bytes; gpu_units and est_duration_ms were removed in ABI 2.0, and named budgets replaced them — see gptps_define_resource). The core starts a task only if it fits the live budget — not an all-or-nothing cap. max_concurrent_tasks=1 is strictly sequential; >1 is concurrent.
  • Scheduling: the dispatcher admits the highest-priority pending task that fits the live budget. A too-large task does not head-of-line-block — smaller work behind it backfills (skip-to-fit), while a bounded reservation keeps the skipped task from starving (it's admitted once enough budget frees). Set priority via gptps_set_task_priority() or config; tune the reservation with [scheduler] reserve_after_skips.
  • Failure policy (per task, overridable): timeout_seconds, max_retries, retry_backoff_seconds, on_failure = dead_letter (default) / drop / requeue.
  • Dead letter: tasks that exhaust retries (or that a constraint denies) are retained. gptps_dead_letter_drain() hands each back to a callback — with the engine lock released, so the callback may re-submit to retry — and empties the list (gptps_shutdown() frees the rest); if memory runs out copying a task's name, it stops there and leaves the rest for the next drain. The list is capped at limits.max_dead_letters (default 1024, oldest evicted, 0 = unbounded; at open, cfg.max_dead_letters, where GPTPS_LIMIT_NONE is unbounded); stats.dead_letters_evicted counts anything the cap dropped, so a host that never drains gets bounded memory instead of silent growth.
  • Durability (optional): addons/gptps_durable_queue.c journals submissions to disk (fsync before enqueue) and replays survivors after a crash — at-least-once delivery. See addons/README.md.
  • Runtime task management: enumerate, pause/resume, clone, and unregister task types live (gptps_task_*, gptps_unregister_task with reject-if-busy / drain / cancel) — the control plane behind the dashboard's task manager. See Manage tasks at runtime.
  • Live dashboard (optional): a portable real-time terminal UI with live metrics, a per-task table, a scrollable event log, a settings editor, a task manager + dead-letter panes, and hotkeys — see Live terminal dashboard above.
  • Add-ons keep the core small. Task logic, transports, GPU quotas, rate limits, priority, time-of-day windows, analytics sinks — all live in add-ons. See below.

Add-ons and plug-ins

Everything domain-specific lives outside the core. One question decides how you build it:

Does the host have to call into your module? Yes → a compiled-in module. It needs a header, so it needs your build. Links core symbols directly; can own threads, own a whole engine, ship platform code. No → a binary plug-in. It needs only the versioned host table, so it needs only your process: named in a config file, dlopen'd at runtime, configured by an operator through settings. It links no core symbols — which is what lets one .so work against a static, shared or amalgamated host alike.

A module is not the lesser thing: most of the bundled add-ons are modules by necessity, because their whole point is an API you call (gptps_dq_submit(), gptps_orch_after(), gptps_tui_run()). gptps_gpu_quota ships in both tiers, and the diff between addons/gptps_gpu_quota.c and addons/gptps_gpu_quota_plugin.c is the shortest honest description of the difference.

A plug-in declares a namespace and gets a guarantee. With GPTPS_ADDON_INIT_NS(…, "myns", …) the loader claims that token — a second plug-in wanting it is refused — and in exchange everything you register during setup() must be "myns."-prefixed. That is what stops two unrelated plug-ins colliding on a task name, a settings key or a resource budget. An operator can list what is loaded (gptps_addon_count / gptps_addon_get_info) and turn one off without unloading it (gptps_addon_disable).

Prove it before you ship it:

cmake -S templates/plugin -B build -DCMAKE_PREFIX_PATH=$(pkg-config --variable=prefix gptps)
cmake --build build
gptps_conformance build/myplugin.so        # installed to bin/

The interesting check is the degradation ladder: the harness synthesises the host table as each released core actually had it and runs your plug-in against every rung, so a routine you called without a struct_size guard is reported by name, with the guard to add — rather than segfaulting in a user's process a year from now. The engine cannot test this itself, because it always hands a plug-in the full table.

Start from templates/plugin/ and read docs/PLUGINS.md for the full contract — the tier decision, the struct_size guard, threading per seam, seam ownership, and the security posture.

Getting one

Each add-on is an installable library, so you can take a subset without cloning (docs/PACKAGING.md):

find_package(gptps 1.0 REQUIRED COMPONENTS durable_queue pool)
target_link_libraries(myapp PRIVATE gptps::durable_queue gptps::pool)
cc -std=c99 myapp.c $(pkg-config --cflags --libs --static gptps-durable_queue gptps-pool) -o myapp

sh tools/amalgamate.sh out --addons durable_queue,pool   # no build system, no clone
cc -std=c99 myapp.c out/gptps.c out/gptps_durable_queue.c out/gptps_pool.c -Iout -lpthread -ldl

gptps.c is byte-identical whichever add-ons you select, so its SHA256 stays pinnable. See addons/README.md for what each one does.

Project layout

gptps/
├── include/
│   ├── gptps.h          ← the public API (start here)
│   └── gptps_hal.h      ← internal platform-abstraction interface
├── src/                 ← the library
│   ├── engine.c         core: dispatcher, queue, admission, scheduler, failure engine, loader
│   ├── settings.c       typed settings registry;  alloc.c  custom-allocator seam
│   ├── config.c         config model + hardware auto-tune;  config_toml.c  TOML parser
│   ├── hal_posix.c      POSIX backend (threads, clock, dynload, detection);  hal_win.c  Win32 backend
│   └── exec_oop_posix.c out-of-process + external-program executors;  exec_win.c  Win32 executor
├── addons/              ← optional modules, one installable library each (gptps::pool, …)
│   ├── gptps_pool.c     scale-UP: N engine shards + a router;  gptps_xport.c  scale-OUT: worker processes, an engine in each
│   ├── gptps_balance.c  late-binding load balancer above pool (join-shortest-queue; any task size)
│   ├── gptps_stats.c    counters / gauges / latency on the observer seam (per engine, per task, mergeable)
│   ├── gptps_await.c    blocking wait(handle);  gptps_orch.c  run-after / fan-in dependencies
│   ├── gptps_durable_queue.c  crash-durable journal;  gptps_gpu_quota.c  named-resource quota
│   ├── gptps_wasm_exec.c  module-as-task;  gptps_tui.c  live terminal dashboard
│   ├── gptps_remote.c   cross-host wire CODEC (built + tested; deliberately not distributed)
│   ├── gptps_gpu_quota_plugin.c  the same quota policy as a dlopen BINARY plug-in
│   └── CMakeLists.txt   one library + header + .pc per add-on
├── templates/plugin/    ← a complete, copyable binary plug-in (built out-of-tree by CI)
├── examples/            ← runnable examples (demo, config_file, task_control, success_gate, item_ledger,
│                          external_program, dashboard, embedded, wasm_program, bench_pool, bench_balance,
│                          edge_ai/)
├── gptps.example.toml   ← annotated sample config file
├── docs/
│   ├── ARCHITECTURE.md  how it works inside
│   ├── PLUGINS.md       writing an add-on: which tier, the ABI contract, proving it
│   ├── PACKAGING.md     getting GPTPS + a subset of its add-ons
│   ├── SAFETY.md        the planned commercial safety-artifacts package and how it is licensed
│   └── SECURITY.md      trust boundary and non-guarantees
├── tests/               ← CTest suite (`ctest -N` counts it) + consumer/ (an out-of-tree find_package consumer)
├── tools/
│   ├── amalgamate.sh    single-file gptps.c + gptps.h, and one .c/.h pair per add-on
│   ├── gptps_conformance.c  prove a binary plug-in before you ship it (installs to bin/)
│   └── check_addon_coverage.sh  every add-on is built, obtainable and documented
├── CMakeLists.txt
└── .github/workflows/ci.yml   Linux/macOS/Windows (mingw + MSVC), i386 + s390x + freestanding,
                               ASan/UBSan + TSan, and a `package` job that installs and consumes

Status

Working today (tested + ThreadSanitizer-clean): the engine, all three executors, result delivery, retries/timeout/dead-letter + dead-letter drain, priority scheduling with skip-to-fit + reservation, accurate cgroup v2 memory enforcement (with RLIMIT_AS fallback), the add-on loader + ABI, constraints + observers, TOML config-file loading (limits + scheduler + per-task overrides + add-on auto-load), the unified settings registry (typed get/set + validation + round-trip persistence + add-on-extensible), single-threaded MANUAL mode (gptps_step) and a custom-allocator hook for embedded / bare-metal hosts, supervised long-running services (GPTPS_TASK_SERVICE), the pluggable scheduler seam (gptps_set_scheduler, with declared ownership so two add-ons cannot silently fight over it), scale-up (gptps_pool shards) and scale-out (gptps_xport worker processes, each with its own engine), the optional platform-optimized HAL (-DGPTPS_HAL_FAST), the live terminal dashboard (with the settings editor), the crash-durable queue, a blocking wait(handle), run-after/fan-in dependencies, GPU-quota and WASM-executor add-ons, observer-seam stats (gptps_stats: totals, gauges, latency, per task, mergeable across shards), measurements of what each process job actually used (peak memory, CPU, I/O, cap hit; sampled while it runs if asked; carried back from gptps_xport workers), the examples + benchmark, CMake + CI + single-file amalgamation.

Binary plug-ins work as of ABI 2.1 — see Add-ons and plug-ins. Each add-on is its own installable library (gptps::pool, …) with a header, a .pc file and an amalgamation pair, so you can take a subset without cloning.

At a glance: 59 public functions · ABI 2.5 (append-only; 2.0 was the first and, by design, the last breaking change) · 11 add-on modules + 1 example binary plug-in · 89 tests · 14 CI runs (13 job definitions; build-test is a 2-way matrix), every one required to pass.

Liveness guarantees. Because GPTPS runs inside your process, anything that can hang it hangs your host's exit path — so these are contractual, and tests/test_hang.c enforces them with a hard test timeout:

  • gptps_shutdown always returns. In-flight work drains for at most limits.shutdown_grace_ms (default 30s; 0, or GPTPS_LIMIT_NONE in cfg.shutdown_grace_ms at open, opts back into waiting forever), then gets cancelled, and work still queued or in backoff is ended by its policy with GPTPS_E_SHUTDOWN — an external child with no timeout of its own cannot wedge teardown, and neither can a constraint that keeps deferring.
  • gptps_shutdown / gptps_step return GPTPS_E_BUSY rather than deadlocking or freeing the engine under the caller when called from a task body or a callback as THREADING in include/gptps.h defines it: including the QUEUED on the submitting thread, the dead-letter drain callback, a settings watcher, a setting's write accessor and an add-on's setup/teardown/disable, but not a setting's read accessor or a hook that runs under the engine lock, which must not call into the engine at all. So does a gptps_unregister_task on an engine thread that would have to wait for work of that type (tests/test_unregister_reentry.c).
  • The engine's growable state is bounded, with one deliberate exception. The dead-letter list (limits.max_dead_letters, default 1024) and the bytes an out-of-process child can make the parent buffer (16 MiB) are capped, and truncation is always counted rather than silent. The intake queue (limits.max_intake_depth) is unbounded by default — right for a host that submits its own work, wrong for one that accepts work from elsewhere; set it and handle GPTPS_E_FULL if a submitter can outrun your workers. Admission and gptps_cancel are O(1) in queue depth either way, so leaving it unbounded costs memory, never throughput (tests/test_admission_perf.c and tests/test_cancel_perf.c gate both). docs/SECURITY.md has the full table.
  • Every submitted handle reaches exactly one terminal event — the invariant the observer seam, and every add-on built on it, depends on (tests/test_reconcile.c). It holds for a one-shot task through success, retries, timeouts, cancellation, removal in any mode, a constraint denial, a budget shrunk under a queued item, and shutdown in every drain state. Two opt-in shapes are deliberately outside it, and both are knowable before you submit: a GPTPS_ON_FAILURE_REQUEUE item is unbounded by design, so its handle stays open for as long as the body keeps failing — shutdown closes it with DEAD_LETTERED; and a GPTPS_TASK_SERVICE handle is a supervised lifetime rather than a completion, so it emits one terminal event per run while it is up, and exactly one FAILED/GPTPS_E_CANCELLED when it is finally stopped. A submit that returned an error created no handle and emits nothing at all.

Platforms (all CI-verified): Linux and macOS are full. Windows (Win32 HAL via src/hal_win.c) runs the engine, scheduler, config, the in-process and external-program executors (CreateProcess + Job Object), and the add-on loader; only GPTPS_EXEC_OOP is POSIX-only, since it forks an in-process function (no fork() on Windows — use GPTPS_EXEC_PROGRAM there for isolated, killable, memory-capped work).

Running WebAssembly works today two ways: via GPTPS_EXEC_PROGRAM + a wasm runtime CLI (examples/wasm_program.c), or the wasm_exec add-on with a pluggable runtime. Optional future work: a bundled default wasm runtime so neither a CLI nor an adapter is needed.

Design notes

The core is deliberately small and general: a mechanism-only engine with four called seams (task / constraint / observer / scheduler) — called because the core invokes them — and one composed pattern (GPTPS_SEAM_COMPOSITION — spelled GPTPS_SEAM_TRANSPORT before ABI 2.1 and still aliased), which is a shape a module takes rather than an interface the core offers. Anything specific — GPU quotas, rate limits, priority, time-of-day windows — is a constraint add-on; the admission order is a scheduler hook; scale-up (gptps_pool) and scale-out (gptps_xport) are composition libraries that sit above the engine and consume no seam at all. So the core stays minimal while the variety, and the scaling, live outside it. The novel piece is single-process self-throttling admission: "can my own process afford to start this task right now, given my own remaining budget?"

That last distinction is load-bearing rather than pedantic. gptps_pool and gptps_xport needed zero core changes because a transport calls into the engine rather than being called by it — which is precisely why the core gives it no interface, and the strongest evidence for the "compose, don't extend" thesis this project is built on.

For the full internals — concurrency model, dispatch loop, scheduler, executors, HAL, and the add-on ABI — see docs/ARCHITECTURE.md.

Non-goals

"Mechanism-only" is not a constraint unless it can reject something, so here is what GPTPS will not grow into. These are not judgements about whether the ideas are good — several are good, and some belong in add-ons. They are statements about what does not go in the core, so that the answer is decided once instead of re-argued per feature.

Not in the core Why, and where it belongs instead
Distributed scheduling (which node runs what, work stealing, membership, failure detection, global fair-share, rebalancing) Note the boundary, because the neighbouring thing IS permitted. Transport — route work to a named remote, marshal it, bring the result back, exclude a dead endpoint, retry elsewhere — is an add-on, and a welcome one: that is exactly the step gptps_xport gestures at — and addons/gptps_remote has already written down the wire format it would need, precisely so nobody mistakes it for a socket swap. Scheduling is where it stops. The moment a module needs the global state of other nodes it needs consensus, and the failure model changes completely: the novel thing here is single-process self-throttling admission, and a cluster scheduler is a different product. Node selection is a router's business (gptps_pool already picks a shard); admission ordering is gptps_set_scheduler's; neither is a cluster scheduler.
Persistence of the queue An engine that survives a crash needs a storage format, a fsync policy, and a recovery protocol — three commitments the core cannot make portably. addons/gptps_durable_queue already does it on the public API.
A metrics format (Prometheus, statsd, OTel) The core emits events and never aggregates. Binding a wire format into it dates the library to whatever was fashionable. Aggregate in an observer add-on — addons/gptps_stats is that add-on (totals, gauges, latency; no format) — and export from its snapshot in your host. If something genuinely cannot be observed from the seam, that is an argument for a specific accessor, not a format.
Futures / promises / async in the engine Result delivery is an event. A blocking wait(handle) does not need to be in the mechanism — and this row no longer asks you to take that on faith: addons/gptps_await is those lines, on the observer seam, with no core change. The core already supplies the one guarantee such a wait needs — a one-shot handle reaches exactly one terminal event (tests/test_reconcile) — so nothing was missing. The two shapes outside that guarantee are the two you would not await anyway: a GPTPS_ON_FAILURE_REQUEUE item has not finished while it is still requeueing, and a GPTPS_TASK_SERVICE handle is an uptime, not a result, so a wait on it returns once per run rather than once ever. Chaining and dependencies are addons/gptps_orch's job, not a future's.
Task graphs / DAG semantics Dependencies are policy over submission order. addons/gptps_orch holds this; a DAG belongs in its handle space, not the dispatcher's. (Note what "terminal" means there: GPTPS_EV_FAILED is emitted per attempt, so a dependency that merely retries must not release a gate. Terminal is not success either: a dependency that dead-letters releases it too, so a success-only rule lives in the host, as examples/success_gate.c shows.)
A logging framework gptps_set_log_sink is one function pointer. Anything more is your host's job.
More executor kinds Three (in-process, forked, external program) span the trust and isolation axes. A fourth is nearly always "an existing one plus a runtime" — which is what addons/gptps_wasm_exec is. The enum is also now closed in code - gptps_register_task rejects a kind it does not know, so an older core meeting a newer add-on refuses the work rather than silently running it as something else.
Convenience wrappers over the C API Bindings and sugar belong in their own repos where they can move at their own pace.

The tie-break, when nothing above decides it: does a user with a name want this? Not "would this be useful" — every proposal is useful to someone hypothetical. This project reached 55 public functions and 11 add-ons before it had a single user, which is the failure mode the rule exists to prevent — and those numbers have only gone up since the rule was written, so it applies to the next proposal harder than it did to the last one.

What would legitimately change the core: a capability that cannot be built on the four called seams at all (task / constraint / observer / scheduler — see Design notes). That is a short list, and the honest way to discover an item on it is to try building the add-on first and report which accessor was missing. ABI 2.1 is what that looks like when it happens: a binary plug-in could not poll for cancellation, so it could not honour a timeout or a cancel — not a preference, an impossibility.

Safety artifacts (commercial)

GPTPS itself is MIT and stays MIT. For teams building it into a product that needs functional-safety certification, a GPTPS Safety Artifacts Package is being prepared under a separate commercial license, the GPTPS Safety Artifacts License. Each package will cover one specific GPTPS release and is planned to hold the safety manual, requirements traceability, verification evidence and related artifacts. It is never part of this repository, it never restricts use of GPTPS itself, and no GPTPS release is certified today. docs/SAFETY.md describes what it will cover, how it is licensed, and what it does not claim.

License

MIT — see LICENSE. No third-party code is vendored; the TOML parser, journal, and all add-ons are first-party. The Safety Artifacts Package is not part of this repository and not covered by the MIT license; see docs/SAFETY.md.

Releases

Packages

Contributors

Languages