Continuous batching

Refill a numerical batch between iterations without holding short requests behind the longest member.

---

The complete example source runs two numerical batch groups and an unrelated heartbeat process on one loop. It uses only public APIs from xtc_batch.h, xtc_sched.h, and the process/loop interfaces. Each group has a distinct scheduling class.

One workload, two admission rules

Each request carries eight unsigned 64-bit components. In each vector step, the callback prepares eight coefficients once and uses them to apply an affine recurrence to every ready request. Requests need different numbers of rounds: 64 for the original long member, 4 for the original short member, and 2 for a late short member. The example checks the result against an independent scalar calculation, including defined unsigned wraparound.

With --mode=static, the application holds the late request until the original cohort finishes. With --mode=continuous, it submits the late request between iterations, as soon as the original short member has finished. Both modes use the same callback and perform 1120 component updates across the two groups. The late short member finishes before the original long member only in continuous mode.

The batch engine owns membership and invokes the callback with a vector of ready requests. The application owns numerical state and decides when to submit a new request. Static mode needs no second engine: withholding new submissions until the cohort drains supplies the static baseline.

Scheduling is a separate choice

--policy=default uses ordinary scheduling; --policy=continuous installs xtc_sched_continuous_set before running the loop, with max_bypass=3. The built-in policy prefers ready batch lanes while bounding bypass of other runnable lanes. It does not change the arithmetic or admission rule. Continuous refill works with either policy, and the self-test runs all four mode/policy combinations.

The default-lane heartbeat yields while the two groups work. No part of the example sleeps or uses elapsed time to manufacture an advantage. Completion order demonstrates the admission trade-off; it is not evidence of higher throughput or a promise about wall-clock latency.

Backpressure and lifecycle

Each group allows three live requests, two active members, and two resource units. A full-capacity submit returns XTC_E_AGAIN; the caller retains the request and retries later. A queued request is cancelled before doing arithmetic. After two rounds, the long request returns XTC_BATCH_BLOCKED to represent an unavailable dependency. It retains its reservation but leaves the ready vector until the owner process calls xtc_batch_resume.

Callbacks only compute or record results. The owner process performs submission, resume, and close operations outside callbacks. The example uses no cross-thread batch calls. It creates groups before the first loop run and keeps borrowed request storage alive until after completion.

For teardown, the owner closes with XTC_BATCH_DRAIN and lets the loop finish naturally. After the drivers exit, the application reads final statistics, destroys the groups, and finalizes the loop. xtc_loop_stop would not provide that drain guarantee.

Build and check

Configure libxtc with --enable-sched-policy and build libxtc.a. In examples/12_continuous_batching, run make XTC_BUILD=/absolute/path/to/configured-build test. See the README and Makefile for the build contract and command-line options.

--self-test checks exact scalar results, one terminal callback per accepted request, cancellation, capacity retry, BLOCKED/resume, heartbeat progress, released resources, and per-group completion order. Success ends with continuous batching self-test: PASS (4 mode/policy combinations). These checks remain enabled with NDEBUG and use no timing thresholds. Feature-enabled builds run the self-test through root make check (or make check-continuous-batching). The scheduler-policy CI job also runs it; feature-disabled builds explicitly skip this example.