Foundry/tt/kernels/resident_gdnc_reader.c

923 lines
45 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/**
* @file resident_gdnc_reader.c
* @brief Reader third of the RESIDENT CHUNKWISE GATED-DELTA-NET triad, and the processor
* that ASSEMBLES THE WINDOW TILE -- the one shape the compute surface cannot express.
*
* The compute thread of this op never indexes an element. It is handed aligned tile streams
* and issues elementwise multiplies, `matmul_tiles` against constant 0/1 band tiles and one
* SiLU. Everything that makes those legal lives here, and for PHASE CONV that is exactly
* three things:
*
* X_c the chunk's input tile column c -- ONE whole-tile read, unchanged from decode
* Win_c the rolling window's `taps-1` ROW-0 planes STACKED into rows 0..taps-2 of one
* tile. `2*(taps-1)` 64-byte NoC reads, straight into the circular buffer page.
* W_tau the `taps` ROW-REPLICATED weight planes at column c -- `taps` whole-tile reads
*
* plus the `2*taps` constant band tiles, read ONCE per descriptor.
*
* ===========================================================================
* WHY THE WINDOW ASSEMBLY IS FREE, AND THE ONE THING THAT IS NOT
* ===========================================================================
* Row 0 of a Float32 32x32 tile is TWO 64-BYTE PIECES 1,024 B apart -- the top row of face 0
* (columns 0..15) and the top row of face 1 (columns 16..31). 64 bytes is exactly the NoC's
* DRAM read granule and exactly the alignment it requires, so "take row 0 of plane p and put
* it at row p of a tile" is two ordinary reads with a computed destination offset. No
* scratch buffer, no word copy, no gather. That is why the window keeps the decode family's
* ROW-0 PLANE layout: the layout that is cheapest for one position is also free to stack.
*
* WHAT IS NOT FREE IS THE TAIL. Rows `taps-1..31` of the assembled tile are never written,
* and they are multiplied by ZERO COLUMNS of every band matrix `C_tau` -- which is fine for
* any finite value and NOT fine for a NaN, because `0 * NaN` is NaN and it would propagate
* through the SiLU into a chunk nobody inspects. So CB_STATE IS ZEROED ONCE, AT RESIDENCY
* BOOT, over both its pages, and this processor writes only rows `0..taps-2` afterwards. The
* zeros are therefore invariant for the life of the residency. Zeroing per descriptor would
* be 29 rows x 2 face rows x 256 columns of word stores -- the ~262,000 scalar L1 copies
* that were PHASE DELTA's whole cost in the decode family, seen through a division.
*
* ===========================================================================
* ONE BARRIER PER COLUMN, AND EVERY READ OF THAT COLUMN BEHIND IT
* ===========================================================================
* A NoC read barrier is a per-CORE drain. This reader issues all `1 + 2*(taps-1) + taps`
* reads of a column, takes ONE barrier, and then pushes all three buffers. The decode
* family's CONV reader cannot do that -- its window is `taps-1` separate reservations
* because `taps-1` does not divide its window buffer -- and here it can, because the window
* is ONE page.
*
* ===========================================================================
* THE CB MAP
* ===========================================================================
* cb 0 CB_IN the chunk's input tile column. THIS PROCESSOR IS ITS ONLY PRODUCER.
* cb 1 CB_STATE the assembled window tile. Zeroed once at boot; rows taps-1..31 stay
* zero for the life of the residency.
* cb 2 CB_BAND the `2*taps` constant band tiles, pushed ONCE per descriptor and popped
* by the compute thread at the END of the descriptor, so the buffer is
* empty at every descriptor boundary and its push is an exact hit.
* cb 3 CB_VECTOR the `taps` row-replicated weight tiles of this column.
* cb 4 CB_PROD the COMPUTE thread's product staging. This processor never touches it;
* the index is here so the refusal below covers its indexed-read grid,
* which is the same three-sided refusal every other buffer gets and which
* this processor is the only one of the three that can evaluate (a page
* count comes off `cb_interface`, which does not exist on the MATH
* thread).
*
* THIS PROCESSOR OWNS `tail`, AND NOTHING ELSE IN THE CONTROL BLOCK
* (resident_queue_protocol.h rule 1). An aborted descriptor advances nothing.
*
* C99 on Foundry's own `ft_tensix_dataflow.h` -- no tt-metal include.
* ===========================================================================
* PHASE DELTA ASKS THIS PROCESSOR FOR ONE THING THE COMPUTE SURFACE CANNOT EXPRESS TOO
* ===========================================================================
* Everything a head needs is CONTIGUOUS -- q, k, v and the `[S,S]` carried state are all runs
* of consecutive tiles, because this family kept the DECODE family's layouts byte for byte.
* Four bulk reads a head. What it cannot express is the GATE COLUMNS: `bcast_col` reads
* COLUMN 0 of its second operand, and element [j][0] of a Float32 tile is 128 bytes from
* element [j+1][0], so a column is a 32-element TRANSPOSE and therefore 32 word stores.
*
* THE GQA REPEAT IS AN ADDRESS HERE AND NEVER A TENSOR: `ftgc_delta_head_tile()` sends v-head
* `vh`'s q and k reads to k-head `vh % k_heads`.
*
* Compile-time args (positional):
* 0: CB_IN index 1: CB_STATE index 2: CB_BAND index 3: CB_VECTOR index
* 4: CB_PROD index (the compute thread's; named only so the grid refusal covers it)
* 5: tile size in bytes
* 6: wait mode -- 0 upstream, 1 stoppable (ships), 2 bound-only
* 7: CB_WORK index (the compute thread's; grid refusal only)
* 8: CB_DECAY index
* 9: CB_CC index (the compute thread's; grid refusal only)
* 10: CB_SCRATCH index -- THIS PROCESSOR'S PRIVATE PAGE, and an index nobody pushes
* 11: CONV_DEPTH -- tile columns behind ONE NoC read barrier. The HOST declares its own on
* every CONV descriptor and `conv_is_executable()` refuses one that disagrees; a SHORT
* line reads 0 here and `conv_block_depth()` turns that into a refusal of every CONV
* descriptor rather than a silent depth, which is the same shape the decode family's
* `resident_gdn_reader.c` chose for the identical argument.
* TWELVE args, and the count is load-bearing: `ft_tensix_compile_time_arg(n)` against a
* short line reads one word PAST the args array and SFPI GCC 15.1.0 FOLDS THAT TO A
* LITERAL ZERO with no diagnostic at any `-Warray-bounds` level. Wait mode is left at index
* 6 for exactly that reason: a short line then reads mode 1 and a CB index of 0, which
* `delta_is_executable()` refuses loudly -- rather than mode 0, upstream's UNSTOPPABLE wait,
* which is how a Tensix gets wedged.
* `tools/check_tensix_kernel_compile_time_arg_counts.py` is the only other guard there is.
* Runtime args: the ten of resident_queue_protocol.h, identical on all five processors
*
* Build (the whole program; this file is the BRISC/NoC 0 third):
* RELOCATE_KERNEL_TEXT=1 \
* READER_CTARGS=0,1,2,3,4,4096,1,5,7,8,6,1 COMPUTE_CTARGS=0,1,2,3,4,16,1,5,7,8,1 \
* WRITER_CTARGS=16,4096,1 CB_TABLE=kernels/cb_tables/gdnc_float32.cbtable \
* bash foundry/tt/build_tensix_program.sh \
* kernels/resident_gdnc_reader.c kernels/resident_gdnc_compute.c \
* kernels/resident_gdnc_writer.c programs/gdnc
*
* @status PHASE CONV green on silicon 2026-08-21. PHASE DELTA NEW.
* @issues The block depth is now a knob (`CONV_DEPTH`, arg 11) and ships at ONE. Raising it is
* still a change to this file, the compute kernel and `place_cbs` TOGETHER, and the host must
* be told the same number -- which is why the host DECLARES its own on the wire and both
* kernels compare for strict equality. The rungs the arena admits are priced off hardware by
* `tools/price_gdnc_conv_depth.c`.
* @todo None
*/
#include <stdbool.h>
#include <stdint.h>
#include "ft_tensix_dataflow.h"
#include "ftgc_compact.h"
#include "resident_gdnc_protocol.h"
#include "resident_queue_protocol.h"
#define CB_IN ft_tensix_compile_time_arg(0)
#define CB_STATE ft_tensix_compile_time_arg(1)
#define CB_BAND ft_tensix_compile_time_arg(2)
#define CB_VECTOR ft_tensix_compile_time_arg(3)
/** The COMPUTE thread's product staging buffer. Named here ONLY so this processor's refusal
* covers it; nothing below reads or writes it. */
#define CB_PROD ft_tensix_compile_time_arg(4)
#define TILE_BYTES ft_tensix_compile_time_arg(5)
#define WAIT_MODE ft_tensix_compile_time_arg(6)
/** PHASE DELTA's four. CB_WORK and CB_CC are the COMPUTE thread's and this processor never
* touches them; they are named so this processor's page-count refusal covers their indexed
* read grids, which it is the only one of the three that can evaluate -- `fifo_num_pages`
* comes off `cb_interface`, and that does not exist on the MATH thread. */
#define CB_WORK ft_tensix_compile_time_arg(7)
#define CB_DECAY ft_tensix_compile_time_arg(8)
#define CB_CC ft_tensix_compile_time_arg(9)
/** THIS PROCESSOR'S PRIVATE SCRATCH, and it must be an index NOBODY PUSHES: the staging idiom
* writes through a CB's write pointer WITHOUT reserving, which is safe only while that
* pointer is provably at the buffer base. */
#define CB_SCRATCH ft_tensix_compile_time_arg(10)
/** PHASE CONV's BLOCK DEPTH: tile columns read behind ONE NoC read barrier. TWELVE args now,
* and the count is load-bearing for the reason stated above — a short line reads one word
* PAST the array and SFPI GCC folds it to a literal 0, which `conv_block_depth()` turns into
* a loud refusal rather than a silent depth. The host declares ITS OWN depth on every CONV
* descriptor and `conv_is_executable()` refuses one that disagrees with this constant; see
* `kGdncByteConvDepth` for the measurement that direction corrupts. */
#define CONV_DEPTH ft_tensix_compile_time_arg(11)
/** The three DRAM endpoints a descriptor names, decoded once. */
typedef struct gdnc_endpoints {
uint32_t activation_x;
uint32_t activation_y;
uint32_t activation_in;
uint32_t constant_x;
uint32_t constant_y;
uint32_t constant_base;
uint32_t gate_base; /**< PHASE DELTA only, and INSIDE the constant bank */
uint32_t state_x;
uint32_t state_y;
uint32_t state_base;
} gdnc_endpoints;
static void load_endpoints(gdnc_endpoints *endpoints, volatile FTQ_L1_PTR uint32_t *parameters)
{
endpoints->activation_x = ftgc_xy_x(parameters[kGdncActivationXy]);
endpoints->activation_y = ftgc_xy_y(parameters[kGdncActivationXy]);
endpoints->activation_in = parameters[kGdncActivationIn];
endpoints->constant_x = ftgc_xy_x(parameters[kGdncConstantXy]);
endpoints->constant_y = ftgc_xy_y(parameters[kGdncConstantXy]);
endpoints->constant_base = parameters[kGdncConstantWeight];
endpoints->gate_base = parameters[kGdncDeltaGate];
endpoints->state_x = ftgc_xy_x(parameters[kGdncStateXy]);
endpoints->state_y = ftgc_xy_y(parameters[kGdncStateXy]);
endpoints->state_base = parameters[kGdncStateConv];
}
/* ===========================================================================
* Face arithmetic. Modelled on resident_gdn_reader.c's; spelled locally because
* resident_gdnc_protocol.h is the WIRE contract and does not carry L1 layout.
* =========================================================================== */
/** Bytes of one face: a 2x2 face grid over the tile. */
static uint32_t ftgc_face_bytes(void)
{
return TILE_BYTES / 4u;
}
/** Bytes of one face ROW: 16 elements of a 32-element row, i.e. tile_bytes/64. 64 for a
* Float32 tile, which is exactly the NoC's DRAM read granule -- the whole reason the window
* assembly is two transactions and not a gather. */
static uint32_t ftgc_face_row_bytes(void)
{
return TILE_BYTES / 64u;
}
/** Byte offset, inside a tile, of the face row holding `row` and columns [16*half, +16). */
static uint32_t ftgc_face_row_offset(uint32_t row, uint32_t half)
{
return ((row / (uint32_t)kGdncFaceRows) * 2u + half) * ftgc_face_bytes() +
(row % (uint32_t)kGdncFaceRows) * ftgc_face_row_bytes();
}
/* ===========================================================================
* Staging
* =========================================================================== */
/**
* The `2*taps` constant band tiles, ONE transaction, ONCE per descriptor.
*
* They ride at the END of the weight buffer (`ftgc_band_base_tile`) rather than in a sixth
* DRAM endpoint, because `FT_TT_QUEUE_PARAM_WORDS` is 16 for every family and a seventeenth
* word moves the shared slot stride.
*/
static bool read_band(ftq_context *ctx, const gdnc_endpoints *endpoints,
const ftgc_shape *shape)
{
const uint32_t tiles = ftgc_band_tiles(shape);
const uint32_t first = ftgc_band_base_tile(shape);
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_BAND, (int32_t)tiles, WAIT_MODE))
return false;
if (!ctx->aborting) {
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->constant_base +
first * TILE_BYTES),
ft_tensix_cb_write_pointer(CB_BAND), tiles * TILE_BYTES);
ft_tensix_noc_read_barrier();
}
ft_tensix_cb_push_back(CB_BAND, tiles);
return true;
}
/**
* One tile column: the chunk's input tile, the assembled window tile and the `taps` weight
* tiles. Every read behind ONE barrier; every push after it.
*
* The reservations all happen FIRST, because a reservation is the only thing that says the
* destination L1 pages are not still being read by the compute thread, and a read issued
* into an unreserved page is corruption a barrier cannot undo.
*/
static bool read_column_group(ftq_context *ctx, const gdnc_endpoints *endpoints,
const ftgc_shape *shape, uint32_t first_column, uint32_t depth)
{
const uint32_t taps = shape->conv_taps;
uint32_t in_write;
uint32_t window_write;
uint32_t weight_write;
uint32_t tap;
uint32_t plane;
uint32_t slot;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_IN, (int32_t)depth, WAIT_MODE))
return false;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_STATE, (int32_t)depth, WAIT_MODE))
return false;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_VECTOR, (int32_t)(depth * taps),
WAIT_MODE))
return false;
in_write = ft_tensix_cb_write_pointer(CB_IN);
window_write = ft_tensix_cb_write_pointer(CB_STATE);
weight_write = ft_tensix_cb_write_pointer(CB_VECTOR);
if (!ctx->aborting) {
/* The chunk's own input tiles: rows 0..chunk_rows-1 are the C positions, and the host
* owes zeros above that (see the protocol header). `depth` CONSECUTIVE tile columns
* are CONSECUTIVE tiles in the strip, so the whole block is ONE read whatever the
* depth — which is the reason the depth costs no extra transactions here. */
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->activation_x,
endpoints->activation_y,
endpoints->activation_in +
first_column * TILE_BYTES),
in_write, depth * TILE_BYTES);
/* THE WINDOW ASSEMBLY. Plane p's ROW 0 becomes ROW p of one tile: two 64-byte pieces
* 1,024 B apart at the source, two computed face-row offsets at the destination.
* Rows taps-1..31 of every page were zeroed at boot and are not touched.
*
* THIS is the part that does NOT collapse with depth: a 64-byte face row of column
* `c` and of column `c+1` are a whole tile apart at BOTH ends, so it stays
* `depth * 2 * (taps-1)` reads. The lever is the BARRIER, not the transaction
* count — at 680.2 us over 256 columns the descriptor is spending 2.66 us a column on
* a serialized round trip, and 13 posted reads do not cost that. */
for (slot = 0; slot < depth; slot++) {
for (plane = 0; plane + 1u < taps; plane++) {
const uint32_t source =
endpoints->state_base +
ftgc_plane_tile(shape, plane, first_column + slot) * TILE_BYTES;
uint32_t half;
for (half = 0; half < 2u; half++) {
ft_tensix_noc_read(
ft_tensix_noc_address(endpoints->state_x, endpoints->state_y,
source +
ftgc_face_row_offset((uint32_t)kGdncStripRow,
half)),
window_write + slot * TILE_BYTES + ftgc_face_row_offset(plane, half),
ftgc_face_row_bytes());
}
}
}
/* The `taps` ROW-REPLICATED weight planes, TAP-MAJOR then column, which is the order
* `CB_VECTOR` is indexed in: tile `tap*depth + slot`. A plane's `depth` columns are
* consecutive tiles (`ftgc_plane_tile` is `tap*conv_tile_cols + column`), so this is
* `taps` reads whatever the depth. */
for (tap = 0; tap < taps; tap++) {
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->constant_x,
endpoints->constant_y,
endpoints->constant_base +
ftgc_plane_tile(shape, tap,
first_column) *
TILE_BYTES),
weight_write + tap * depth * TILE_BYTES, depth * TILE_BYTES);
}
ft_tensix_noc_read_barrier();
}
ft_tensix_cb_push_back(CB_IN, (int32_t)depth);
ft_tensix_cb_push_back(CB_STATE, (int32_t)depth);
ft_tensix_cb_push_back(CB_VECTOR, (int32_t)(depth * taps));
return true;
}
/**
* Tile columns one CONV block covers, or ZERO for a refusal.
*
* Bounded by four things at once: the compile-time constant; the SLICE this descriptor owns,
* so there is no short last block; half of each buffer, so this processor can fill one block
* while the compute thread drains the other; and the INDEX GRID, because a group that does not
* divide the page count walks a consumer's `read_pointer + tile*page_size` off the end.
*
* The shrink is a SHRINK and not a fallback: it is the same expression the compute thread runs
* — that thread cannot see `cb_interface` at all, so the two must agree from the same inputs —
* and a shrunk depth under a placement rounded onto the UNSHRUNK one fails the grid test right
* below, loudly. That is why the reachable rungs are the divisors and not every integer.
*/
static uint32_t conv_block_depth(const ftgc_shape *shape)
{
const uint32_t taps = shape->conv_taps;
const uint32_t in_pages = ft_tensix_cb(CB_IN)->fifo_num_pages;
const uint32_t window_pages = ft_tensix_cb(CB_STATE)->fifo_num_pages;
const uint32_t weight_pages = ft_tensix_cb(CB_VECTOR)->fifo_num_pages;
uint32_t depth = CONV_DEPTH;
if (taps < 2u || shape->col_count == 0u || depth == 0u)
return 0u;
if (depth > shape->col_count)
depth = shape->col_count;
while (depth > 0u && (shape->col_count % depth) != 0u)
depth--;
if (depth == 0u)
return 0u;
if (in_pages < 2u * depth || window_pages < 2u * depth ||
weight_pages < 2u * depth * taps)
return 0u;
if ((in_pages % depth) != 0u || (window_pages % depth) != 0u ||
(weight_pages % (depth * taps)) != 0u)
return 0u;
return depth;
}
/* ===========================================================================
* PHASE DELTA -- staging
*
* Everything a head needs is CONTIGUOUS in DRAM, which is the whole dividend of keeping the
* decode family's layouts: q, k and v are `state_tiles` consecutive tiles each, the carried
* state is `state_tiles^2` consecutive tiles, and the decay-ratio matrix is one tile. Four
* bulk reads and one 640-byte read a head, all behind ONE barrier.
*
* The ONLY thing this processor builds rather than moves is the five gate columns, and that is
* a TRANSPOSE of 32 scalars: `bcast_col` reads column 0 of its second operand, and element
* [j][0] of a Float32 tile is not contiguous with element [j+1][0]. 32 word stores a column.
* The other 31 columns are NEVER READ and are left as they are -- zeroing them would be 1,024
* word stores a column against 32, and the decode family's reader states the same thing about
* the same operand.
* =========================================================================== */
/** Byte offset, inside a tile, of element [row][column]. */
static uint32_t ftgc_element_offset(uint32_t row, uint32_t column)
{
return ftgc_face_row_offset(row, column / (uint32_t)kGdncFaceRows) +
(column % (uint32_t)kGdncFaceRows) * (TILE_BYTES / 1024u);
}
/**
* One gate column into CB_VECTOR: `[j][0] = scratch[run][j]`, the other lanes untouched.
*
* The reservation is the caller's -- a head reserves all five at once so its five pushes
* cannot interleave with the compute thread's absolute indexing of them.
*/
static void push_gate_column(uint32_t scratch, uint32_t run)
{
const uint32_t destination = ft_tensix_cb_write_pointer(CB_VECTOR);
const uint32_t source = scratch + run * (uint32_t)kGdncTileRows * 4u;
uint32_t row;
for (row = 0; row < (uint32_t)kGdncTileRows; row++)
*(uint32_t *)(uintptr_t)(destination + ftgc_element_offset(row, 0u)) =
*(const uint32_t *)(uintptr_t)(source + row * 4u);
ft_tensix_cb_push_back(CB_VECTOR, 1);
}
/** The four PROGRAM CONSTANT tiles, ONE transaction, ONCE per descriptor. They sit at the head
* of the constant buffer, ahead of the per-head decay tiles. */
static bool read_delta_constants(ftq_context *ctx, const gdnc_endpoints *endpoints)
{
const uint32_t tiles = (uint32_t)kGdncDeltaConstTiles;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_BAND, (int32_t)tiles, WAIT_MODE))
return false;
if (!ctx->aborting) {
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->constant_base),
ft_tensix_cb_write_pointer(CB_BAND), tiles * TILE_BYTES);
ft_tensix_noc_read_barrier();
}
ft_tensix_cb_push_back(CB_BAND, tiles);
return true;
}
/** L11b (kGdncDeltaFlagCompact): this descriptor's gate block is COMPACT — header (scale bits
* at word 0) then decay[32] | beta[32] a head — and the decay-ratio tile and gate runs are
* built here (ftgc_compact.h) instead of read. */
static bool gdnc_compact;
static uint32_t gdnc_first_row;
/** Where the compact header and the head's run land in CB_SCRATCH, past the 640 B of runs. */
#define COMPACT_SCRATCH_OFFSET 1024u
/** One v-head: q | k | v, the carried state, the decay-ratio tile and the five gate columns. */
static bool read_one_head(ftq_context *ctx, const gdnc_endpoints *endpoints,
const ftgc_shape *shape, uint32_t head)
{
const uint32_t tiles = shape->state_tiles;
const uint32_t state = tiles * tiles;
uint32_t in_write;
uint32_t state_write;
uint32_t decay_write;
uint32_t scratch;
uint32_t stream;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_IN, (int32_t)(3u * tiles), WAIT_MODE))
return false;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_STATE, (int32_t)state, WAIT_MODE))
return false;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_DECAY, 1, WAIT_MODE))
return false;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_VECTOR,
(int32_t)kGdncDeltaVectorSpan, WAIT_MODE))
return false;
in_write = ft_tensix_cb_write_pointer(CB_IN);
state_write = ft_tensix_cb_write_pointer(CB_STATE);
decay_write = ft_tensix_cb_write_pointer(CB_DECAY);
scratch = ft_tensix_cb_write_pointer(CB_SCRATCH);
if (!ctx->aborting && gdnc_compact) {
/* The compact header and this head's decay|beta first, behind their own barrier: the
* build below needs them, and it runs while the big reads are in flight. */
const uint32_t compact = scratch + COMPACT_SCRATCH_OFFSET;
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->gate_base),
compact, FTGC_COMPACT_HEADER_WORDS * 4u);
ft_tensix_noc_read(ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->gate_base +
(FTGC_COMPACT_HEADER_WORDS +
head * FTGC_COMPACT_HEAD_WORDS) * 4u),
compact + FTGC_COMPACT_HEADER_WORDS * 4u,
FTGC_COMPACT_HEAD_WORDS * 4u);
ft_tensix_noc_read_barrier();
}
if (!ctx->aborting) {
/* q, then k, then v. THE GQA REPEAT IS AN ADDRESS and never a materialised tensor:
* `ftgc_delta_head_tile` sends the q and k reads to k-head `vh % k_heads`, which is
* the INTERLEAVED mapping the oracle uses. The blocked one is the plausible wrong
* answer and has produced gibberish in this architecture before. */
for (stream = 0; stream < 3u; stream++)
ft_tensix_noc_read(
ft_tensix_noc_address(endpoints->activation_x, endpoints->activation_y,
endpoints->activation_in +
ftgc_delta_head_tile(shape, stream, head) *
TILE_BYTES),
in_write + stream * tiles * TILE_BYTES, tiles * TILE_BYTES);
ft_tensix_noc_read(
ft_tensix_noc_address(endpoints->state_x, endpoints->state_y,
endpoints->state_base +
ftgc_state_tile(shape, head, 0u, 0u) * TILE_BYTES),
state_write, state * TILE_BYTES);
if (gdnc_compact) {
const volatile uint32_t *header =
(const volatile uint32_t *)(uintptr_t)(scratch + COMPACT_SCRATCH_OFFSET);
const volatile uint32_t *run = header + FTGC_COMPACT_HEADER_WORDS;
ftgc_compact_decay_tile(run, gdnc_first_row, shape->chunk_rows,
(volatile uint32_t *)(uintptr_t)decay_write);
ftgc_compact_gate(run, run + 32u, gdnc_first_row, shape->chunk_rows, header[0],
(volatile uint32_t *)(uintptr_t)scratch);
} else {
ft_tensix_noc_read(
ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->constant_base +
ftgc_delta_decay_tile(head) * TILE_BYTES),
decay_write, TILE_BYTES);
/* The gate block's run for this head. `kGdncGateStride * 4` is 640 bytes, a
* multiple of the NoC's 64-byte granule by construction, so no rounding is needed
* and no read runs past the head's own slot. */
ft_tensix_noc_read(
ft_tensix_noc_address(endpoints->constant_x, endpoints->constant_y,
endpoints->gate_base + ftgc_gate_offset(head, 0u) * 4u),
scratch, (uint32_t)kGdncGateStride * 4u);
}
ft_tensix_noc_read_barrier();
for (stream = 0; stream < (uint32_t)kGdncGateRuns; stream++)
push_gate_column(scratch, stream);
} else {
/* AN ABORT DRAINS. The compute thread is waiting on `kGdncDeltaVectorSpan` pages and
* gets them; what it does not get is a barrier, and it will abort on its own words. */
for (stream = 0; stream < (uint32_t)kGdncGateRuns; stream++)
ft_tensix_cb_push_back(CB_VECTOR, 1);
}
ft_tensix_cb_push_back(CB_IN, 3u * tiles);
ft_tensix_cb_push_back(CB_STATE, state);
ft_tensix_cb_push_back(CB_DECAY, 1);
return true;
}
/* ===========================================================================
* Refusals
* =========================================================================== */
/**
* Does `cb_index` hold a whole number of `chunk_pages` blocks?
*
* THE GRID A CONSUMER INDEXES ACROSS, which is a different rule from the push grid and the
* one the decode family did not have -- `ft_tensix_unpack_ab(cb, ..., tile)` resolves to
* `read_pointer + tile * page_size` with NO WRAP, so a compute thread that waits for `n`
* pages and then names tiles `0..n-1` needs the front low enough that the whole window fits.
*/
static bool index_grid_is_safe(uint32_t cb_index, uint32_t chunk_pages)
{
const uint32_t pages = ft_tensix_cb(cb_index)->fifo_num_pages;
return chunk_pages != 0u && pages >= chunk_pages && (pages % chunk_pages) == 0u;
}
static bool conv_is_executable(const ftgc_shape *shape)
{
const uint32_t taps = shape->conv_taps;
const uint32_t band = ftgc_band_tiles(shape);
const uint32_t depth = conv_block_depth(shape);
/* THE DIRECTION THAT WOULD OTHERWISE BE SILENT. Every check below asks whether the PLACED
* BUFFERS CAN SERVE this image's depth, and a host that placed for a DEEPER block passes
* all of them — it runs at THIS image's depth inside the host's placement and reports the
* control's timing as the arm's. Strict equality against the declaration, on this
* processor AND on the compute thread, so neither is left waiting on pages the other
* decided not to send. See `kGdncByteConvDepth`. */
if (!ftgc_conv_depth_agrees(shape->declared_conv_depth, (uint32_t)CONV_DEPTH))
return false;
/* A depth no block satisfies is a REFUSAL, not a fallback to one column. */
if (depth == 0u)
return false;
/* Two BLOCKS on every streamed buffer, so this processor can fill one while the compute
* thread drains the other. One block is a hard handshake per group. The block-sized
* capacity and grid questions are `conv_block_depth()`'s and it has already asked them;
* these are the depth-independent floors. */
if (ft_tensix_cb(CB_IN)->fifo_num_pages < 2u)
return false;
if (ft_tensix_cb(CB_STATE)->fifo_num_pages < 2u)
return false;
if (ft_tensix_cb(CB_BAND)->fifo_num_pages < band)
return false;
if (ft_tensix_cb(CB_VECTOR)->fifo_num_pages < 2u * taps)
return false;
if (ft_tensix_cb(CB_PROD)->fifo_num_pages < 2u * band)
return false;
/* THE PUSH GRIDS AS A PAGE-COUNT QUESTION, NOT A POINTER ONE — THE SAME DEFECT PHASE
* DELTA'S REFUSAL CARRIED, IN THE DIRECTION NOTHING EXERCISED.
*
* These four were `ft_tensix_cb_push_grid_is_safe()`, which tests the CURRENT write
* pointer's alignment to the chunk grid — and this refusal runs BEFORE
* `rebase_produced_buffers()`, so at this point every pointer is wherever the PREVIOUS
* descriptor left it. PHASE DELTA's twin was corrected for exactly this reason on
* 2026-08-21 and PHASE CONV's was not, because the suite's `stage_delta` is its LAST
* stage: no CONV descriptor in it ever followed a DELTA one.
*
* MEASURED ON SILICON 2026-08-21, on the first caller that alternates them — a real
* prefill runs CONV, DELTA, CONV, DELTA, one pair per SSM layer. A PHASE DELTA descriptor
* pushes CB_BAND in groups of `kGdncDeltaConstTiles` = 4 into an 8-page buffer and leaves
* the pointer at page 4; `4 % 8` is 4, so **EVERY PHASE CONV DESCRIPTOR AFTER ANY PHASE
* DELTA DESCRIPTOR WAS REFUSED**. The host's re-arm-once recovery hid it as a number
* rather than as a failure: 215 refusals and 215 residency re-arms over a 260-position
* prefill, every one recovering, with PHASE CONV reading 4,502 us a descriptor against
* the 681.4 us the same kernel measures in `tests/test_phase6_gdnc.c`.
*
* What the reader needs is that the page COUNT admits the group; the POINTER is
* guaranteed by the rebase that runs immediately after this returns. */
if (!index_grid_is_safe(CB_IN, depth) || !index_grid_is_safe(CB_STATE, depth))
return false;
/* What the compute thread INDEXES ACROSS: the whole band in one window, this BLOCK's
* `depth*taps` weight tiles, its `depth` input tiles, its `depth` window tiles, and the
* `2*taps` products it stages and reduces in one DST section PER COLUMN — CB_PROD keeps a
* per-column grid at every depth, which is what leaves the DST accumulation argument
* exactly where it was. */
if (!index_grid_is_safe(CB_BAND, band) || !index_grid_is_safe(CB_VECTOR, depth * taps) ||
!index_grid_is_safe(CB_PROD, band))
return false;
return true;
}
/**
* PHASE DELTA's placement refusal.
*
* EVERY ONE OF THESE IS AN INDEXED-READ GRID and not a comfort setting. The compute thread
* names tiles ABSOLUTELY off each buffer's front for the whole of one head and pops once at
* the end of it, and `ft_tensix_unpack_ab(cb, ..., tile)` is `read_pointer + tile*page_size`
* with NO WRAP -- so a buffer whose page count is not a multiple of the span reads a tile out
* of the buffer entirely, at some head that is not the first.
*
* This processor is the only one of the three that can evaluate them: a page count comes off
* `cb_interface`, which does not exist on the MATH thread and fails at LINK time there.
*/
static bool delta_is_executable(const ftgc_shape *shape)
{
const uint32_t tiles = shape->state_tiles;
const uint32_t input = 3u * tiles;
const uint32_t state = tiles * tiles;
const uint32_t work = (uint32_t)kGdncDeltaWorkPerRow * tiles;
if (!index_grid_is_safe(CB_IN, input) || !index_grid_is_safe(CB_STATE, state) ||
!index_grid_is_safe(CB_BAND, (uint32_t)kGdncDeltaConstTiles) ||
!index_grid_is_safe(CB_VECTOR, (uint32_t)kGdncDeltaVectorSpan) ||
!index_grid_is_safe(CB_CC, (uint32_t)kGdncDeltaCcSpan) ||
!index_grid_is_safe(CB_WORK, work))
return false;
if (ft_tensix_cb(CB_DECAY)->fifo_num_pages < 1u)
return false;
if (ft_tensix_cb(CB_SCRATCH)->fifo_num_pages < 1u)
return false;
/* THE PUSH GRIDS TOO -- BUT AS A PAGE-COUNT QUESTION AND NOT AS A POINTER ONE, AND THAT
* DISTINCTION COST A RUN.
*
* `ft_tensix_cb_push_grid_is_safe()` tests the CURRENT write pointer's alignment to the
* chunk grid, and this refusal is made BEFORE `rebase_produced_buffers()` -- deliberately,
* because rule 1 says a refused descriptor must leave no circular-buffer traffic behind
* it. So at this point every write pointer is wherever the PREVIOUS DESCRIPTOR left it,
* and asking whether THAT is aligned to THIS descriptor's group size is asking the wrong
* question about the wrong descriptor.
*
* MEASURED ON SILICON 2026-08-21: a PHASE CONV descriptor pushes CB_IN one page at a time,
* 256 of them into a 24-page buffer, and leaves the pointer at page 16. `16 % 12` is 4, so
* the first PHASE DELTA descriptor after any CONV descriptor was refused with
* `kErrBadParameters` -- the reader exited, the compute thread spun out its whole
* 400,000,000-iteration bound on CB_BAND, and the triad retired clean with nothing wrong
* with it. PHASE CONV never saw this because its own group is ONE page and every pointer
* is one-page aligned.
*
* What the reader actually needs is that the page COUNT admits the group, which is
* `index_grid_is_safe`; the POINTER is guaranteed by the rebase that runs immediately
* after this returns. */
if (!index_grid_is_safe(CB_DECAY, 1u) || !index_grid_is_safe(CB_SCRATCH, 1u))
return false;
/* CB_SCRATCH IS WRITTEN WITHOUT A RESERVATION, which is safe only while its write pointer
* is provably at the base -- i.e. only if NOBODY EVER PUSHES IT. Nothing on the device can
* check "nobody pushes"; what it can check is that the index is not one of the eight this
* program does push, which is the same statement by exhaustion. */
if (CB_SCRATCH == CB_IN || CB_SCRATCH == CB_STATE || CB_SCRATCH == CB_BAND ||
CB_SCRATCH == CB_VECTOR || CB_SCRATCH == CB_PROD || CB_SCRATCH == CB_WORK ||
CB_SCRATCH == CB_DECAY || CB_SCRATCH == CB_CC)
return false;
return true;
}
/**
* Put this processor's write pointers back on their buffers' bases.
*
* A FIFO pointer wraps on an EXACT HIT of the limit and on nothing else, and a descriptor's
* traffic is not a whole number of buffers: this one moves `col_count` CB_IN pages and
* `taps * col_count` CB_VECTOR pages against page counts of 2 and `2*taps`. Descriptor N+1
* would otherwise start wherever N left off, and the consumer's indexed reads are absolute
* off its front.
*
* The whole-buffer reservation is the precondition `ft_tensix_cb_rebase_write_pointer()`
* states: it does not move a pointer, it waits until nothing live is behind it.
*/
static bool rebase_produced_buffers(ftq_context *ctx)
{
const uint32_t buffers[4] = { CB_IN, CB_STATE, CB_VECTOR, CB_DECAY };
uint32_t index;
for (index = 0; index < 4u; index++) {
const uint32_t cb = buffers[index];
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)cb,
(int32_t)ft_tensix_cb(cb)->fifo_num_pages,
WAIT_MODE))
return false;
ft_tensix_cb_rebase_write_pointer(cb);
}
/* CB_BAND's traffic is `2*taps` into a `2*taps`-page buffer, i.e. an EXACT HIT, so it
* returns to its base by arithmetic. Rebasing it anyway is what makes that a property of
* the code rather than of the shape -- the same accidental agreement made the decode
* family's CB_PROD look safe while CB_STATE and CB_VECTOR read out of their buffers for a
* whole run. */
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_BAND,
(int32_t)ft_tensix_cb(CB_BAND)->fifo_num_pages,
WAIT_MODE))
return false;
ft_tensix_cb_rebase_write_pointer(CB_BAND);
return true;
}
/**
* The reader half of one GDNC descriptor.
*
* @return true if the whole descriptor was read; false if a wait was abandoned, in which case
* the caller must not retire it (rule 5).
*/
/**
* kGdncFlagSerialize: wait until every descriptor before this one has PUBLISHED completion —
* the writer publishes after its write barrier, so the previous descriptor's window / state
* writes are in DRAM. Stop-aware and bounded; wrap-safe on the counts.
*/
static bool wait_previous_completed(ftq_context *ctx)
{
uint32_t spins = 0u;
for (;;) {
ft_bh_l1_cache_invalidate();
if ((int32_t)(ctx->control[kFieldCompletedCount] - ctx->cursor) >= 0)
return true;
if (ftq_stop_requested(ctx)) {
ftq_abort(ctx, kErrNone);
return false;
}
if (++spins >= ctx->cb_spin_limit) {
ftq_abort(ctx, kErrCbWaitTimeout);
return false;
}
}
}
static bool read_one_gdnc(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
{
const ftgc_shape shape = ftgc_decode(parameters);
const bool delta = shape.phase == (uint32_t)kGdncPhaseDelta;
gdnc_endpoints endpoints;
uint32_t index;
if (TILE_BYTES == 0u || shape.tile_bytes != TILE_BYTES ||
!ftgc_shape_is_supported(&shape) ||
!(delta ? delta_is_executable(&shape) : conv_is_executable(&shape))) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
if ((parameters[kGdncFlags] & (uint32_t)kGdncFlagSerialize) != 0u &&
!wait_previous_completed(ctx))
return false;
load_endpoints(&endpoints, parameters);
gdnc_compact = delta && (parameters[kGdncFlags] & (uint32_t)kGdncDeltaFlagCompact) != 0u;
gdnc_first_row = parameters[kGdncFirstRow];
if (!rebase_produced_buffers(ctx))
return false;
if (delta) {
if (!read_delta_constants(ctx, &endpoints))
return false;
for (index = 0; index < shape.head_count; index++)
if (!read_one_head(ctx, &endpoints, &shape, shape.head_first + index))
return false;
return !ctx->aborting;
}
if (!read_band(ctx, &endpoints, &shape))
return false;
{
/* The depth is re-derived rather than carried from the refusal, because the refusal is
* the OTHER caller's and this one must not depend on having been through it. It is a
* pure function of the shape and the placed page counts, both of which are fixed for
* the descriptor. */
const uint32_t depth = conv_block_depth(&shape);
if (depth == 0u)
return false;
for (index = 0; index < shape.col_count; index += depth)
if (!read_column_group(ctx, &endpoints, &shape, shape.first_col + index, depth))
return false;
}
return !ctx->aborting;
}
/**
* Zero CB_STATE, once, before any descriptor.
*
* Rows `taps-1..31` of the assembled window tile are multiplied by ZERO COLUMNS of the band
* matrices, and `0 * NaN` is NaN. This processor writes only rows `0..taps-2` afterwards, so
* one pass over the buffer makes the tail zero for the life of the residency.
*
* It RESERVES and does not PUSH: the write pointer only moves on a push, so the buffer is
* still empty and still based when the first descriptor arrives. `taps` is not known here --
* it is a per-descriptor word -- so the whole buffer is zeroed rather than only the tail,
* which is 2,048 word stores once against ~237,000 a descriptor for the alternative.
*/
static bool zero_window_buffer(ftq_context *ctx)
{
const uint32_t pages = ft_tensix_cb(CB_STATE)->fifo_num_pages;
uint32_t *base;
uint32_t words;
uint32_t index;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_STATE, (int32_t)pages, WAIT_MODE))
return false;
base = (uint32_t *)(uintptr_t)ft_tensix_cb_write_pointer(CB_STATE);
words = (pages * TILE_BYTES) / 4u;
for (index = 0; index < words; index++)
base[index] = 0u;
return true;
}
void kernel_main(void)
{
ftq_context ctx;
bool running = true;
ftq_context_init(&ctx);
ftq_status_set(&ctx, kStateBoot);
ctx.control[kFieldState] = kStateBoot;
if (!ftq_check_control(&ctx)) {
ctx.control[kFieldError] = ctx.error;
ctx.control[kFieldState] = kStateExit;
ftq_status_set(&ctx, kStateExit);
return;
}
/* RESUME rather than restart. All five processors resume from completed_count, the ONE
* number that means "every stage finished this descriptor". */
if (ctx.control[kFieldTail] != ctx.control[kFieldCompletedCount]) {
ctx.error = kErrBadParameters;
ctx.control[kFieldError] = ctx.error;
ctx.control[kFieldState] = kStateExit;
ftq_status_set(&ctx, kStateExit);
return;
}
if (!zero_window_buffer(&ctx)) {
ctx.control[kFieldError] = ctx.error;
ctx.control[kFieldState] = kStateExit;
ftq_status_set(&ctx, kStateExit);
return;
}
ctx.cursor = ctx.control[kFieldCompletedCount];
while (running) {
volatile FTQ_L1_PTR uint32_t *descriptor;
uint32_t opcode;
bool done;
if (!ftq_wait_for_work(&ctx, kFieldHead))
break;
ctx.aborting = false;
ftq_status_set(&ctx, kStateExec);
descriptor = ftq_descriptor(&ctx);
opcode = descriptor[kDescOpcode];
ctx.control[kFieldLastOpcode] = opcode;
ftq_status_opcode(&ctx, opcode);
if (opcode == kOpGdnChunk) {
ftq_status_set(&ctx, kStateCbReserveBack);
done = read_one_gdnc(&ctx, ftq_parameters(descriptor));
ftq_status_set(&ctx, kStateExec);
if (!done)
running = false;
} else if (opcode == kOpNop) {
done = true;
} else if (opcode == kOpStop) {
done = true;
running = false;
} else {
/* A binary is bound to one slab, so an opcode another program implements is
* genuinely not available here. Refusing names that; executing nothing would
* retire work that never happened. */
ctx.error = kErrBadOpcode;
done = false;
running = false;
}
/* Rule 5. An abandoned descriptor was handed to nobody, so it advances NOTHING. */
if (done) {
ctx.cursor++;
ctx.control[kFieldTail] = ctx.cursor;
}
}
ft_tensix_noc_read_barrier();
ft_tensix_noc_write_barrier();
ctx.control[kFieldError] = ctx.error;
ctx.control[kFieldState] = kStateExit;
ftq_status_set(&ctx, kStateExit);
}