261 lines
11 KiB
C
261 lines
11 KiB
C
/* 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 <uint32_t kMode>` 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 <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#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);
|
|
}
|