/* SPDX-License-Identifier: AGPL-3.0-or-later */ /** * @file resident_queue.c * @brief The Phase 5 resident kernel: it does not return after one op. It sits on BRISC * reading 32-byte descriptors out of a ring in its own L1, executing them, and * recording what it executed -- so the host enqueues work instead of launching it and * the per-op dispatch cost goes away (spec 7.2). * * THE PROTOCOL IS ft_tt_queue.h. Field indices, opcodes and state values are * transliterated from it below and the two must agree; tests/test_queue.c pins the * host half and tests/test_phase5_resident.c pins the pair on silicon. * * THREE THINGS ABOUT THIS KERNEL ARE SAFETY, NOT FEATURES: * * 1. COMPLETION IS PRODUCED BY THE WORK ITEM. completed_token is copied out of the * descriptor and completed_count is incremented only AFTER the descriptor's work * has been done. Neither can be satisfied by a core that merely booted, which is * what RUN_MSG_DONE cannot say (dev notes 3: wait_done_all reported eight cores * complete with one never sent a go). * * 2. EVERY BLOCKING WAIT POLLS stop_request. A resident kernel is a kernel that * blocks, and killing a blocked kernel with assert_reset is exactly how Tensix 0 * was wedged on 2026-08-12 (dev notes 1). So the host retires this kernel by * ASKING -- one word in L1 -- and the ask is visible from inside the semaphore wait * as well as from the idle poll. There is no wait in this file that only a reset * can break. * * 3. RESIDENCY IS BOUNDED. idle_spin_limit consecutive empty polls and the kernel * returns on its own. That is for a host that DIED, not for flow control; a normal * exit is stop_request or a STOP descriptor. * * THE WIRE CONTRACT NOW LIVES IN resident_queue_protocol.h, shared with the resident * matmul triad. It used to be copied into this file; two copies of a contract is how a * field index drifts, and the triad made it three. * * * C99 on Foundry's own `ft_tensix_dataflow.h` — no tt-metal include (plan Phase 6.6). * The wait mode, which used to be a `template ` argument, is now an * ordinary parameter carrying the same compile-time value; `-Os` folds it identically. * Compile-time args: * 0: items per launch. 0 = RESIDENT, the Phase 5 model. 1 = ONE-SHOT, which is the * Phase 4 slow-dispatch baseline built from this same source so that an A/B * measures the DISPATCH and not two different implementations of the work. * Runtime args: the ten of resident_queue_protocol.h, identical on all five processors. * This kernel uses the first four, plus the status slot and the group post; the CB * bound and the group fields are simply 0 when the host has no use for them. * * The kernel RESUMES: tail and completed_count are read from the control block at * entry rather than started at zero, so one-shot mode is the resident loop stopped * after one item and a resident kernel can be retired and relaunched without the host * rebuilding its queue. * * Build (BRISC, NoC 0, relocated into the Phase 4 kernel-config slab): * CTARGS=0 NOC=0 KERNEL_TEXT_BASE=0x15000 bash \ * foundry/tt/build_tensix_dataflow_kernel.sh \ * foundry/tt/kernels/resident_queue.c brisc /tmp/ttk_resident * CTARGS=1 ... /tmp/ttk_oneshot # the baseline arm * * @status RUN ON HARDWARE 2026-08-12 on the p150a, cores 71-74 and 76-79. 1316 B of * text on BRISC. 1.36 us per descriptor of host dispatch, 0.39 us device-side. * @issues None * @todo None */ #include #include #include "ft_tensix_dataflow.h" #include "resident_queue_protocol.h" /** Items this launch executes before returning. 0 = RESIDENT, the Phase 5 model; 1 = * ONE-SHOT, the Phase 4 slow-dispatch baseline built from this same source. A compile-time * argument, so `-Os` folds the comparison away in both arms. */ #define ITEMS_PER_LAUNCH ft_tensix_compile_time_arg(0) void kernel_main(void) { ftq_context ctx; ftq_context_init(&ctx); volatile FTQ_L1_PTR uint32_t *control = ctx.control; volatile FTQ_L1_PTR uint32_t *ring = ctx.ring; const uint32_t capacity_mask = ctx.capacity_mask; const uint32_t idle_spin_limit = ctx.idle_spin_limit; control[kFieldState] = kStateBoot; ftq_status_set(&ctx, kStateBoot); // Refuse a queue that was never set up rather than interpreting whatever L1 held // before. A resident kernel walking a garbage ring issues NoC transactions at // addresses nobody chose, and a wrong endpoint hangs the chip (spec 7.3). // // The CB bound is not checked here, unlike in the triad: this kernel is CB-FREE by // design and never reaches a wait that bound governs. if (control[kFieldMagic] != kMagic) { control[kFieldError] = kErrBadMagic; control[kFieldState] = kStateExit; ftq_status_set(&ctx, kStateExit); return; } if (capacity_mask == 0 || ((capacity_mask + 1u) & capacity_mask) != 0) { control[kFieldError] = kErrBadCapacity; control[kFieldState] = kStateExit; ftq_status_set(&ctx, kStateExit); return; } if (control[kFieldCapacity] != capacity_mask + 1u || idle_spin_limit == 0u) { control[kFieldError] = kErrBadCapacity; control[kFieldState] = kStateExit; ftq_status_set(&ctx, kStateExit); return; } // RESUME rather than restart. One-shot mode is this loop stopped after one item, so // the two arms of the dispatch A/B execute byte-identical per-descriptor code. uint32_t tail = control[kFieldTail]; uint32_t completed = control[kFieldCompletedCount]; uint32_t executed_this_launch = 0; uint32_t idle_spins = 0; uint32_t heartbeat = control[kFieldHeartbeat]; uint32_t error = kErrNone; bool running = true; while (running) { control[kFieldHeartbeat] = ++heartbeat; if (control[kFieldStopRequest] != 0) { break; } if (control[kFieldHead] == tail) { control[kFieldState] = kStateIdle; control[kFieldIdleSpins] = ++idle_spins; if (idle_spins >= idle_spin_limit) { error = kErrIdleTimeout; break; } continue; } idle_spins = 0; control[kFieldState] = kStateExec; bool item_completed = true; volatile FTQ_L1_PTR uint32_t *descriptor = ring + (tail & capacity_mask) * kDescriptorWords; const uint32_t opcode = descriptor[kDescOpcode]; const uint32_t token = descriptor[kDescToken]; const uint32_t a0 = descriptor[kDescA0]; const uint32_t a1 = descriptor[kDescA1]; const uint32_t a2 = descriptor[kDescA2]; const uint32_t a3 = descriptor[kDescA3]; const uint32_t a4 = descriptor[kDescA4]; control[kFieldLastOpcode] = opcode; switch (opcode) { case kOpNop: break; case kOpWriteWord: *(volatile FTQ_L1_PTR uint32_t *)(uintptr_t)(a0) = a1; break; case kOpL1Copy: // Source, destination AND length all arrive on the wire, and this is the one // transfer site in the family with no compile-time-constant length behind it. // ft_tensix_l1_copy_is_issuable() carries the three refusals; a zero length is // the shape that most likely took the card down on 2026-08-12, and a // destination below the read-only end overwrites the far core's firmware // mailboxes and leaves it unbootable at its NEXT boot. if (!ft_tensix_l1_copy_is_issuable(a3, a2, a4)) { error = kErrBadParameters; item_completed = false; running = false; break; } ft_tensix_noc_write(a3, ft_tensix_noc_address(a0, a1, a2), a4); ft_tensix_noc_write_barrier(); break; case kOpSemPost: ft_tensix_noc_semaphore_increment(ft_tensix_noc_address(a0, a1, a2), a3); break; case kOpSemWait: { // The blocking opcode, and the only place a group can deadlock. It polls // stop_request in the SAME loop as the semaphore, so the host's recovery // is a write rather than a reset (rule 2 at the top of this file), and it // carries its own spin bound on top of that. volatile FTQ_L1_PTR uint32_t *semaphore = (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)(a0); const uint32_t bound = (a2 != 0) ? a2 : idle_spin_limit; uint32_t spins = 0; control[kFieldState] = kStateSemWait; while (*semaphore < a1) { control[kFieldHeartbeat] = ++heartbeat; if (control[kFieldStopRequest] != 0) { // Abandoned, not finished. Counting it would retire work that // never happened -- the exact lie rule 1 forbids. item_completed = false; running = false; break; } if (++spins >= bound) { error = kErrSemWaitTimeout; item_completed = false; running = false; break; } } control[kFieldState] = kStateExec; break; } case kOpDelay: { volatile uint32_t counter = 0; for (uint32_t iteration = 0; iteration < a0; iteration++) { counter = counter + 1; } break; } case kOpStop: running = false; break; default: error = kErrBadOpcode; item_completed = false; running = false; break; } // AN ABANDONED DESCRIPTOR ADVANCES NOTHING. It was completed by nobody and handed // to nobody, so neither `tail` nor `completed_count` moves and the next launch // re-attempts it. (This changed with the resident triad: there `tail` is the // handoff pointer the WRITER's cursor follows, so advancing it for an item the // reader abandoned would let the writer retire work that never ran. One meaning // for the word in both kernels is worth more than the older "taken" reading, which // nothing depended on.) if (item_completed) { tail++; control[kFieldTail] = tail; // Token before count: a host that sees the count is entitled to trust the // token it reads next. Both are plain stores from one in-order RISC-V. control[kFieldCompletedToken] = token; control[kFieldCompletedCount] = ++completed; ftq_group_post(&ctx, descriptor[kDescA5], token); executed_this_launch++; if (ITEMS_PER_LAUNCH != 0 && executed_this_launch >= ITEMS_PER_LAUNCH) { running = false; } } } control[kFieldError] = error; control[kFieldState] = kStateExit; ctx.error = error; ftq_status_set(&ctx, kStateExit); }