503 lines
25 KiB
C
503 lines
25 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file cooperative_matmul_reader.c
|
|
* @brief The reader third of a matmul that runs on N cores at once, with the shared A block
|
|
* MULTICAST once instead of read from DRAM by every core.
|
|
*
|
|
* This is the first Foundry kernel that uses more than one Tensix on one op. Everything
|
|
* measured in this backend so far is one core of 140, and the blocker has been named since
|
|
* Phase 4: "every A tile re-read Nt times, every B tile Mt times" — no reuse across cores.
|
|
* The primitive that fixes it is kernels/ft_tensix_noc_multicast.h, measured 2026-08-13 to
|
|
* turn the A-side re-read from O(N) into O(1): the DRAM arm grows +1.93 us per extra core
|
|
* while multicast grows +0.15 us, crossing over at N=3 for a depth-48 A block and reaching
|
|
* 1.39x at N=4.
|
|
*
|
|
* **IT IS matmul_reader.c's LOOP NEST, UNCHANGED.** The blocked outer product, the
|
|
* one-transaction A block, the N-major B block and the depth-divides-Kt contract are exactly
|
|
* as that file has them and its header documents them. The only edits are that the A read
|
|
* may come from a peer instead of DRAM, that B is indexed with a COLUMN OFFSET so a core can
|
|
* own a slice, and that a status block is published at the end. Keeping the arithmetic
|
|
* identical is deliberate: the solo/cooperative A/B has to measure COOPERATION, not two
|
|
* readers.
|
|
*
|
|
* The whole wire contract — roles, argument indices, status words, refusal codes — and the
|
|
* reasoning for splitting over N rather than K or M are in cooperative_matmul_protocol.h,
|
|
* which the host includes verbatim.
|
|
*
|
|
* NOTHING ABOUT THE COMPUTE OR WRITER THIRDS CHANGES. The compute kernel waits on circular
|
|
* buffers regardless of who filled them, so it takes its own slice width as runtime arg 2
|
|
* and is otherwise the shipped matmul_compute.c; the writer streams `Mt * Nt_local` tiles
|
|
* to this core's own output panel and is the shipped vector_add_writer.c. A cooperating
|
|
* matmul is a reader-side and host-side change, and this file is the evidence for that.
|
|
*
|
|
* **The SOLO role is also how the two CONTROLS are expressed**, which is why it takes a
|
|
* column offset it does not otherwise need: N cores in SOLO role over disjoint column
|
|
* slices is "more cores, no sharing", and N cores in SOLO role over the WHOLE width is "N
|
|
* independent matmuls", which is the concurrent-stream and DRAM-saturation question. All
|
|
* four arms are one binary and one runtime word apart.
|
|
*
|
|
* C99 on Foundry's own ft_tensix_dataflow.h — no tt-metal include (plan Phase 6.6).
|
|
*
|
|
* Compile-time args (identical to matmul_reader.c, so the two are interchangeable):
|
|
* 0: cb_in0 index 1: cb_in1 index
|
|
* 2: in0 tile size in bytes 3: in1 tile size in bytes
|
|
* Runtime args: the 28 of cooperative_matmul_protocol.h.
|
|
*
|
|
* Build (BRISC, NoC 0):
|
|
* CTARGS=0,1,2048,2048 NOC=0 KERNEL_TEXT_BASE=0x15000 bash \
|
|
* foundry/tt/build_tensix_dataflow_kernel.sh \
|
|
* foundry/tt/kernels/cooperative_matmul_reader.c brisc /tmp/ttk_coop
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT IT MEASURED, AND THE CONDITION THE MULTICAST NEEDS
|
|
* ---------------------------------------------------------------------------
|
|
* MEASURED on the p150a card host 2026-08-13, MiniCPM5-1B shapes at 32 tokens, byte-identical to the
|
|
* single-core result at every N. **The multicast pays only when the SHARED operand's DRAM bank
|
|
* is the bottleneck, and with A and B in separate banks it never is.**
|
|
*
|
|
* The reader's traffic is Mt*Nt*Kt tiles of A and Mt*Nt*Kt of B — always EQUAL. Multicast
|
|
* divides the A side by N and leaves B alone, so with one bank per operand the binding
|
|
* constraint goes from max(X, X) to max(X/N, X): unchanged. Put both operands in ONE bank and
|
|
* it goes from 2X to X(1 + 1/N), and the multicast wins:
|
|
*
|
|
* ffn_gate/up, 4 cores A and B in TWO banks A and B in ONE bank
|
|
* no sharing (SPLIT) 0.386 ms 0.677 ms
|
|
* multicast (COOP) 0.559 ms (0.69x) 0.638 ms (1.06x, WINS)
|
|
*
|
|
* The crossover is at N=3 in the one-bank case — exactly where tests/test_multicast_bench.c
|
|
* put it for a 98304-byte block, reproduced here inside a real op. The rendezvous costs ~6 us
|
|
* a block at N=4, which is also that file's number.
|
|
*
|
|
* So this kernel is not the win on today's single-bank-per-operand layout; it is the
|
|
* PREREQUISITE for the layout that scales. Once the weight is striped across DRAM banks — the
|
|
* only way past the measured ~42 GB/s per-bank read wall — A becomes the contended operand and
|
|
* this is what removes it.
|
|
*
|
|
* @status MEASURED on the card 2026-08-13 by tests/test_cooperative_matmul.c: byte-identical
|
|
* to the single-core arm on ffn_gate/up, ffn_down and attn_k/v at N = 2, 3, 4 and 8.
|
|
* @issues A 2x4 group (8 receivers, 4 columns wide) left ONE of its cores unbootable with the
|
|
* recoverable `soft reset 0x00047000` signature — the same family as the 5-wide hazard in
|
|
* tests/test_multicast_bench.c, and a new data point that implicates RECEIVER COUNT rather
|
|
* than width. Groups of 4 and below ran clean throughout.
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "cooperative_matmul_protocol.h"
|
|
#include "ft_tensix_dataflow.h"
|
|
#include "ft_tensix_noc_multicast.h"
|
|
|
|
#define CB_IN0 ft_tensix_compile_time_arg(0)
|
|
#define CB_IN1 ft_tensix_compile_time_arg(1)
|
|
#define IN0_TILE_BYTES ft_tensix_compile_time_arg(2)
|
|
#define IN1_TILE_BYTES ft_tensix_compile_time_arg(3)
|
|
|
|
/** DRAM's read alignment, the stricter of the two ends of a DRAM->L1 transfer. A destination
|
|
* that is only 16- or 32-byte aligned reads back byte-exact until its LAST 64 bytes,
|
|
* silently (tests/ft_tt_phase2_l1_layout.h). */
|
|
#define FTCM_DRAM_READ_ALIGNMENT 64u
|
|
|
|
/** Everything the roles share, read once at entry so the block loop indexes a struct instead
|
|
* of re-reading L1 through the runtime-arg accessor on every use. */
|
|
typedef struct ftcm_context {
|
|
uint32_t role;
|
|
uint32_t tile_rows;
|
|
uint32_t tile_inner;
|
|
uint32_t tile_cols;
|
|
uint32_t a_x;
|
|
uint32_t a_y;
|
|
uint32_t a_base;
|
|
uint32_t b_x;
|
|
uint32_t b_y;
|
|
uint32_t b_base;
|
|
uint32_t b_column_first;
|
|
uint32_t b_full_cols;
|
|
uint32_t depth;
|
|
uint32_t b_n_major;
|
|
uint32_t cb_in0_base;
|
|
uint32_t rectangle_x_low;
|
|
uint32_t rectangle_y_low;
|
|
uint32_t rectangle_x_high;
|
|
uint32_t rectangle_y_high;
|
|
uint32_t destination_count;
|
|
uint32_t leader_x;
|
|
uint32_t leader_y;
|
|
uint32_t ready_semaphore_address;
|
|
uint32_t arrival_semaphore_address;
|
|
uint32_t valid_word_address;
|
|
volatile uint32_t *status;
|
|
volatile const uint32_t *stop_flag;
|
|
uint32_t spin_limit;
|
|
|
|
/* Run state. `error` keeps the FIRST failure; `aborting` is what stops every issue while
|
|
* the loop runs on to drain the circular buffers. */
|
|
int error;
|
|
int last_issue_code;
|
|
bool aborting;
|
|
uint32_t observed_write_pointer;
|
|
} ftcm_context;
|
|
|
|
static void ftcm_read_arguments(ftcm_context *context)
|
|
{
|
|
context->role = ft_tensix_runtime_arg((int)FTCM_ARG_ROLE);
|
|
context->tile_rows = ft_tensix_runtime_arg((int)FTCM_ARG_TILE_ROWS);
|
|
context->tile_inner = ft_tensix_runtime_arg((int)FTCM_ARG_TILE_INNER);
|
|
context->tile_cols = ft_tensix_runtime_arg((int)FTCM_ARG_TILE_COLS);
|
|
context->a_x = ft_tensix_runtime_arg((int)FTCM_ARG_A_X);
|
|
context->a_y = ft_tensix_runtime_arg((int)FTCM_ARG_A_Y);
|
|
context->a_base = ft_tensix_runtime_arg((int)FTCM_ARG_A_BASE);
|
|
context->b_x = ft_tensix_runtime_arg((int)FTCM_ARG_B_X);
|
|
context->b_y = ft_tensix_runtime_arg((int)FTCM_ARG_B_Y);
|
|
context->b_base = ft_tensix_runtime_arg((int)FTCM_ARG_B_BASE);
|
|
context->b_column_first = ft_tensix_runtime_arg((int)FTCM_ARG_B_COLUMN_FIRST);
|
|
context->b_full_cols = ft_tensix_runtime_arg((int)FTCM_ARG_B_FULL_COLS);
|
|
context->depth = ft_tensix_runtime_arg((int)FTCM_ARG_DEPTH);
|
|
context->b_n_major = ft_tensix_runtime_arg((int)FTCM_ARG_B_N_MAJOR);
|
|
context->cb_in0_base = ft_tensix_runtime_arg((int)FTCM_ARG_CB_IN0_BASE);
|
|
context->rectangle_x_low = ft_tensix_runtime_arg((int)FTCM_ARG_RECTANGLE_X_LOW);
|
|
context->rectangle_y_low = ft_tensix_runtime_arg((int)FTCM_ARG_RECTANGLE_Y_LOW);
|
|
context->rectangle_x_high = ft_tensix_runtime_arg((int)FTCM_ARG_RECTANGLE_X_HIGH);
|
|
context->rectangle_y_high = ft_tensix_runtime_arg((int)FTCM_ARG_RECTANGLE_Y_HIGH);
|
|
context->destination_count = ft_tensix_runtime_arg((int)FTCM_ARG_DESTINATION_COUNT);
|
|
context->leader_x = ft_tensix_runtime_arg((int)FTCM_ARG_LEADER_X);
|
|
context->leader_y = ft_tensix_runtime_arg((int)FTCM_ARG_LEADER_Y);
|
|
context->ready_semaphore_address = ft_tensix_runtime_arg((int)FTCM_ARG_READY_SEMAPHORE);
|
|
context->arrival_semaphore_address = ft_tensix_runtime_arg((int)FTCM_ARG_ARRIVAL_SEMAPHORE);
|
|
context->valid_word_address = ft_tensix_runtime_arg((int)FTCM_ARG_VALID_WORD);
|
|
context->status =
|
|
(volatile uint32_t *)(unsigned long)ft_tensix_runtime_arg((int)FTCM_ARG_STATUS);
|
|
context->stop_flag =
|
|
(volatile const uint32_t *)(unsigned long)ft_tensix_runtime_arg((int)FTCM_ARG_STOP_FLAG);
|
|
context->spin_limit = ft_tensix_runtime_arg((int)FTCM_ARG_SPIN_LIMIT);
|
|
|
|
context->error = FTCM_OK;
|
|
context->last_issue_code = FTCM_OK;
|
|
context->aborting = false;
|
|
context->observed_write_pointer = 0u;
|
|
}
|
|
|
|
/** Record the FIRST failure and switch the loop into its drain. It never returns early — see
|
|
* the protocol header's drain rule and DEV_NOTES.md §1 (core 62, where skipping the loop
|
|
* body during an abort stalled the matrix engine in a way no flag could reach). */
|
|
static void ftcm_abort(ftcm_context *context, int error)
|
|
{
|
|
if (context->error == FTCM_OK)
|
|
context->error = error;
|
|
context->aborting = true;
|
|
}
|
|
|
|
static void ftcm_publish(const ftcm_context *context, uint32_t blocks_completed,
|
|
uint32_t blocks_expected, uint32_t clock_at_entry,
|
|
uint32_t clock_after_first_block, uint32_t clock_at_exit)
|
|
{
|
|
/* Magic LAST: a host reading a torn block must see "never ran", not a half-filled result
|
|
* (kernels/resident_queue_protocol.h rule 1). */
|
|
context->status[FTCM_STATUS_ROLE] = context->role;
|
|
context->status[FTCM_STATUS_BLOCKS_COMPLETED] = blocks_completed;
|
|
context->status[FTCM_STATUS_BLOCKS_EXPECTED] = blocks_expected;
|
|
context->status[FTCM_STATUS_ERROR] = (uint32_t)context->error;
|
|
context->status[FTCM_STATUS_LAST_ISSUE_CODE] = (uint32_t)context->last_issue_code;
|
|
context->status[FTCM_STATUS_CLOCK_AT_ENTRY] = clock_at_entry;
|
|
context->status[FTCM_STATUS_CLOCK_AFTER_FIRST_BLOCK] = clock_after_first_block;
|
|
context->status[FTCM_STATUS_CLOCK_AT_EXIT] = clock_at_exit;
|
|
context->status[FTCM_STATUS_OBSERVED_WRITE_POINTER] = context->observed_write_pointer;
|
|
context->status[FTCM_STATUS_MAGIC_WORD] = FTCM_STATUS_MAGIC;
|
|
}
|
|
|
|
/**
|
|
* Refuse a shape this kernel cannot execute, BEFORE anything is issued.
|
|
*
|
|
* The depth rule is matmul_reader.c's contract restated as a REFUSAL rather than a silent
|
|
* collapse to depth 1. On the cooperative path a silent collapse would be worse than a wrong
|
|
* answer: it changes this core's block COUNT, and the rendezvous is only correct because every
|
|
* member walks the same number of blocks. The host makes depth a divisor; a value that is not
|
|
* one is a host bug and is refused.
|
|
*/
|
|
static bool ftcm_shape_is_executable(ftcm_context *context)
|
|
{
|
|
if (context->tile_rows == 0u || context->tile_inner == 0u || context->tile_cols == 0u ||
|
|
context->depth == 0u || IN0_TILE_BYTES == 0u || IN1_TILE_BYTES == 0u) {
|
|
ftcm_abort(context, FTCM_ERROR_BAD_PARAMETERS);
|
|
return false;
|
|
}
|
|
if ((context->tile_inner % context->depth) != 0u) {
|
|
ftcm_abort(context, FTCM_ERROR_BAD_PARAMETERS);
|
|
return false;
|
|
}
|
|
if (context->b_full_cols == 0u ||
|
|
context->b_column_first + context->tile_cols > context->b_full_cols) {
|
|
ftcm_abort(context, FTCM_ERROR_BAD_PARAMETERS);
|
|
return false;
|
|
}
|
|
if (context->role != FTCM_ROLE_SOLO && context->role != FTCM_ROLE_LEADER &&
|
|
context->role != FTCM_ROLE_RECEIVER) {
|
|
ftcm_abort(context, FTCM_ERROR_BAD_ROLE);
|
|
return false;
|
|
}
|
|
/* A block of `depth` A tiles is ONE DRAM read, so its length and its L1 destination must
|
|
* satisfy DRAM's 64 bytes and not L1's 16. Checked HERE, in the kernel, because
|
|
* ft_tt_noc_check_transfer() runs host-side and cannot see a kernel-issued transfer —
|
|
* which is exactly how `noc_inline_dw_write` to DRAM cost cores 50 and 52 with a suite
|
|
* reporting 0 failures (DEV_NOTES.md §1). */
|
|
if (((context->depth * IN0_TILE_BYTES) % FTCM_DRAM_READ_ALIGNMENT) != 0u ||
|
|
(context->cb_in0_base % FTCM_DRAM_READ_ALIGNMENT) != 0u) {
|
|
ftcm_abort(context, FTCM_ERROR_ALIGNMENT);
|
|
return false;
|
|
}
|
|
if (context->role != FTCM_ROLE_SOLO && context->destination_count == 0u) {
|
|
/* The zero-length NoC transfer in multicast clothing. Refuse it; never "optimise" it
|
|
* into a solo run, which would turn a mis-planned group into a wrong answer that looks
|
|
* like a right one. */
|
|
ftcm_abort(context, FTCM_ERROR_ZERO_DESTINATIONS);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Where block `block_index`'s A tiles MUST land, from the CB wrap rule alone.
|
|
*
|
|
* A 2*depth-page buffer pushed `depth` at a time puts the write pointer at the base or one
|
|
* block up and nowhere else — the pointer resets by subtraction on an EXACT hit of the limit
|
|
* and no other wrap is legal (ft_tensix_circular_buffer.h). So the address is a function of
|
|
* the block index, identical on every member, which is what makes ONE multicast destination
|
|
* correct for all of them. This is checked every block rather than assumed: a multicast writes
|
|
* whatever address it is given into every node of the rectangle, and no hardware would refuse
|
|
* the wrong one.
|
|
*/
|
|
static uint32_t ftcm_expected_write_pointer(const ftcm_context *context, uint32_t block_index)
|
|
{
|
|
return context->cb_in0_base + (block_index & 1u) * context->depth * IN0_TILE_BYTES;
|
|
}
|
|
|
|
/** The shared A block: `depth` consecutive tiles of row `mt`, which are consecutive bytes in
|
|
* DRAM, so ONE transaction (matmul_reader.c's measured 27-37%). */
|
|
static void ftcm_read_a_block(const ftcm_context *context, uint32_t mt, uint32_t kt,
|
|
uint32_t a_write)
|
|
{
|
|
const uint32_t a_offset = (mt * context->tile_inner + kt) * IN0_TILE_BYTES;
|
|
|
|
ft_tensix_noc_read(ft_tensix_noc_address(context->a_x, context->a_y,
|
|
context->a_base + a_offset),
|
|
a_write, context->depth * IN0_TILE_BYTES);
|
|
}
|
|
|
|
/** This core's own B block, which no other core reads. One transaction when the weight is
|
|
* stored N-major, `depth` of them when it is K-major — matmul_reader.c's two cases, with the
|
|
* column index shifted into the FULL grid so a core can own a slice of it. */
|
|
static void ftcm_read_b_block(const ftcm_context *context, uint32_t nt, uint32_t kt,
|
|
uint32_t b_write)
|
|
{
|
|
const uint32_t column = context->b_column_first + nt;
|
|
|
|
if (context->b_n_major) {
|
|
const uint32_t b_offset = (column * context->tile_inner + kt) * IN1_TILE_BYTES;
|
|
|
|
ft_tensix_noc_read(ft_tensix_noc_address(context->b_x, context->b_y,
|
|
context->b_base + b_offset),
|
|
b_write, context->depth * IN1_TILE_BYTES);
|
|
} else {
|
|
for (uint32_t step = 0u; step < context->depth; step++) {
|
|
const uint32_t b_tile = (kt + step) * context->b_full_cols + column;
|
|
|
|
ft_tensix_noc_read(ft_tensix_noc_address(context->b_x, context->b_y,
|
|
context->b_base +
|
|
b_tile * IN1_TILE_BYTES),
|
|
b_write + step * IN1_TILE_BYTES, IN1_TILE_BYTES);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** SOLO: matmul_reader.c's block body, with B's column offset. No semaphores, no rectangle,
|
|
* no multicast — this is the arm every cooperative number is measured against. */
|
|
static void ftcm_solo_block(ftcm_context *context, uint32_t mt, uint32_t nt, uint32_t kt,
|
|
uint32_t a_write, uint32_t b_write)
|
|
{
|
|
ftcm_read_a_block(context, mt, kt, a_write);
|
|
ftcm_read_b_block(context, nt, kt, b_write);
|
|
/* ONE barrier for the whole block: it is per-core, not per-transaction, which is exactly
|
|
* why depth converts (matmul_reader.c). */
|
|
ft_tensix_noc_read_barrier();
|
|
}
|
|
|
|
/**
|
|
* LEADER: collect every receiver, fetch A and its own B, then hand A to the group.
|
|
*
|
|
* The order of the last four steps is the protocol and none of it is rearrangeable —
|
|
* kernels/ft_tensix_noc_multicast.h's header block has the full account, including why the
|
|
* flush between the two multicasts is mandatory on Blackhole and nowhere else.
|
|
*
|
|
* The payload's SOURCE is this core's own CB_IN0 block, so there is no staging buffer and none
|
|
* is needed. Reusing that block next round is safe for exactly one reason: the writes-flushed
|
|
* barrier below guarantees the payload's request has LEFT, which is what frees the source. A
|
|
* full write barrier would additionally wait for acknowledgements this core has no use for
|
|
* per block, and is taken once after the loop instead.
|
|
*/
|
|
static void ftcm_leader_block(ftcm_context *context, uint32_t mt, uint32_t nt, uint32_t kt,
|
|
uint32_t a_write, uint32_t b_write)
|
|
{
|
|
volatile uint32_t *ready_semaphore =
|
|
(volatile uint32_t *)(unsigned long)context->ready_semaphore_address;
|
|
uint64_t payload_destination;
|
|
uint64_t arrival_destination;
|
|
int code;
|
|
|
|
code = ft_tensix_wait_semaphore_at_least(ready_semaphore, context->destination_count,
|
|
context->spin_limit, context->stop_flag);
|
|
if (code != FTCM_OK) {
|
|
context->last_issue_code = code;
|
|
ftcm_abort(context, code);
|
|
return;
|
|
}
|
|
/* Safe here and only here: a receiver cannot increment again until it has been told this
|
|
* round landed, which is the last thing this function does. */
|
|
ft_tensix_set_local_semaphore(ready_semaphore, 0u);
|
|
|
|
ftcm_read_a_block(context, mt, kt, a_write);
|
|
ftcm_read_b_block(context, nt, kt, b_write);
|
|
ft_tensix_noc_read_barrier();
|
|
|
|
/* The destination L1 address is this core's own write pointer, which the caller has
|
|
* already proven equals every member's. */
|
|
code = ft_tensix_multicast_address(context->rectangle_x_low, context->rectangle_y_low,
|
|
context->rectangle_x_high, context->rectangle_y_high,
|
|
a_write, &payload_destination);
|
|
if (code == FTCM_OK)
|
|
code = ft_tensix_noc_write_multicast(a_write, payload_destination,
|
|
context->depth * IN0_TILE_BYTES,
|
|
context->destination_count);
|
|
context->last_issue_code = code;
|
|
if (code != FTCM_OK) {
|
|
ftcm_abort(context, code);
|
|
return;
|
|
}
|
|
|
|
/* BLACKHOLE ONLY, and load-bearing: the payload multicast and the semaphore multicast go
|
|
* into different command-buffer FIFOs and are not guaranteed to leave in issue order, so
|
|
* without this a receiver can be told the block arrived before it did and read the
|
|
* PREVIOUS block's bytes with nothing reporting an error. */
|
|
ft_tensix_noc_writes_flushed_barrier();
|
|
|
|
code = ft_tensix_multicast_address(context->rectangle_x_low, context->rectangle_y_low,
|
|
context->rectangle_x_high, context->rectangle_y_high,
|
|
context->arrival_semaphore_address, &arrival_destination);
|
|
if (code == FTCM_OK)
|
|
code = ft_tensix_noc_semaphore_set_multicast(context->valid_word_address,
|
|
arrival_destination,
|
|
context->destination_count);
|
|
context->last_issue_code = code;
|
|
if (code != FTCM_OK)
|
|
ftcm_abort(context, code);
|
|
}
|
|
|
|
/**
|
|
* RECEIVER: clear the arrival flag, start its own B read, announce readiness, wait for A.
|
|
*
|
|
* The local clear comes FIRST. The leader cannot set the flag until it has seen this core's
|
|
* atomic, and this core issues that atomic after the clear — so a VALID can never be erased by
|
|
* the clear that was meant to precede it.
|
|
*
|
|
* The B read is issued BEFORE the announcement so it overlaps the whole rendezvous: this
|
|
* core's own DRAM traffic and the leader's multicast are then in flight together, which is the
|
|
* only reason a receiver's block costs anything less than the sum of the two.
|
|
*
|
|
* The read barrier is taken whether or not the wait succeeded, because the B read was already
|
|
* issued and a transaction left outstanding is how L1 the next kernel is about to use gets
|
|
* written underneath it.
|
|
*/
|
|
static void ftcm_receiver_block(ftcm_context *context, uint32_t nt, uint32_t kt,
|
|
uint32_t b_write)
|
|
{
|
|
volatile uint32_t *arrival_semaphore =
|
|
(volatile uint32_t *)(unsigned long)context->arrival_semaphore_address;
|
|
int code;
|
|
|
|
ft_tensix_set_local_semaphore(arrival_semaphore, FT_TENSIX_SEMAPHORE_INVALID);
|
|
ftcm_read_b_block(context, nt, kt, b_write);
|
|
ft_tensix_noc_semaphore_increment(
|
|
ft_tensix_noc_address(context->leader_x, context->leader_y,
|
|
context->ready_semaphore_address),
|
|
1u);
|
|
|
|
code = ft_tensix_wait_semaphore_equals(arrival_semaphore, FT_TENSIX_SEMAPHORE_VALID,
|
|
context->spin_limit, context->stop_flag);
|
|
ft_tensix_noc_read_barrier();
|
|
if (code != FTCM_OK) {
|
|
context->last_issue_code = code;
|
|
ftcm_abort(context, code);
|
|
}
|
|
}
|
|
|
|
void kernel_main(void)
|
|
{
|
|
ftcm_context context;
|
|
uint32_t block_index = 0u;
|
|
uint32_t blocks_completed = 0u;
|
|
uint32_t blocks_expected = 0u;
|
|
uint32_t clock_at_entry;
|
|
uint32_t clock_after_first_block = 0u;
|
|
uint32_t clock_at_exit;
|
|
|
|
ftcm_read_arguments(&context);
|
|
clock_at_entry = ft_bh_wall_clock_low();
|
|
|
|
if (ftcm_shape_is_executable(&context))
|
|
blocks_expected = context.tile_rows * context.tile_cols *
|
|
(context.tile_inner / context.depth);
|
|
|
|
for (uint32_t mt = 0u; mt < context.tile_rows && blocks_expected != 0u; mt++) {
|
|
for (uint32_t nt = 0u; nt < context.tile_cols; nt++) {
|
|
for (uint32_t kt = 0u; kt < context.tile_inner; kt += context.depth) {
|
|
uint32_t a_write;
|
|
uint32_t b_write;
|
|
|
|
ft_tensix_cb_reserve_back((int32_t)CB_IN0, (int32_t)context.depth);
|
|
ft_tensix_cb_reserve_back((int32_t)CB_IN1, (int32_t)context.depth);
|
|
a_write = ft_tensix_cb_write_pointer(CB_IN0);
|
|
b_write = ft_tensix_cb_write_pointer(CB_IN1);
|
|
|
|
if (!context.aborting &&
|
|
a_write != ftcm_expected_write_pointer(&context, block_index)) {
|
|
context.observed_write_pointer = a_write;
|
|
ftcm_abort(&context, FTCM_ERROR_WRITE_POINTER);
|
|
}
|
|
|
|
/* Rule 3: while draining, issue NOTHING. The pushes below still happen so the
|
|
* page counters stay balanced and the compute and writer thirds run to
|
|
* completion — a reader that returned here would leave them in cb_wait_front,
|
|
* which is the shape that wedges a core (DEV_NOTES.md §1). */
|
|
if (!context.aborting) {
|
|
if (context.role == FTCM_ROLE_SOLO)
|
|
ftcm_solo_block(&context, mt, nt, kt, a_write, b_write);
|
|
else if (context.role == FTCM_ROLE_LEADER)
|
|
ftcm_leader_block(&context, mt, nt, kt, a_write, b_write);
|
|
else
|
|
ftcm_receiver_block(&context, nt, kt, b_write);
|
|
if (!context.aborting)
|
|
blocks_completed++;
|
|
}
|
|
|
|
ft_tensix_cb_push_back(CB_IN0, (int32_t)context.depth);
|
|
ft_tensix_cb_push_back(CB_IN1, (int32_t)context.depth);
|
|
block_index++;
|
|
if (block_index == 1u)
|
|
clock_after_first_block = ft_bh_wall_clock_low();
|
|
}
|
|
}
|
|
}
|
|
|
|
clock_at_exit = ft_bh_wall_clock_low();
|
|
|
|
/* Outside the timed window and unconditional. An abandoned block may have left a read, a
|
|
* write or an atomic in flight, and brisck.cc asserts the NoC counters are settled after
|
|
* kernel_main() returns. */
|
|
ft_tensix_noc_read_barrier();
|
|
ft_tensix_noc_write_barrier();
|
|
ft_tensix_noc_atomic_barrier();
|
|
|
|
ftcm_publish(&context, blocks_completed, blocks_expected, clock_at_entry,
|
|
clock_after_first_block, clock_at_exit);
|
|
}
|