Foundry/tt/kernels/resident_place_writer.c

270 lines
12 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/**
* @file resident_place_writer.c
* @brief Writer half of the RESIDENT MEMORY triad — EMBEDDING and KV_APPEND — and the processor
* that PUBLISHES COMPLETION, per core and per group.
*
* IT IS NOT resident_vector_writer.c, AND THE DIFFERENCE IS THE WHOLE OP. That writer
* drains `tile_count` whole tiles to consecutive destinations. This one writes a PARTIAL
* destination: an append into a live KV cache must land `row_count` rows of each tile and
* leave the other rows exactly as they were, because those rows are earlier positions of
* the same sequence. So it emits FACE-ROW RUNS (resident_memory_protocol.h,
* ftm_destination_runs) rather than tiles.
*
* THE RUNS COLLAPSE, WHICH IS WHY ONE CODE PATH SERVES DECODE AND PREFILL:
* rows [r, r+n) of one face column-half are CONTIGUOUS, so a range is at most four
* runs per tile;
* rows [0, 32) is the whole tile, and a whole GROUP of tiles is contiguous, so a
* 32-row append is ONE transaction of `group_tiles * TILE_BYTES`.
* A one-token decode append is therefore `group_tiles * 2` writes of 32 bytes, and a
* 32-token prefill append is one write of 8 KiB, out of the same loop.
*
* 32 BYTES IS A LEGAL DRAM WRITE and that is the load-bearing fact. The NoC's write
* alignment is 16 B; only the read direction needs 64 (ft_tt_noc.c,
* ft_tt_noc_alignment()). tests/test_phase6_memory.c measures it rather than trusting it,
* and checks the NEIGHBOURS as well as the row — a partial write that quietly rounded up
* to 64 bytes would pass a check that only looked at what it meant to write.
*
* WHY THE WRITER OWNS COMPLETION (resident_queue_protocol.h rule 1). An op is done when
* the result has left L1. This processor owns `completed_count` and `completed_token`,
* the reader owns `tail`, and nothing else writes either. It FETCHES on `head` so it
* drains while the reader is still filling, and PUBLISHES only after `tail` has passed
* its cursor — fetching on `tail` is a pipeline deadlock, not a precaution
* (resident_matmul_writer.c records the run that cost).
*
*
* 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_OUT_EMBEDDING index 1: tile size in bytes
* 2: wait mode -- 0 upstream, 1 stoppable (ships), 2 bound-only
* 3: CB_OUT_KV index -- KV_APPEND's own output CB, because A CB CARRIES ONE GROUP SIZE
* (ft_tt_op_memory.h has the arithmetic and the incident). It must name the same two
* buffers, in the same roles, as the reader's args 0 and 5: the two processors are
* the producer and the consumer of ONE buffer and nothing checks that they agree.
* Runtime args: the ten of resident_queue_protocol.h, identical on all five processors
*
* Build (NCRISC, NoC 1):
* CTARGS=16,2048,1,17 NOC=1 KERNEL_TEXT_BASE=0x18000 bash \
* foundry/tt/build_tensix_dataflow_kernel.sh \
* foundry/tt/kernels/resident_place_writer.c ncrisc /tmp/ttk_memory
*
* @status GREEN ON SILICON 2026-08-12 — Tensix 30 and Tensix 32, 17 checks and 0 failures
* each. The 32-byte DRAM write is MEASURED, neighbours included. 2656 B of text
* and 24 B of bss on NCRISC, against a 12 KiB slab. RE-GREEN on Tensix 40,
* 2026-08-13, with the output CB split per group size (ft_tt_op_memory.h @status
* has the fix's two proofs).
* @issues None
* @todo None
*/
#include <stdbool.h>
#include <stdint.h>
#include "resident_memory_protocol.h"
#include "ft_tensix_dataflow.h"
#include "resident_queue_protocol.h"
#define CB_OUT_EMBEDDING ft_tensix_compile_time_arg(0)
#define TILE_BYTES ft_tensix_compile_time_arg(1)
#define WAIT_MODE ft_tensix_compile_time_arg(2)
#define CB_OUT_KV ft_tensix_compile_time_arg(3)
/** The output CB an opcode's group size belongs to — the consumer half of the reader's
* output_circular_buffer(). ONE GROUP SIZE PER CB (ft_tt_op_memory.h). */
static uint32_t output_circular_buffer(uint32_t opcode)
{
return (opcode == kOpKvAppend) ? CB_OUT_KV : CB_OUT_EMBEDDING;
}
/** Put one group's assembled tiles on the destination.
*
* ONE barrier for the whole group, never one per run: a barrier drains this core's
* outstanding transactions and is per core, not per transaction. */
/** @return false if any length came out zero — the caller refuses the descriptor rather
* than issuing it (resident_memory_protocol.h, ftm_length_is_issuable). */
static bool write_one_group(uint32_t block, uint32_t destination_x, uint32_t destination_y,
uint32_t destination_base, uint32_t group_tiles, uint32_t row0,
uint32_t row_count, bool whole_tiles)
{
ftm_run runs[kMemMaxRuns];
uint32_t run_count;
if (!ftm_length_is_issuable(group_tiles * TILE_BYTES))
return false;
if (whole_tiles || (row0 == 0u && row_count == kMemTileRows)) {
// The descriptor OWNS the destination tile row — either it said so, or it covers
// every row of it. Both ends are contiguous, so it is ONE transaction. The rows it
// did not place are the zeros the reader put in the circular-buffer page.
ft_tensix_noc_write(block, ft_tensix_noc_address(destination_x, destination_y,
destination_base),
group_tiles * TILE_BYTES);
ft_tensix_noc_write_barrier();
return true;
}
run_count = ftm_destination_runs(row0, row_count, TILE_BYTES, runs);
if (run_count == 0u)
return false;
for (uint32_t index = 0; index < run_count; index++) {
if (!ftm_length_is_issuable(runs[index].bytes))
return false;
}
for (uint32_t tile = 0; tile < group_tiles; tile++) {
const uint32_t tile_offset = tile * TILE_BYTES;
for (uint32_t index = 0; index < run_count; index++) {
const uint32_t offset = tile_offset + runs[index].offset;
ft_tensix_noc_write(block + offset,
ft_tensix_noc_address(destination_x, destination_y,
destination_base + offset),
runs[index].bytes);
}
}
ft_tensix_noc_write_barrier();
return true;
}
/**
* Drain one descriptor: `group_count` groups of `group_tiles` tiles each.
*
* @return true if the whole descriptor was written; false if a wait was abandoned
*/
static bool write_one_memory_op(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters,
uint32_t cb_out)
{
const uint32_t flags = parameters[kMemFlags];
const uint32_t row_count = parameters[kMemRowCount];
const uint32_t group_count = parameters[kMemGroupCount];
const uint32_t group_tiles = parameters[kMemGroupTiles];
const uint32_t destination_x = parameters[kMemDstX];
const uint32_t destination_y = parameters[kMemDstY];
const uint32_t destination_base = parameters[kMemDstBase];
const uint32_t destination_group_stride = parameters[kMemDstGroupStride];
const uint32_t destination_row0 = parameters[kMemDstRow0];
const uint32_t row_in_tile = destination_row0 % kMemTileRows;
const uint32_t tile_row = destination_row0 / kMemTileRows;
uint32_t group_bytes;
uint32_t tile_row_offset;
if (row_count == 0u || row_count > kMemMaxRows || group_count == 0u || group_tiles == 0u ||
row_in_tile + row_count > kMemTileRows) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
/* THE DESTINATION WAS UNBOUNDED. `destination_row0` is an absolute KV position, so
* `tile_row * group_tiles * TILE_BYTES` is a raw byte offset with nothing above it — a
* position past the cache capacity wrote into whatever DRAM follows, at full length,
* with nothing faulting. The bound is kMemDstGroupStride, the bytes each group OWNS;
* see ftm_destination_span_fits(). */
if (!ft_tensix_product_fits(group_tiles, TILE_BYTES, &group_bytes) ||
!ft_tensix_product_fits(tile_row, group_bytes, &tile_row_offset)) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
/* And the CB CYCLE this file did not have: the output CB is POPPED `group_tiles` at a
* time and the read pointer wraps by the same exact-hit rule as the write pointer, so a
* descriptor that does not close the cycle leaves every later one off the grid. Kept
* unweakened after the one-CB-per-group-size fix, for the reason the reader states. */
if (!ft_tensix_cb_pop_grid_is_safe(cb_out, group_tiles)) {
ftq_abort(ctx, kErrBadParameters);
return false;
}
for (uint32_t group = 0; group < group_count; group++) {
const bool have_tiles =
ftq_cb_wait_front_stoppable(ctx, (int32_t)cb_out, (int32_t)group_tiles, WAIT_MODE);
if (have_tiles) {
uint32_t group_offset;
/* Inside the GROUP, not merely inside the region: the tighter statement, and the
* one that also stops head h's append landing in head h+1. */
const bool bounded =
ft_tensix_product_fits(group, destination_group_stride, &group_offset) &&
group_offset <= 0xFFFFFFFFu - tile_row_offset &&
ftm_destination_span_fits(destination_group_stride, tile_row_offset,
group_bytes);
const uint32_t base = destination_base + group_offset + tile_row_offset;
if (!bounded ||
!write_one_group(ft_tensix_cb_read_pointer(cb_out), destination_x, destination_y,
base, group_tiles, row_in_tile, row_count,
(flags & kMemFlagDstWholeTiles) != 0u)) {
ftq_abort(ctx, kErrBadParameters);
ft_tensix_cb_pop_front(cb_out, group_tiles);
return false;
}
}
ft_tensix_cb_pop_front(cb_out, group_tiles);
}
return !ctx->aborting;
}
void kernel_main(void)
{
ftq_context ctx;
uint32_t completed;
bool running = true;
ftq_context_init(&ctx);
ftq_status_set(&ctx, kStateBoot);
if (!ftq_check_control(&ctx)) {
ftq_status_set(&ctx, kStateExit);
return;
}
completed = ctx.control[kFieldCompletedCount];
ctx.cursor = completed;
while (running) {
// FETCH on `head`, so the writer drains the output CB while the reader is still
// filling it. Publishing waits on `tail`, below.
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];
const uint32_t token = descriptor[kDescToken];
const uint32_t group_sequence = descriptor[kDescA5];
bool done = true;
ftq_status_opcode(&ctx, opcode);
if (opcode == kOpEmbedding || opcode == kOpKvAppend) {
ftq_status_set(&ctx, kStateCbWaitFront);
done = write_one_memory_op(&ctx, ftq_parameters(descriptor),
output_circular_buffer(opcode));
ftq_status_set(&ctx, kStateExec);
} else if (opcode == kOpStop) {
running = false;
}
if (!done) {
running = false;
continue;
}
// THE PRECONDITION, and the only place it belongs: the reader must have finished
// this descriptor before its completion is published.
if (!ftq_wait_until_reaches(&ctx, kFieldTail, ctx.cursor + 1u)) {
running = false;
continue;
}
ctx.cursor++;
// Token before count: a host that sees the count is entitled to trust the token it
// reads next. Both are plain stores from ONE in-order RISC-V.
ctx.control[kFieldCompletedToken] = token;
ctx.control[kFieldCompletedCount] = ++completed;
ftq_group_post(&ctx, group_sequence, token);
}
// The writer does NOT touch kFieldError or kFieldState: the reader owns those, and one
// writer per word is the whole ordering argument.
ftq_status_set(&ctx, kStateExit);
}