/* SPDX-License-Identifier: AGPL-3.0-or-later */ /** * @file resident_queue_protocol.h * @brief Device half of Foundry's resident command queue: the wire contract, the per-kernel * queue context, and the stoppable, bounded waits every resident kernel uses. * * The host half is tt/ft_tt_queue.h; every number in the wire contract below must agree with * it. The header is shared by the resident data-movement kernels (BRISC/NCRISC) and the * resident compute kernels (TRISC, built once per thread), and it is a content input to all * of their images. * * Safety model: * - Every control word has exactly one writer. Completion is published by the last * processor of the pipeline. * - Every blocking CB wait can be asked to stop (the host writes the stop word) and is * bounded by a spin limit. * - An abort does not return early from the descriptor. It sets `aborting`; every later * wait in the same descriptor then returns false at once, while the kernel's loops still * run their full trip counts so the peer threads' hardware handshakes stay balanced. An * abandoned descriptor is never counted complete. * - NoC barriers and the MATH/PACK/dvalid hardware stalls are deliberately not stoppable. * * WIRE CONTRACT FORM: off-hardware tests parse this file line by line, so every wire constant * is its own one-line anonymous enum `enum { name = value };`. Opcodes are the only * identifiers here that start with `kOp`. kMatmulColumnBlock, kMatmulWords and kDescA2 are * parsed as decimal. * * @status Complete for the data-movement and C99 compute arms. * @issues None * @todo None */ #ifndef RESIDENT_QUEUE_PROTOCOL_H #define RESIDENT_QUEUE_PROTOCOL_H #ifdef __cplusplus #include #else #include #include #endif #if defined(COMPILE_FOR_TRISC) && defined(__cplusplus) #error "resident_queue_protocol.h: there is no C++ compute arm; build compute kernels as C" #endif #if !defined(COMPILE_FOR_TRISC) #include "ft_tensix_dataflow.h" #else #include "ft_tensix_compute_cb.h" #include "ft_tensix_kernel_args.h" #endif /* Linkage of every helper here (family protocol headers spell it identically). */ #ifndef FTQ_INLINE #ifdef __cplusplus #define FTQ_INLINE inline #else #define FTQ_INLINE static inline #endif #endif /* The compiler's L1 pointer attribute, spelled locally so this header does not depend on the * register header for it. */ #ifndef FTQ_L1_PTR #define FTQ_L1_PTR __attribute__((rvtt_l1_ptr)) #endif #ifdef __cplusplus #define FTQ_STATIC_ASSERT(condition, message) static_assert(condition, message) #else #define FTQ_STATIC_ASSERT(condition, message) _Static_assert(condition, message) #endif #define FTQ_RUNTIME_ARG(index) ft_tensix_runtime_arg(index) /* ---- Control-block word indices (4-byte words from the control block base) ---- */ enum { kFieldMagic = 0 }; enum { kFieldCapacity = 1 }; enum { kFieldHead = 2 }; /* descriptors published by the host */ enum { kFieldTail = 3 }; /* descriptors taken by the reader */ enum { kFieldStopRequest = 4 }; /* host-written stop word */ enum { kFieldCompletedCount = 5 }; /* written by the last processor (writer) */ enum { kFieldCompletedToken = 6 }; enum { kFieldState = 7 }; enum { kFieldLastOpcode = 8 }; enum { kFieldError = 9 }; enum { kFieldIdleSpins = 10 }; enum { kFieldHeartbeat = 11 }; enum { kFieldGroupDone = 56 }; /* byte 0xE0: finished group sequence */ enum { kFieldIdleClock = 60 }; /* NCRISC's accumulated idle-for-work clock */ /* ---- Magic, states, errors ---- */ enum { kMagic = 0x46545131 }; /* "FTQ1" */ enum { kStatePoison = 0x7B1B1B1B }; /* host-written poison */ enum { kStateBoot = 0x51010001 }; enum { kStateIdle = 0x51010002 }; enum { kStateExec = 0x51010003 }; enum { kStateSemWait = 0x51010004 }; enum { kStateExit = 0x51010005 }; enum { kStateCbWaitFront = 0x51010006 }; enum { kStateCbReserveBack = 0x51010007 }; enum { kErrNone = 0 }; enum { kErrBadMagic = 1 }; enum { kErrBadOpcode = 2 }; enum { kErrIdleTimeout = 3 }; enum { kErrSemWaitTimeout = 4 }; enum { kErrBadCapacity = 5 }; enum { kErrCbWaitTimeout = 6 }; enum { kErrBadParameters = 7 }; enum { kErrCoopRoleMismatch = 8 }; enum { kErrCoopWritePointer = 9 }; enum { kErrCoopRendezvous = 10 }; enum { kErrCoopIssue = 11 }; enum { kErrCoopBadPartition = 12 }; /* ---- Opcodes ---- */ enum { kOpNop = 0x01 }; enum { kOpWriteWord = 0x02 }; enum { kOpL1Copy = 0x03 }; enum { kOpSemPost = 0x04 }; enum { kOpSemWait = 0x05 }; enum { kOpDelay = 0x06 }; enum { kOpStop = 0x07 }; enum { kOpMatmul = 0x08 }; enum { kOpEltwise = 0x09 }; enum { kOpSoftmax = 0x0A }; enum { kOpReduce = 0x0B }; enum { kOpEmbedding = 0x0C }; enum { kOpKvAppend = 0x0D }; enum { kOpRope = 0x0E }; enum { kOpTopk = 0x0F }; enum { kOpSample = 0x10 }; enum { kOpAttention = 0x11 }; enum { kOpGdn = 0x12 }; /* ---- Descriptor (one ring entry, 8 words) ---- */ enum { kDescriptorWords = 8 }; enum { kDescOpcode = 0 }; enum { kDescToken = 1 }; enum { kDescA0 = 2 }; enum { kDescA1 = 3 }; enum { kDescA2 = 4 }; enum { kDescA3 = 5 }; enum { kDescA4 = 6 }; enum { kDescA5 = 7 }; /* group sequence number; 0 = no group-completion post */ /* ---- Per-processor status slot (8 words = 32 bytes, so no two processors' heartbeats * share a 16-byte L1 line) ---- */ enum { kStatusState = 0 }; enum { kStatusCursor = 1 }; enum { kStatusHeartbeat = 2 }; enum { kStatusError = 3 }; enum { kStatusLastOpcode = 4 }; enum { kStatusSpins = 5 }; enum { kStatusPayload = 6 }; /* 2 words: accumulated wait clock, wait count */ /* ---- Group completion block: [sequence, token] per member from byte 0x40 ---- */ enum { kGroupSlotOffsetWords = 16 }; enum { kGroupSlotWords = 2 }; /* ---- Pull completion (matmul writer), offsets from the control block ---- */ enum { kPullTableOffset = 0xF900 }; /* magic, count, then x | y << 16 per member */ enum { kPullScratchOffset = 0xFF00 }; /* 16-byte landing for the remote read */ enum { kPullMagic = 0x46545150 }; /* "FTQP" */ enum { kPullMaxMembers = 64 }; enum { kPullRedSkip = 0x80000000u }; /* bit 31 of a3: skip (unsigned enumerator) */ /* ---- Matmul parameter block (16 words at descriptor a0) ---- */ enum { kMatmulTileRows = 0 }; enum { kMatmulTileInner = 1 }; enum { kMatmulTileCols = 2 }; enum { kMatmulAX = 3 }; enum { kMatmulAY = 4 }; enum { kMatmulABase = 5 }; enum { kMatmulBX = 6 }; enum { kMatmulBY = 7 }; enum { kMatmulBBase = 8 }; enum { kMatmulCX = 9 }; enum { kMatmulCY = 10 }; enum { kMatmulCBase = 11 }; enum { kMatmulDepth = 12 }; enum { kMatmulBNMajor = 13 }; /* flag word: bit 0 N-major, 8 pipelined, 9 RED never-wait */ enum { kMatmulCooperative = 14 }; enum { kMatmulColumnBlock = 15 }; enum { kMatmulWords = 16 }; enum { kMatmulRow0 = 1u << 10 }; enum { kMatmulProbeNoRead = 1u << 12 }; enum { kMatmulProbeNoCompute = 1u << 13 }; enum { kMatmulProbeNoWrite = 1u << 14 }; enum { kMatmulWriterFlush = 1u << 15 }; enum { kMatmulAReuse = 1u << 18 }; enum { kMatmulAReuseRed = 1u << 19 }; /* ---- Runtime-argument layout (same on all five processors) ---- */ enum { kArgControl = 0 }; /* control-block L1 address */ enum { kArgCapacityMask = 1 }; /* capacity - 1 */ enum { kArgRing = 2 }; /* descriptor ring L1 address */ enum { kArgIdleSpinLimit = 3 }; enum { kArgStatus = 4 }; /* this processor's status slot, 0 = none */ enum { kArgCbSpinLimit = 5 }; enum { kArgGroupX = 6 }; enum { kArgGroupY = 7 }; enum { kArgGroupAddress = 8 }; /* 0 = no group completion */ enum { kArgGroupMember = 9 }; /* ---- Poll masks for blocked waits ---- */ enum { kStopPollMask = 0 }; /* poll the stop word every blocked pass */ enum { kHeartbeatPollMask = 0xFF }; /* bump the heartbeat every 256 blocked passes */ /* ---- Wait modes (compile-time argument passed as a parameter). Mode 2 (bound only, stop * ignored) needs no code of its own: only mode 1 polls the stop word. ---- */ enum { kWaitUpstream = 0 }; /* plain blocking wait, no escape, no bound */ enum { kWaitStoppable = 1 }; /* stop-aware and bounded */ FTQ_STATIC_ASSERT(kStatePoison != kStateExit, "poison must not read as a clean exit"); FTQ_STATIC_ASSERT(kStatePoison != 0, "poison must not read as zero"); /** One processor's view of the queue. */ typedef struct ftq_context { volatile FTQ_L1_PTR uint32_t *control; volatile FTQ_L1_PTR uint32_t *ring; volatile FTQ_L1_PTR uint32_t *status; /* may be null */ uint32_t capacity_mask; uint32_t idle_spin_limit; uint32_t cb_spin_limit; uint32_t group_x; uint32_t group_y; uint32_t group_address; /* 0 = no group completion */ uint32_t group_member; uint32_t cursor; /* descriptors this processor has taken */ uint32_t heartbeat; uint32_t error; bool aborting; } ftq_context; /** Fill `ctx` from this launch's runtime arguments. Statement order is kept as written. */ FTQ_INLINE void ftq_context_init(ftq_context *ctx) { ctx->control = (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)FTQ_RUNTIME_ARG(kArgControl); ctx->capacity_mask = FTQ_RUNTIME_ARG(kArgCapacityMask); ctx->ring = (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)FTQ_RUNTIME_ARG(kArgRing); ctx->idle_spin_limit = FTQ_RUNTIME_ARG(kArgIdleSpinLimit); const uint32_t status_address = FTQ_RUNTIME_ARG(kArgStatus); ctx->status = status_address ? (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)status_address : (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)0; ctx->cb_spin_limit = FTQ_RUNTIME_ARG(kArgCbSpinLimit); if (ctx->status) { ctx->status[kStatusPayload] = 0u; ctx->status[kStatusPayload + 1] = 0u; } #if defined(COMPILE_FOR_NCRISC) ctx->control[kFieldIdleClock] = 0u; #endif ctx->group_x = FTQ_RUNTIME_ARG(kArgGroupX); ctx->group_y = FTQ_RUNTIME_ARG(kArgGroupY); ctx->group_address = FTQ_RUNTIME_ARG(kArgGroupAddress); ctx->group_member = FTQ_RUNTIME_ARG(kArgGroupMember); ctx->cursor = 0u; ctx->heartbeat = 0u; ctx->error = kErrNone; ctx->aborting = false; } /** Publish state, cursor, a fresh heartbeat and the error into the status slot. */ FTQ_INLINE void ftq_status_set(ftq_context *ctx, uint32_t state) { if (ctx->status) { ctx->status[kStatusState] = state; ctx->status[kStatusCursor] = ctx->cursor; ctx->status[kStatusHeartbeat] = ++ctx->heartbeat; ctx->status[kStatusError] = ctx->error; } } FTQ_INLINE void ftq_status_opcode(const ftq_context *ctx, uint32_t opcode) { if (ctx->status) ctx->status[kStatusLastOpcode] = opcode; } FTQ_INLINE bool ftq_stop_requested(const ftq_context *ctx) { return ctx->control[kFieldStopRequest] != 0u; } /** Abandon the current descriptor: later waits return false at once. The first non-zero * reason is kept. */ FTQ_INLINE void ftq_abort(ftq_context *ctx, uint32_t reason) { ctx->aborting = true; if (reason != kErrNone && ctx->error == kErrNone) ctx->error = reason; if (ctx->status) ctx->status[kStatusError] = ctx->error; } /** Add one blocked wait (clock since `started`, in 16-cycle units) to the status payload. */ FTQ_INLINE void ftq_wait_account(ftq_context *ctx, uint32_t started) { if (ctx->status) { ctx->status[kStatusPayload] += (ft_bh_wall_clock_low() - started) >> 4; ctx->status[kStatusPayload + 1] += 1u; } } /** Validate the control block and the launch parameters; sets ctx->error on failure. */ FTQ_INLINE bool ftq_check_control(ftq_context *ctx) { if (ctx->control[kFieldMagic] != (uint32_t)kMagic) { ctx->error = kErrBadMagic; return false; } if (ctx->capacity_mask == 0u || ((ctx->capacity_mask + 1u) & ctx->capacity_mask) != 0u) { ctx->error = kErrBadCapacity; return false; } if (ctx->control[kFieldCapacity] != ctx->capacity_mask + 1u) { ctx->error = kErrBadCapacity; return false; } if (ctx->cb_spin_limit == 0u || ctx->idle_spin_limit == 0u) { ctx->error = kErrBadParameters; return false; } return true; } /** The ring entry at this processor's cursor. */ FTQ_INLINE volatile FTQ_L1_PTR uint32_t *ftq_descriptor(const ftq_context *ctx) { return ctx->ring + (ctx->cursor & ctx->capacity_mask) * kDescriptorWords; } /** The parameter block a descriptor points at (word a0). */ FTQ_INLINE volatile FTQ_L1_PTR uint32_t *ftq_parameters(volatile FTQ_L1_PTR uint32_t *descriptor) { return (volatile FTQ_L1_PTR uint32_t *)(uintptr_t)descriptor[kDescA0]; } /** Idle until control[barrier_field] differs from the cursor. False = leave the resident * loop (stop requested, or the idle bound ran out with kErrIdleTimeout). */ FTQ_INLINE bool ftq_wait_for_work(ftq_context *ctx, uint32_t barrier_field) { uint32_t idle_spins = 0u; #if defined(COMPILE_FOR_NCRISC) const uint32_t idle_from = ft_bh_wall_clock_low(); #endif for (;;) { ctx->heartbeat++; if (ctx->status) ctx->status[kStatusHeartbeat] = ctx->heartbeat; if (ftq_stop_requested(ctx)) return false; if (ctx->control[barrier_field] != ctx->cursor) { #if defined(COMPILE_FOR_NCRISC) if (idle_spins != 0u) ctx->control[kFieldIdleClock] += (ft_bh_wall_clock_low() - idle_from) >> 4; #endif return true; } if (ctx->status) ctx->status[kStatusState] = kStateIdle; if (++idle_spins >= ctx->idle_spin_limit) { ctx->error = kErrIdleTimeout; return false; } } } /** Wait until control[field] >= value; stoppable and bounded by the CB spin limit. */ FTQ_INLINE bool ftq_wait_until_reaches(ftq_context *ctx, uint32_t field, uint32_t value) { uint32_t spins = 0u; while (ctx->control[field] < value) { ctx->heartbeat++; if (ctx->status) ctx->status[kStatusHeartbeat] = ctx->heartbeat; if (ftq_stop_requested(ctx)) { ftq_abort(ctx, kErrNone); return false; } if (++spins >= ctx->cb_spin_limit) { ftq_abort(ctx, kErrCbWaitTimeout); return false; } } return true; } /* What a blocked CB wait does after an unsatisfied test: start the wait clock on the first * pass, publish a heartbeat every 256 passes, and give up (aborting the descriptor) when the * stop word is set in stoppable mode or the spin bound runs out. Expects `ctx` and * `wait_mode` in scope; returns false from the enclosing function. */ #define FTQ_CB_WAIT_ESCAPE(spins, wait_started) \ do { \ if ((spins) == 0u) \ (wait_started) = ft_bh_wall_clock_low(); \ (spins)++; \ if (((spins) & (uint32_t)kHeartbeatPollMask) == 0u && ctx->status) { \ ctx->heartbeat++; \ ctx->status[kStatusHeartbeat] = ctx->heartbeat; \ ctx->status[kStatusSpins] = (spins); \ } \ if (((spins) & (uint32_t)kStopPollMask) == 0u) { \ if (wait_mode == (uint32_t)kWaitStoppable && ftq_stop_requested(ctx)) { \ ftq_abort(ctx, kErrNone); \ return false; \ } \ if ((spins) >= ctx->cb_spin_limit) { \ ftq_abort(ctx, kErrCbWaitTimeout); \ return false; \ } \ } \ } while (0) #if !defined(COMPILE_FOR_TRISC) /** Data-movement consumer: wait until `num_pages` pages are readable in CB `operand`. */ FTQ_INLINE bool ftq_cb_wait_front_stoppable(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { uint32_t pages_acked; uintptr_t pages_received_pointer; uint16_t pages_received; uint32_t spins = 0u; uint32_t wait_started = 0u; if (wait_mode == (uint32_t)kWaitUpstream) { ft_tensix_cb_wait_front(operand, num_pages); return true; } if (ctx->aborting) return false; pages_acked = ft_tensix_cb_pages_acked_pointer(operand)[0]; pages_received_pointer = (uintptr_t)ft_tensix_cb_pages_received_pointer(operand); do { pages_received = ((uint16_t)ft_bh_reg_read(pages_received_pointer)) - (uint16_t)pages_acked; if (pages_received >= (uint16_t)num_pages) break; FTQ_CB_WAIT_ESCAPE(spins, wait_started); } while (true); if (spins != 0u) ftq_wait_account(ctx, wait_started); return true; } /** Data-movement producer: wait until `num_pages` pages are free in CB `operand`. */ FTQ_INLINE bool ftq_cb_reserve_back_stoppable(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { uintptr_t pages_acked_pointer; uint32_t pages_received; int32_t free_space_pages; uint32_t spins = 0u; uint32_t wait_started = 0u; if (wait_mode == (uint32_t)kWaitUpstream) { ft_tensix_cb_reserve_back(operand, num_pages); return true; } if (ctx->aborting) return false; pages_acked_pointer = (uintptr_t)ft_tensix_cb_pages_acked_pointer(operand); pages_received = ft_tensix_cb_pages_received_pointer(operand)[0]; do { uint16_t pages_acked; uint16_t free_space_pages_wrap; ft_bh_l1_cache_invalidate(); pages_acked = (uint16_t)ft_bh_reg_read(pages_acked_pointer); free_space_pages_wrap = ft_tensix_cb((uint32_t)operand)->fifo_num_pages - ((uint16_t)pages_received - pages_acked); free_space_pages = (int32_t)free_space_pages_wrap; if (free_space_pages >= num_pages) break; FTQ_CB_WAIT_ESCAPE(spins, wait_started); } while (true); if (spins != 0u) ftq_wait_account(ctx, wait_started); return true; } /** Write [sequence, token] into this member's slot of the group block on another core, as * two inline word writes, token first. Called after completed_count is published; a refusal * aborts with kErrBadParameters and is not rolled back. */ FTQ_INLINE void ftq_group_post(ftq_context *ctx, uint32_t sequence, uint32_t token) { uint32_t slot_address; if (ctx->group_address == 0u || sequence == 0u) return; slot_address = ctx->group_address + (kGroupSlotOffsetWords + ctx->group_member * kGroupSlotWords) * 4u; if (ft_tensix_noc_inline_dword_write_l1( ft_tensix_noc_address(ctx->group_x, ctx->group_y, slot_address + 4u), token) != FT_TENSIX_NOC_ISSUED) { ftq_abort(ctx, kErrBadParameters); return; } ft_tensix_noc_write_barrier(); if (ft_tensix_noc_inline_dword_write_l1( ft_tensix_noc_address(ctx->group_x, ctx->group_y, slot_address), sequence) != FT_TENSIX_NOC_ISSUED) { ftq_abort(ctx, kErrBadParameters); return; } ft_tensix_noc_write_barrier(); } #else /* COMPILE_FOR_TRISC */ /** Unpack-thread consumer wait. The acked count is the thread's own 16-bit record field, * not the stream register. */ FTQ_INLINE bool ftq_llk_wait_tiles_body(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { uint32_t input = (uint32_t)operand; volatile FT_BH_REG_PTR uint32_t *tiles_received_pointer = ft_tensix_cb_pages_received_pointer(operand); uint16_t num_tiles_u = (uint16_t)num_pages; uint16_t tiles_received; uint16_t num_tiles_recv; uint32_t spins = 0u; uint32_t wait_started = 0u; do { tiles_received = (uint16_t)ft_bh_reg_read((uint32_t)(uintptr_t)tiles_received_pointer); num_tiles_recv = (uint16_t)(tiles_received - ft_tensix_cb(input)->tiles.counters.tiles_acked); if (num_tiles_recv >= num_tiles_u) { if (spins != 0u) ftq_wait_account(ctx, wait_started); return true; } FTQ_CB_WAIT_ESCAPE(spins, wait_started); } while (true); } /** Compute consumer wait; only the unpack thread waits, the others just honour aborting. */ FTQ_INLINE bool ftq_cb_wait_front_stoppable(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { bool arrived = true; (void)operand; (void)num_pages; if (wait_mode == (uint32_t)kWaitUpstream) { FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front((uint32_t)operand, (uint32_t)num_pages)); return true; } if (ctx->aborting) return false; FT_TENSIX_UNPACK((arrived = ftq_llk_wait_tiles_body(ctx, operand, num_pages, wait_mode))); return arrived; } /** Pack-thread producer wait: free tiles = pages minus (received - acked), received being the * thread's own record field, read once. */ FTQ_INLINE bool ftq_llk_wait_for_free_tiles_body(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { uint32_t output = (uint32_t)operand; volatile FT_BH_REG_PTR uint32_t *tiles_acked_pointer = ft_tensix_cb_pages_acked_pointer(operand); uint16_t tiles_received = ft_tensix_cb(output)->tiles.counters.tiles_received; int32_t free_tiles; uint32_t spins = 0u; uint32_t wait_started = 0u; do { uint16_t tiles_acked = (uint16_t)ft_bh_reg_read((uint32_t)(uintptr_t)tiles_acked_pointer); uint16_t free_tiles_wrap = (uint16_t)(ft_tensix_cb(output)->fifo_num_pages - (uint16_t)(tiles_received - tiles_acked)); free_tiles = (int32_t)free_tiles_wrap; if (free_tiles >= (int32_t)num_pages) { if (spins != 0u) ftq_wait_account(ctx, wait_started); return true; } FTQ_CB_WAIT_ESCAPE(spins, wait_started); } while (true); } /** Compute producer wait; only the pack thread waits, the others just honour aborting. */ FTQ_INLINE bool ftq_cb_reserve_back_stoppable(ftq_context *ctx, int32_t operand, int32_t num_pages, uint32_t wait_mode) { bool reserved = true; (void)operand; (void)num_pages; if (wait_mode == (uint32_t)kWaitUpstream) { FT_TENSIX_PACK( ft_tensix_compute_cb_reserve_back((uint32_t)operand, (uint32_t)num_pages)); return true; } if (ctx->aborting) return false; FT_TENSIX_PACK( (reserved = ftq_llk_wait_for_free_tiles_body(ctx, operand, num_pages, wait_mode))); return reserved; } #endif /* COMPILE_FOR_TRISC */ #endif /* RESIDENT_QUEUE_PROTOCOL_H */