372 lines
16 KiB
C
372 lines
16 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file resident_attention_reader.c
|
|
* @brief Reader third of the RESIDENT FLASH-DECODE ATTENTION triad: the group's Q, packed into
|
|
* `dim_tiles` tiles whose rows are Q heads; the four constant tiles this descriptor
|
|
* selects; and then the KV cache, ONE BLOCK of K and one of V at a time, each block a
|
|
* SINGLE contiguous `noc_async_read`.
|
|
*
|
|
* THE KV BLOCK IS ONE TRANSACTION AND THAT IS THE WHOLE POINT OF THE LAYOUT.
|
|
* ft_tt_op_memory.h PART 1 §1.2: positions [32a, 32b) of one head are one contiguous byte
|
|
* range, walked linearly in exactly the order the matmul reader already wants. So a block
|
|
* of `block_tiles` position tiles is `block_tiles * dim_tiles` consecutive tiles, i.e.
|
|
* ONE read and ONE barrier -- the shape that was worth 5.9x on the matmul
|
|
* (docs/tt_kernel_model.md §12) rather than a strided walk.
|
|
*
|
|
* THE Q PACK IS THE ONE PIECE OF REAL WORK THE READER DOES.
|
|
* Q arrives in the model's activation layout -- head h's dim tile d at tile column
|
|
* `h*dim_tiles + d`, the decode token at row `position % 32` -- and the compute thread
|
|
* wants ONE tile per dim tile whose ROW h is head `q_first + h`. The group's tiles are
|
|
* consecutive, so the whole set is one read into a staging buffer, and the pack is
|
|
* `q_rows * dim_tiles * 2` copies of a 32-byte face row. It reads 64 KiB to use 2 KiB of
|
|
* it, and that is the right trade: it is ~0.4% of one layer's KV traffic at 8k context,
|
|
* and the alternative -- a 32-byte strided DRAM read per face row -- is not legal at all
|
|
* (64 B read alignment, ft_tt_noc_alignment()).
|
|
*
|
|
* THIS PROCESSOR OWNS `tail`, AND NOTHING ELSE IN THE CONTROL BLOCK.
|
|
*
|
|
*
|
|
* 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_Q index 1: CB_K index 2: CB_V index 3: CB_CONST index
|
|
* 4: CB_QSTAGE index (reader-private staging; never pushed, never popped)
|
|
* 5: tile size in bytes
|
|
* 6: 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,13,2048,1 NOC=0 KERNEL_TEXT_BASE=0x15000 bash \
|
|
* foundry/tt/build_tensix_dataflow_kernel.sh \
|
|
* foundry/tt/kernels/resident_attention_reader.c brisc /tmp/ttk_attn
|
|
*
|
|
* @status GREEN ON SILICON on Tensix 70/71/72/73, 0 checks failed.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "ft_tensix_dataflow.h"
|
|
#include "resident_queue_protocol.h"
|
|
#include "resident_attention_protocol.h"
|
|
|
|
#define CB_Q ft_tensix_compile_time_arg(0)
|
|
#define CB_K ft_tensix_compile_time_arg(1)
|
|
#define CB_V ft_tensix_compile_time_arg(2)
|
|
#define CB_CONST ft_tensix_compile_time_arg(3)
|
|
#define CB_QSTAGE ft_tensix_compile_time_arg(4)
|
|
#define TILE_BYTES ft_tensix_compile_time_arg(5)
|
|
#define WAIT_MODE ft_tensix_compile_time_arg(6)
|
|
/**
|
|
* Refuse a descriptor the circular buffers this residency was STAGED with cannot serve.
|
|
*
|
|
* The page counts are fixed when the resident program is staged; the shape is a
|
|
* per-descriptor word. So the host CAN ask for a block bigger than the buffer -- it did, on
|
|
* another op's first hardware run, and the result was ten seconds of blocked polling
|
|
* reported as an idle timeout (resident_topk_reader.c). And the two block-prefetch
|
|
* conditions are the §5 invariant: `pages % depth == 0` so every wrap lands exactly on the
|
|
* limit, and `tile_count % pages == 0` so the descriptor leaves the write pointer where it
|
|
* found it. Without the second, a later descriptor's block steps OVER the limit and walks
|
|
* up L1 -- measured, on this family, landing on the queue control block with nothing
|
|
* faulting.
|
|
*/
|
|
static bool descriptor_is_executable(ftq_context *ctx, const fta_shape *shape)
|
|
{
|
|
const uint32_t depth = shape->block_tiles * shape->dim_tiles;
|
|
const uint32_t k_pages = ft_tensix_cb(CB_K)->fifo_num_pages;
|
|
const uint32_t v_pages = ft_tensix_cb(CB_V)->fifo_num_pages;
|
|
const uint32_t q_pages = ft_tensix_cb(CB_Q)->fifo_num_pages;
|
|
const uint32_t stage_pages = ft_tensix_cb(CB_QSTAGE)->fifo_num_pages;
|
|
const uint32_t const_pages = ft_tensix_cb(CB_CONST)->fifo_num_pages;
|
|
const uint32_t tile_count = shape->padded_tiles * shape->dim_tiles;
|
|
|
|
/* CHUNK mode reads the Q tiles straight into CB_Q, so it never touches the staging
|
|
* buffer and must not be measured against it: at C = 32 and dim_tiles = 8 the decode
|
|
* residency's 32 staging pages would refuse every chunk descriptor for a buffer the
|
|
* descriptor does not use. */
|
|
const uint32_t stage_needed = fta_is_chunk(shape) ? 0u : shape->q_rows * shape->dim_tiles;
|
|
|
|
if (!fta_shape_valid(shape) || TILE_BYTES == 0u || depth == 0u)
|
|
ctx->error = kErrBadParameters;
|
|
else if (k_pages == 0u || v_pages == 0u || q_pages < shape->dim_tiles ||
|
|
const_pages < kAttnCbConstPages || stage_pages < stage_needed)
|
|
ctx->error = kErrBadParameters;
|
|
else if (depth > k_pages || (k_pages % depth) != 0u || (tile_count % k_pages) != 0u)
|
|
ctx->error = kErrBadParameters;
|
|
else if (depth > v_pages || (v_pages % depth) != 0u || (tile_count % v_pages) != 0u)
|
|
ctx->error = kErrBadParameters;
|
|
else
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Read `tiles` consecutive tiles at `base + first*TILE_BYTES` into `cb` as ONE block.
|
|
*
|
|
* @return false if a reservation was abandoned, or if the length came out zero -- which is
|
|
* a refusal and never a skipped transfer (§6)
|
|
*/
|
|
static bool read_block(ftq_context *ctx, uint32_t cb, uint32_t x, uint32_t y, uint32_t base,
|
|
uint32_t first, uint32_t tiles)
|
|
{
|
|
const uint32_t bytes = tiles * TILE_BYTES;
|
|
|
|
if (!fta_length_is_issuable(bytes)) {
|
|
ftq_abort(ctx, kErrBadParameters);
|
|
return false;
|
|
}
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)cb, (int32_t)tiles, WAIT_MODE))
|
|
return false;
|
|
|
|
// Rule 3: while draining, issue NOTHING. The push still happens 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(x, y, base + first * TILE_BYTES),
|
|
ft_tensix_cb_write_pointer(cb), bytes);
|
|
// ONE barrier for the whole block: it is per-core, not per-transaction.
|
|
ft_tensix_noc_read_barrier();
|
|
}
|
|
ft_tensix_cb_push_back(cb, tiles);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The four constants this descriptor uses, in the order the compute thread indexes them:
|
|
* the reduce scaler, the 1/sqrt(head_dim) scale, the tail mask and the all-dead mask.
|
|
*
|
|
* Two of the four are adjacent in the operand so they are one transaction; the two masks
|
|
* are selected by `kv_positions % 32` and by nothing else, which is why the mask INDEX is
|
|
* derived here rather than sent (a second field that must agree with the first is a second
|
|
* field that can disagree).
|
|
*/
|
|
static bool read_constants(ftq_context *ctx, const fta_shape *shape, uint32_t x, uint32_t y,
|
|
uint32_t base, uint32_t rows_mask)
|
|
{
|
|
/* The TAIL slot carries the causal tile in chunk mode and the tail mask otherwise --
|
|
* one function, resident_attention_protocol.h, so this and the off-hardware suite
|
|
* cannot spell the selection differently. */
|
|
const uint32_t tail_mask = fta_tail_slot_source_tile(shape);
|
|
const uint32_t bytes = kAttnCbConstPages * TILE_BYTES;
|
|
|
|
if (!fta_length_is_issuable(bytes)) {
|
|
ftq_abort(ctx, kErrBadParameters);
|
|
return false;
|
|
}
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_CONST,
|
|
(int32_t)kAttnCbConstPages, WAIT_MODE))
|
|
return false;
|
|
|
|
if (!ctx->aborting) {
|
|
const uint32_t write = ft_tensix_cb_write_pointer(CB_CONST);
|
|
|
|
/* Tiles 0 and 1 are adjacent in the operand: one transaction. */
|
|
ft_tensix_noc_read(ft_tensix_noc_address(x, y, base + kAttnConstScalerTile * TILE_BYTES),
|
|
write,
|
|
2u * TILE_BYTES);
|
|
ft_tensix_noc_read(ft_tensix_noc_address(x, y,
|
|
(shape->flags & (uint32_t)kAttnFlagRows) != 0u
|
|
? rows_mask
|
|
: base + tail_mask * TILE_BYTES),
|
|
write + kAttnCbConstTailMask * TILE_BYTES, TILE_BYTES);
|
|
ft_tensix_noc_read(ft_tensix_noc_address(x, y, base + kAttnConstMaskTile * TILE_BYTES),
|
|
write + kAttnCbConstDeadMask * TILE_BYTES, TILE_BYTES);
|
|
ft_tensix_noc_read_barrier();
|
|
}
|
|
ft_tensix_cb_push_back(CB_CONST, kAttnCbConstPages);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Build the packed Q: `dim_tiles` tiles whose row h is Q head `q_first + h`.
|
|
*
|
|
* The group's `q_rows * dim_tiles` Q tiles are consecutive in the activation buffer, so
|
|
* they arrive in ONE transaction; the pack is then a face-row scatter inside L1. Source
|
|
* tile (h, dt) is at staging index `h * dim_tiles + dt` and contributes its row
|
|
* `query_row` to row h of packed tile dt.
|
|
*
|
|
* IN CHUNK MODE THERE IS NO PACK AT ALL — see the branch below.
|
|
*/
|
|
static bool read_and_pack_q(ftq_context *ctx, const fta_shape *shape, uint32_t x, uint32_t y,
|
|
uint32_t base)
|
|
{
|
|
/* Chunk mode reads ONE head's `dim_tiles` tiles; decode reads the whole group's
|
|
* `q_rows * dim_tiles` into staging and packs one row out of each. */
|
|
const uint32_t source_tiles =
|
|
fta_is_chunk(shape) ? shape->dim_tiles : shape->q_rows * shape->dim_tiles;
|
|
const uint32_t bytes = source_tiles * TILE_BYTES;
|
|
const uint32_t chunk = fta_chunk_bytes(TILE_BYTES);
|
|
uint32_t stage;
|
|
uint32_t packed;
|
|
|
|
if (!fta_length_is_issuable(bytes) || !fta_length_is_issuable(chunk)) {
|
|
ftq_abort(ctx, kErrBadParameters);
|
|
return false;
|
|
}
|
|
if (!ftq_cb_reserve_back_stoppable(ctx, (int32_t)CB_Q, (int32_t)shape->dim_tiles, WAIT_MODE))
|
|
return false;
|
|
|
|
/* CHUNK MODE: THERE IS NOTHING TO PACK. The activation tile already has position t in
|
|
* row t, which IS the packed Q -- `dim_tiles` tiles whose rows are what the score
|
|
* matmul's M axis should be. One contiguous read, no staging buffer, no face-row
|
|
* copies: 16 transactions a layer instead of 64 KiB read to use 2 KiB of it. */
|
|
if (fta_is_chunk(shape)) {
|
|
if (!ctx->aborting) {
|
|
ft_tensix_noc_read(ft_tensix_noc_address(x, y, base),
|
|
ft_tensix_cb_write_pointer(CB_Q), bytes);
|
|
ft_tensix_noc_read_barrier();
|
|
}
|
|
ft_tensix_cb_push_back(CB_Q, shape->dim_tiles);
|
|
return true;
|
|
}
|
|
|
|
stage = ft_tensix_cb_write_pointer(CB_QSTAGE);
|
|
packed = ft_tensix_cb_write_pointer(CB_Q);
|
|
if (!ctx->aborting) {
|
|
ft_tensix_noc_read(ft_tensix_noc_address(x, y, base), stage, bytes);
|
|
ft_tensix_noc_read_barrier();
|
|
|
|
for (uint32_t dt = 0; dt < shape->dim_tiles; dt++) {
|
|
for (uint32_t p = 0; p < shape->positions; p++) {
|
|
for (uint32_t head = 0; head < shape->q_rows; head++) {
|
|
const uint32_t source_tile = (head * shape->dim_tiles + dt) * TILE_BYTES;
|
|
|
|
for (uint32_t half = 0; half < 2u; half++) {
|
|
const uint32_t from = stage + source_tile +
|
|
fta_chunk_offset(shape->query_row + p, half,
|
|
TILE_BYTES);
|
|
const uint32_t to = packed + dt * TILE_BYTES +
|
|
fta_chunk_offset(p * shape->q_rows + head, half,
|
|
TILE_BYTES);
|
|
|
|
fta_copy_words((uint32_t *)(uintptr_t)(to),
|
|
(const uint32_t *)(uintptr_t)(from), chunk / 4u);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ft_tensix_cb_push_back(CB_Q, shape->dim_tiles);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The reader half of one ATTENTION descriptor.
|
|
*
|
|
* Every trip count comes from the parameter block and none from a wait, which is what makes
|
|
* the drain safe (resident_queue_protocol.h rule 3).
|
|
*
|
|
* @return true if the whole descriptor was read; false if a wait was abandoned, in which
|
|
* case the loop still ran to completion and the caller must not retire it
|
|
*/
|
|
static bool read_one_attention(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
|
|
{
|
|
const fta_shape shape = fta_decode(parameters);
|
|
const uint32_t q_x = fta_coord_x(parameters[kAttnQCoord]);
|
|
const uint32_t q_y = fta_coord_y(parameters[kAttnQCoord]);
|
|
const uint32_t k_x = fta_coord_x(parameters[kAttnKCoord]);
|
|
const uint32_t k_y = fta_coord_y(parameters[kAttnKCoord]);
|
|
const uint32_t v_x = fta_coord_x(parameters[kAttnVCoord]);
|
|
const uint32_t v_y = fta_coord_y(parameters[kAttnVCoord]);
|
|
const uint32_t c_x = fta_coord_x(parameters[kAttnConstCoord]);
|
|
const uint32_t c_y = fta_coord_y(parameters[kAttnConstCoord]);
|
|
const uint32_t depth = shape.block_tiles * shape.dim_tiles;
|
|
|
|
if (!descriptor_is_executable(ctx, &shape)) {
|
|
ftq_abort(ctx, ctx->error);
|
|
return false;
|
|
}
|
|
|
|
if (!read_and_pack_q(ctx, &shape, q_x, q_y, parameters[kAttnQBase]))
|
|
return false;
|
|
if (!read_constants(ctx, &shape, c_x, c_y, parameters[kAttnConstBase],
|
|
parameters[kAttnReserved]))
|
|
return false;
|
|
|
|
for (uint32_t block = 0; block < shape.blocks; block++) {
|
|
const uint32_t first = block * depth;
|
|
|
|
if (!read_block(ctx, CB_K, k_x, k_y, parameters[kAttnKBase], first, depth))
|
|
return false;
|
|
if (!read_block(ctx, CB_V, v_x, v_y, parameters[kAttnVBase], first, depth))
|
|
return false;
|
|
}
|
|
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; resident_matmul_reader.c carries the 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 == kOpAttention) {
|
|
ftq_status_set(&ctx, kStateCbReserveBack);
|
|
done = read_one_attention(&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.
|
|
ctx.error = kErrBadOpcode;
|
|
done = false;
|
|
running = false;
|
|
}
|
|
|
|
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);
|
|
}
|