483 lines
25 KiB
C
483 lines
25 KiB
C
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
//
|
|
// resident_attention_protocol.h
|
|
// The device half of ft_tt_op_attention.h: the sixteen parameter words the FLASH-DECODE
|
|
// ATTENTION triad reads out of L1, the circular-buffer contract the three kernels share,
|
|
// and the face arithmetic the reader and the writer both need. The host half is
|
|
// ft_tt_op_attention.h; the two must agree word for word and tests/test_attention_ops.c
|
|
// pins the pair off-hardware.
|
|
//
|
|
// ===========================================================================
|
|
// 1. WHAT ONE DESCRIPTOR IS: ONE KV HEAD'S GROUP, AT ONE DECODE POSITION
|
|
// ===========================================================================
|
|
// MiniCPM5-1B is GQA: 16 Q heads, 2 KV heads, head_dim 128 (docs/tt_phase0_decisions.md
|
|
// §3, read from the GGUF header). So each KV head serves EIGHT Q heads, and the unit of
|
|
// work here is ONE KV GROUP -- one KV head's K and V, and the eight Q heads that read it.
|
|
//
|
|
// THAT CHOICE IS THE OP'S MAIN DESIGN DECISION AND IT IS AN ARITHMETIC ONE. Decode reads
|
|
// the whole KV cache and does almost no arithmetic per byte, so the only thing that matters
|
|
// is how many times the cache is read. A core that owned ONE Q HEAD would re-read its KV
|
|
// head's cache eight times per layer per token; a core that owns the GROUP reads it once
|
|
// and serves all eight. At 8k context that is the difference between 8 MiB and 64 MiB of
|
|
// NoC traffic per layer per token.
|
|
//
|
|
// The eight Q heads become the eight ROWS of one tile (§3), so the score matmul that a
|
|
// single-head design would issue eight times is issued ONCE. That matters at the other end
|
|
// too: one HiFi4 tile matmul is 64 cycles = 80 ns at 800 MHz against a reader delivering a
|
|
// tile pair every ~104 ns (docs/tt_kernel_model.md §12, §16), so eight of them per pair
|
|
// would move this op from data-movement bound to MATH bound. Packing keeps it at one.
|
|
//
|
|
// ===========================================================================
|
|
// 2. THE ONLINE SOFTMAX IS REQUIRED, AND THE REASON IS CAPACITY
|
|
// ===========================================================================
|
|
// A standalone softmax holds its row TWICE over -- the activations to subtract the max
|
|
// from, and the exponentials to divide by the sum -- so at 2048 B a tile it tops out at
|
|
// Wt = 325 (ft_tt_op_reduce_max_tile_cols(), docs/tt_bringup_log.md §22.3). A 32k-context
|
|
// attention row is Wt = 1024 and DOES NOT FIT AT ANY TILE SIZE.
|
|
//
|
|
// So the score row is never materialised. This op walks the KV cache in BLOCKS and carries
|
|
// three running quantities across them -- the row max `m`, the row sum `l`, and the output
|
|
// accumulator `O` -- rescaling the last two whenever the max moves:
|
|
//
|
|
// m_new = max(m_old, rowmax(S_block))
|
|
// c = exp(m_old - m_new)
|
|
// l_new = c * l_old + rowsum(exp(S_block - m_new))
|
|
// O_new = c * O_old + exp(S_block - m_new) . V_block
|
|
// out = O_final / l_final
|
|
//
|
|
// L1 then holds ONE BLOCK of scores, not a row, and the context length stops being a
|
|
// capacity question at all.
|
|
//
|
|
// **`dst_accum_mode 1` is not optional here and it is not a transplant.** The running sum
|
|
// and max are accumulated across EVERY block, so a 16-bit destination accumulator
|
|
// re-rounds them once per block -- docs/tt_bringup_log.md §19's failure mode multiplied by
|
|
// the block count, which at 32k context is 128 blocks rather than the 144 K-tiles that
|
|
// made a matmul's error grow 159x.
|
|
//
|
|
// ===========================================================================
|
|
// 3. THE THREE LAYOUTS, STATED COMPLETELY
|
|
// ===========================================================================
|
|
// **K and V** are ft_tt_op_memory.h PART 1's contract, unchanged and relied on: tile row =
|
|
// POSITION, tile column = HEAD DIMENSION, axis order HEAD -> POSITION-TILE -> DIM-TILE. A
|
|
// position range of one head is therefore ONE CONTIGUOUS BYTE RUN (F2), which is what lets
|
|
// a whole block be one `noc_async_read` -- the shape the measured fast path was built for.
|
|
//
|
|
// **Q and the output** are the model's ordinary activation layout: a tile grid
|
|
// `q_head_count * dim_tiles` wide, head h's dim-tile d at tile column `h*dim_tiles + d`,
|
|
// and the decode token's own row is `position % 32` (ft_tt_op_memory_position_row(), the
|
|
// §1.5b row-phase invariant this op inherits rather than re-decides).
|
|
//
|
|
// **The packed Q the compute thread consumes** is `dim_tiles` tiles in which ROW h is Q
|
|
// head `q_first + h` and the column is the head dimension. The READER builds it, by
|
|
// reading the group's `q_rows * dim_tiles` consecutive Q tiles in ONE transaction and
|
|
// copying the two 32-byte face rows of row `query_row` out of each. The WRITER performs
|
|
// the inverse scatter on the way out. Neither is a new idea: it is
|
|
// resident_place_writer.c's placed copy, and the face arithmetic below is the same
|
|
// arithmetic (tests/test_attention_ops.c asserts it against ft_tt_tilize.c rather than
|
|
// trusting the rewrite).
|
|
//
|
|
// ===========================================================================
|
|
// 4. THE TAIL, AND WHY IT IS AN ADDITIVE MASK RATHER THAN A SHORTER LOOP
|
|
// ===========================================================================
|
|
// The live length is not a multiple of 32, and the cache beyond it holds whatever the
|
|
// previous sequence left -- so it must be MASKED, not merely ignored, and masked BEFORE the
|
|
// row max. A 0/1 multiply after the exponential cannot do it: one stale score of +500 in a
|
|
// dead column would set the running max and drive every live weight to zero. So every
|
|
// position tile that is not entirely live has a tile of `0` / `-1e30` ADDED to its scores,
|
|
// selected by `kv_positions % 32`, and a tile entirely past the end gets the all-dead mask.
|
|
//
|
|
// **EVERY BLOCK THEREFORE PROCESSES EXACTLY `block_tiles` POSITION TILES**, including the
|
|
// ones past the end, and that uniformity is load-bearing rather than lazy: it is what keeps
|
|
// every circular buffer's push and pop counts identical for every descriptor, which is what
|
|
// the block-prefetch invariant (§5) requires of an op whose depth varies with the context.
|
|
// The waste is at most `2*block_tiles - 1` tiles per descriptor.
|
|
//
|
|
// ===========================================================================
|
|
// 5. THE BLOCK-PREFETCH INVARIANT, WHICH IS STRONGER THAN §12 SAYS
|
|
// ===========================================================================
|
|
// `cb_push_back` wraps the FIFO write pointer only when it lands EXACTLY on the limit. On a RESIDENT op that runs many descriptors, a descriptor that
|
|
// leaves the pointer off the next descriptor's block grid makes the next block step OVER
|
|
// the limit and walk up L1 -- MEASURED on the topk reader, where a read landed on the queue
|
|
// control block at 0x170000 and nothing faulted (resident_topk_reader.c). Two conditions
|
|
// make it impossible by induction:
|
|
//
|
|
// pages % depth == 0 every block wrap lands exactly on the limit
|
|
// tile_count % pages == 0 the descriptor leaves the pointer where it found it
|
|
//
|
|
// Both are checked HERE, in the kernel, as refusals -- and both are why §4's uniform block
|
|
// exists. K and V hold `2 * block_tiles * dim_tiles` pages and are pushed a whole block at
|
|
// a time; the padded position-tile count is a multiple of `2 * block_tiles`, so a
|
|
// descriptor ends where it started.
|
|
//
|
|
// ===========================================================================
|
|
// 6. NEVER ISSUE A ZERO-LENGTH NoC TRANSFER
|
|
// ===========================================================================
|
|
// A zero-length `noc_async_read` most likely took the whole card down on 2026-08-12 (dev
|
|
// notes §1: INFERRED, and deliberately not re-confirmed, because confirming it costs the
|
|
// card for every worker). Attention is full of computed lengths -- a block that comes out
|
|
// empty, a last partial block, a range that collapses -- so every length is checked against
|
|
// zero at the point of issue by fta_length_is_issuable(), and a zero is a REFUSAL rather
|
|
// than a skipped transfer.
|
|
//
|
|
#ifndef FOUNDRY_RESIDENT_ATTENTION_PROTOCOL_H
|
|
#define FOUNDRY_RESIDENT_ATTENTION_PROTOCOL_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifndef __cplusplus
|
|
#include <stdbool.h>
|
|
#endif
|
|
|
|
/* The SFPI GCC fork's own pointer attribute — an access through it goes to L1 rather than
|
|
* to ordinary memory, and dropping it changes the code that is emitted. Identical to
|
|
* tt-metal's `tt_l1_ptr`; spelled here as well as in resident_queue_protocol.h so this
|
|
* header does not depend on include order. */
|
|
#ifndef FTQ_L1_PTR
|
|
#define FTQ_L1_PTR __attribute__((rvtt_l1_ptr))
|
|
#endif
|
|
|
|
/* Header-only helper linkage, spelled the same way in both languages -- `inline` in C++ and
|
|
* `static inline` in C. NOT cosmetic: internal linkage changes GCC's inlining and cloning
|
|
* decisions, and the COMPUTE images that also include this header must not move for a
|
|
* change that is about the data-movement ones. */
|
|
#ifndef FTQ_INLINE
|
|
#ifdef __cplusplus
|
|
#define FTQ_INLINE inline
|
|
#else
|
|
#define FTQ_INLINE static inline
|
|
#endif
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The sixteen words. Mirrors ft_tt_op_attention.h -- change both or neither.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** byte 0 = q_rows (DECODE: Q heads in this group, 1..32; CHUNK: live positions C, 1..32);
|
|
* byte 1 = query_row (position % 32; 0 in CHUNK mode); byte 2 = flags. Three small numbers
|
|
* in one word because the block has no room for three, and bytes rather than bit fields
|
|
* because a field a human reads out of a hex dump on a stuck core is worth a nibble pair. */
|
|
enum { kAttnHeads = 0 };
|
|
|
|
/** CHUNK MODE — flags bit 0. One descriptor is then ONE Q HEAD over C consecutive
|
|
* positions, not one KV group at one position.
|
|
*
|
|
* WHY THE ROW AXIS FLIPS MEANING RATHER THAN GROWING. Decode packs the group's q_rows Q
|
|
* heads into the rows of one tile so the score matmul is issued once for all of them; the
|
|
* other 32 - q_rows rows are padding the FPU computes anyway. Prefill wants C positions of
|
|
* ONE head in those same rows, which is the SAME tile ops -- so a chunk descriptor costs
|
|
* what a decode descriptor costs and serves C positions instead of one. Keeping the GROUP
|
|
* as the unit instead would need q_rows*C = 128 rows, i.e. Mt = 4 in the compute thread:
|
|
* four running maxima, four row sums, four output accumulators, against a DST section of
|
|
* four tiles. That buys 4x fewer KV reads on an op this family measures as COMPUTE bound
|
|
* (ft_tt_op_attention.h @issues) and costs the whole compute kernel. Not taken.
|
|
*
|
|
* WHAT IT CHANGES, EXHAUSTIVELY:
|
|
* - the READER reads `dim_tiles` Q tiles straight into CB_Q. No staging buffer, no
|
|
* face-row pack: in chunk geometry the activation tile ALREADY has position t in row t,
|
|
* which is exactly the packed Q the compute thread wants (ft_tt_op_gdnc.h's "a chunk is
|
|
* the same strip a single position is", applied to attention);
|
|
* - the WRITER writes `dim_tiles` WHOLE tiles. No scatter, same reason;
|
|
* - the last LIVE position tile takes a CAUSAL mask instead of a tail mask (below);
|
|
* - the COMPUTE thread does not change at all. It never reads q_rows or query_row.
|
|
*
|
|
* WHAT IT REQUIRES OF THE CALLER: the chunk's first position is a multiple of 32, so the
|
|
* chunk occupies exactly the LAST live position tile and row t is position first + t. The
|
|
* host refuses anything else, because a chunk straddling two position tiles would need two
|
|
* masks and there is one mask slot. */
|
|
enum { kAttnFlagChunk = 1 };
|
|
/** S2f ROWS mode (the speculative verify): DECODE packing for `positions` consecutive strip
|
|
* rows at once — packed row p*q_rows + h is head h of strip row query_row + p, written back
|
|
* the same way. Every position's live length must end in the SAME (last) position tile; the
|
|
* tail slot then takes a per-row STAIRCASE mask the host writes at word 5 (on the constants'
|
|
* coordinate), applied to the last live tile even when the longest row fills it. */
|
|
enum { kAttnFlagRows = 2 };
|
|
enum { kAttnFlagsKnown = kAttnFlagChunk | kAttnFlagRows };
|
|
|
|
/** head_dim / 32. Four for MiniCPM5-1B. */
|
|
enum { kAttnDimTiles = 1 };
|
|
|
|
/** LIVE positions in the cache for this sequence, i.e. how many the query attends to.
|
|
* The live position-tile count and the tail width are DERIVED from it and never sent
|
|
* beside it -- two fields that must agree are two fields that can disagree. */
|
|
enum { kAttnKvPositions = 2 };
|
|
|
|
/** Position tiles per K/V block. Fixed by the RESIDENCY, because it fixes the circular
|
|
* buffers' page counts; the kernel refuses a descriptor whose value disagrees with the
|
|
* buffers it was staged with. */
|
|
enum { kAttnBlockTiles = 3 };
|
|
|
|
/** Position tiles the cache region actually holds. The padded read window may exceed the
|
|
* live length by up to 2*block_tiles - 1 tiles (§4), and reading past the region is the
|
|
* one thing that turns a masked-away tile into an out-of-bounds NoC read. */
|
|
enum { kAttnCapacityTiles = 4 };
|
|
|
|
/** Written 0 and unread, except in ROWS mode: the byte address of the staircase mask tile. */
|
|
enum { kAttnReserved = 5 };
|
|
|
|
/** Every operand's NoC coordinate is ONE word, `(y << 16) | x`. The vector and matmul
|
|
* families spend two words on a coordinate; this op has FIVE operands and sixteen words,
|
|
* so it packs. The pack is symmetric on both sides of the wire and pinned by the
|
|
* off-hardware suite. */
|
|
enum { kAttnQCoord = 6 };
|
|
/** Byte address of THIS GROUP's first Q tile: head `q_first`, dim tile 0, of the row block
|
|
* the decode token sits in. The host folds the head offset and any row-block offset in. */
|
|
enum { kAttnQBase = 7 };
|
|
|
|
enum { kAttnKCoord = 8 };
|
|
/** Byte address of THIS KV HEAD's K region at position 0 (ft_tt_kv_cache_run()). */
|
|
enum { kAttnKBase = 9 };
|
|
|
|
enum { kAttnVCoord = 10 };
|
|
enum { kAttnVBase = 11 };
|
|
|
|
enum { kAttnConstCoord = 12 };
|
|
/** The constants operand, ft_tt_op_attention_build_constants(): tile 0 the reduce scaler,
|
|
* tile 1 the 1/sqrt(head_dim) scale, tiles 2..33 the 32 tail masks. Built once per model,
|
|
* not per call. */
|
|
enum { kAttnConstBase = 13 };
|
|
|
|
enum { kAttnOutCoord = 14 };
|
|
/** Byte address of the output's first tile for this group -- the same layout as Q. */
|
|
enum { kAttnOutBase = 15 };
|
|
|
|
enum { kAttnWords = 16 };
|
|
|
|
/** Tile layout of the constants operand. Fixed for every descriptor: a layout that moved
|
|
* with the shape would be a wrong address for one call in four.
|
|
*
|
|
* THE CAUSAL BLOCK IS APPENDED, NOT INTERLEAVED, so tiles 0..33 are byte for byte what
|
|
* decode already reads and the decode arm's constants upload is unchanged. */
|
|
enum { kAttnConstScalerTile = 0 };
|
|
enum { kAttnConstScaleTile = 1 };
|
|
enum { kAttnConstMaskTile = 2 }; /* + tail, 32 of them */
|
|
/** Causal mask `kAttnConstCausalTile + (C - 1)`: element (row r, column c) is live iff
|
|
* `c <= r && c < C`, dead otherwise. Row r IS position `first + r`, column c IS position
|
|
* `first + c`, so `c <= r` is causality inside the chunk and `c < C` kills the columns of
|
|
* a ragged chunk that hold no position at all. 32 of them, C = 1..32. */
|
|
enum { kAttnConstCausalTile = 34 };
|
|
enum { kAttnConstTiles = kAttnConstCausalTile + 32 };
|
|
|
|
/** How the reader stacks the four constants it pushes, and how the compute thread indexes
|
|
* them. Only four of the 34 constant tiles are fetched per descriptor -- the two that
|
|
* every call needs, and the two masks §4 can select. */
|
|
enum { kAttnCbConstScaler = 0 };
|
|
enum { kAttnCbConstScale = 1 };
|
|
enum { kAttnCbConstTailMask = 2 };
|
|
enum { kAttnCbConstDeadMask = 3 };
|
|
enum { kAttnCbConstPages = 4 };
|
|
|
|
/** A tile is 32x32 of four 16x16 faces; a face row is 16 elements on one side of the face
|
|
* column split, and it is the granule every partial-row copy in this op is made of. */
|
|
enum { kAttnTileRows = 32 };
|
|
enum { kAttnFaceRows = 16 };
|
|
|
|
/** One decoded descriptor. Everything the three kernels need, derived ONCE at the top of
|
|
* the op so no loop re-derives a bound and the three TRISC threads cannot disagree. */
|
|
typedef struct fta_shape {
|
|
uint32_t q_rows;
|
|
uint32_t query_row;
|
|
uint32_t flags;
|
|
uint32_t positions; /**< ROWS mode: strip rows a descriptor serves (byte 3); else 1 */
|
|
uint32_t dim_tiles;
|
|
uint32_t kv_positions;
|
|
uint32_t block_tiles;
|
|
uint32_t capacity_tiles;
|
|
uint32_t live_tiles; /**< ceil(kv_positions / 32) */
|
|
uint32_t tail; /**< kv_positions % 32; 0 = the last live tile is full */
|
|
uint32_t padded_tiles; /**< live_tiles rounded up to a multiple of 2*block_tiles */
|
|
uint32_t blocks; /**< padded_tiles / block_tiles, always even */
|
|
} fta_shape;
|
|
|
|
/** Is this a CHUNK descriptor -- one Q head over C positions -- rather than a decode one? */
|
|
FTQ_INLINE bool fta_is_chunk(const fta_shape *shape)
|
|
{
|
|
return (shape->flags & (uint32_t)kAttnFlagChunk) != 0u;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t fta_byte(uint32_t word, uint32_t index)
|
|
{
|
|
return (word >> (index * 8u)) & 0xFFu;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t fta_coord_x(uint32_t coord)
|
|
{
|
|
return coord & 0xFFFFu;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t fta_coord_y(uint32_t coord)
|
|
{
|
|
return (coord >> 16) & 0xFFFFu;
|
|
}
|
|
|
|
/** Round `value` up to a multiple of `multiple`. `multiple` is never 0 here -- it is
|
|
* 2*block_tiles and block_tiles is refused at 0 by fta_shape_valid(). */
|
|
FTQ_INLINE uint32_t fta_round_up(uint32_t value, uint32_t multiple)
|
|
{
|
|
return ((value + multiple - 1u) / multiple) * multiple;
|
|
}
|
|
|
|
/** Decode one descriptor's parameter block. Pure arithmetic; validity is a separate
|
|
* question and fta_shape_valid() is the only thing that answers it. */
|
|
FTQ_INLINE fta_shape fta_decode(volatile FTQ_L1_PTR uint32_t *parameters)
|
|
{
|
|
const uint32_t heads = parameters[kAttnHeads];
|
|
fta_shape shape;
|
|
|
|
shape.q_rows = fta_byte(heads, 0);
|
|
shape.query_row = fta_byte(heads, 1);
|
|
shape.flags = fta_byte(heads, 2);
|
|
shape.positions = (shape.flags & (uint32_t)kAttnFlagRows) != 0u ? fta_byte(heads, 3) : 1u;
|
|
shape.dim_tiles = parameters[kAttnDimTiles];
|
|
shape.kv_positions = parameters[kAttnKvPositions];
|
|
shape.block_tiles = parameters[kAttnBlockTiles];
|
|
shape.capacity_tiles = parameters[kAttnCapacityTiles];
|
|
shape.live_tiles = (shape.kv_positions + kAttnTileRows - 1u) / kAttnTileRows;
|
|
shape.tail = shape.kv_positions % kAttnTileRows;
|
|
shape.padded_tiles =
|
|
(shape.block_tiles == 0u) ? 0u : fta_round_up(shape.live_tiles, 2u * shape.block_tiles);
|
|
shape.blocks = (shape.block_tiles == 0u) ? 0u : shape.padded_tiles / shape.block_tiles;
|
|
return shape;
|
|
}
|
|
|
|
/**
|
|
* Is this a descriptor the program can execute at all?
|
|
*
|
|
* Checked on the DEVICE and not only on the host, because none of these failures is a
|
|
* wrong answer that shows up as a wrong number:
|
|
* - a zero anywhere becomes a zero-length NoC read (§6);
|
|
* - a padded window past the region becomes a read outside the cache;
|
|
* - a query row or head count past a tile becomes a copy outside the circular buffer.
|
|
* A refusal names it in one word instead.
|
|
*/
|
|
/* HOW A DECODED SHAPE IS PASSED. A POINTER, and the wrapper that used to bind the same
|
|
* name to a C++ REFERENCE is gone with the last C++ compute kernel (plan Phase 6.6,
|
|
* 2026-08-13).
|
|
*
|
|
* All three forms were MEASURED against the pre-conversion images and the ranking was not
|
|
* obvious: pointer-plus-wrapper left 18 of the 21 compute images byte-identical, a body
|
|
* that copies `*ptr` into a local left 15, and passing the struct BY VALUE -- which needed
|
|
* no wrapper at all -- left only 6. Kept as the record of why the pointer form was chosen;
|
|
* with no C++ arm left there is nothing to bind twice. */
|
|
FTQ_INLINE bool fta_shape_is_valid(const fta_shape *shape)
|
|
{
|
|
if (shape->q_rows == 0u || shape->q_rows > kAttnTileRows)
|
|
return false;
|
|
if (shape->query_row >= kAttnTileRows)
|
|
return false;
|
|
if (shape->dim_tiles == 0u || shape->kv_positions == 0u || shape->block_tiles == 0u)
|
|
return false;
|
|
if ((shape->flags & ~(uint32_t)kAttnFlagsKnown) != 0u)
|
|
return false;
|
|
/* CHUNK geometry, checked on the DEVICE because getting it wrong is a wrong mask on a
|
|
* live tile -- a plausible wrong answer with nothing out of range. The chunk's C
|
|
* positions END the live range and START at a tile boundary, so
|
|
* `kv_positions - q_rows` is the chunk's first position and it must be 32-aligned. */
|
|
if ((shape->flags & (uint32_t)kAttnFlagChunk) != 0u) {
|
|
if (shape->query_row != 0u)
|
|
return false;
|
|
if (shape->kv_positions < shape->q_rows)
|
|
return false;
|
|
if (((shape->kv_positions - shape->q_rows) % kAttnTileRows) != 0u)
|
|
return false;
|
|
}
|
|
if ((shape->flags & (uint32_t)kAttnFlagRows) != 0u) {
|
|
if ((shape->flags & (uint32_t)kAttnFlagChunk) != 0u || shape->positions == 0u ||
|
|
shape->q_rows * shape->positions > kAttnTileRows ||
|
|
shape->query_row + shape->positions > kAttnTileRows ||
|
|
shape->kv_positions < shape->positions ||
|
|
(shape->kv_positions - shape->positions) / kAttnTileRows !=
|
|
(shape->kv_positions - 1u) / kAttnTileRows)
|
|
return false;
|
|
}
|
|
if (shape->live_tiles == 0u || shape->blocks == 0u)
|
|
return false;
|
|
/* The padded window is what the reader actually fetches -- it, not the live length, is
|
|
* what has to fit the cache region. */
|
|
if (shape->padded_tiles > shape->capacity_tiles)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/** Which mask tile position tile `global_tile` needs: the tail mask, the all-dead mask, or
|
|
* none at all. Returns kAttnCbConstPages for "no mask".
|
|
*
|
|
* IN CHUNK MODE THE LAST LIVE TILE IS ALWAYS MASKED, INCLUDING WHEN `tail` IS ZERO. That
|
|
* is the one place the two modes disagree and it is the whole of causality: a full 32-wide
|
|
* chunk has `tail == 0` and decode would call that tile entirely live, which is every
|
|
* position in the chunk attending to every other one. The reader loads the causal tile into
|
|
* the TAIL slot, so this function answers "which CB page", not "which DRAM tile", and the
|
|
* compute thread needs no chunk awareness at all. */
|
|
FTQ_INLINE uint32_t fta_mask_for_global_tile(const fta_shape *shape, uint32_t global_tile)
|
|
{
|
|
if (global_tile + 1u < shape->live_tiles)
|
|
return kAttnCbConstPages; /* entirely live */
|
|
if (global_tile + 1u == shape->live_tiles) {
|
|
if ((shape->flags & (uint32_t)(kAttnFlagChunk | kAttnFlagRows)) != 0u)
|
|
return kAttnCbConstTailMask; /* the causal / staircase tile, in the tail slot */
|
|
return (shape->tail == 0u) ? kAttnCbConstPages : kAttnCbConstTailMask;
|
|
}
|
|
return kAttnCbConstDeadMask; /* entirely past the end */
|
|
}
|
|
|
|
/** The DRAM constant tile the reader must land in the TAIL slot for this descriptor: the
|
|
* causal tile in chunk mode, the tail mask otherwise. One function so the reader and the
|
|
* off-hardware suite cannot spell the selection differently. */
|
|
FTQ_INLINE uint32_t fta_tail_slot_source_tile(const fta_shape *shape)
|
|
{
|
|
if ((shape->flags & (uint32_t)kAttnFlagChunk) != 0u)
|
|
return (uint32_t)kAttnConstCausalTile + shape->q_rows - 1u;
|
|
return (uint32_t)kAttnConstMaskTile + shape->tail;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Face arithmetic -- the same as resident_memory_protocol.h's, and asserted against
|
|
// ft_tt_tilize.c rather than trusted
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Bytes of one FACE ROW: 16 elements of one row of a tile, on one side of the face column
|
|
* split. 32 B for bf16. A multiple of the NoC's 16-byte DRAM write alignment, which is the
|
|
* whole reason a single row can be written at all (ft_tt_op_memory.h §1.4). */
|
|
FTQ_INLINE uint32_t fta_chunk_bytes(uint32_t tile_bytes)
|
|
{
|
|
return tile_bytes / 64u;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t fta_face_bytes(uint32_t tile_bytes)
|
|
{
|
|
return tile_bytes / 4u;
|
|
}
|
|
|
|
/** Byte offset, inside a tile, of the face row holding `row` and columns
|
|
* [16*half, 16*half+16). Equals ft_tt_tile_face_index(row, 16*half) * element_bytes. */
|
|
FTQ_INLINE uint32_t fta_chunk_offset(uint32_t row, uint32_t half, uint32_t tile_bytes)
|
|
{
|
|
return ((row / kAttnFaceRows) * 2u + half) * fta_face_bytes(tile_bytes) +
|
|
(row % kAttnFaceRows) * fta_chunk_bytes(tile_bytes);
|
|
}
|
|
|
|
/** §6, as the one function every issue site calls. A length of zero is a REFUSAL. */
|
|
FTQ_INLINE bool fta_length_is_issuable(uint32_t bytes)
|
|
{
|
|
return bytes != 0u;
|
|
}
|
|
|
|
/** A word copy inside L1. L1 is ordinary memory to the RISC-V and its data cache is
|
|
* disabled (ft_tt_queue.h, THE ORDERING ASSUMPTION), so this is a plain loop -- an order
|
|
* below the ~50 ns a NoC transaction costs to issue. Both ends are 4-byte aligned by
|
|
* construction: a chunk offset is a multiple of chunk_bytes, itself a multiple of 4. */
|
|
FTQ_INLINE void fta_copy_words(uint32_t* destination, const uint32_t* source, uint32_t words)
|
|
{
|
|
for (uint32_t index = 0; index < words; index++)
|
|
destination[index] = source[index];
|
|
}
|
|
|
|
/* The compute kernel's two spellings. They used to be bound TWICE -- a C++ reference
|
|
* overload beside these macros -- because `resident_attention_compute.cpp` was written
|
|
* against references and this header did not own it. Plan Phase 6.6 converted that kernel
|
|
* (2026-08-13) and the C++ half is gone; the body has always taken a POINTER, which is what
|
|
* C wants and what the measurement in the block above picked. */
|
|
#define fta_shape_valid(shape) fta_shape_is_valid(shape)
|
|
#define fta_mask_for_tile(shape, global_tile) fta_mask_for_global_tile(shape, global_tile)
|
|
|
|
#endif // FOUNDRY_RESIDENT_ATTENTION_PROTOCOL_H
|