322 lines
14 KiB
C
322 lines
14 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file resident_rope_reader.c
|
|
* @brief Reader third of the RESIDENT ROPE triad, and THE PROCESSOR THAT IMPLEMENTS THE PAIRING.
|
|
*
|
|
* Everything RoPE-specific about the tile layout is here, as ADDRESSING. The compute
|
|
* thread never indexes an element and never knows which half a tile is in: it is handed
|
|
* four aligned tile streams and multiplies them.
|
|
*
|
|
* CB_IN the activation, tile column c of a head, in order
|
|
* CB_ROT the PARTNER tile, ftr_partner_tile(c) of the same head — the whole rotation
|
|
* CB_COS cos(angle) for column c, from the table
|
|
* CB_SIN SIGNED sin(angle) for column c: -sin below the half, +sin above it
|
|
*
|
|
* THE PARTNER STREAM IS TWO NoC READS PER BLOCK, NEVER MORE. Inside one head the partner
|
|
* index is a rotation by half modulo rot_tiles, so a block of `depth` consecutive columns
|
|
* maps to at most TWO contiguous runs — the run from (c0 + half) to the end, and the
|
|
* wrapped remainder from 0. Both land in the contiguous CB block the reserve just gave us.
|
|
*
|
|
* AND IT IS PER-BLOCK INTERLEAVED, WHICH IS A DEADLOCK ARGUMENT AND NOT A STYLE CHOICE.
|
|
* The compute thread consumes column c of all four streams together, so a reader that
|
|
* pushed a whole head of CB_IN before starting CB_ROT would block in cb_reserve_back once
|
|
* CB_IN was full while compute blocked in cb_wait_front on CB_ROT — three processors
|
|
* waiting on each other, exactly the pipeline deadlock resident_matmul_writer.c records.
|
|
* Every stream advances one block at a time, in the order the consumer wants them.
|
|
*
|
|
* ONE BARRIER PER BLOCK, for four streams and up to five transactions. It is a per-CORE
|
|
* drain, not a per-transaction one, and paying it per tile is
|
|
* what cost 5.9x on the matmul (docs/tt_bringup_log.md 16.1).
|
|
*
|
|
* 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 (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: CB_IN index 1: CB_ROT index 2: CB_COS index 3: CB_SIN index
|
|
* 4: tile size in bytes
|
|
* 5: wait mode -- 0 upstream, 1 stoppable (ships), 2 bound-only
|
|
* Runtime args: the ten of resident_queue_protocol.h, identical on all five processors
|
|
*
|
|
* Build (BRISC, NoC 0):
|
|
* CTARGS=0,1,2,3,2048,1 NOC=0 KERNEL_TEXT_BASE=0x15000 bash \
|
|
* foundry/tt/build_tensix_dataflow_kernel.sh \
|
|
* foundry/tt/kernels/resident_rope_reader.c brisc /tmp/ttk_rope
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "ft_tensix_dataflow.h"
|
|
#include "resident_queue_protocol.h"
|
|
#include "resident_rope_protocol.h"
|
|
|
|
#define CB_IN ft_tensix_compile_time_arg(0)
|
|
#define CB_ROT ft_tensix_compile_time_arg(1)
|
|
#define CB_COS ft_tensix_compile_time_arg(2)
|
|
#define CB_SIN ft_tensix_compile_time_arg(3)
|
|
#define TILE_BYTES ft_tensix_compile_time_arg(4)
|
|
#define WAIT_MODE ft_tensix_compile_time_arg(5)
|
|
/** One descriptor's shape, read once so the block loop indexes registers. */
|
|
typedef struct rope_shape {
|
|
uint32_t tile_count;
|
|
uint32_t head_tiles;
|
|
uint32_t rot_tiles;
|
|
uint32_t heads;
|
|
uint32_t depth;
|
|
uint32_t a_x;
|
|
uint32_t a_y;
|
|
uint32_t a_base;
|
|
uint32_t table_x;
|
|
uint32_t table_y;
|
|
uint32_t table_base;
|
|
} rope_shape;
|
|
|
|
static void load_shape(rope_shape *shape, volatile FTQ_L1_PTR uint32_t *parameters)
|
|
{
|
|
shape->tile_count = parameters[kRopeTileCount];
|
|
shape->head_tiles = parameters[kRopeHeadTiles];
|
|
shape->rot_tiles = parameters[kRopeRotTiles];
|
|
shape->heads = parameters[kRopeHeads];
|
|
shape->a_x = parameters[kRopeAX];
|
|
shape->a_y = parameters[kRopeAY];
|
|
shape->a_base = parameters[kRopeABase];
|
|
shape->table_x = parameters[kRopeTableX];
|
|
shape->table_y = parameters[kRopeTableY];
|
|
shape->table_base = parameters[kRopeTableBase];
|
|
|
|
/* The depth must divide BOTH runs it blocks over: head_tiles for the activation and
|
|
* rot_tiles for the three rotated streams. The host already shrinks it; this is the
|
|
* kernel refusing to guess, which is the same belt the matmul reader wears. */
|
|
const uint32_t requested = parameters[kRopeDepth];
|
|
const uint32_t on_head = ftr_block_depth(shape->head_tiles, requested);
|
|
shape->depth = ftr_block_depth(shape->rot_tiles, on_head);
|
|
}
|
|
|
|
/**
|
|
* Refuse a descriptor this program cannot execute, INSTEAD of writing outside the four
|
|
* circular buffers.
|
|
*
|
|
* `ftr_block_depth()` makes `depth` divide BOTH runs, which is the ONE-SHOT invariant and
|
|
* only half of the resident one. The FIFO write pointer wraps on an EXACT hit of the limit
|
|
* and nothing else, so each buffer also has to end a descriptor
|
|
* where it started it: `pages % depth == 0` AND `pushes % pages == 0`.
|
|
* `ft_tensix_cb_push_grid_is_safe()` carries the derivation and the measured incident — a read
|
|
* that walked onto the queue control block at 0x170000 with nothing faulting.
|
|
*
|
|
* THE FOUR STREAMS PUSH DIFFERENT TOTALS, which is what makes this op the worst case in the
|
|
* family: CB_IN takes `tile_count` pages while the three rotated streams take
|
|
* `instances * rot_tiles`, and `rot_tiles` is HALF `head_tiles` in the shapes this op
|
|
* supports. A single test against CB_IN would pass while the other three walked.
|
|
*/
|
|
static bool descriptor_is_executable(ftq_context *ctx, const rope_shape *shape)
|
|
{
|
|
if (shape->depth == 0u || shape->head_tiles == 0u || shape->rot_tiles == 0u ||
|
|
TILE_BYTES == 0u)
|
|
ctx->error = kErrBadParameters;
|
|
else if (!ft_tensix_cb_push_grid_is_safe(CB_IN, shape->depth) ||
|
|
!ft_tensix_cb_push_grid_is_safe(CB_ROT, shape->depth) ||
|
|
!ft_tensix_cb_push_grid_is_safe(CB_COS, shape->depth) ||
|
|
!ft_tensix_cb_push_grid_is_safe(CB_SIN, shape->depth))
|
|
ctx->error = kErrBadParameters;
|
|
else
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Issue the partner block for columns [c0, c0 + depth) of the head based at `head_base`.
|
|
*
|
|
* At most TWO transactions by construction: the partner index is (c + half) mod rot_tiles,
|
|
* which runs contiguously up to the wrap and contiguously after it.
|
|
*/
|
|
static void issue_partner_block(const rope_shape *shape, uint32_t head_base, uint32_t write_address,
|
|
uint32_t c0)
|
|
{
|
|
const uint32_t start = ftr_partner_tile(c0, shape->rot_tiles);
|
|
const uint32_t to_end = shape->rot_tiles - start;
|
|
const uint32_t first = (shape->depth < to_end) ? shape->depth : to_end;
|
|
|
|
ft_tensix_noc_read(ft_tensix_noc_address(shape->a_x, shape->a_y,
|
|
head_base + start * TILE_BYTES),
|
|
write_address, first * TILE_BYTES);
|
|
if (first < shape->depth) {
|
|
ft_tensix_noc_read(ft_tensix_noc_address(shape->a_x, shape->a_y, head_base),
|
|
write_address + first * TILE_BYTES, (shape->depth - first) * TILE_BYTES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The reader half of one ROPE descriptor.
|
|
*
|
|
* @return true if the whole descriptor was read; false if a wait was abandoned, in which
|
|
* case the loop still ran to completion (rule 3) and the caller must not retire it
|
|
*/
|
|
static bool read_one_rope(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
|
|
{
|
|
rope_shape shape;
|
|
|
|
load_shape(&shape, parameters);
|
|
if (!ftr_shape_is_supported(shape.head_tiles, shape.rot_tiles) || shape.heads == 0u ||
|
|
shape.tile_count == 0u || (shape.tile_count % shape.head_tiles) != 0u ||
|
|
!descriptor_is_executable(ctx, &shape)) {
|
|
ftq_abort(ctx, kErrBadParameters);
|
|
return false;
|
|
}
|
|
|
|
/* One "instance" is one head of one row block: head_tiles activation tiles. The TABLE
|
|
* advances once per ROW BLOCK and is shared by every head in it, which is why `heads` is
|
|
* a field — every head of a row block rotates by the same angles. */
|
|
const uint32_t instances = shape.tile_count / shape.head_tiles;
|
|
const uint32_t table_block_tiles = 2u * shape.rot_tiles;
|
|
|
|
for (uint32_t instance = 0; instance < instances; instance++) {
|
|
const uint32_t row_block = instance / shape.heads;
|
|
const uint32_t head_base = shape.a_base + instance * shape.head_tiles * TILE_BYTES;
|
|
const uint32_t table = shape.table_base + row_block * table_block_tiles * TILE_BYTES;
|
|
|
|
for (uint32_t c0 = 0; c0 < shape.head_tiles; c0 += shape.depth) {
|
|
const bool rotated = c0 < shape.rot_tiles;
|
|
uint32_t in_write;
|
|
uint32_t rot_write = 0u;
|
|
uint32_t cos_write = 0u;
|
|
uint32_t sin_write = 0u;
|
|
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_IN,
|
|
(int32_t)shape.depth, WAIT_MODE))
|
|
return false;
|
|
in_write = ft_tensix_cb_write_pointer(CB_IN);
|
|
if (rotated) {
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_ROT,
|
|
(int32_t)shape.depth, WAIT_MODE))
|
|
return false;
|
|
rot_write = ft_tensix_cb_write_pointer(CB_ROT);
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_COS,
|
|
(int32_t)shape.depth, WAIT_MODE))
|
|
return false;
|
|
cos_write = ft_tensix_cb_write_pointer(CB_COS);
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_SIN,
|
|
(int32_t)shape.depth, WAIT_MODE))
|
|
return false;
|
|
sin_write = ft_tensix_cb_write_pointer(CB_SIN);
|
|
}
|
|
|
|
// Rule 3: while draining, issue NOTHING. The pushes below still happen so the
|
|
// page counters stay balanced, but a transaction issued during a drain would
|
|
// still be in flight when the kernel returns.
|
|
if (!ctx->aborting) {
|
|
ft_tensix_noc_read(ft_tensix_noc_address(shape.a_x, shape.a_y,
|
|
head_base + c0 * TILE_BYTES),
|
|
in_write, shape.depth * TILE_BYTES);
|
|
if (rotated) {
|
|
issue_partner_block(&shape, head_base, rot_write, c0);
|
|
const uint32_t sin_offset = (shape.rot_tiles + c0) * TILE_BYTES;
|
|
|
|
ft_tensix_noc_read(ft_tensix_noc_address(shape.table_x, shape.table_y,
|
|
table + c0 * TILE_BYTES),
|
|
cos_write, shape.depth * TILE_BYTES);
|
|
ft_tensix_noc_read(ft_tensix_noc_address(shape.table_x, shape.table_y,
|
|
table + sin_offset),
|
|
sin_write, shape.depth * TILE_BYTES);
|
|
}
|
|
// ONE barrier for the whole block, all four streams.
|
|
ft_tensix_noc_read_barrier();
|
|
}
|
|
|
|
ft_tensix_cb_push_back(CB_IN, shape.depth);
|
|
if (rotated) {
|
|
ft_tensix_cb_push_back(CB_ROT, shape.depth);
|
|
ft_tensix_cb_push_back(CB_COS, shape.depth);
|
|
ft_tensix_cb_push_back(CB_SIN, shape.depth);
|
|
}
|
|
}
|
|
}
|
|
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. All five processors resume from completed_count, the ONE
|
|
// number that means "every stage finished this descriptor" — resident_matmul_reader.c
|
|
// carries the full argument and the refusal that enforces it.
|
|
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 == kOpRope) {
|
|
ftq_status_set(&ctx, kStateCbReserveBack);
|
|
done = read_one_rope(&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 and
|
|
// the next launch re-attempts it.
|
|
if (done) {
|
|
ctx.cursor++;
|
|
ctx.control[kFieldTail] = ctx.cursor;
|
|
}
|
|
}
|
|
|
|
// Nothing is outstanding here: the drain path issues no transactions. The barrier is
|
|
// the cheap statement of that invariant rather than a fix for a case that can happen.
|
|
ft_tensix_noc_read_barrier();
|
|
ft_tensix_noc_write_barrier();
|
|
ctx.control[kFieldError] = ctx.error;
|
|
ctx.control[kFieldState] = kStateExit;
|
|
ftq_status_set(&ctx, kStateExit);
|
|
}
|