Skip to main content

vyre_runtime/megakernel/
builder.rs

1//! IR program builders  -  construct the megakernel `Program` from vyre IR.
2//!
3//! Two flavours:
4//! - **Interpreted** (`build_program_sharded`)  -  If-tree opcode dispatch.
5//! - **JIT** (`build_program_jit`)  -  payload processor fused directly.
6
7use std::sync::Arc;
8
9use vyre_foundation::ir::{BufferDecl, DataType, Expr, Node, Program};
10
11use super::handlers::{claimed_slot_bindings, claimed_slot_body, load_miss_body, OpcodeHandler};
12use super::io::{
13    io_word, IO_DESTINATION_CAPABILITY_TABLE, IO_QUEUE_DMA_TAG, IO_SLOT_COUNT, IO_SLOT_WORDS,
14    IO_SOURCE_CAPABILITY_TABLE,
15};
16use super::ir_util::atomic_load_relaxed;
17use super::protocol::*;
18use super::workspace_adapter::ResidentWorkspaceAdapter;
19mod cache;
20mod jit;
21mod priority;
22pub use jit::{build_program_jit, build_program_jit_slots, persistent_body_jit};
23pub use priority::{
24    build_program_priority, build_program_priority_slots, persistent_body_priority,
25    persistent_body_priority_slots,
26};
27
28/// Build the default megakernel IR (256 lanes × 1 workgroup, no custom opcodes).
29#[must_use]
30pub fn build_program() -> Program {
31    build_program_sharded(256, &[])
32}
33
34/// Build the megakernel IR with a custom workgroup size and optional
35/// custom opcodes.
36///
37/// Buffers are declared with concrete `with_count(...)` sizes so the
38/// backend readback layer allocates the right static staging size  -  a
39/// `count=0` default reads back 4 bytes regardless of how much the
40/// kernel wrote.
41#[must_use]
42pub fn build_program_sharded(workgroup_size_x: u32, opcodes: &[OpcodeHandler]) -> Program {
43    build_program_sharded_slots(workgroup_size_x, workgroup_size_x.max(1), opcodes)
44}
45
46/// Build the megakernel IR for an explicit number of ring slots.
47///
48/// This is the production sharded ABI: `slot_count` sizes the ring buffer,
49/// while `workgroup_size_x` controls lanes per workgroup. Dispatch must launch
50/// `slot_count / workgroup_size_x` workgroups so every slot has an owning lane.
51#[must_use]
52pub fn build_program_sharded_slots(
53    workgroup_size_x: u32,
54    slot_count: u32,
55    opcodes: &[OpcodeHandler],
56) -> Program {
57    build_program_sharded_slots_with_io(workgroup_size_x, slot_count, opcodes, false)
58}
59
60/// Build the sharded megakernel IR as a shared immutable template.
61///
62/// Empty opcode sets use the thread-local template cache directly, allowing
63/// compile paths to avoid cloning the cached Program before wrapping it in
64/// `Arc` again.
65#[must_use]
66pub fn build_program_sharded_slots_shared(
67    workgroup_size_x: u32,
68    slot_count: u32,
69    opcodes: &[OpcodeHandler],
70) -> Arc<Program> {
71    if opcodes.is_empty() {
72        return cache::cached_empty_sharded_program_shared(workgroup_size_x, slot_count, false);
73    }
74    Arc::new(build_program_sharded_slots(
75        workgroup_size_x,
76        slot_count,
77        opcodes,
78    ))
79}
80
81/// Build the sharded megakernel IR with a consumer-owned resident workspace.
82#[must_use]
83pub fn build_program_sharded_with_workspace_adapter(
84    workgroup_size_x: u32,
85    slot_count: u32,
86    opcodes: &[OpcodeHandler],
87    adapter: &impl ResidentWorkspaceAdapter,
88) -> Program {
89    wrap_persistent_megakernel_program_with_buffers(
90        default_buffers_with_workspace_adapter(slot_count, adapter),
91        workgroup_size_x,
92        persistent_body_with_workspace_adapter(workgroup_size_x, opcodes, adapter),
93    )
94}
95
96/// Build a finite one-pass sharded megakernel IR for host-submitted batches.
97///
98/// Unlike [`build_program_sharded_slots`], this program does not wrap the body
99/// in `Node::forever`; each lane attempts to drain its owning slot once and the
100/// dispatch returns. Use this for synchronous batch APIs that need a completion
101/// report from the same queue submission.
102#[must_use]
103pub fn build_program_sharded_once_slots(
104    workgroup_size_x: u32,
105    slot_count: u32,
106    opcodes: &[OpcodeHandler],
107) -> Program {
108    if opcodes.is_empty() {
109        return cache::cached_empty_sharded_once_program(workgroup_size_x, slot_count);
110    }
111    wrap_megakernel_program(
112        workgroup_size_x,
113        slot_count,
114        finite_body_with_io(workgroup_size_x, opcodes, false),
115    )
116}
117
118/// Shared-Arc variant of [`build_program_sharded_once_slots`] for hot runtime
119/// dispatchers that must not clone the megakernel template every launch.
120#[must_use]
121pub fn build_program_sharded_once_slots_shared(
122    workgroup_size_x: u32,
123    slot_count: u32,
124    opcodes: &[OpcodeHandler],
125) -> Arc<Program> {
126    if opcodes.is_empty() {
127        return cache::cached_empty_sharded_once_program_shared(workgroup_size_x, slot_count);
128    }
129    Arc::new(build_program_sharded_once_slots(
130        workgroup_size_x,
131        slot_count,
132        opcodes,
133    ))
134}
135
136/// Build a finite one-pass megakernel that reports completion through the
137/// control buffer only.
138///
139/// Ring, debug, and IO buffers remain read-write device buffers, but their
140/// host readback ranges are empty. This is the hot dispatcher path: completion
141/// is already accumulated into control, so reading back the full ring/debug/IO
142/// surfaces is redundant launch latency.
143#[must_use]
144pub fn build_program_sharded_once_slots_control_report_shared(
145    workgroup_size_x: u32,
146    slot_count: u32,
147    opcodes: &[OpcodeHandler],
148) -> Arc<Program> {
149    if opcodes.is_empty() {
150        return cache::cached_empty_sharded_once_control_report_program_shared(
151            workgroup_size_x,
152            slot_count,
153        );
154    }
155    let mut buffers = default_buffers(slot_count);
156    for buffer in buffers.iter_mut().skip(1) {
157        buffer.output_byte_range = Some(0..0);
158    }
159    Arc::new(prepare_megakernel_program(Program::wrapped(
160        buffers,
161        [workgroup_size_x, 1, 1],
162        finite_body_with_io(workgroup_size_x, opcodes, false),
163    )))
164}
165
166/// Build the megakernel IR without the IO polling sidecar.
167///
168/// This is the dispatch path for host-provided [`super::ResidentWorkItem`]
169/// queues. It keeps the executable kernel free of `AsyncLoad` nodes until the
170/// runtime scheduler owns a concrete async-lowering pass.
171#[must_use]
172pub fn build_program_sharded_no_io(workgroup_size_x: u32, opcodes: &[OpcodeHandler]) -> Program {
173    build_program_sharded_slots(workgroup_size_x, workgroup_size_x.max(1), opcodes)
174}
175
176/// Build the megakernel IR with the experimental IO polling sidecar.
177///
178/// The returned Program contains `AsyncLoad` nodes and must be lowered through
179/// a runtime scheduler pass before reaching a concrete backend lowering path.
180#[must_use]
181pub fn build_program_sharded_with_io_polling(
182    workgroup_size_x: u32,
183    opcodes: &[OpcodeHandler],
184) -> Program {
185    build_program_sharded_slots_with_io(workgroup_size_x, workgroup_size_x.max(1), opcodes, true)
186}
187
188/// Build the megakernel IR with a self-loading load-miss handler.
189///
190/// The persistent loop is extended with an [`opcode::LOAD_MISS`] handler.
191/// When the GPU sees this opcode it scans the IO queue for an empty slot,
192/// writes a DMA-read request, and polls until the host/runtime marks it
193/// complete. The `arg0` field of the slot is the consumer's opaque
194/// resource identifier; vyre does not interpret it.
195#[must_use]
196#[cfg(test)]
197pub fn build_program_with_self_loading_miss_handler(
198    workgroup_size_x: u32,
199    slot_count: u32,
200    opcodes: &[OpcodeHandler],
201) -> Program {
202    match try_build_program_with_self_loading_miss_handler(workgroup_size_x, slot_count, opcodes) {
203        Ok(program) => program,
204        Err(error) => panic!("{error}"),
205    }
206}
207
208/// Fallible variant of `build_program_with_self_loading_miss_handler` (test-only panic shim exists; production uses this fallible entry).
209pub fn try_build_program_with_self_loading_miss_handler(
210    workgroup_size_x: u32,
211    slot_count: u32,
212    opcodes: &[OpcodeHandler],
213) -> Result<Program, String> {
214    let mut extended = Vec::new();
215    let extended_len = opcodes.len().checked_add(1).ok_or_else(|| {
216        "megakernel self-loading opcode extension count overflowed usize. Fix: split opcode handler sets before building the megakernel."
217            .to_string()
218    })?;
219    vyre_foundation::allocation::try_reserve_vec_to_capacity(&mut extended, extended_len).map_err(|error| {
220        format!(
221            "megakernel self-loading opcode extension allocation failed: {error}. Fix: split opcode handler sets before building the megakernel."
222        )
223    })?;
224    extended.extend_from_slice(opcodes);
225    extended.push(OpcodeHandler {
226        opcode: super::protocol::opcode::LOAD_MISS,
227        body: load_miss_body(),
228    });
229    Ok(wrap_persistent_megakernel_program(
230        workgroup_size_x,
231        slot_count,
232        persistent_body_with_io(workgroup_size_x, &extended, false),
233    ))
234}
235
236fn build_program_sharded_slots_with_io(
237    workgroup_size_x: u32,
238    slot_count: u32,
239    opcodes: &[OpcodeHandler],
240    include_io_polling: bool,
241) -> Program {
242    if opcodes.is_empty() {
243        return cache::cached_empty_sharded_program(
244            workgroup_size_x,
245            slot_count,
246            include_io_polling,
247        );
248    }
249    wrap_persistent_megakernel_program(
250        workgroup_size_x,
251        slot_count,
252        persistent_body_with_io(workgroup_size_x, opcodes, include_io_polling),
253    )
254}
255
256fn wrap_persistent_megakernel_program(
257    workgroup_size_x: u32,
258    slot_count: u32,
259    body: Vec<Node>,
260) -> Program {
261    wrap_megakernel_program(workgroup_size_x, slot_count, vec![Node::forever(body)])
262}
263
264fn wrap_persistent_megakernel_program_with_buffers(
265    buffers: Vec<BufferDecl>,
266    workgroup_size_x: u32,
267    body: Vec<Node>,
268) -> Program {
269    prepare_megakernel_program(Program::wrapped(
270        buffers,
271        [workgroup_size_x, 1, 1],
272        vec![Node::forever(body)],
273    ))
274}
275
276fn wrap_megakernel_program(workgroup_size_x: u32, slot_count: u32, body: Vec<Node>) -> Program {
277    prepare_megakernel_program(Program::wrapped(
278        default_buffers(slot_count),
279        [workgroup_size_x, 1, 1],
280        body,
281    ))
282}
283
284fn prepare_megakernel_program(program: Program) -> Program {
285    // Barrier elision is infallible because its working buffers are bounded by
286    // the IR node count. Semantic optimization runs once in `lower_verified`.
287    super::planner::elide_value_flow_barriers(program).0
288}
289
290/// Reserve sizes for the megakernel's four host-visible buffers. All
291/// four go through the static-readback path so every buffer needs
292/// a concrete `count` (u32 elements). The numbers mirror the wire
293/// layout in `protocol.rs`:
294///
295/// - **control**: 128 u32 words covers SHUTDOWN, DONE_COUNT, EPOCH,
296///   METRICS_BASE..METRICS_BASE+METRICS_SLOTS, OBSERVABLE_BASE, and
297///   the 32-entry tenant-mask table.
298/// - **ring_buffer**: `slot_count` slots × `SLOT_WORDS`.
299///   `slot_count` must match host-published ring bytes and dispatch geometry.
300/// - **debug_log**: cursor word + `debug::RECORD_CAPACITY` × 4-word records.
301/// - **io_queue**: 64 slots × 8 words (source, destination,
302///   offset_low, offset_high, size, status, tag, pad).
303fn default_buffers(slot_count: u32) -> Vec<BufferDecl> {
304    let ring_slots = slot_count.max(1);
305    let control = BufferDecl::read_write("control", 0, DataType::U32).with_count(CONTROL_MIN_WORDS);
306    let ring_buffer = BufferDecl::read_write("ring_buffer", 1, DataType::U32)
307        .with_count(ring_slots.saturating_mul(SLOT_WORDS));
308    let debug_log =
309        BufferDecl::read_write("debug_log", 2, DataType::U32).with_count(debug::BUFFER_WORDS);
310    let io_queue = BufferDecl::read_write("io_queue", 3, DataType::U32).with_count(64 * 8);
311    vec![control, ring_buffer, debug_log, io_queue]
312}
313
314fn default_buffers_with_workspace_adapter(
315    slot_count: u32,
316    adapter: &impl ResidentWorkspaceAdapter,
317) -> Vec<BufferDecl> {
318    let mut buffers = default_buffers(slot_count);
319    buffers.push(adapter.buffer_decl());
320    buffers
321}
322
323/// The body that runs once per iteration per lane. Exposed for tests
324/// and downstream crates that splice additional opcodes.
325#[must_use]
326pub fn persistent_body(workgroup_size_x: u32, opcodes: &[OpcodeHandler]) -> Vec<Node> {
327    persistent_body_with_io(workgroup_size_x, opcodes, false)
328}
329
330/// Fallible persistent body builder with explicit staging-allocation reporting.
331pub fn try_persistent_body(
332    workgroup_size_x: u32,
333    opcodes: &[OpcodeHandler],
334) -> Result<Vec<Node>, String> {
335    try_persistent_body_with_io(workgroup_size_x, opcodes, false)
336}
337
338fn persistent_body_with_io(
339    workgroup_size_x: u32,
340    opcodes: &[OpcodeHandler],
341    include_io_polling: bool,
342) -> Vec<Node> {
343    let mut body = persistent_lane_prologue(workgroup_size_x);
344    let additional_nodes = if include_io_polling { 3 } else { 2 };
345    if let Some(body_capacity) = body.len().checked_add(additional_nodes) {
346        let _ = vyre_foundation::allocation::try_reserve_vec_to_capacity(&mut body, body_capacity);
347    }
348    body.push(direct_slot_base_binding());
349    body.push(Node::Block(execute_slot_body(opcodes)));
350    if include_io_polling {
351        body.push(Node::Block(process_io_requests()));
352    }
353    body
354}
355
356fn finite_body_with_io(
357    workgroup_size_x: u32,
358    opcodes: &[OpcodeHandler],
359    include_io_polling: bool,
360) -> Vec<Node> {
361    let mut body = vec![Node::let_bind("lane_id", lane_id_expr(workgroup_size_x))];
362    let additional_nodes = if include_io_polling { 3 } else { 2 };
363    if let Some(body_capacity) = body.len().checked_add(additional_nodes) {
364        let _ = vyre_foundation::allocation::try_reserve_vec_to_capacity(&mut body, body_capacity);
365    }
366    body.push(direct_slot_base_binding());
367    body.push(Node::Block(execute_slot_body(opcodes)));
368    if include_io_polling {
369        body.push(Node::Block(process_io_requests()));
370    }
371    body
372}
373
374fn try_persistent_body_with_io(
375    workgroup_size_x: u32,
376    opcodes: &[OpcodeHandler],
377    include_io_polling: bool,
378) -> Result<Vec<Node>, String> {
379    let mut body = persistent_lane_prologue(workgroup_size_x);
380    let additional_nodes = if include_io_polling { 3 } else { 2 };
381    let body_capacity = body.len().checked_add(additional_nodes).ok_or_else(|| {
382        "megakernel persistent body node reservation overflowed usize. Fix: reduce fused IO/body staging before building the megakernel."
383            .to_string()
384    })?;
385    vyre_foundation::allocation::try_reserve_vec_to_capacity(&mut body, body_capacity).map_err(|error| {
386        format!(
387            "megakernel persistent body node reservation failed: {error}. Fix: reduce fused IO/body staging before building the megakernel."
388        )
389    })?;
390    body.push(direct_slot_base_binding());
391    body.push(Node::Block(execute_slot_body(opcodes)));
392    if include_io_polling {
393        body.push(Node::Block(process_io_requests()));
394    }
395    Ok(body)
396}
397
398fn persistent_lane_prologue(workgroup_size_x: u32) -> Vec<Node> {
399    vec![
400        Node::let_bind(
401            "shutdown_flag",
402            atomic_load_relaxed("control", Expr::u32(control::SHUTDOWN)),
403        ),
404        Node::if_then(
405            Expr::ne(Expr::var("shutdown_flag"), Expr::u32(0)),
406            vec![Node::Return],
407        ),
408        Node::let_bind("lane_id", lane_id_expr(workgroup_size_x)),
409    ]
410}
411
412fn direct_slot_base_binding() -> Node {
413    Node::let_bind(
414        "slot_base",
415        Expr::mul(Expr::var("lane_id"), Expr::u32(SLOT_WORDS)),
416    )
417}
418
419fn slot_tenant_id_load() -> Expr {
420    Expr::load(
421        "ring_buffer",
422        Expr::add(Expr::var("slot_base"), Expr::u32(TENANT_WORD)),
423    )
424}
425
426fn tenant_authorized_body(tenant_id: Expr, authorized_body: Vec<Node>) -> Vec<Node> {
427    vec![
428        Node::let_bind("tenant_id", tenant_id),
429        Node::let_bind(
430            "tenant_base",
431            atomic_load_relaxed("control", Expr::u32(control::TENANT_BASE)),
432        ),
433        Node::let_bind(
434            "tenant_mask",
435            atomic_load_relaxed(
436                "control",
437                Expr::add(Expr::var("tenant_base"), Expr::var("tenant_id")),
438            ),
439        ),
440        Node::if_then(
441            Expr::ne(Expr::var("tenant_mask"), Expr::u32(0)),
442            authorized_body,
443        ),
444    ]
445}
446
447fn lane_id_expr(workgroup_size_x: u32) -> Expr {
448    Expr::add(
449        Expr::mul(Expr::workgroup_x(), Expr::u32(workgroup_size_x)),
450        Expr::local_x(),
451    )
452}
453
454fn persistent_body_with_workspace_adapter(
455    workgroup_size_x: u32,
456    opcodes: &[OpcodeHandler],
457    adapter: &impl ResidentWorkspaceAdapter,
458) -> Vec<Node> {
459    let mut body = adapter.bootstrap_nodes();
460    body.extend(adapter.guard_nodes());
461    body.extend(adapter.dispatch_nodes());
462    body.extend(persistent_body_with_io(workgroup_size_x, opcodes, false));
463    body
464}
465
466fn process_io_requests() -> Vec<Node> {
467    let nodes = vec![Node::loop_for(
468        "io_idx",
469        Expr::u32(0),
470        Expr::u32(IO_SLOT_COUNT),
471        vec![
472            Node::let_bind(
473                "io_base",
474                Expr::mul(Expr::var("io_idx"), Expr::u32(IO_SLOT_WORDS)),
475            ),
476            Node::let_bind(
477                "io_status_idx",
478                Expr::add(Expr::var("io_base"), Expr::u32(io_word::STATUS)),
479            ),
480            // CAS PUBLISHED -> CLAIMED
481            Node::let_bind(
482                "prev_io_status",
483                Expr::atomic_compare_exchange(
484                    "io_queue",
485                    Expr::var("io_status_idx"),
486                    Expr::u32(slot::PUBLISHED),
487                    Expr::u32(slot::CLAIMED),
488                ),
489            ),
490            Node::if_then(
491                Expr::eq(Expr::var("prev_io_status"), Expr::u32(slot::PUBLISHED)),
492                vec![
493                    Node::let_bind(
494                        "io_src_handle",
495                        Expr::load(
496                            "io_queue",
497                            Expr::add(Expr::var("io_base"), Expr::u32(io_word::SRC_HANDLE)),
498                        ),
499                    ),
500                    Node::let_bind(
501                        "io_dst_handle",
502                        Expr::load(
503                            "io_queue",
504                            Expr::add(Expr::var("io_base"), Expr::u32(io_word::DST_HANDLE)),
505                        ),
506                    ),
507                    Node::AsyncLoad {
508                        source: IO_SOURCE_CAPABILITY_TABLE.into(),
509                        destination: IO_DESTINATION_CAPABILITY_TABLE.into(),
510                        offset: Box::new(Expr::load(
511                            "io_queue",
512                            Expr::add(Expr::var("io_base"), Expr::u32(io_word::OFFSET_LO)),
513                        )),
514                        size: Box::new(Expr::load(
515                            "io_queue",
516                            Expr::add(Expr::var("io_base"), Expr::u32(io_word::BYTE_COUNT)),
517                        )),
518                        tag: IO_QUEUE_DMA_TAG.into(),
519                    },
520                    // Mark as DONE
521                    Node::store(
522                        "io_queue",
523                        Expr::var("io_status_idx"),
524                        Expr::u32(slot::DONE),
525                    ),
526                ],
527            ),
528        ],
529    )];
530
531    nodes
532}
533
534fn execute_slot_body(opcodes: &[OpcodeHandler]) -> Vec<Node> {
535    vec![
536        Node::let_bind(
537            "status_index",
538            Expr::add(Expr::var("slot_base"), Expr::u32(STATUS_WORD)),
539        ),
540        Node::let_bind(
541            "observed_status",
542            atomic_load_relaxed("ring_buffer", Expr::var("status_index")),
543        ),
544        Node::if_then(
545            Expr::eq(Expr::var("observed_status"), Expr::u32(slot::PUBLISHED)),
546            tenant_authorized_claim_body(slot_tenant_id_load(), claimed_slot_body(opcodes)),
547        ),
548    ]
549}
550
551fn tenant_authorized_claim_body(tenant_id: Expr, claimed_body: Vec<Node>) -> Vec<Node> {
552    tenant_authorized_body(
553        tenant_id,
554        vec![
555            // CAS PUBLISHED -> CLAIMED after authorization. This keeps
556            // disabled tenants visible to the host instead of converting
557            // their slots into stuck CLAIMED work.
558            Node::let_bind(
559                "prev_status",
560                Expr::atomic_compare_exchange(
561                    "ring_buffer",
562                    Expr::var("status_index"),
563                    Expr::u32(slot::PUBLISHED),
564                    Expr::u32(slot::CLAIMED),
565                ),
566            ),
567            Node::if_then(
568                Expr::eq(Expr::var("prev_status"), Expr::u32(slot::PUBLISHED)),
569                claimed_body,
570            ),
571        ],
572    )
573}
574
575fn execute_already_claimed_slot_body(tenant_id: Expr, claimed_body: Vec<Node>) -> Vec<Node> {
576    let mut body = vec![Node::let_bind(
577        "status_index",
578        Expr::add(Expr::var("slot_base"), Expr::u32(STATUS_WORD)),
579    )];
580    body.extend(tenant_authorized_body(tenant_id, claimed_body));
581    body
582}
583
584#[cfg(test)]
585mod tests;