Foundry/tt/kernels/resident_gdngate_reader.c

265 lines
11 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/**
* @file resident_gdngate_reader.c
* @brief Reader half of the RESIDENT GDN GATE pair: reads each band's alpha and beta tiles and
* the layer's constants, computes every position's decay and beta with ft_det_math.h, and
* assembles each 16-row sub-chunk's COMPACT gate block in the output CB.
* resident_gdngate_writer.c puts the blocks in DRAM. resident_gdngate_protocol.h has the
* layout and the reasons.
*
* THE ARITHMETIC IS THE HOST'S, NOT A COPY OF IT: the host compiles the same ft_det_math.h,
* which is integer-only, so the bits cannot depend on which compiler, flags or FPU ran it.
* That is the whole claim this family rests on and tests/test_det_math.c + the card sweep
* are its proof.
*
* THIS PROCESSOR OWNS `tail`, AND NOTHING ELSE IN THE CONTROL BLOCK (resident_queue_protocol.h
* rule 1). An aborted descriptor advances nothing.
*
* Compile-time args:
* 0: CB_STAGE index (addressed, never cycled: 2 tiles + the 256-byte constants block)
* 1: CB_OUT index (one page = one block)
* 2: tile size in bytes (2048, bf16)
* 3: wait mode -- 0 upstream, 1 stoppable (ships), 2 bound-only
* Runtime args: the ten of resident_queue_protocol.h, identical on every processor
*
* Build (BRISC, NoC 0):
* CTARGS=0,16,2048,1 NOC=0 KERNEL_TEXT_BASE=0x15000 bash \
* foundry/tt/build_tensix_dataflow_kernel.sh \
* foundry/tt/kernels/resident_gdngate_reader.c brisc <out>
*
* @status Written PM 7 (2026-10-08). Unexecuted.
* @issues None
* @todo None
*/
#include <stdbool.h>
#include <stdint.h>
#include "ft_tensix_dataflow.h"
#include "resident_gdngate_protocol.h"
#include "resident_queue_protocol.h"
#define CB_STAGE ft_tensix_compile_time_arg(0)
#define CB_OUT ft_tensix_compile_time_arg(1)
#define TILE_BYTES ft_tensix_compile_time_arg(2)
#define WAIT_MODE ft_tensix_compile_time_arg(3)
/** Everything one descriptor says, read once. */
typedef struct gate_request {
uint32_t flags, rows, band_count, sub_rows, heads;
uint32_t alpha_x, alpha_y, alpha_base;
uint32_t beta_x, beta_y, beta_base;
uint32_t band_stride;
uint32_t const_x, const_y, const_base;
uint32_t dst_base, block_stride;
uint32_t scale_bits;
} gate_request;
static void load_request(gate_request *request, volatile FTQ_L1_PTR uint32_t *parameters)
{
request->flags = parameters[kGgFlags];
request->rows = parameters[kGgRows];
request->band_count = parameters[kGgBandCount];
request->sub_rows = parameters[kGgSubRows];
request->heads = parameters[kGgHeads];
request->alpha_x = parameters[kGgAlphaXY] & 0xFFFFu;
request->alpha_y = parameters[kGgAlphaXY] >> 16;
request->alpha_base = parameters[kGgAlphaBase];
request->beta_x = parameters[kGgBetaXY] & 0xFFFFu;
request->beta_y = parameters[kGgBetaXY] >> 16;
request->beta_base = parameters[kGgBetaBase];
request->band_stride = parameters[kGgBandStride];
request->const_x = parameters[kGgConstXY] & 0xFFFFu;
request->const_y = parameters[kGgConstXY] >> 16;
request->const_base = parameters[kGgConstBase];
request->dst_base = parameters[kGgDstBase];
request->block_stride = parameters[kGgBlockStride];
request->scale_bits = parameters[kGgScaleBits];
}
/** Refuse rather than half-execute: a parameter block is a pointer in a descriptor, and a
* wrong one becomes NoC traffic at addresses nobody chose (spec §7.3). */
static bool request_is_sane(const gate_request *request)
{
if (request->rows == 0u || request->band_count == 0u || request->band_count > kGgMaxBands)
return false;
if ((request->rows + kGgTileRows - 1u) / kGgTileRows != request->band_count)
return false;
if (request->heads == 0u || request->heads > kGgMaxHeads)
return false;
if (!ft_tensix_cb_push_grid_is_safe(CB_OUT, 1u))
return false;
// The writer's refusals too: a descriptor one half refuses and the other runs would
// leave the reader filling a CB nobody drains.
if ((request->flags & kGgFlagFlat) != 0u) {
// ONE band, a head slice (first = sub_rows word) inside the total (band_stride word),
// and the rows' span inside the one page.
const uint32_t total = request->band_stride;
return request->band_count == 1u && total <= kGgMaxHeads &&
request->sub_rows + request->heads <= total &&
request->block_stride >= 2u * total * 4u && (request->block_stride % 16u) == 0u &&
(request->dst_base % 16u) == 0u &&
ftgg_flat_bytes(request->rows, total, request->block_stride) <=
ftgg_block_bytes(kGgMaxHeads);
}
if (request->sub_rows != 16u && request->sub_rows != 32u)
return false;
if (request->band_stride < TILE_BYTES || (request->band_stride % 64u) != 0u)
return false;
return request->block_stride >= ftgg_block_bytes(request->heads) &&
(request->block_stride % 64u) == 0u && (request->dst_base % 64u) == 0u;
}
/** Read band `band`'s two tiles into the stage. */
static void read_band(const gate_request *request, uint32_t band, uint32_t stage)
{
const uint32_t offset = band * request->band_stride;
ft_tensix_noc_read(ft_tensix_noc_address(request->alpha_x, request->alpha_y,
request->alpha_base + offset),
stage, TILE_BYTES);
ft_tensix_noc_read(ft_tensix_noc_address(request->beta_x, request->beta_y,
request->beta_base + offset),
stage + TILE_BYTES, TILE_BYTES);
ft_tensix_noc_read_barrier();
}
/**
* The reader half of one GDN_GATE descriptor.
* @return true if every block was assembled; false if a wait was abandoned (the loop still
* ran to completion, rule 3) or the descriptor was refused
*/
static bool read_one_gate_op(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
{
const uint32_t stage = ft_tensix_cb_write_pointer(CB_STAGE);
const uint32_t constants = stage + 2u * TILE_BYTES;
gate_request request;
if (TILE_BYTES == 0u) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
load_request(&request, parameters);
if (!request_is_sane(&request)) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
ft_tensix_noc_read(ft_tensix_noc_address(request.const_x, request.const_y,
request.const_base),
constants, kGgConstWords * 4u);
ft_tensix_noc_read_barrier();
if ((request.flags & kGgFlagFlat) != 0u) {
if (!ctx->aborting)
read_band(&request, 0u, stage);
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_OUT, 1, WAIT_MODE))
return false;
if (!ctx->aborting)
ftgg_build_flat((uint32_t *)(uintptr_t)ft_tensix_cb_write_pointer(CB_OUT),
request.sub_rows, request.heads, request.band_stride,
(const uint16_t *)(uintptr_t)stage,
(const uint16_t *)(uintptr_t)(stage + TILE_BYTES),
(const uint32_t *)(uintptr_t)constants, request.rows,
request.block_stride,
(request.flags & kGgFlagRedBetaAsDecay) != 0u);
ft_tensix_cb_push_back(CB_OUT, 1u);
return !ctx->aborting;
}
for (uint32_t band = 0; band < request.band_count; band++) {
const uint32_t done = band * kGgTileRows;
const uint32_t band_rows = request.rows - done < kGgTileRows ? request.rows - done
: kGgTileRows;
if (!ctx->aborting)
read_band(&request, band, stage);
for (uint32_t sub = 0; sub < 2u; sub++) {
const uint32_t live = ftgg_sub_live_rows(band_rows, request.sub_rows, sub);
if (live == 0u)
continue;
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_OUT, 1, WAIT_MODE))
return false;
// Rule 3: while draining, compute nothing; the push still balances the counters.
if (!ctx->aborting)
ftgg_build_block((uint32_t *)(uintptr_t)ft_tensix_cb_write_pointer(CB_OUT),
request.heads, request.scale_bits,
(const uint16_t *)(uintptr_t)stage,
(const uint16_t *)(uintptr_t)(stage + TILE_BYTES),
(const uint32_t *)(uintptr_t)constants, sub * request.sub_rows,
live, (request.flags & kGgFlagRedBetaAsDecay) != 0u);
ft_tensix_cb_push_back(CB_OUT, 1u);
}
}
return !ctx->aborting;
}
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, from completed_count (resident_matmul_reader.c).
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;
}
ctx.cursor = ctx.control[kFieldCompletedCount];
while (running) {
if (!ftq_wait_for_work(&ctx, kFieldHead))
break;
ctx.aborting = false;
ftq_status_set(&ctx, kStateExec);
volatile FTQ_L1_PTR uint32_t *descriptor = ftq_descriptor(&ctx);
const uint32_t opcode = descriptor[kDescOpcode];
bool done;
ctx.control[kFieldLastOpcode] = opcode;
ftq_status_opcode(&ctx, opcode);
if (opcode == kOpGdnGate) {
ftq_status_set(&ctx, kStateCbReserveBack);
done = read_one_gate_op(&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 {
ctx.error = kErrBadOpcode;
done = false;
running = false;
}
// Rule 5: an abandoned descriptor 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);
}