nx_batch_scheduler.nx
buildroot/runtime/nx_batch_scheduler.nx
about
nx_batch_scheduler.nx -- H2 continuous batching (bits-up).
Per NISHI_ELDER_AI_OFF_DOCKER_2026_05_20.md ยง2.1 H2: pack pending
requests at TOKEN level not request level so the GPU stays
saturated. vLLM + TGI ship this pattern at scale (~10x throughput
over naive request-by-request scheduling).
V1 mechanics: N-slot fixed scheduler. Each slot holds at most
one active request (sequence_id + tokens_remaining + state).
admit() places a request in a free slot OR queues it. step()
reports the slots currently in RUNNING state -- those are the
active batch for the next forward pass. complete() marks a
slot DONE. recycle() reclaims DONE slots and drains the
pending queue into the newly-free slots.
The "continuous" part: at each scheduler step, the active batch
composition CAN CHANGE. A slot whose request finished at token
N gets reclaimed and refilled with a queued request at token
0 -- so the GPU forward pass never has to wait for the whole
batch to finish.
Pure substrate logic. No Linux features. Composes with shipped
nx_kv_arena (request's sequence holds its KV pages) and the
future actor cardinal (sequences serve text + image-prompt +
any other endpoint).
V1 honest scope:
- Fixed slot count (N <= NX_BATCH_MAX_SLOTS = 32)
- Fixed pending queue (M <= NX_BATCH_MAX_PENDING = 64)
- First-come-first-served queue ordering (no priority yet)
- No per-sequence preemption (a slot runs to DONE; preemption
queued for V2 when attention-class arbitration lands)
genealogy_id: vllm_continuous_batching_2023 + tgi_2023 +
cardinal_2026-05-20_elder_ai_off_docker +
cardinal_2026-05-20_bits_up_nishi_not_linux
lineage_id: substrate_batch_scheduler_v1
nx_capability_manifest:
variant_class: batch_scheduler
dependencies 1 imports · 2 importers
imports: nx_syscalls.nx
imported by: nx_batch_scheduler_test.nxnx_hackers_algo_compose_test.nx
structs
| 107 | struct NxBatchSlot |
| 114 | struct NxBatchScheduler |
consts
| 65 | const NX_BATCH_MAX_SLOTS: i64 = 32 |
| 66 | const NX_BATCH_MAX_PENDING: i64 = 64 |
| 69 | const NX_SLOT_FREE: i64 = 0 |
| 70 | const NX_SLOT_RUNNING: i64 = 1 |
| 71 | const NX_SLOT_DONE: i64 = 2 |
| 72 | const NX_SLOT_N_STATES: i64 = 3 |
| 87 | const NX_BATCH_OK: i64 = 0 |
| 88 | const NX_BATCH_BAD_INPUT: i64 = 1 |
| 89 | const NX_BATCH_QUEUED: i64 = 2 // admitted to queue, not slot |
| 90 | const NX_BATCH_FULL: i64 = 3 // slots + queue both full |
| 91 | const NX_BATCH_SLOT_NOT_RUNNING: i64 = 4 |
| 92 | const NX_BATCH_BAD_SLOT: i64 = 5 |
| 93 | const NX_BATCH_TAMPER: i64 = 6 |
| 94 | const NX_BATCH_N_VERDICTS: i64 = 7 |
| 103 | const NX_BATCH_CANARY_PRE: i64 = 0x4E5842415453504C // "NXBATSPL" |
| 104 | const NX_BATCH_CANARY_POST: i64 = 0x4E58424154454E44 // "NXBATEND" |
functions
| 74 | func nx_slot_state_is_valid(s: i64) -> i64 called by 1: main |
| 96 | func nx_batch_verdict_is_valid(v: i64) -> i64 called by 1: main |
| 131 | func nx_batch_scheduler_new(max_slots: i64, max_pending: i64) -> *NxBatchScheduler |
| 163 | func nx_batch_scheduler_is_valid(sch: *NxBatchScheduler) -> i64 |
| 178 | func _batch_find_free_slot(sch: *NxBatchScheduler) -> i64 |
| 193 | func nx_batch_scheduler_admit( |
| 222 | func nx_batch_scheduler_step( |
| 247 | func nx_batch_scheduler_tick( |
| 271 | func nx_batch_scheduler_complete( |
| 292 | func nx_batch_scheduler_recycle(sch: *NxBatchScheduler) -> i64 |
| 332 | func nx_batch_scheduler_n_running(sch: *NxBatchScheduler) -> i64 |
| 337 | func nx_batch_scheduler_n_pending(sch: *NxBatchScheduler) -> i64 |
| 342 | func nx_batch_scheduler_n_done(sch: *NxBatchScheduler) -> i64 |
| 347 | func nx_batch_scheduler_slot_state(sch: *NxBatchScheduler, slot_idx: i64) -> i64 |
| 355 | func nx_batch_scheduler_slot_request_id(sch: *NxBatchScheduler, slot_idx: i64) -> i64 |
| 363 | func nx_batch_scheduler_slot_tokens_remaining(sch: *NxBatchScheduler, slot_idx: i64) -> i64 |