2156 lines
102 KiB
C
2156 lines
102 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* test_cooperative_matmul.c — one matmul across N Tensix cores, with the shared A block
|
|
* multicast once instead of read from DRAM by every core.
|
|
*
|
|
* Every performance number in this backend is one Tensix of 140. This program is the first
|
|
* that is not, and it is built to answer four questions in order:
|
|
*
|
|
* 1. Is a cooperative result the SAME result? Byte for byte against the single-core arm,
|
|
* not by PCC — PCC is blind to a consistent permutation and a split is exactly a
|
|
* permutation of where output tiles come from (DEV_NOTES.md §3).
|
|
* 2. What does splitting over N cores buy, WITHOUT the multicast? (the SPLIT arm)
|
|
* 3. What does the MULTICAST add on top of that? (the COOPERATIVE arm)
|
|
* 4. Where does it stop scaling, and what stops it?
|
|
*
|
|
* ===========================================================================
|
|
* THE FOUR ARMS, AND WHY ALL FOUR ARE NEEDED
|
|
* ===========================================================================
|
|
* All four are ONE binary (kernels/cooperative_matmul_reader.c) one runtime word apart, so no
|
|
* difference between them can be a codegen difference in disguise — the same rule
|
|
* `weight_n_major` follows in matmul_reader.c (docs/tt_kernel_model.md §14).
|
|
*
|
|
* SOLO 1 core, the whole Nt. The baseline every other number is a ratio of.
|
|
* SPLIT(N) N cores, Nt/N columns each, every core reads A from DRAM itself. This is
|
|
* what "more cores" is worth on its own. WITHOUT it, a cooperative speedup
|
|
* cannot be attributed to the multicast at all.
|
|
* COOP(N) N cores, Nt/N columns each, A multicast once per block by the leader.
|
|
* COOP/SPLIT is the multicast's contribution and nothing else.
|
|
* INDEPENDENT(N) N cores each doing the WHOLE matmul into its own output. Not a split:
|
|
* it is N concurrent streams, and it is the DRAM/interconnect capacity
|
|
* question rather than this op's — how many cores it takes to saturate the
|
|
* memory system, which for a memory-bound decode matters more than how fast
|
|
* one matmul goes.
|
|
*
|
|
* ===========================================================================
|
|
* WHAT IS TIMED, AND WHAT IS NOT
|
|
* ===========================================================================
|
|
* The reported op time is the window from "send GO to every member" to "every member reports
|
|
* DONE", best of `repeats`. CB configuration and the runtime-arg blocks are staged ONCE per
|
|
* arm, outside that window, because they are per-shape and not per-token: a serving path
|
|
* dispatches an already-staged op, and the leanest go/done shape is what DEV_NOTES.md §6
|
|
* records as 10.6-12.3 us. Staging is reported separately rather than hidden — at large N it
|
|
* is the fixed cost that ends the scaling, and saying so is half the answer.
|
|
*
|
|
* Each reader ALSO records its own wall clock, so a device-side number exists that no host
|
|
* scheduling can move. The card host is not a quiet host — a `pantheon serve` on the K80s takes
|
|
* ~58% of two cores — and short best-of-N host measurements survive that while long ones do
|
|
* not (DEV_NOTES.md §1). Both columns are printed; when they disagree, the device one is the
|
|
* op and the difference is dispatch.
|
|
*
|
|
* ===========================================================================
|
|
* CORES, AND WHY THE GROUP CANNOT SIMPLY GROW
|
|
* ===========================================================================
|
|
* A multicast rectangle may not straddle NoC x = 8 and 9, which carry the ARC/L2CPU spine and
|
|
* the east DRAM column, so the Tensix columns are 1..7 and 10..16 and a rectangle is at most
|
|
* SEVEN wide. It is held to FOUR here because a 5-wide rectangle was measured on 2026-08-13 to
|
|
* leave a minority of its receivers unbootable (1 of 5 on a 1x5, 3 of 10 on a 2x5) with width
|
|
* and receiver count confounded and unresolved. `ft_tt_group_rectangle_coordinates()` refuses
|
|
* the gap-crossing case outright; the 4-column limit is this program's, and deliberate.
|
|
*
|
|
* Usage:
|
|
* test_cooperative_matmul <program-directory> [repeats]
|
|
* Environment:
|
|
* FT_TT_COOP_FIRST_INDEX live Tensix index of the group's first core (default 100)
|
|
* FT_TT_COOP_ROWS pool rows (default 2)
|
|
* FT_TT_COOP_COLUMNS pool columns (default 4; DO NOT raise past 4, see above)
|
|
* FT_TT_COOP_DRAM_A/B/C DRAM logical banks for A, B and the output (default 5, 6, 7)
|
|
* FT_TT_FIRMWARE_DIR where the five firmware ELFs live (the same directory)
|
|
*
|
|
* Build:
|
|
* make -f Makefile.cooperative PHASE1B=1 all
|
|
*/
|
|
|
|
/* clock_gettime(CLOCK_MONOTONIC) is POSIX.1-2008, not C99. */
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "../convert/ft_float_convert.h"
|
|
#include "../ft_tt_dram_shard.h"
|
|
#include "../ft_tt_dram_stripe.h"
|
|
#include "../ft_tt_group_shared_operand.h"
|
|
#include "../ft_tt_noc.h"
|
|
#include "../ft_tt_op_dispatch.h"
|
|
#include "../ft_tt_op_l1_layout.h"
|
|
#include "../ft_tt_op_matmul_cooperative.h"
|
|
#include "../ft_tt_tilize.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_program_directory.h"
|
|
#include "ft_tt_test_report.h"
|
|
|
|
#define TILE_DIM FT_TT_OP_TILE_DIM
|
|
#define TILE_ELEMENTS (TILE_DIM * TILE_DIM)
|
|
#define PCC_ACCEPT 0.99
|
|
#define POISON_BYTE 0xA5
|
|
#define RUN_TIMEOUT_MS 30000u
|
|
|
|
/** Default DRAM logical banks. 5..7 rather than test_ops.c's 2..4 and the multicast bench's 3,
|
|
* so three suites can be live on one card without one overwriting another's operands. */
|
|
/** The DRAM banks A, B and the output live in. Separate banks by default: a single bank is
|
|
* the first thing that saturates, and putting two operands in one would make every scaling
|
|
* number a bank measurement without saying so. FT_TT_COOP_DRAM_B + FT_TT_COOP_DRAM_B_BASE
|
|
* together let a run deliberately collapse them into one, which is the experiment that
|
|
* separates a per-bank wall from a global one. */
|
|
#define DEFAULT_DRAM_BANK_A 5u
|
|
#define DEFAULT_DRAM_BANK_B 6u
|
|
#define DEFAULT_DRAM_BANK_OUTPUT 7u
|
|
#define DRAM_BUFFER_BASE 0x00100000u
|
|
|
|
/** Where the BANK-STRIPED copies go. Well clear of DRAM_BUFFER_BASE and of the weight base,
|
|
* because bank striping puts a member's panel in whichever bank its index names - including
|
|
* the banks the un-striped arms use for the activation, the weight and the OUTPUT - and two
|
|
* arms of one measurement may not overwrite each other's operands. 0x10000000 and 0x20000000
|
|
* are 268 and 537 MiB into a 4,278 MiB bank. */
|
|
#define SHARD_WEIGHT_BASE 0x10000000u
|
|
#define SHARD_ACTIVATION_BASE 0x20000000u
|
|
|
|
/**
|
|
* The widest COOPERATIVE group this suite will run on silicon.
|
|
*
|
|
* MEASURED, twice, and it costs a core each time: a 2x4 group — seven receivers — left core
|
|
* 115 unbootable on 2026-08-13 and again on 2026-08-14, both times at the re-boot AFTER a
|
|
* clean, byte-exact run, both times the same slot, with the `soft reset 0x00047000` signature.
|
|
* Groups of four or fewer ran clean through every stage on both days. Recovering it is a
|
|
* WHOLE-CARD reset, so the sweep stops rather than pays that again; SPLIT and INDEPENDENT
|
|
* issue no multicast and are swept to the full pool.
|
|
*/
|
|
#define COOP_MAX_MEMBERS_ON_SILICON 4u
|
|
|
|
#define DEFAULT_FIRST_INDEX 100u
|
|
#define DEFAULT_POOL_ROWS 2u
|
|
#define DEFAULT_POOL_COLUMNS 4u
|
|
#define DEFAULT_REPEATS 5u
|
|
|
|
/** Bounded, per resident-kernel rule 4: a wait that expires must report, never hang. At
|
|
* 800 MHz a spin is a few cycles, so 200 million is well under a second and far longer than
|
|
* any legitimate wait here. */
|
|
#define SPIN_LIMIT 200000000u
|
|
|
|
/** The prefetch depth every shape runs at, shrunk to a divisor of Kt by the plan. 48 is what
|
|
* the single-core numbers on record were measured at, and it makes the shared A block 98304
|
|
* bytes — the size at which multicast was measured to cross over at N=3. */
|
|
#define PREFETCH_TILES 48u
|
|
|
|
/** One Tensix's derived peak at HiFi4 and this card's measured 800 MHz busy clock:
|
|
* 2048 FLOP/cycle x 800e6. DERIVED, not measured — every "% of one Tensix" here inherits
|
|
* that. */
|
|
#define ONE_TENSIX_GFLOPS 1638.4
|
|
|
|
/** The composed one-core forward pass this project quotes: 3.745 ms/layer x 24 layers +
|
|
* 22.0 ms lm_head = 111.9 ms = 8.94 tok/s. No end-to-end pass has ever been run; it is a
|
|
* composition of measured per-op costs, and every tok/s projection below inherits that. */
|
|
#define FORWARD_PASS_MS 111.9
|
|
#define LM_HEAD_MS 22.0
|
|
#define MODEL_LAYERS 24u
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
typedef struct coop_fixture {
|
|
ft_tt_hardware_session *session;
|
|
ft_tt_noc_path *dram_path;
|
|
const ft_tt_program_directory *program;
|
|
const ft_tt_firmware_set *firmware;
|
|
ft_tt_noc_coord dram_a;
|
|
ft_tt_noc_coord dram_b;
|
|
ft_tt_noc_coord dram_output;
|
|
/** Every NoC door of the activation's bank and of the weight's bank, from live telemetry.
|
|
* The STRIPED arms rotate members over these; the FLAT arms use dram_a/dram_b, which are
|
|
* port 0 of each — `ft_tt_coord_dram_bank()` IS port 0. */
|
|
ft_tt_dram_port_set dram_a_ports;
|
|
ft_tt_dram_port_set dram_b_ports;
|
|
/**
|
|
* Concurrent streams aimed at the busiest and the quietest DRAM bank by the LAST bank
|
|
* shard plan staged, from ft_tt_dram_shard.h.
|
|
*
|
|
* Carried here so the sweep can print it, because it is the BALANCE rule made visible and
|
|
* it is what the bank-striped arm's wall clock actually tracks: with 8 banks and two
|
|
* operands per member the busiest bank holds ceil(2N/8) streams, and the arm's wall clock
|
|
* steps at exactly the N where that number does. A plan whose two figures differ by more
|
|
* than one is unbalanced, and an unbalanced map measured 30% worse than either
|
|
* neighbouring balanced count (plan Phase 6.7).
|
|
*/
|
|
uint32_t shard_busiest_bank;
|
|
uint32_t shard_quietest_bank;
|
|
/** The POOL: pool_rows x pool_columns cores in row-major order, booted once. */
|
|
ft_tt_noc_coord members[FT_TT_OP_COOP_MAX_MEMBERS];
|
|
uint32_t member_count;
|
|
uint32_t first_index;
|
|
uint32_t pool_rows;
|
|
uint32_t pool_columns;
|
|
uint32_t repeats;
|
|
uint32_t weight_base;
|
|
/** Spread stage (FT_TT_COOP_SPREAD_ROUNDS). 0 leaves the suite exactly as it was. */
|
|
uint32_t spread_rounds;
|
|
uint32_t spread_inner;
|
|
bool sample_device_cycles;
|
|
ft_tt_launch_config config[FT_TT_OP_COOP_MAX_MEMBERS];
|
|
/**
|
|
* The cores ONE run uses, and which pool slot each of them is.
|
|
*
|
|
* A run is not simply "the first N of the pool". The pool is stored row-major, so the
|
|
* first 6 cores of a 2x4 pool are four of row 0 and two of row 1 — which is NOT a
|
|
* rectangle, and a multicast to its bounding box would write into the two cores the wrap
|
|
* picked up. `ft_tt_multicast_rectangle_from_cores()` refuses it, which is how this bug
|
|
* was found rather than broadcast; select_rectangle() is the fix.
|
|
*/
|
|
ft_tt_noc_coord active[FT_TT_OP_COOP_MAX_MEMBERS];
|
|
uint32_t active_slot[FT_TT_OP_COOP_MAX_MEMBERS];
|
|
} coop_fixture;
|
|
|
|
/** The top-left rows x columns sub-block of the pool, which IS a rectangle because the pool
|
|
* is one. */
|
|
static void select_rectangle(coop_fixture *fixture, uint32_t rows, uint32_t columns)
|
|
{
|
|
uint32_t next = 0u;
|
|
uint32_t row;
|
|
uint32_t column;
|
|
|
|
for (row = 0u; row < rows; row++) {
|
|
for (column = 0u; column < columns; column++) {
|
|
uint32_t slot = row * fixture->pool_columns + column;
|
|
|
|
fixture->active_slot[next] = slot;
|
|
fixture->active[next] = fixture->members[slot];
|
|
next++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The first `count` cores of the pool, in pool order. Only for arms that never multicast —
|
|
* they need no rectangle, and demanding one would refuse group sizes that are perfectly
|
|
* legal for independent work. */
|
|
static void select_linear(coop_fixture *fixture, uint32_t count)
|
|
{
|
|
uint32_t index;
|
|
|
|
for (index = 0u; index < count; index++) {
|
|
fixture->active_slot[index] = index;
|
|
fixture->active[index] = fixture->members[index];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* How many repeats of one arm the per-repeat sample arrays can hold.
|
|
*
|
|
* Best/median/worst is a summary, and a summary cannot tell a BIMODAL result (two behaviours,
|
|
* one of them slow) from a broad unimodal one (contention). Only the distribution can, so the
|
|
* spread stage keeps every sample rather than three order statistics of them.
|
|
*/
|
|
#define MAX_SPREAD_SAMPLES 64u
|
|
|
|
/** What one arm of one run produced. */
|
|
typedef struct arm_result {
|
|
int rc;
|
|
double best_ms; /**< best-of-repeats GO -> all DONE, host clock */
|
|
double worst_ms; /**< the slowest repeat of the same run — the spread, reported */
|
|
/** Every repeat's own host wall clock and, when the fixture asks for it, the same
|
|
* repeat's DEVICE cycles. Two clocks on ONE run is what separates a host-dispatch
|
|
* spread from an on-card one: a wall clock that moves while the device cycles do not
|
|
* is the 2-core host, and no histogram of a single clock can say that. */
|
|
double sample_ms[MAX_SPREAD_SAMPLES];
|
|
uint32_t sample_cycles[MAX_SPREAD_SAMPLES];
|
|
uint32_t sample_count;
|
|
double staging_ms; /**< CB config + runtime args for every member, once */
|
|
/** DRAM doors this arm's readers used, and how many pairs of them share a NoC row. 0 and 0
|
|
* on an un-striped arm, which is how the report distinguishes the two. */
|
|
uint32_t distinct_endpoints;
|
|
uint32_t row_collisions;
|
|
uint32_t device_cycles; /**< the slowest member's reader, entry to exit */
|
|
uint32_t first_error; /**< first non-zero kernel error word, in member order */
|
|
uint32_t blocks_short; /**< members that did not complete every block */
|
|
/** Each member's own error word. A group red proof has to make exactly ONE member wrong
|
|
* and show the others stay green, and a single first-error cannot express that — a leader
|
|
* that timed out waiting for a refusing receiver reports first and hides it. */
|
|
int32_t member_error[FT_TT_OP_COOP_MAX_MEMBERS];
|
|
bool bytes_match_solo;
|
|
/** Byte offset of the first difference from the solo result, or output_bytes if none.
|
|
* Which MEMBER'S PANEL that offset falls in is what says whether one member was wrong or
|
|
* all of them were. */
|
|
size_t first_difference;
|
|
} arm_result;
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Small helpers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static uint32_t environment_u32(const char *name, uint32_t fallback)
|
|
{
|
|
const char *text = getenv(name);
|
|
char *end = NULL;
|
|
unsigned long value;
|
|
|
|
if (text == NULL || *text == '\0')
|
|
return fallback;
|
|
value = strtoul(text, &end, 0);
|
|
if (end == NULL || *end != '\0')
|
|
return fallback;
|
|
return (uint32_t)value;
|
|
}
|
|
|
|
static double monotonic_seconds(void)
|
|
{
|
|
struct timespec now;
|
|
|
|
(void)clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
|
|
}
|
|
|
|
static int compare_double(const void *left, const void *right)
|
|
{
|
|
double a = *(const double *)left;
|
|
double b = *(const double *)right;
|
|
|
|
return (a > b) - (a < b);
|
|
}
|
|
|
|
/** One-minute load average. `pantheon` serves on the K80s of this same 2-core box and takes
|
|
* ~58% of it, so a spread stage that does not record the host is measuring two things. */
|
|
static double read_host_load(void)
|
|
{
|
|
FILE *file = fopen("/proc/loadavg", "r");
|
|
double value = -1.0;
|
|
|
|
if (file == NULL)
|
|
return -1.0;
|
|
if (fscanf(file, "%lf", &value) != 1)
|
|
value = -1.0;
|
|
fclose(file);
|
|
return value;
|
|
}
|
|
|
|
static uint64_t random_state = 1u;
|
|
|
|
static void random_seed(uint64_t seed)
|
|
{
|
|
random_state = seed ? seed : 1u;
|
|
}
|
|
|
|
static float random_value(void)
|
|
{
|
|
random_state ^= random_state >> 12;
|
|
random_state ^= random_state << 25;
|
|
random_state ^= random_state >> 27;
|
|
return (float)((double)(random_state * 0x2545F4914F6CDD1Dull >> 11) / (double)(1ull << 53) *
|
|
2.0 -
|
|
1.0);
|
|
}
|
|
|
|
static void round_to_bf16(float *matrix, size_t count)
|
|
{
|
|
size_t index;
|
|
|
|
for (index = 0; index < count; index++)
|
|
matrix[index] = ft_float_from_bf16(ft_bf16_from_float(matrix[index]));
|
|
}
|
|
|
|
/** Row-major fp32 -> face-ordered bf16 tiles. */
|
|
static void to_bf16_tiles(const float *matrix, uint32_t rows, uint32_t cols, uint16_t *out)
|
|
{
|
|
uint16_t *strip = malloc((size_t)cols * TILE_DIM * sizeof(uint16_t));
|
|
uint32_t tiles_across = ft_tt_tile_count_for(cols);
|
|
uint32_t block;
|
|
|
|
if (!strip)
|
|
return;
|
|
for (block = 0; block < rows / TILE_DIM; block++) {
|
|
uint32_t row;
|
|
uint32_t column;
|
|
|
|
for (row = 0; row < TILE_DIM; row++)
|
|
for (column = 0; column < cols; column++)
|
|
strip[row * cols + column] =
|
|
ft_bf16_from_float(matrix[(block * TILE_DIM + row) * cols + column]);
|
|
ft_tt_tilize_strip(strip, sizeof(uint16_t), TILE_DIM, cols, cols * sizeof(uint16_t),
|
|
out + (size_t)block * tiles_across * TILE_ELEMENTS);
|
|
}
|
|
free(strip);
|
|
}
|
|
|
|
static void from_bf16_tiles(const uint16_t *tiles, uint32_t rows, uint32_t cols, float *out)
|
|
{
|
|
uint16_t *strip = malloc((size_t)cols * TILE_DIM * sizeof(uint16_t));
|
|
uint32_t tiles_across = ft_tt_tile_count_for(cols);
|
|
uint32_t block;
|
|
|
|
if (!strip)
|
|
return;
|
|
for (block = 0; block < rows / TILE_DIM; block++) {
|
|
uint32_t row;
|
|
uint32_t column;
|
|
|
|
ft_tt_untilize_strip(tiles + (size_t)block * tiles_across * TILE_ELEMENTS,
|
|
sizeof(uint16_t), TILE_DIM, cols, cols * sizeof(uint16_t), strip);
|
|
for (row = 0; row < TILE_DIM; row++)
|
|
for (column = 0; column < cols; column++)
|
|
out[(block * TILE_DIM + row) * cols + column] =
|
|
ft_float_from_bf16(strip[row * cols + column]);
|
|
}
|
|
free(strip);
|
|
}
|
|
|
|
static void reference_matmul(const float *a, const float *b, uint32_t m, uint32_t k, uint32_t n,
|
|
float *out)
|
|
{
|
|
double *accumulator = malloc((size_t)n * sizeof(double));
|
|
uint32_t row;
|
|
|
|
if (!accumulator)
|
|
return;
|
|
for (row = 0; row < m; row++) {
|
|
uint32_t column;
|
|
uint32_t inner;
|
|
|
|
for (column = 0; column < n; column++)
|
|
accumulator[column] = 0.0;
|
|
for (inner = 0; inner < k; inner++) {
|
|
double scale = (double)a[(size_t)row * k + inner];
|
|
const float *b_row = b + (size_t)inner * n;
|
|
|
|
for (column = 0; column < n; column++)
|
|
accumulator[column] += scale * (double)b_row[column];
|
|
}
|
|
for (column = 0; column < n; column++)
|
|
out[(size_t)row * n + column] = (float)accumulator[column];
|
|
}
|
|
free(accumulator);
|
|
}
|
|
|
|
static double pearson(const float *a, const float *b, size_t count)
|
|
{
|
|
double mean_a = 0.0;
|
|
double mean_b = 0.0;
|
|
double cov = 0.0;
|
|
double var_a = 0.0;
|
|
double var_b = 0.0;
|
|
size_t index;
|
|
|
|
if (count == 0u)
|
|
return 0.0;
|
|
for (index = 0; index < count; index++) {
|
|
mean_a += a[index];
|
|
mean_b += b[index];
|
|
}
|
|
mean_a /= (double)count;
|
|
mean_b /= (double)count;
|
|
for (index = 0; index < count; index++) {
|
|
double da = (double)a[index] - mean_a;
|
|
double db = (double)b[index] - mean_b;
|
|
|
|
cov += da * db;
|
|
var_a += da * da;
|
|
var_b += db * db;
|
|
}
|
|
if (var_a <= 0.0 || var_b <= 0.0)
|
|
return 0.0;
|
|
return cov / sqrt(var_a * var_b);
|
|
}
|
|
|
|
/** GGUF-order (K-major) tile grid -> N-major, i.e. tile index nt*Kt + kt. Every arm here runs
|
|
* N-major, because that is what makes a prefetch block's B tiles one transaction AND what
|
|
* makes a member's column slice a CONTIGUOUS run of tiles. */
|
|
static void permute_tiles_to_n_major(const void *source, void *destination, uint32_t tile_rows,
|
|
uint32_t tile_cols, size_t tile_bytes)
|
|
{
|
|
const unsigned char *from = source;
|
|
unsigned char *to = destination;
|
|
uint32_t kt;
|
|
|
|
for (kt = 0; kt < tile_rows; kt++) {
|
|
uint32_t nt;
|
|
|
|
for (nt = 0; nt < tile_cols; nt++)
|
|
memcpy(to + ((size_t)nt * tile_rows + kt) * tile_bytes,
|
|
from + ((size_t)kt * tile_cols + nt) * tile_bytes, tile_bytes);
|
|
}
|
|
}
|
|
|
|
static size_t poison_remaining(const unsigned char *bytes, size_t count)
|
|
{
|
|
size_t left = 0u;
|
|
size_t index;
|
|
|
|
for (index = 0; index < count; index++)
|
|
if (bytes[index] == POISON_BYTE)
|
|
left++;
|
|
return left;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Boot, stage, launch */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Bring one core from unknown to "firmware running, program loaded".
|
|
*
|
|
* SEQUENTIAL by construction — one process, one core at a time. At 12-way and above, firmware
|
|
* boots from INDEPENDENT host processes start returning `device I/O failed` and leave cores
|
|
* mid-handshake; it wedged cores 110 and 111 during the DRAM fan-out (DEV_NOTES.md §1). This
|
|
* program is one process and never overlaps a boot.
|
|
*/
|
|
static int boot_core(coop_fixture *fixture, uint32_t slot)
|
|
{
|
|
const ft_tt_noc_access *noc = &fixture->session->noc;
|
|
const uint16_t x = (uint16_t)fixture->members[slot].x;
|
|
const uint16_t y = (uint16_t)fixture->members[slot].y;
|
|
int rc;
|
|
|
|
rc = ft_tt_core_park(noc, x, y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_core_boot_firmware(noc, x, y, fixture->firmware, NULL);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_op_program_load(noc, x, y, &fixture->program->program, NULL,
|
|
&fixture->config[slot]);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
fixture->config[slot].local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
/* The reader is on BRISC and was built with NOC_INDEX=0; the firmware re-reads this from
|
|
* the launch message at run time, so the two must agree. */
|
|
fixture->config[slot].brisc_noc_id = 0u;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** Poison the status block and clear the stop flag, so "never ran" is its own bit pattern
|
|
* rather than the same zero as "finished" (DEV_NOTES.md §3). */
|
|
static int poison_control_block(const coop_fixture *fixture, uint32_t slot)
|
|
{
|
|
const ft_tt_noc_access *noc = &fixture->session->noc;
|
|
uint32_t poison[FTCM_STATUS_WORDS];
|
|
uint32_t zero = 0u;
|
|
unsigned index;
|
|
int rc;
|
|
|
|
for (index = 0u; index < FTCM_STATUS_WORDS; index++)
|
|
poison[index] = FTCM_STATUS_POISON;
|
|
rc = noc->write(noc->user, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, ft_tt_op_coop_status_address(), poison,
|
|
sizeof(poison));
|
|
if (rc == FT_TT_OK)
|
|
rc = noc->write(noc->user, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, ft_tt_op_coop_stop_flag_address(),
|
|
&zero, sizeof(zero));
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Everything before the kick, for every member: the CB descriptor array, the reader's args,
|
|
* the compute third's three and the writer's four.
|
|
*
|
|
* `arg_override` lets a red proof plant a deliberately wrong word on ONE member without a
|
|
* second staging path — a mutation that had to be built specially would be testing the
|
|
* mutation harness rather than the op.
|
|
*/
|
|
static int stage_members(const coop_fixture *fixture,
|
|
const ft_tt_op_matmul_cooperative_plan *plan,
|
|
const ft_tt_op_matmul_cooperative_request *request,
|
|
uint32_t override_member, uint32_t override_index,
|
|
uint32_t override_value)
|
|
{
|
|
const ft_tt_noc_access *noc = &fixture->session->noc;
|
|
uint32_t slot;
|
|
int rc = FT_TT_OK;
|
|
|
|
for (slot = 0u; slot < plan->member_count && rc == FT_TT_OK; slot++) {
|
|
uint32_t reader_args[FTCM_ARG_COUNT];
|
|
uint32_t compute_args[3];
|
|
uint32_t writer_args[4];
|
|
ft_tt_op_args args[5];
|
|
ft_tt_launch_config config = fixture->config[fixture->active_slot[slot]];
|
|
|
|
rc = ft_tt_op_matmul_cooperative_reader_arguments(plan, request, fixture->active, slot,
|
|
reader_args, FTCM_ARG_COUNT);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_matmul_cooperative_compute_arguments(plan, request, slot, compute_args,
|
|
3u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_matmul_cooperative_writer_arguments(plan, request, slot, writer_args,
|
|
4u);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
if (slot == override_member && override_index < FTCM_ARG_COUNT)
|
|
reader_args[override_index] = override_value;
|
|
|
|
config.local_cb_mask = plan->cb_mask;
|
|
args[0].processor = FT_TT_PROC_DM0;
|
|
args[0].values = reader_args;
|
|
args[0].count = FTCM_ARG_COUNT;
|
|
args[1].processor = FT_TT_PROC_DM1;
|
|
args[1].values = writer_args;
|
|
args[1].count = 4u;
|
|
args[2].processor = FT_TT_PROC_MATH0;
|
|
args[2].values = compute_args;
|
|
args[2].count = 3u;
|
|
args[3] = args[2];
|
|
args[3].processor = FT_TT_PROC_MATH1;
|
|
args[4] = args[2];
|
|
args[4].processor = FT_TT_PROC_MATH2;
|
|
|
|
rc = poison_control_block(fixture, slot);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_stage(noc, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, &config, plan->cb_buffer,
|
|
sizeof(plan->cb_buffer), args, 5u);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* The timed window: GO to every member, then wait for every member.
|
|
*
|
|
* All the GOs go out before any DONE is polled, so the members overlap. The serialisation
|
|
* that remains — one host write per member over an x1 link — is real and is part of the
|
|
* number; it is also exactly the cost the resident work queue removes (1.36 us an op against
|
|
* this path's 10.6-12.3), which is why it is reported rather than subtracted.
|
|
*/
|
|
static int go_and_wait(const coop_fixture *fixture, uint32_t member_count, double *out_ms)
|
|
{
|
|
const ft_tt_noc_access *noc = &fixture->session->noc;
|
|
double started;
|
|
uint32_t slot;
|
|
int rc = FT_TT_OK;
|
|
|
|
started = monotonic_seconds();
|
|
for (slot = 0u; slot < member_count && rc == FT_TT_OK; slot++)
|
|
rc = ft_tt_launch_send_go(noc, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y);
|
|
for (slot = 0u; slot < member_count && rc == FT_TT_OK; slot++)
|
|
rc = ft_tt_launch_wait_done(noc, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, RUN_TIMEOUT_MS, NULL);
|
|
*out_ms = (monotonic_seconds() - started) * 1e3;
|
|
|
|
/* BRISC asks TRISC0 to re-zero all 64 circular-buffer counter registers AFTER writing
|
|
* RUN_MSG_DONE, so a host that relaunches without waiting for that can have its next CB
|
|
* configuration zeroed underneath it (ft_tt_hardware_session.h). Outside the timed
|
|
* window, and on every member. */
|
|
for (slot = 0u; slot < member_count && rc == FT_TT_OK; slot++)
|
|
rc = ft_tt_core_wait_sync_registers_ready(noc, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, 2000u);
|
|
return rc;
|
|
}
|
|
|
|
/** Read every member's status block and fold it into the result. */
|
|
static int collect_status(const coop_fixture *fixture,
|
|
const ft_tt_op_matmul_cooperative_plan *plan, arm_result *out)
|
|
{
|
|
const ft_tt_noc_access *noc = &fixture->session->noc;
|
|
uint32_t slot;
|
|
|
|
for (slot = 0u; slot < plan->member_count; slot++) {
|
|
uint32_t status[FTCM_STATUS_WORDS];
|
|
int rc = noc->read(noc->user, (uint16_t)fixture->active[slot].x,
|
|
(uint16_t)fixture->active[slot].y, ft_tt_op_coop_status_address(),
|
|
status, sizeof(status));
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
if (status[FTCM_STATUS_MAGIC_WORD] != FTCM_STATUS_MAGIC) {
|
|
/* Still the host poison: the kernel never ran, or never reached its publish. */
|
|
if (out->first_error == 0u)
|
|
out->first_error = status[FTCM_STATUS_MAGIC_WORD];
|
|
out->member_error[slot] = (int32_t)status[FTCM_STATUS_MAGIC_WORD];
|
|
out->blocks_short++;
|
|
continue;
|
|
}
|
|
out->member_error[slot] = (int32_t)status[FTCM_STATUS_ERROR];
|
|
if (status[FTCM_STATUS_ERROR] != 0u && out->first_error == 0u)
|
|
out->first_error = status[FTCM_STATUS_ERROR];
|
|
if (status[FTCM_STATUS_BLOCKS_COMPLETED] != status[FTCM_STATUS_BLOCKS_EXPECTED])
|
|
out->blocks_short++;
|
|
{
|
|
uint32_t cycles =
|
|
status[FTCM_STATUS_CLOCK_AT_EXIT] - status[FTCM_STATUS_CLOCK_AT_ENTRY];
|
|
|
|
if (cycles > out->device_cycles)
|
|
out->device_cycles = cycles;
|
|
}
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* One arm */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** The shape and buffers one sweep entry shares across all its arms. */
|
|
typedef struct shape_run {
|
|
const char *label;
|
|
uint32_t m;
|
|
uint32_t k;
|
|
uint32_t n;
|
|
uint32_t tile_rows;
|
|
uint32_t tile_inner;
|
|
uint32_t tile_cols;
|
|
size_t output_bytes; /**< bytes of ONE full result */
|
|
uint16_t *solo_tiles; /**< the single-core result: the byte-comparison reference */
|
|
uint16_t *readback; /**< room for FT_TT_OP_COOP_MAX_MEMBERS results, for INDEPENDENT */
|
|
float *reference; /**< the CPU fp32 answer, used only to score the SOLO arm */
|
|
/** The staged operands, KEPT rather than freed after the first upload: a bank-striped arm
|
|
* re-lays them out for every group size, because which bytes go in which bank is a
|
|
* function of N. Re-tilizing them each time would put host arithmetic in the loop. */
|
|
uint16_t *a_tiles;
|
|
uint16_t *b_n_major;
|
|
} shape_run;
|
|
|
|
/** Which addressing an arm uses. The three are the same bytes reached three ways, not three
|
|
* algorithms — every one of them is byte-compared against the same single-core result. */
|
|
typedef enum stripe_mode {
|
|
/** One door per operand, both operands in one bank each. The recorded baseline, and what
|
|
* `ft_tt_coord_dram_bank()` gives every caller in the backend today. */
|
|
STRIPE_NONE = 0,
|
|
/** PART A. The same two banks, but all three doors of each, members rotated over them.
|
|
* Moves no data — a port is a door into one memory. Ceiling: one bank, ~64 GB/s. */
|
|
STRIPE_PORTS,
|
|
/** PART B. A bank per member: its weight PANEL relocated into its own bank and a copy of
|
|
* the activation placed in another, each on its own door. Moves data. Ceiling: N banks. */
|
|
STRIPE_BANKS
|
|
} stripe_mode;
|
|
|
|
static const char *const g_stripe_suffix[] = {"", "+ports", "+banks"};
|
|
static const char *const g_stripe_name[] = {"one door each", "port-rotated",
|
|
"bank-striped"};
|
|
|
|
/**
|
|
* Stage, poison the output, run `repeats` times, read back, and compare.
|
|
*
|
|
* The output poison is written ONCE before the first repeat and not refreshed: every repeat
|
|
* writes the same answer to the same place, so a repeat that silently did nothing still leaves
|
|
* the previous one's result, and refreshing the poison would let the last repeat alone carry
|
|
* the whole check (test_ops.c makes the same choice for the same reason).
|
|
*/
|
|
static int run_arm(coop_fixture *fixture, shape_run *shape,
|
|
const ft_tt_op_matmul_cooperative_request *request, uint32_t member_count,
|
|
uint32_t override_member, uint32_t override_index, uint32_t override_value,
|
|
bool compare_to_solo, arm_result *out)
|
|
{
|
|
ft_tt_op_matmul_cooperative_plan plan;
|
|
size_t total_bytes;
|
|
double started;
|
|
uint32_t repeat;
|
|
int rc;
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
out->bytes_match_solo = true;
|
|
|
|
rc = ft_tt_op_matmul_cooperative_plan_build(fixture->active, member_count,
|
|
0u /* leader = member 0 */, request, &plan);
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
out->distinct_endpoints = plan.distinct_endpoints;
|
|
out->row_collisions = plan.row_collisions;
|
|
|
|
/* INDEPENDENT writes member_count whole results side by side; the split arms write one. */
|
|
total_bytes = (request->mode == FT_TT_OP_MATMUL_INDEPENDENT)
|
|
? shape->output_bytes * member_count
|
|
: shape->output_bytes;
|
|
|
|
memset(shape->readback, POISON_BYTE, total_bytes);
|
|
rc = ft_tt_write_dram(fixture->dram_path, fixture->dram_output, DRAM_BUFFER_BASE,
|
|
shape->readback, total_bytes);
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
|
|
if (plan.shared_valid) {
|
|
rc = ft_tt_group_shared_operand_reset(&fixture->session->noc, &plan.shared,
|
|
fixture->active, member_count);
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
started = monotonic_seconds();
|
|
rc = stage_members(fixture, &plan, request, override_member, override_index, override_value);
|
|
out->staging_ms = (monotonic_seconds() - started) * 1e3;
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
|
|
for (repeat = 0u; repeat < fixture->repeats; repeat++) {
|
|
double milliseconds = 0.0;
|
|
|
|
/* The barrier state has to be reset before EVERY run, not once at setup: a ready
|
|
* semaphore left non-zero by an abandoned run lets the next leader believe receivers
|
|
* are ready that have not started, and unlike a completion block a stale semaphore
|
|
* value is indistinguishable from a fresh one (ft_tt_group_shared_operand.h). */
|
|
if (plan.shared_valid && repeat > 0u) {
|
|
rc = ft_tt_group_shared_operand_reset(&fixture->session->noc, &plan.shared,
|
|
fixture->active, member_count);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
rc = go_and_wait(fixture, member_count, &milliseconds);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
if (repeat == 0u || milliseconds < out->best_ms)
|
|
out->best_ms = milliseconds;
|
|
if (repeat == 0u || milliseconds > out->worst_ms)
|
|
out->worst_ms = milliseconds;
|
|
if (out->sample_count < MAX_SPREAD_SAMPLES) {
|
|
out->sample_ms[out->sample_count] = milliseconds;
|
|
/* Read into a SCRATCH result, not `out`: collect_status folds error and
|
|
* short-block state in, and a per-repeat call would make the arm's final verdict
|
|
* the union of every repeat's rather than the last one's. It is a pure read, and
|
|
* it is outside the timed window. */
|
|
if (fixture->sample_device_cycles) {
|
|
arm_result probe;
|
|
|
|
memset(&probe, 0, sizeof(probe));
|
|
if (collect_status(fixture, &plan, &probe) == FT_TT_OK)
|
|
out->sample_cycles[out->sample_count] = probe.device_cycles;
|
|
}
|
|
out->sample_count++;
|
|
}
|
|
}
|
|
out->rc = rc;
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
rc = collect_status(fixture, &plan, out);
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
|
|
rc = ft_tt_read_dram(fixture->dram_path, fixture->dram_output, DRAM_BUFFER_BASE,
|
|
shape->readback, total_bytes);
|
|
if (rc != FT_TT_OK) {
|
|
out->rc = rc;
|
|
return rc;
|
|
}
|
|
out->first_difference = shape->output_bytes;
|
|
if (poison_remaining((const unsigned char *)shape->readback, total_bytes) * 100u >
|
|
total_bytes)
|
|
out->bytes_match_solo = false;
|
|
if (compare_to_solo && shape->solo_tiles != NULL) {
|
|
const unsigned char *left = (const unsigned char *)shape->solo_tiles;
|
|
const unsigned char *right = (const unsigned char *)shape->readback;
|
|
size_t offset;
|
|
|
|
for (offset = 0u; offset < shape->output_bytes; offset++) {
|
|
if (left[offset] != right[offset]) {
|
|
out->first_difference = offset;
|
|
out->bytes_match_solo = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Reporting */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static double gflops_of(const shape_run *shape, double milliseconds)
|
|
{
|
|
double flops = 2.0 * (double)shape->m * (double)shape->k * (double)shape->n;
|
|
|
|
return milliseconds > 0.0 ? flops / (milliseconds * 1e6) : 0.0;
|
|
}
|
|
|
|
/**
|
|
* The bytes this arm's readers actually pull out of DRAM for ONE result.
|
|
*
|
|
* Exact, from what the reader issues rather than from the shape alone: every member reads its
|
|
* own B panel, and every member reads A for each of its own columns — except in COOPERATIVE,
|
|
* where only the LEADER reads A and the rest receive it over the NoC. Getting that wrong would
|
|
* make the striped and un-striped GB/s incomparable, which is the whole column.
|
|
*/
|
|
static double dram_read_bytes(const shape_run *shape, ft_tt_op_matmul_cooperative_mode mode,
|
|
uint32_t members)
|
|
{
|
|
const double tile_bytes = (double)FT_TT_OP_TILE_BYTES_BF16;
|
|
const uint32_t columns =
|
|
(mode == FT_TT_OP_MATMUL_INDEPENDENT) ? shape->tile_cols : shape->tile_cols / members;
|
|
const double a_per_member = (double)shape->tile_rows * shape->tile_inner * columns;
|
|
const double b_per_member = (double)shape->tile_inner * columns;
|
|
const double a_readers = (mode == FT_TT_OP_MATMUL_COOPERATIVE) ? 1.0 : (double)members;
|
|
|
|
return (a_readers * a_per_member + (double)members * b_per_member) * tile_bytes;
|
|
}
|
|
|
|
static void print_row(const char *arm, uint32_t n, uint32_t rows, uint32_t columns,
|
|
const shape_run *shape, ft_tt_op_matmul_cooperative_mode mode,
|
|
const arm_result *result, double solo_ms)
|
|
{
|
|
double aggregate = gflops_of(shape, result->best_ms);
|
|
double read_gbs = result->best_ms > 0.0
|
|
? dram_read_bytes(shape, mode, n) / (result->best_ms * 1e-3) / 1e9
|
|
: 0.0;
|
|
|
|
printf(" %-12s %2u %ux%u | %8.3f | %6.1f%% | %9.2f | %8.1f | %2u/%u | %6.2fx | %s\n", arm,
|
|
n, rows, columns, result->best_ms,
|
|
result->best_ms > 0.0 ? (result->worst_ms - result->best_ms) / result->best_ms * 100.0
|
|
: 0.0,
|
|
aggregate, read_gbs, result->distinct_endpoints, result->row_collisions,
|
|
result->best_ms > 0.0 ? solo_ms / result->best_ms : 0.0,
|
|
result->first_error != 0u ? "KERNEL ERROR"
|
|
: (result->bytes_match_solo ? "bytes == solo" : "BYTES DIFFER"));
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stages */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** The rectangle shapes the sweep walks, widest-last so a failure is reported at the smallest
|
|
* N that shows it. Never wider than 4 columns — see the file header. */
|
|
typedef struct sweep_shape {
|
|
uint32_t rows;
|
|
uint32_t columns;
|
|
} sweep_shape;
|
|
|
|
static const sweep_shape g_sweep[] = {{1u, 2u}, {1u, 3u}, {1u, 4u}, {2u, 2u},
|
|
{2u, 3u}, {2u, 4u}, {3u, 4u}, {4u, 4u}};
|
|
#define SWEEP_COUNT ((uint32_t)(sizeof(g_sweep) / sizeof(g_sweep[0])))
|
|
|
|
/** Operand order inside a shard plan. It is the order the DOORS are assigned in — the
|
|
* planner is greedy in stream order — so it is named rather than left to a literal. */
|
|
#define SHARD_OPERAND_WEIGHT 0u
|
|
#define SHARD_OPERAND_ACTIVATION 1u
|
|
#define SHARD_OPERAND_COUNT 2u
|
|
|
|
/**
|
|
* PART B — lay the operands out ACROSS BANKS for one group size, and stage the bytes.
|
|
*
|
|
* The WEIGHT is PARTITIONED and the ACTIVATION is REPLICATED, and the asymmetry is the whole
|
|
* design: the weight is stored N-major, so member c's column slice is ALREADY a contiguous run
|
|
* of bytes and relocating it is a copy rather than a re-tiling; the activation is needed whole
|
|
* by every member and is 98 KiB against the weight's 14 MiB, so a copy per bank is affordable.
|
|
*
|
|
* **The placement itself is not this file's.** It is ft_tt_dram_shard.h, because the layout
|
|
* depends on N and an artifact cannot be shipped once per core count (plan Phase 11) — so
|
|
* deciding it has to be something a LOADER does on the machine that is about to run the
|
|
* model, against the group size that machine chose. What is left here is what a loader really
|
|
* does own: taking the plan's `source_offset` out of the CANONICAL bytes and writing them
|
|
* where the plan says. Nothing below knows which bank anything is in.
|
|
*/
|
|
static int stage_bank_shards(coop_fixture *f, const shape_run *shape,
|
|
ft_tt_op_matmul_cooperative_mode mode, uint32_t members,
|
|
ft_tt_op_matmul_cooperative_request *request)
|
|
{
|
|
/* INDEPENDENT is not a split: every member computes the WHOLE width, so its "panel" is a
|
|
* whole copy of the weight, REPLICATED, and its source offset is 0. Getting this wrong
|
|
* would give every member a different slice of the same answer and still complete. */
|
|
const uint32_t columns = (mode == FT_TT_OP_MATMUL_INDEPENDENT)
|
|
? shape->tile_cols
|
|
: shape->tile_cols / members;
|
|
ft_tt_dram_shard_operand operands[SHARD_OPERAND_COUNT];
|
|
ft_tt_dram_shard_plan plan;
|
|
uint32_t member;
|
|
int rc;
|
|
|
|
memset(operands, 0, sizeof(operands));
|
|
operands[SHARD_OPERAND_WEIGHT].spread = (mode == FT_TT_OP_MATMUL_INDEPENDENT)
|
|
? FT_TT_DRAM_SHARD_REPLICATED
|
|
: FT_TT_DRAM_SHARD_PARTITIONED;
|
|
operands[SHARD_OPERAND_WEIGHT].bank_base_address = SHARD_WEIGHT_BASE;
|
|
operands[SHARD_OPERAND_WEIGHT].member_bytes =
|
|
columns * shape->tile_inner * TILE_ELEMENTS * (uint32_t)sizeof(shape->b_n_major[0]);
|
|
operands[SHARD_OPERAND_ACTIVATION].spread = FT_TT_DRAM_SHARD_REPLICATED;
|
|
operands[SHARD_OPERAND_ACTIVATION].bank_base_address = SHARD_ACTIVATION_BASE;
|
|
operands[SHARD_OPERAND_ACTIVATION].member_bytes =
|
|
shape->tile_rows * shape->tile_inner * FT_TT_OP_TILE_BYTES_BF16;
|
|
|
|
rc = ft_tt_dram_shard_plan_build(&f->session->grid, operands, SHARD_OPERAND_COUNT, members,
|
|
FT_TT_COORD_TRANSLATED, &plan);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
f->shard_busiest_bank = plan.busiest_bank_streams;
|
|
f->shard_quietest_bank = plan.quietest_bank_streams;
|
|
|
|
for (member = 0u; member < members; member++) {
|
|
const ft_tt_dram_shard_placement *weight =
|
|
ft_tt_dram_shard_placement_of(&plan, SHARD_OPERAND_WEIGHT, member);
|
|
const ft_tt_dram_shard_placement *activation =
|
|
ft_tt_dram_shard_placement_of(&plan, SHARD_OPERAND_ACTIVATION, member);
|
|
|
|
if (weight == NULL || activation == NULL)
|
|
return FT_TT_EINVAL;
|
|
|
|
request->weight_shards[member].endpoint = weight->endpoint;
|
|
request->weight_shards[member].address = weight->address;
|
|
request->activation_shards[member].endpoint = activation->endpoint;
|
|
request->activation_shards[member].address = activation->address;
|
|
|
|
/* The canonical artifact is `b_n_major` / `a_tiles` and is never re-laid-out; the
|
|
* plan says which BYTES of it belong here, so the cast to a byte pointer is the whole
|
|
* of "the loader's copy". */
|
|
rc = ft_tt_write_dram(f->dram_path, weight->endpoint.coord, weight->address,
|
|
(const unsigned char *)shape->b_n_major + weight->source_offset,
|
|
weight->bytes);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_write_dram(
|
|
f->dram_path, activation->endpoint.coord, activation->address,
|
|
(const unsigned char *)shape->a_tiles + activation->source_offset,
|
|
activation->bytes);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
request->weight_shard_count = members;
|
|
request->activation_shard_count = members;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* The shape, the mode, and WHERE each member reads its operands from.
|
|
*
|
|
* STRIPE_NONE is the pre-striping request exactly: every member is given the operand's own
|
|
* coordinate, which `ft_tt_coord_dram_bank()` made port 0. STRIPE_PORTS hands the planner all
|
|
* three doors of the SAME two banks. STRIPE_BANKS relocates the bytes. Nothing else differs
|
|
* between the arms - same shape, same kernels, same cores - which is what makes the byte
|
|
* comparison a comparison of ADDRESSINGS rather than of programs.
|
|
*/
|
|
static int fill_request(ft_tt_op_matmul_cooperative_request *request, coop_fixture *f,
|
|
const shape_run *shape, ft_tt_op_matmul_cooperative_mode mode,
|
|
stripe_mode stripe, uint32_t members)
|
|
{
|
|
memset(request, 0, sizeof(*request));
|
|
request->tile_rows = shape->tile_rows;
|
|
request->tile_inner = shape->tile_inner;
|
|
request->tile_cols = shape->tile_cols;
|
|
request->prefetch_tiles = PREFETCH_TILES;
|
|
request->weight_n_major = 1u;
|
|
request->mode = mode;
|
|
request->spin_limit = SPIN_LIMIT;
|
|
request->activation.x = (uint16_t)f->dram_a.x;
|
|
request->activation.y = (uint16_t)f->dram_a.y;
|
|
request->activation.address = DRAM_BUFFER_BASE;
|
|
request->activation.tile_bytes = FT_TT_OP_TILE_BYTES_BF16;
|
|
request->weight = request->activation;
|
|
request->weight.x = (uint16_t)f->dram_b.x;
|
|
request->weight.y = (uint16_t)f->dram_b.y;
|
|
request->weight.address = f->weight_base;
|
|
request->output = request->activation;
|
|
request->output.x = (uint16_t)f->dram_output.x;
|
|
request->output.y = (uint16_t)f->dram_output.y;
|
|
if (stripe == STRIPE_PORTS) {
|
|
request->activation_ports = f->dram_a_ports;
|
|
request->weight_ports = f->dram_b_ports;
|
|
}
|
|
if (stripe == STRIPE_BANKS)
|
|
return stage_bank_shards(f, shape, mode, members, request);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static void release_shape(shape_run *shape)
|
|
{
|
|
free(shape->solo_tiles);
|
|
free(shape->readback);
|
|
free(shape->reference);
|
|
free(shape->a_tiles);
|
|
free(shape->b_n_major);
|
|
shape->solo_tiles = NULL;
|
|
shape->readback = NULL;
|
|
shape->reference = NULL;
|
|
shape->a_tiles = NULL;
|
|
shape->b_n_major = NULL;
|
|
}
|
|
|
|
/**
|
|
* Allocate one shape's buffers, upload its operands, and compute the CPU reference.
|
|
*
|
|
* The weight is stored N-MAJOR in DRAM for every arm. Both halves or neither: the flag alone
|
|
* reads the right bytes from the wrong places. N-major is also what makes a member's column
|
|
* slice a CONTIGUOUS run of tiles, which is the whole reason a member needs only a column
|
|
* OFFSET rather than a strided index.
|
|
*/
|
|
static int prepare_shape(coop_fixture *fixture, shape_run *shape)
|
|
{
|
|
float *a = malloc((size_t)shape->m * shape->k * sizeof(float));
|
|
float *b = malloc((size_t)shape->k * shape->n * sizeof(float));
|
|
uint16_t *b_tiles = malloc((size_t)shape->tile_inner * shape->tile_cols * TILE_ELEMENTS *
|
|
sizeof(uint16_t));
|
|
uint16_t *a_tiles;
|
|
uint16_t *reordered;
|
|
uint32_t index;
|
|
int rc = FT_TT_ENOMEM;
|
|
|
|
/* The tilized operands are KEPT on the shape rather than freed here: a bank-striped arm
|
|
* re-lays them out for every group size, and re-tilizing on this 2-core 1.8 GHz host each
|
|
* time would put host arithmetic inside the sweep. */
|
|
shape->a_tiles = malloc((size_t)shape->tile_rows * shape->tile_inner * TILE_ELEMENTS *
|
|
sizeof(uint16_t));
|
|
shape->b_n_major = malloc((size_t)shape->tile_inner * shape->tile_cols * TILE_ELEMENTS *
|
|
sizeof(uint16_t));
|
|
shape->solo_tiles = malloc(shape->output_bytes);
|
|
shape->readback = malloc(shape->output_bytes * FT_TT_OP_COOP_MAX_MEMBERS);
|
|
shape->reference = malloc((size_t)shape->m * shape->n * sizeof(float));
|
|
if (!a || !b || !b_tiles || !shape->a_tiles || !shape->b_n_major || !shape->solo_tiles ||
|
|
!shape->readback || !shape->reference)
|
|
goto cleanup;
|
|
a_tiles = shape->a_tiles;
|
|
reordered = shape->b_n_major;
|
|
|
|
/* Scaled down so a Kt-long accumulation stays well inside bf16's range at the output; the
|
|
* kernel accumulates in DST, not in fp32, and an overflow would be measuring the test's
|
|
* arithmetic rather than the kernel's. */
|
|
random_seed(0x5EEDu + shape->m + shape->k + shape->n);
|
|
for (index = 0; index < shape->m * shape->k; index++)
|
|
a[index] = random_value() * 0.5f;
|
|
for (index = 0; index < shape->k * shape->n; index++)
|
|
b[index] = random_value() * 0.5f;
|
|
round_to_bf16(a, (size_t)shape->m * shape->k);
|
|
round_to_bf16(b, (size_t)shape->k * shape->n);
|
|
|
|
to_bf16_tiles(a, shape->m, shape->k, a_tiles);
|
|
to_bf16_tiles(b, shape->k, shape->n, b_tiles);
|
|
permute_tiles_to_n_major(b_tiles, reordered, shape->tile_inner, shape->tile_cols,
|
|
FT_TT_OP_TILE_BYTES_BF16);
|
|
|
|
rc = ft_tt_write_dram(fixture->dram_path, fixture->dram_a, DRAM_BUFFER_BASE, a_tiles,
|
|
(size_t)shape->tile_rows * shape->tile_inner *
|
|
FT_TT_OP_TILE_BYTES_BF16);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_write_dram(fixture->dram_path, fixture->dram_b, fixture->weight_base,
|
|
reordered,
|
|
(size_t)shape->tile_inner * shape->tile_cols *
|
|
FT_TT_OP_TILE_BYTES_BF16);
|
|
if (rc != FT_TT_OK)
|
|
goto cleanup;
|
|
|
|
reference_matmul(a, b, shape->m, shape->k, shape->n, shape->reference);
|
|
|
|
cleanup:
|
|
free(a);
|
|
free(b);
|
|
free(b_tiles);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Stage 1 — the SOLO baseline, scored against the CPU, and kept as the byte reference.
|
|
*
|
|
* A single member in any mode is the same program: one core, column 0, the whole width. It is
|
|
* expressed as INDEPENDENT because that mode's slicing is "the whole width" by definition, so
|
|
* the baseline needs no special case anywhere.
|
|
*/
|
|
static bool run_solo_baseline(coop_fixture *fixture, shape_run *shape, arm_result *out)
|
|
{
|
|
ft_tt_op_matmul_cooperative_request request;
|
|
float *actual = malloc((size_t)shape->m * shape->n * sizeof(float));
|
|
bool ok = false;
|
|
int rc;
|
|
|
|
if (actual == NULL)
|
|
return false;
|
|
|
|
select_linear(fixture, 1u);
|
|
(void)fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_INDEPENDENT, STRIPE_NONE, 1u);
|
|
rc = run_arm(fixture, shape, &request, 1u, UINT32_MAX, 0u, 0u, false, out);
|
|
ft_tt_test_check_fmt("solo baseline ran", rc == FT_TT_OK && out->first_error == 0u &&
|
|
out->blocks_short == 0u,
|
|
"%s: rc=%d (%s), kernel error 0x%08x, %u members short of their "
|
|
"block count",
|
|
shape->label, rc, ft_tt_error_string(rc), out->first_error,
|
|
out->blocks_short);
|
|
if (rc != FT_TT_OK || out->first_error != 0u) {
|
|
free(actual);
|
|
return false;
|
|
}
|
|
|
|
memcpy(shape->solo_tiles, shape->readback, shape->output_bytes);
|
|
from_bf16_tiles(shape->solo_tiles, shape->m, shape->n, actual);
|
|
{
|
|
double pcc = pearson(shape->reference, actual, (size_t)shape->m * shape->n);
|
|
size_t poison_left =
|
|
poison_remaining((const unsigned char *)shape->solo_tiles, shape->output_bytes);
|
|
|
|
ok = pcc >= PCC_ACCEPT && poison_left * 100u < shape->output_bytes;
|
|
ft_tt_test_check_fmt("solo baseline agrees with the CPU reference", ok,
|
|
"%s [%ux%u]x[%ux%u]: PCC %.9f, %zu poison bytes left, %.3f ms, "
|
|
"%.2f GFLOP/s (%.1f%% of one Tensix)",
|
|
shape->label, shape->m, shape->k, shape->k, shape->n, pcc,
|
|
poison_left, out->best_ms, gflops_of(shape, out->best_ms),
|
|
gflops_of(shape, out->best_ms) / ONE_TENSIX_GFLOPS * 100.0);
|
|
}
|
|
free(actual);
|
|
return ok;
|
|
}
|
|
|
|
/**
|
|
* How many distinct DRAM doors an arm's readers should end up on.
|
|
*
|
|
* Asserted rather than merely printed, because it is the BALANCE half of the striping rules —
|
|
* the outstanding-read budget is per endpoint — and because it is the cheapest way to catch an
|
|
* allocator that quietly put two streams through one door.
|
|
*
|
|
* PORTS: two banks, each rotated over min(members, doors-per-bank) of its doors.
|
|
* BANKS: one door per reader stream, and every member has two streams (its weight panel and
|
|
* its activation copy), even in COOPERATIVE where only the leader uses the A one —
|
|
* until the streams outnumber the CARD'S doors, at which point the chooser has to
|
|
* double up and the ceiling is the door count itself.
|
|
*/
|
|
static uint32_t expected_doors(const coop_fixture *f, stripe_mode stripe, uint32_t members)
|
|
{
|
|
const uint32_t ports = f->dram_a_ports.port_count;
|
|
|
|
if (stripe == STRIPE_PORTS)
|
|
return 2u * (members < ports ? members : ports);
|
|
if (stripe == STRIPE_BANKS) {
|
|
const uint32_t doors = ft_tt_coord_dram_bank_count(&f->session->grid) * ports;
|
|
|
|
return 2u * members < doors ? 2u * members : doors;
|
|
}
|
|
return 0u;
|
|
}
|
|
|
|
/** One arm of the sweep: lay the operands out for it, run it, assert it is byte-identical to
|
|
* the single-core result, and print its row. Every addressing goes through this, so none can
|
|
* be scored by a different rule than the others. */
|
|
static int run_sweep_arm(coop_fixture *fixture, shape_run *shape, const sweep_shape *rectangle,
|
|
ft_tt_op_matmul_cooperative_mode mode, stripe_mode stripe,
|
|
double solo_ms, const char *label, arm_result *out)
|
|
{
|
|
const uint32_t members = rectangle->rows * rectangle->columns;
|
|
ft_tt_op_matmul_cooperative_request request;
|
|
int rc;
|
|
|
|
select_rectangle(fixture, rectangle->rows, rectangle->columns);
|
|
rc = fill_request(&request, fixture, shape, mode, stripe, members);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("an arm's operands were laid out and staged", false,
|
|
"%s %s N=%u: rc=%d (%s)", shape->label, label, members, rc,
|
|
ft_tt_error_string(rc));
|
|
return rc;
|
|
}
|
|
rc = run_arm(fixture, shape, &request, members, UINT32_MAX, 0u, 0u, true, out);
|
|
ft_tt_test_check_fmt("an arm is byte-identical to the single-core result",
|
|
rc == FT_TT_OK && out->first_error == 0u && out->blocks_short == 0u &&
|
|
out->bytes_match_solo,
|
|
"%s %s N=%u (%ux%u): rc=%d (%s), kernel error 0x%08x, %u short, "
|
|
"bytes %s",
|
|
shape->label, label, members, rectangle->rows, rectangle->columns, rc,
|
|
ft_tt_error_string(rc), out->first_error, out->blocks_short,
|
|
out->bytes_match_solo ? "IDENTICAL" : "DIFFER");
|
|
/* Every reader gets its OWN door, which is the balance half of the striping rules. The ROW
|
|
* half cannot always be met and is reported rather than asserted: the part has 24 DRAM
|
|
* endpoints on only 12 rows, so any arm with more than 12 concurrent streams must double
|
|
* up somewhere, and at these rates a shared row is far from its ~60 GB/s. */
|
|
if (stripe != STRIPE_NONE)
|
|
ft_tt_test_check_fmt("every reader stream of a striped arm gets its own door",
|
|
rc == FT_TT_OK &&
|
|
out->distinct_endpoints ==
|
|
expected_doors(fixture, stripe, members),
|
|
"%s %s N=%u: %u distinct doors, wanted %u; %u pairs of them share "
|
|
"a NoC row",
|
|
shape->label, label, members, out->distinct_endpoints,
|
|
expected_doors(fixture, stripe, members), out->row_collisions);
|
|
if (rc == FT_TT_OK)
|
|
print_row(label, members, rectangle->rows, rectangle->columns, shape, mode, out,
|
|
solo_ms);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Stage 2 - the sweep. SPLIT and COOPERATIVE at every rectangle the pool allows, each in all
|
|
* THREE addressings, every arm interleaved so a host that drifts over the sweep drifts through
|
|
* all of them equally (DEV_NOTES.md section 8).
|
|
*
|
|
* COOPERATIVE stops at COOP_MAX_MEMBERS_ON_SILICON. A 2x4 group - seven receivers - wedged
|
|
* core 115 on 2026-08-13 AND again on 2026-08-14, both times at the re-boot AFTER a clean run,
|
|
* always the same slot. Groups of <= 4 members ran clean through both. SPLIT and INDEPENDENT
|
|
* issue no multicast at all and are swept to the full pool.
|
|
*/
|
|
static void run_sweep(coop_fixture *fixture, shape_run *shape, double solo_ms)
|
|
{
|
|
uint32_t entry;
|
|
|
|
printf("\n %-12s %2s %3s | %8s | %7s | %9s | %8s | %5s | %7s | %s\n", "arm", "N", "r x c",
|
|
"ms", "spread", "aggregate", "read GB/s", "doors", "vs solo", "correctness");
|
|
printf(" --------------------------+----------+---------+-----------+----------+-------+"
|
|
"---------+---------------\n");
|
|
|
|
for (entry = 0u; entry < SWEEP_COUNT; entry++) {
|
|
const uint32_t members = g_sweep[entry].rows * g_sweep[entry].columns;
|
|
arm_result split[3];
|
|
arm_result coop[3];
|
|
stripe_mode stripe;
|
|
char label[24];
|
|
int rc = FT_TT_OK;
|
|
|
|
if (g_sweep[entry].rows > fixture->pool_rows ||
|
|
g_sweep[entry].columns > fixture->pool_columns)
|
|
continue;
|
|
if ((shape->tile_cols % members) != 0u) {
|
|
printf(" %-12s %2u %ux%u | Nt = %u does not divide by %u members - refused by the "
|
|
"planner, not rounded\n", "-", members, g_sweep[entry].rows,
|
|
g_sweep[entry].columns, shape->tile_cols, members);
|
|
continue;
|
|
}
|
|
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS && rc == FT_TT_OK; stripe++) {
|
|
snprintf(label, sizeof(label), "SPLIT%s", g_stripe_suffix[stripe]);
|
|
rc = run_sweep_arm(fixture, shape, &g_sweep[entry], FT_TT_OP_MATMUL_SPLIT, stripe,
|
|
solo_ms, label, &split[stripe]);
|
|
}
|
|
if (rc == FT_TT_OK && split[STRIPE_NONE].best_ms > 0.0)
|
|
printf(" %2u %ux%u | against one door each: ports %.3fx, banks "
|
|
"%.3fx; device cycles %u -> %u -> %u; busiest bank %u streams, quietest "
|
|
"%u\n",
|
|
members, g_sweep[entry].rows, g_sweep[entry].columns,
|
|
split[STRIPE_NONE].best_ms / split[STRIPE_PORTS].best_ms,
|
|
split[STRIPE_NONE].best_ms / split[STRIPE_BANKS].best_ms,
|
|
split[STRIPE_NONE].device_cycles, split[STRIPE_PORTS].device_cycles,
|
|
split[STRIPE_BANKS].device_cycles, fixture->shard_busiest_bank,
|
|
fixture->shard_quietest_bank);
|
|
|
|
if (rc == FT_TT_OK && members <= COOP_MAX_MEMBERS_ON_SILICON) {
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS && rc == FT_TT_OK; stripe++) {
|
|
snprintf(label, sizeof(label), "COOP%s", g_stripe_suffix[stripe]);
|
|
rc = run_sweep_arm(fixture, shape, &g_sweep[entry],
|
|
FT_TT_OP_MATMUL_COOPERATIVE, stripe, solo_ms, label,
|
|
&coop[stripe]);
|
|
}
|
|
if (rc == FT_TT_OK && split[STRIPE_NONE].best_ms > 0.0 &&
|
|
coop[STRIPE_NONE].best_ms > 0.0)
|
|
printf(" %2u %ux%u | multicast alone: %.3fx un-striped, %.3fx "
|
|
"bank-striped\n", members, g_sweep[entry].rows, g_sweep[entry].columns,
|
|
split[STRIPE_NONE].best_ms / coop[STRIPE_NONE].best_ms,
|
|
split[STRIPE_BANKS].best_ms / coop[STRIPE_BANKS].best_ms);
|
|
} else if (members > COOP_MAX_MEMBERS_ON_SILICON) {
|
|
printf(" %2u %ux%u | COOPERATIVE not run: %u members is %u receivers, "
|
|
"and a 2x4 group wedged core 115 twice\n", members, g_sweep[entry].rows,
|
|
g_sweep[entry].columns, members, members - 1u);
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Spread stage — the DISTRIBUTION, not three order statistics of it */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** One arm's samples, gathered round-robin with the other arms' so a host that drifts
|
|
* through the stage drifts through all three equally. */
|
|
typedef struct spread_samples {
|
|
double ms[MAX_SPREAD_SAMPLES];
|
|
double cycles[MAX_SPREAD_SAMPLES];
|
|
uint32_t count;
|
|
uint32_t mismatches;
|
|
uint32_t failures;
|
|
} spread_samples;
|
|
|
|
static void spread_statistics(const double *values, uint32_t count, double *out_min,
|
|
double *out_median, double *out_mean, double *out_max,
|
|
double *out_cv)
|
|
{
|
|
static double sorted[MAX_SPREAD_SAMPLES];
|
|
double total = 0.0;
|
|
double variance = 0.0;
|
|
uint32_t index;
|
|
|
|
*out_min = 0.0;
|
|
*out_median = 0.0;
|
|
*out_mean = 0.0;
|
|
*out_max = 0.0;
|
|
*out_cv = 0.0;
|
|
if (count == 0u)
|
|
return;
|
|
memcpy(sorted, values, count * sizeof(sorted[0]));
|
|
qsort(sorted, count, sizeof(sorted[0]), compare_double);
|
|
for (index = 0u; index < count; index++)
|
|
total += values[index];
|
|
*out_mean = total / (double)count;
|
|
for (index = 0u; index < count; index++)
|
|
variance += (values[index] - *out_mean) * (values[index] - *out_mean);
|
|
*out_min = sorted[0];
|
|
*out_median = sorted[count / 2u];
|
|
*out_max = sorted[count - 1u];
|
|
if (*out_mean > 0.0 && count > 1u)
|
|
*out_cv = sqrt(variance / (double)(count - 1u)) / *out_mean * 100.0;
|
|
}
|
|
|
|
/**
|
|
* The distribution as a picture, because the SHAPE is the whole question.
|
|
*
|
|
* A broad unimodal spread is contention — many small delays adding up. Two clusters is two
|
|
* behaviours, one of them slow, and that is a different mechanism with a different fix. No
|
|
* summary statistic distinguishes them, which is why min-of-N was the right thing to publish
|
|
* and the wrong thing to stop at.
|
|
*/
|
|
static void print_spread_histogram(const char *what, const double *values, uint32_t count,
|
|
double low, double high)
|
|
{
|
|
enum { BINS = 24u };
|
|
uint32_t histogram[BINS];
|
|
uint32_t tallest = 1u;
|
|
uint32_t bin;
|
|
uint32_t index;
|
|
|
|
memset(histogram, 0, sizeof(histogram));
|
|
if (count == 0u || high <= low)
|
|
return;
|
|
for (index = 0u; index < count; index++) {
|
|
double position = (values[index] - low) / (high - low) * (double)BINS;
|
|
uint32_t slot = position <= 0.0 ? 0u : (uint32_t)position;
|
|
|
|
if (slot >= BINS)
|
|
slot = BINS - 1u;
|
|
histogram[slot]++;
|
|
}
|
|
for (bin = 0u; bin < BINS; bin++)
|
|
if (histogram[bin] > tallest)
|
|
tallest = histogram[bin];
|
|
printf(" %s, %u samples over %.3f .. %.3f\n", what, count, low, high);
|
|
for (bin = 0u; bin < BINS; bin++) {
|
|
uint32_t width = histogram[bin] * 40u / tallest;
|
|
char bar[41];
|
|
|
|
memset(bar, '#', width);
|
|
bar[width] = '\0';
|
|
printf(" %8.3f |%-40s %u\n", low + (high - low) * (double)bin / (double)BINS, bar,
|
|
histogram[bin]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stage S — WHY do striped arms spread 10-50%% run to run where flat arms spread 1-3%%?
|
|
*
|
|
* Min-of-N was used and the spread reported, so no published number is wrong — but a 10-50%
|
|
* spread on the same cores, the same host and the same session is the instrument saying
|
|
* something. What distinguishes a striped arm is that its traffic crosses several banks and
|
|
* several NoC rows, and the per-member map changes with N.
|
|
*
|
|
* TWO CLOCKS ON ONE RUN is the discriminator, and it is stronger than any histogram of a
|
|
* single clock. The host wall clock is GO -> all DONE over an x1 link driven by two 1.8 GHz
|
|
* cores that are also serving eight K80s; the device cycles are the slowest member's own
|
|
* reader, entry to exit, which no host scheduling can move. Phase 6.7 already measured that
|
|
* the bank-striped arm's wall clock is FLAT while its device cycles keep falling — i.e. that
|
|
* arm is dispatch-bound — so if the spread is the host, the wall clock moves and the device
|
|
* cycles do not. If both move, it is on the card.
|
|
*
|
|
* The three addressings are run ROUND-ROBIN, one sample each, not one arm to completion:
|
|
* this host drifts enough over a stage to swamp the quantity (DEV_NOTES.md §8).
|
|
*/
|
|
static void run_spread_stage(coop_fixture *fixture, shape_run *shape, uint32_t members)
|
|
{
|
|
static spread_samples samples[3];
|
|
const uint32_t rounds = fixture->spread_rounds;
|
|
const uint32_t inner = fixture->spread_inner;
|
|
const uint32_t saved_repeats = fixture->repeats;
|
|
stripe_mode stripe;
|
|
uint32_t round;
|
|
|
|
ft_tt_test_step("stage S — the run-to-run DISTRIBUTION of all three addressings at N=%u, "
|
|
"%u rounds x %u BACK-TO-BACK repeats, ROUND-ROBIN over the arms, host "
|
|
"clock and device cycles on every sample",
|
|
members, rounds, inner);
|
|
if (members == 0u || members > fixture->member_count || rounds == 0u || inner == 0u ||
|
|
rounds * inner > MAX_SPREAD_SAMPLES || (shape->tile_cols % members) != 0u) {
|
|
ft_tt_test_check_fmt("the spread stage's N and round count are expressible", false,
|
|
"N=%u of pool %u, %u x %u samples (max %u), Nt=%u", members,
|
|
fixture->member_count, rounds, inner, (unsigned)MAX_SPREAD_SAMPLES,
|
|
shape->tile_cols);
|
|
return;
|
|
}
|
|
|
|
memset(samples, 0, sizeof(samples));
|
|
/* `inner` repeats BACK TO BACK inside one staging is how the sweep has always measured
|
|
* an arm, and it is what the 10-50%% spread was reported from. Re-staging between every
|
|
* sample puts ~50 ms of host work in between and is a DIFFERENT experiment — it was tried
|
|
* first and gave 2-4%% on every arm, which is itself a result and is why both shapes of
|
|
* the measurement are named here. `rounds` interleaves the arms on top. */
|
|
fixture->repeats = inner;
|
|
fixture->sample_device_cycles = true;
|
|
for (round = 0u; round < rounds; round++) {
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS; stripe++) {
|
|
ft_tt_op_matmul_cooperative_request request;
|
|
arm_result result;
|
|
uint32_t sample;
|
|
int rc;
|
|
|
|
select_linear(fixture, members);
|
|
rc = fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_SPLIT, stripe, members);
|
|
if (rc == FT_TT_OK)
|
|
rc = run_arm(fixture, shape, &request, members, UINT32_MAX, 0u, 0u, true,
|
|
&result);
|
|
if (rc != FT_TT_OK || result.sample_count == 0u) {
|
|
samples[stripe].failures++;
|
|
continue;
|
|
}
|
|
if (!result.bytes_match_solo || result.first_error != 0u)
|
|
samples[stripe].mismatches++;
|
|
for (sample = 0u; sample < result.sample_count; sample++) {
|
|
samples[stripe].ms[samples[stripe].count] = result.sample_ms[sample];
|
|
samples[stripe].cycles[samples[stripe].count] =
|
|
(double)result.sample_cycles[sample];
|
|
samples[stripe].count++;
|
|
}
|
|
}
|
|
printf(" round %2u of %u, host load %.2f\n", round, rounds, read_host_load());
|
|
fflush(stdout);
|
|
}
|
|
fixture->repeats = saved_repeats;
|
|
fixture->sample_device_cycles = false;
|
|
|
|
printf("\n %-14s %5s | %8s %8s %8s %8s %7s | %9s %9s %7s | %s\n", "arm", "n", "min ms",
|
|
"median", "mean", "max", "CV%", "min cycles", "median", "CV%", "bytes");
|
|
printf(" ------------------------------------------------------------------------------"
|
|
"----------------------------------\n");
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS; stripe++) {
|
|
double low;
|
|
double median;
|
|
double mean;
|
|
double high;
|
|
double cv;
|
|
double cycle_low;
|
|
double cycle_median;
|
|
double cycle_mean;
|
|
double cycle_high;
|
|
double cycle_cv;
|
|
char label[24];
|
|
|
|
spread_statistics(samples[stripe].ms, samples[stripe].count, &low, &median, &mean, &high,
|
|
&cv);
|
|
spread_statistics(samples[stripe].cycles, samples[stripe].count, &cycle_low,
|
|
&cycle_median, &cycle_mean, &cycle_high, &cycle_cv);
|
|
snprintf(label, sizeof(label), "SPLIT%s", g_stripe_suffix[stripe]);
|
|
printf(" %-14s %5u | %8.3f %8.3f %8.3f %8.3f %6.1f%% | %9.0f %9.0f %6.1f%% | %s\n",
|
|
label, samples[stripe].count, low, median, mean, high, cv, cycle_low,
|
|
cycle_median, cycle_cv,
|
|
samples[stripe].mismatches == 0u ? "all == solo" : "MISMATCH");
|
|
ft_tt_test_check_fmt("every sample of a spread arm is byte-identical to solo",
|
|
samples[stripe].mismatches == 0u && samples[stripe].failures == 0u,
|
|
"%s: %u mismatches, %u failures of %u rounds", label,
|
|
samples[stripe].mismatches, samples[stripe].failures, rounds);
|
|
}
|
|
printf("\n CV%% is the sample standard deviation over the mean. The HOST column is GO -> all\n"
|
|
" DONE over the x1 link; the DEVICE column is the slowest member's reader, entry to\n"
|
|
" exit, on its own 800 MHz clock. A host CV far above the device CV means the\n"
|
|
" spread is this 2-core box, not the card.\n\n");
|
|
|
|
/* The histogram is the answer to "bimodal or broad", and it is only worth the page when
|
|
* there is a spread to shape. 5% is well above the 1-3% a flat arm has always shown. */
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS; stripe++) {
|
|
double low;
|
|
double median;
|
|
double mean;
|
|
double high;
|
|
double cv;
|
|
char what[64];
|
|
|
|
spread_statistics(samples[stripe].ms, samples[stripe].count, &low, &median, &mean, &high,
|
|
&cv);
|
|
if (cv < 5.0)
|
|
continue;
|
|
snprintf(what, sizeof(what), "N=%u SPLIT%s host ms", members, g_stripe_suffix[stripe]);
|
|
print_spread_histogram(what, samples[stripe].ms, samples[stripe].count, low, high);
|
|
spread_statistics(samples[stripe].cycles, samples[stripe].count, &low, &median, &mean,
|
|
&high, &cv);
|
|
snprintf(what, sizeof(what), "N=%u SPLIT%s device cycles", members,
|
|
g_stripe_suffix[stripe]);
|
|
print_spread_histogram(what, samples[stripe].cycles, samples[stripe].count, low, high);
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
/** Every group size the spread stage asks about. The striped arms' map — which bank, which
|
|
* door, how many streams share one — is a function of N, so a spread that is a property of
|
|
* the MAP shows up at some N and not others, and one N cannot say that. */
|
|
static void run_spread_stage_all_n(coop_fixture *fixture, shape_run *shape)
|
|
{
|
|
static const uint32_t group_sizes[] = {2u, 4u, 6u, 8u};
|
|
uint32_t index;
|
|
|
|
for (index = 0u; index < sizeof(group_sizes) / sizeof(group_sizes[0]); index++)
|
|
if (group_sizes[index] <= fixture->member_count &&
|
|
(shape->tile_cols % group_sizes[index]) == 0u)
|
|
run_spread_stage(fixture, shape, group_sizes[index]);
|
|
}
|
|
|
|
/**
|
|
* Stage 3 — N INDEPENDENT streams of the whole matmul.
|
|
*
|
|
* This is not a split and it is not scored against solo: it is the memory-system question.
|
|
* Decode reads every weight per token, so what decides how many concurrent streams a card
|
|
* supports is where AGGREGATE DRAM bandwidth flattens, not how fast one matmul goes. The
|
|
* aggregate column is what to watch: while it keeps rising linearly the memory system is not
|
|
* the limit, and the first N where it stops rising is the answer.
|
|
*/
|
|
static bool run_independent_arm(coop_fixture *fixture, shape_run *shape, uint32_t members,
|
|
stripe_mode stripe, arm_result *out)
|
|
{
|
|
ft_tt_op_matmul_cooperative_request request;
|
|
int rc;
|
|
|
|
select_linear(fixture, members);
|
|
rc = fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_INDEPENDENT, stripe, members);
|
|
if (rc == FT_TT_OK)
|
|
rc = run_arm(fixture, shape, &request, members, UINT32_MAX, 0u, 0u, false, out);
|
|
ft_tt_test_check_fmt("an independent-stream run completed on every member",
|
|
rc == FT_TT_OK && out->first_error == 0u && out->blocks_short == 0u,
|
|
"%s N=%u %s: rc=%d (%s), kernel error 0x%08x, %u short", shape->label,
|
|
members, g_stripe_name[stripe], rc, ft_tt_error_string(rc),
|
|
out->first_error, out->blocks_short);
|
|
return rc == FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* Stage 3 - N INDEPENDENT streams of the whole matmul, in all three addressings.
|
|
*
|
|
* This is not a split and it is not scored against solo: it is the memory-system question, and
|
|
* it is where striping is most legible because every core is pulling its own full copy. The
|
|
* read GB/s column is the answer - the un-striped arm flattens at two endpoints' worth, the
|
|
* port arm at two banks' worth, and the bank arm has a bank per core.
|
|
*
|
|
* The bank arm re-uploads N copies of the weight for every N, which is why this stage runs on
|
|
* one shape only.
|
|
*/
|
|
static void run_independent_sweep(coop_fixture *fixture, shape_run *shape, double solo_ms)
|
|
{
|
|
const uint32_t pool = fixture->pool_rows * fixture->pool_columns;
|
|
uint32_t members;
|
|
|
|
ft_tt_test_step("stage 3 - %s on N INDEPENDENT cores, each computing the WHOLE matmul, in "
|
|
"ALL THREE addressings. Aggregate is N results per run, so this is DRAM "
|
|
"throughput and it is where striping is most legible", shape->label);
|
|
printf("\n %-12s %2s | %8s | %7s | %9s | %10s | %5s | %s\n", "arm", "N", "ms", "spread",
|
|
"aggregate", "read GB/s", "doors", "notes");
|
|
printf(" ---------------+----------+---------+-----------+------------+-------+-------\n");
|
|
|
|
for (members = 1u; members <= pool; members++) {
|
|
const double read_bytes = dram_read_bytes(shape, FT_TT_OP_MATMUL_INDEPENDENT, members);
|
|
arm_result result[3];
|
|
stripe_mode stripe;
|
|
bool ok = true;
|
|
char label[24];
|
|
|
|
/* Interleaved within one N, not one arm swept and then the other: two 1.8 GHz cores
|
|
* also driving eight K80s drift enough over a sweep to swamp the difference being
|
|
* measured (DEV_NOTES.md section 8). */
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS && ok; stripe++)
|
|
ok = run_independent_arm(fixture, shape, members, stripe, &result[stripe]);
|
|
if (!ok)
|
|
continue;
|
|
|
|
for (stripe = STRIPE_NONE; stripe <= STRIPE_BANKS; stripe++) {
|
|
snprintf(label, sizeof(label), "INDEP%s", g_stripe_suffix[stripe]);
|
|
printf(" %-12s %2u | %8.3f | %6.1f%% | %9.2f | %10.1f | %2u/%u | %s\n", label,
|
|
members, result[stripe].best_ms,
|
|
(result[stripe].worst_ms - result[stripe].best_ms) /
|
|
result[stripe].best_ms * 100.0,
|
|
gflops_of(shape, result[stripe].best_ms) * (double)members,
|
|
read_bytes / (result[stripe].best_ms * 1e-3) / 1e9,
|
|
result[stripe].distinct_endpoints, result[stripe].row_collisions,
|
|
stripe == STRIPE_NONE
|
|
? (result[stripe].best_ms <= solo_ms * 1.05
|
|
? "still flat - DRAM not the limit"
|
|
: "slower per core than solo")
|
|
: "");
|
|
}
|
|
printf(" %2u | against one door each: ports %.3fx, banks %.3fx\n",
|
|
members, result[STRIPE_NONE].best_ms / result[STRIPE_PORTS].best_ms,
|
|
result[STRIPE_NONE].best_ms / result[STRIPE_BANKS].best_ms);
|
|
fflush(stdout);
|
|
}
|
|
printf("\n read GB/s counts the A and B bytes the readers pull from DRAM, aggregated over\n"
|
|
" the group. `doors` is distinct DRAM endpoints / pairs of them sharing a NoC row.\n"
|
|
" The un-striped arm reads through TWO endpoints however many cores it has and\n"
|
|
" ~45 GB/s each is what it flattens at; the port arm has three doors of each of\n"
|
|
" two banks and a bank serves ~64; the bank arm has a bank per core and the CARD\n"
|
|
" does 502. Row collisions above 12 streams are structural - 24 endpoints, 12\n"
|
|
" rows - and a shared row is ~60 GB/s, far above what these streams ask of it.\n");
|
|
}
|
|
|
|
/**
|
|
* Stage 4 — the RED HALF. Two mutations that must be CAUGHT, on silicon.
|
|
*
|
|
* A multicore test that only detects all-cores-broken is the failure mode to design against:
|
|
* every red proof for a group has to make exactly ONE member wrong and show the others stay
|
|
* green (DEV_NOTES.md §3).
|
|
*
|
|
* (a) ONE member is given the wrong first column. Its panel is then a different, valid piece
|
|
* of the same matmul — no error anywhere, and the byte comparison is the only thing that
|
|
* can see it. If this comes back byte-identical, the comparison is decorative.
|
|
* (b) ONE receiver is given a wrong CB_IN0 base. The kernel's per-block invariant must REFUSE
|
|
* with FTCM_ERROR_WRITE_POINTER rather than let the leader multicast into an address the
|
|
* receivers did not reserve — and the whole group must still finish, drain, and leave
|
|
* every core bootable, which is what the drain rule is for.
|
|
*/
|
|
static void run_red_proofs(coop_fixture *fixture, shape_run *shape)
|
|
{
|
|
ft_tt_op_matmul_cooperative_request request;
|
|
arm_result result;
|
|
const uint32_t members = 2u;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 4 — the red half: one member wrong, on silicon");
|
|
|
|
if ((shape->tile_cols % members) != 0u || fixture->pool_columns < 2u) {
|
|
ft_tt_test_check("the red proofs have a two-member group to run on", false,
|
|
"pool too small or Nt does not divide by 2");
|
|
return;
|
|
}
|
|
|
|
/* (a) Member 1 is told its slice starts at column 0, which is member 0's slice. Nothing
|
|
* fails: it computes a valid piece of the same matmul into its own panel. Only the byte
|
|
* comparison can see it — and the FIRST DIFFERENCE must land at or after member 0's panel
|
|
* ends, which is what says one member was wrong and the other was not. */
|
|
select_rectangle(fixture, 1u, 2u);
|
|
(void)fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_SPLIT, STRIPE_NONE, members);
|
|
rc = run_arm(fixture, shape, &request, members, 1u, FTCM_ARG_B_COLUMN_FIRST, 0u, true,
|
|
&result);
|
|
{
|
|
const size_t panel_bytes = shape->output_bytes / members;
|
|
|
|
ft_tt_test_check_fmt("ONE member given the wrong first column differs in BYTES, and "
|
|
"only in ITS panel",
|
|
rc == FT_TT_OK && !result.bytes_match_solo &&
|
|
result.first_difference >= panel_bytes,
|
|
"rc=%d (%s), kernel error 0x%08x, bytes %s, first difference at "
|
|
"%zu of %zu (member 0's panel ends at %zu) — a green comparison "
|
|
"here would mean the comparison cannot fail",
|
|
rc, ft_tt_error_string(rc), result.first_error,
|
|
result.bytes_match_solo ? "IDENTICAL (BAD)" : "differ (correct)",
|
|
result.first_difference, shape->output_bytes, panel_bytes);
|
|
}
|
|
|
|
/* (b) Member 1 — a RECEIVER, since the leader is member 0 — is given a CB_IN0 base that is
|
|
* legally aligned and simply wrong. Its per-block invariant must REFUSE rather than let
|
|
* the leader multicast into an address the receivers did not reserve. Two things then have
|
|
* to hold and each is a separate check: the refusing member must report exactly
|
|
* FTCM_ERROR_WRITE_POINTER, and the LEADER must give up on its bounded wait rather than
|
|
* spin forever — a group where one member refuses has to end, not hang. */
|
|
select_rectangle(fixture, 1u, 2u);
|
|
(void)fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_COOPERATIVE, STRIPE_NONE, members);
|
|
rc = run_arm(fixture, shape, &request, members, 1u, FTCM_ARG_CB_IN0_BASE,
|
|
FT_TT_OP_CB_ARENA_BASE + 0x40u, false, &result);
|
|
ft_tt_test_check_fmt("the receiver whose CB_IN0 base is wrong REFUSES with the write-"
|
|
"pointer code",
|
|
rc == FT_TT_OK && result.member_error[1] == FTCM_ERROR_WRITE_POINTER,
|
|
"rc=%d (%s), member 1 reported %d (wanted %d), member 0 reported %d",
|
|
rc, ft_tt_error_string(rc), (int)result.member_error[1],
|
|
FTCM_ERROR_WRITE_POINTER, (int)result.member_error[0]);
|
|
ft_tt_test_check_fmt("the LEADER gives up on its bounded wait instead of hanging",
|
|
rc == FT_TT_OK && result.member_error[0] == FTCM_ERROR_WAIT_TIMEOUT,
|
|
"member 0 reported %d (wanted %d, the bounded-wait expiry). A hang "
|
|
"here is a core only assert_reset could end, which is how cores were "
|
|
"wedged (DEV_NOTES.md §1)",
|
|
(int)result.member_error[0], FTCM_ERROR_WAIT_TIMEOUT);
|
|
|
|
/* (c) THE STRIPING RED PROOF. A striped run is byte-identical to the un-striped one, and
|
|
* on its own that is exactly what a run whose door args were IGNORED would also look like.
|
|
* So one member is given a door that is a real, live, telemetry-derived DRAM endpoint —
|
|
* the ACTIVATION bank's port 0 — for its WEIGHT reads. Its panel then comes out of the
|
|
* wrong memory while everything else is untouched, and the first difference has to land at
|
|
* exactly its panel boundary. Green here would mean the whole striping arm is decorative.
|
|
*
|
|
* Only the y word is overridden, and only because the two banks were CHECKED at startup to
|
|
* share a translated x; a half-overridden coordinate that did not would be a made-up
|
|
* endpoint, which is the class of mistake that hangs the NoC rather than failing. */
|
|
if (fixture->dram_a.x != fixture->dram_b.x) {
|
|
ft_tt_test_check("the striping red proof has a same-column bank pair to use", false,
|
|
"A and B banks are in different translated columns, so overriding y "
|
|
"alone would name a coordinate neither of them is");
|
|
return;
|
|
}
|
|
select_rectangle(fixture, 1u, 2u);
|
|
(void)fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_SPLIT, STRIPE_PORTS, members);
|
|
rc = run_arm(fixture, shape, &request, members, 1u, FTCM_ARG_B_Y,
|
|
(uint32_t)fixture->dram_a.y, true, &result);
|
|
{
|
|
const size_t panel_bytes = shape->output_bytes / members;
|
|
|
|
ft_tt_test_check_fmt("ONE striped member given a door on the WRONG BANK differs in "
|
|
"BYTES, and only in ITS panel",
|
|
rc == FT_TT_OK && !result.bytes_match_solo &&
|
|
result.first_difference >= panel_bytes,
|
|
"rc=%d (%s), kernel error 0x%08x, bytes %s, first difference at "
|
|
"%zu of %zu (member 0's panel ends at %zu) — a green comparison "
|
|
"here would mean the reader never used the door it was given, "
|
|
"and every striped number above would be measuring nothing",
|
|
rc, ft_tt_error_string(rc), result.first_error,
|
|
result.bytes_match_solo ? "IDENTICAL (BAD)" : "differ (correct)",
|
|
result.first_difference, shape->output_bytes, panel_bytes);
|
|
}
|
|
|
|
/* (d) THE BANK-STRIPING RED PROOF. Same shape of argument one layer out: a bank-striped
|
|
* run is byte-identical too, and a build that ignored the SHARD LIST would look the same.
|
|
* So member 1's weight shard is pointed at member 0's — a real endpoint holding a real
|
|
* panel of the same matmul, just not member 1's. Its panel then repeats member 0's answer.
|
|
*
|
|
* Planted in the REQUEST rather than as a reader-arg override because a shard is a host
|
|
* layout decision, and overriding one arg word would leave the other three describing the
|
|
* shard the layout actually made. */
|
|
select_rectangle(fixture, 1u, 2u);
|
|
rc = fill_request(&request, fixture, shape, FT_TT_OP_MATMUL_SPLIT, STRIPE_BANKS, members);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the bank-striping red proof staged its shards", false, "rc=%d (%s)",
|
|
rc, ft_tt_error_string(rc));
|
|
return;
|
|
}
|
|
request.weight_shards[1] = request.weight_shards[0];
|
|
rc = run_arm(fixture, shape, &request, members, UINT32_MAX, 0u, 0u, true, &result);
|
|
{
|
|
const size_t panel_bytes = shape->output_bytes / members;
|
|
|
|
ft_tt_test_check_fmt("ONE bank-striped member pointed at ANOTHER member's shard differs "
|
|
"in BYTES, and only in ITS panel",
|
|
rc == FT_TT_OK && !result.bytes_match_solo &&
|
|
result.first_difference >= panel_bytes,
|
|
"rc=%d (%s), kernel error 0x%08x, bytes %s, first difference at "
|
|
"%zu of %zu (member 0's panel ends at %zu) — a green comparison "
|
|
"here would mean the shard list was never read, and the whole "
|
|
"bank-striped column would be measuring the port rotation",
|
|
rc, ft_tt_error_string(rc), result.first_error,
|
|
result.bytes_match_solo ? "IDENTICAL (BAD)" : "differ (correct)",
|
|
result.first_difference, shape->output_bytes, panel_bytes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stage 5 — what state every core is in BEFORE anything re-boots it. READS ONLY, so it is safe
|
|
* on a stuck core, and it runs before stage 6 touches anything. It exists because "the suite
|
|
* left a core unbootable" has two very different causes and a boot failure alone cannot tell
|
|
* the WORK from the RE-BOOT apart (DEV_NOTES.md §1, core 65).
|
|
*/
|
|
static void run_state_before_reboot(coop_fixture *fixture)
|
|
{
|
|
uint32_t slot;
|
|
|
|
ft_tt_test_step("stage 5 — every core's run state BEFORE any re-boot (reads only)");
|
|
|
|
for (slot = 0u; slot < fixture->member_count; slot++) {
|
|
ft_tt_launch_state state;
|
|
int rc = ft_tt_launch_read_state(&fixture->session->noc,
|
|
(uint16_t)fixture->members[slot].x,
|
|
(uint16_t)fixture->members[slot].y, &state);
|
|
|
|
ft_tt_test_check_fmt("a core answers with a plausible run state", rc == FT_TT_OK,
|
|
"slot %u at translated (%u,%u): rc=%d (%s), soft reset 0x%08x, "
|
|
"subordinate sync %02x %02x %02x %02x",
|
|
slot, fixture->members[slot].x, fixture->members[slot].y, rc,
|
|
ft_tt_error_string(rc), state.reset_register,
|
|
state.subordinate_sync[0], state.subordinate_sync[1],
|
|
state.subordinate_sync[2], state.subordinate_sync[3]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stage 6 — every core this suite touched still BOOTS. UNCONDITIONAL, even when an earlier
|
|
* stage failed, because a read cannot tell a wedged core from a healthy idle one and a
|
|
* multicast bug is exactly the kind that wedges a receiver quietly (DEV_NOTES.md §1). The
|
|
* COMPUTE half of the rule is `ft_tt_core probe`, a DIFFERENT triad, run by
|
|
* run_cooperative_matmul_suite.sh immediately after this program.
|
|
*/
|
|
static void run_cores_still_boot(coop_fixture *fixture)
|
|
{
|
|
uint32_t slot;
|
|
|
|
ft_tt_test_step("stage 6 — every core touched still BOOTS (run unconditionally)");
|
|
|
|
for (slot = 0u; slot < fixture->member_count; slot++) {
|
|
int rc = boot_core(fixture, slot);
|
|
|
|
ft_tt_test_check_fmt("a core this suite touched still boots", rc == FT_TT_OK,
|
|
"slot %u at translated (%u,%u): rc=%d (%s)", slot,
|
|
fixture->members[slot].x, fixture->members[slot].y, rc,
|
|
ft_tt_error_string(rc));
|
|
}
|
|
printf(" The COMPUTE half of this check is `ft_tt_core probe`, which drives its own\n"
|
|
" vector_add triad and must be run next — see run_cooperative_matmul_suite.sh.\n\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* The doors the striped arms will use, printed, and the two structural facts they must have.
|
|
*
|
|
* Both are properties of the LIVE layout rather than of this suite, and both decide whether
|
|
* any GB/s below means anything: three doors of one bank must sit on three DIFFERENT NoC rows
|
|
* (a rotation onto one row would just re-share a ~60 GB/s ceiling), and the two banks' doors
|
|
* must not collide with each other's. Printed as well as asserted, because the row numbers are
|
|
* what a later reader needs to check a different bank pair against.
|
|
*/
|
|
static void report_the_doors(const coop_fixture *fixture)
|
|
{
|
|
ft_tt_dram_endpoint all[FT_TT_DRAM_STRIPE_MAX_PORTS * 2u];
|
|
uint32_t count = 0u;
|
|
uint32_t port;
|
|
uint32_t collisions;
|
|
|
|
ft_tt_test_step("the DRAM doors — three NoC ports per bank, from live telemetry");
|
|
for (port = 0u; port < fixture->dram_a_ports.port_count; port++) {
|
|
const ft_tt_dram_endpoint *a = &fixture->dram_a_ports.port[port];
|
|
const ft_tt_dram_endpoint *b = &fixture->dram_b_ports.port[port];
|
|
|
|
printf(" port %u A bank %u -> translated (%u,%u), NoC 0 row %u | "
|
|
"B bank %u -> translated (%u,%u), NoC 0 row %u\n",
|
|
port, a->logical_bank, a->coord.x, a->coord.y, a->noc_row, b->logical_bank,
|
|
b->coord.x, b->coord.y, b->noc_row);
|
|
all[count++] = *a;
|
|
all[count++] = *b;
|
|
}
|
|
collisions = ft_tt_dram_stripe_shared_rows(all, count, NULL, NULL);
|
|
ft_tt_test_check_fmt("the six doors the striped arms use sit on six DIFFERENT NoC rows",
|
|
collisions == 0u,
|
|
"%u colliding pairs of %u doors — two DRAM endpoints on one row share "
|
|
"~60 GB/s, so a rotation onto a shared row buys nothing and the "
|
|
"measurement would be of the row, not of the ports",
|
|
collisions, count);
|
|
ft_tt_test_check_fmt("port 0 of each bank IS what the un-striped arms address",
|
|
fixture->dram_a_ports.port[0].coord.x == fixture->dram_a.x &&
|
|
fixture->dram_a_ports.port[0].coord.y == fixture->dram_a.y &&
|
|
fixture->dram_b_ports.port[0].coord.x == fixture->dram_b.x &&
|
|
fixture->dram_b_ports.port[0].coord.y == fixture->dram_b.y,
|
|
"A port 0 (%u,%u) vs dram_a (%u,%u); B port 0 (%u,%u) vs dram_b "
|
|
"(%u,%u). If these ever differ the two arms are not the same bytes",
|
|
fixture->dram_a_ports.port[0].coord.x,
|
|
fixture->dram_a_ports.port[0].coord.y, fixture->dram_a.x,
|
|
fixture->dram_a.y, fixture->dram_b_ports.port[0].coord.x,
|
|
fixture->dram_b_ports.port[0].coord.y, fixture->dram_b.x,
|
|
fixture->dram_b.y);
|
|
}
|
|
|
|
/** The MiniCPM5-1B shapes, at 32 tokens (Mt = 1), as K x N in the decided orientation.
|
|
* `ffn_gate/up` is the best case for sharing (Nt = 144, so the split has room), `ffn_down`
|
|
* the middle (Nt = 48) and `attn_k/v` the worst (Nt = 8, where a group of 8 leaves one output
|
|
* tile per core). */
|
|
typedef struct model_shape {
|
|
const char *label;
|
|
uint32_t m;
|
|
uint32_t k;
|
|
uint32_t n;
|
|
} model_shape;
|
|
|
|
static const model_shape g_shapes[] = {
|
|
{"ffn_gate/up", 32u, 1536u, 4608u},
|
|
{"ffn_down", 32u, 4608u, 1536u},
|
|
{"attn_k/v", 32u, 1536u, 256u},
|
|
};
|
|
#define SHAPE_COUNT ((uint32_t)(sizeof(g_shapes) / sizeof(g_shapes[0])))
|
|
|
|
static void run_one_shape(coop_fixture *fixture, const model_shape *model, bool independent)
|
|
{
|
|
shape_run shape;
|
|
arm_result solo;
|
|
int rc;
|
|
|
|
memset(&shape, 0, sizeof(shape));
|
|
shape.label = model->label;
|
|
shape.m = model->m;
|
|
shape.k = model->k;
|
|
shape.n = model->n;
|
|
shape.tile_rows = model->m / TILE_DIM;
|
|
shape.tile_inner = model->k / TILE_DIM;
|
|
shape.tile_cols = model->n / TILE_DIM;
|
|
shape.output_bytes = (size_t)shape.tile_rows * shape.tile_cols * FT_TT_OP_TILE_BYTES_BF16;
|
|
|
|
ft_tt_test_step("shape %s — [%ux%u] x [%ux%u], Mt %u Kt %u Nt %u, prefetch %u",
|
|
shape.label, shape.m, shape.k, shape.k, shape.n, shape.tile_rows,
|
|
shape.tile_inner, shape.tile_cols, PREFETCH_TILES);
|
|
|
|
rc = prepare_shape(fixture, &shape);
|
|
ft_tt_test_check_fmt("operands staged in DRAM and the CPU reference computed",
|
|
rc == FT_TT_OK, "%s: rc=%d (%s)", shape.label, rc,
|
|
ft_tt_error_string(rc));
|
|
if (rc != FT_TT_OK) {
|
|
release_shape(&shape);
|
|
return;
|
|
}
|
|
|
|
if (fixture->spread_rounds != 0u) {
|
|
/* One question, one stage: the sweep and the red proofs would put minutes of other
|
|
* work between the samples this stage is trying to compare. */
|
|
if (run_solo_baseline(fixture, &shape, &solo))
|
|
run_spread_stage_all_n(fixture, &shape);
|
|
release_shape(&shape);
|
|
return;
|
|
}
|
|
if (run_solo_baseline(fixture, &shape, &solo)) {
|
|
run_sweep(fixture, &shape, solo.best_ms);
|
|
if (independent)
|
|
run_independent_sweep(fixture, &shape, solo.best_ms);
|
|
run_red_proofs(fixture, &shape);
|
|
}
|
|
release_shape(&shape);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_firmware_set firmware;
|
|
ft_tt_program_directory program;
|
|
ft_tt_noc_path dram_path;
|
|
coop_fixture fixture;
|
|
const char *directory;
|
|
uint32_t slot;
|
|
uint32_t shape_index;
|
|
int rc;
|
|
|
|
if (argc < 2) {
|
|
fprintf(stderr, "usage: %s <program-directory> [repeats]\n", argv[0]);
|
|
return 2;
|
|
}
|
|
directory = argv[1];
|
|
|
|
memset(&fixture, 0, sizeof(fixture));
|
|
fixture.repeats = argc > 2 ? (uint32_t)strtoul(argv[2], NULL, 0) : DEFAULT_REPEATS;
|
|
fixture.first_index = environment_u32("FT_TT_COOP_FIRST_INDEX", DEFAULT_FIRST_INDEX);
|
|
fixture.pool_rows = environment_u32("FT_TT_COOP_ROWS", DEFAULT_POOL_ROWS);
|
|
fixture.pool_columns = environment_u32("FT_TT_COOP_COLUMNS", DEFAULT_POOL_COLUMNS);
|
|
/* Where the WEIGHT sits inside its bank. Overridable so the weight can be put in the SAME
|
|
* DRAM bank as the activation at a different address, which is the only way this harness
|
|
* can ask whether the scaling wall is per-BANK bandwidth or a global one: same cores, same
|
|
* bytes, one bank instead of two. */
|
|
fixture.weight_base = environment_u32("FT_TT_COOP_DRAM_B_BASE", DRAM_BUFFER_BASE);
|
|
/* Non-zero replaces the whole suite with stage S — the run-to-run distribution of the
|
|
* three addressings at one N. Zero (the default) leaves the suite exactly as it was. */
|
|
fixture.spread_rounds = environment_u32("FT_TT_COOP_SPREAD_ROUNDS", 0u);
|
|
fixture.spread_inner = environment_u32("FT_TT_COOP_SPREAD_INNER", 8u);
|
|
if (fixture.repeats == 0u || fixture.pool_rows == 0u || fixture.pool_columns == 0u ||
|
|
fixture.pool_rows * fixture.pool_columns > FT_TT_OP_COOP_MAX_MEMBERS) {
|
|
fprintf(stderr, "repeats must be >= 1 and the pool must be 1..%u cores\n",
|
|
FT_TT_OP_COOP_MAX_MEMBERS);
|
|
return 2;
|
|
}
|
|
if (fixture.pool_columns > 4u)
|
|
fprintf(stderr, "WARNING: a rectangle wider than 4 columns was MEASURED on 2026-08-13 "
|
|
"to leave a minority of its receivers unbootable. Proceeding because "
|
|
"you asked for %u.\n", fixture.pool_columns);
|
|
fixture.member_count = fixture.pool_rows * fixture.pool_columns;
|
|
|
|
/* The wire contract's refusal codes are reproduced by value in the protocol header so it
|
|
* can stay include-free; if the two ever drift, every red proof silently passes. */
|
|
ft_tt_test_step("cooperative matmul — %ux%u cores from live index %u, %u repeats",
|
|
fixture.pool_rows, fixture.pool_columns, fixture.first_index,
|
|
fixture.repeats);
|
|
ft_tt_test_check("the protocol header's refusal codes match the multicast kernel's",
|
|
FTCM_ERROR_ZERO_LENGTH == -1 && FTCM_ERROR_ZERO_DESTINATIONS == -2 &&
|
|
FTCM_ERROR_ALIGNMENT == -3 && FTCM_ERROR_RECTANGLE == -4 &&
|
|
FTCM_ERROR_WAIT_TIMEOUT == -5 && FTCM_ERROR_STOPPED == -6,
|
|
"cooperative_matmul_protocol.h vs kernels/ft_tensix_noc_multicast.h");
|
|
|
|
rc = ft_tt_hardware_session_open(&session);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
fixture.session = &session;
|
|
|
|
/* This arm opens its OWN write-combined window through the tt-kmd context, so it
|
|
* is one of the harnesses that cannot run on the userspace driver: the udrv bulk
|
|
* path is an ft_tt_noc_access (runtime.bulk_dram), not an ft_tt_noc_path, and this
|
|
* fixture holds the latter. It names the transport it needs instead of
|
|
* dereferencing a handle that is NULL there. */
|
|
if (!ft_tt_hardware_session_kmd_context(&session)) {
|
|
ft_tt_log_error("this suite opens a second WC window through the tt-kmd context; "
|
|
"run it with FT_TT_DRIVER=kmd");
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
|
|
rc = ft_tt_noc_path_open(ft_tt_hardware_session_kmd_context(&session), FT_TT_KMD_CACHE_WC,
|
|
&dram_path);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
fixture.dram_path = &dram_path;
|
|
|
|
rc = ft_tt_coord_dram_bank(&session.grid, environment_u32("FT_TT_COOP_DRAM_A",
|
|
DEFAULT_DRAM_BANK_A),
|
|
FT_TT_COORD_TRANSLATED, &fixture.dram_a);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_coord_dram_bank(&session.grid,
|
|
environment_u32("FT_TT_COOP_DRAM_B", DEFAULT_DRAM_BANK_B),
|
|
FT_TT_COORD_TRANSLATED, &fixture.dram_b);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_coord_dram_bank(&session.grid,
|
|
environment_u32("FT_TT_COOP_DRAM_C",
|
|
DEFAULT_DRAM_BANK_OUTPUT),
|
|
FT_TT_COORD_TRANSLATED, &fixture.dram_output);
|
|
/* Every door of the two banks the readers pull from. DERIVED from live telemetry, never
|
|
* probed: a wrong DRAM endpoint does not fail a read, it hangs the chip (DEV_NOTES.md §1).
|
|
* fixture.dram_a / dram_b stay port 0 of the same banks, which is what the un-striped arms
|
|
* use and what `ft_tt_coord_dram_bank()` has always returned. */
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_dram_port_set_build(&session.grid,
|
|
environment_u32("FT_TT_COOP_DRAM_A", DEFAULT_DRAM_BANK_A),
|
|
FT_TT_COORD_TRANSLATED, &fixture.dram_a_ports);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_dram_port_set_build(&session.grid,
|
|
environment_u32("FT_TT_COOP_DRAM_B", DEFAULT_DRAM_BANK_B),
|
|
FT_TT_COORD_TRANSLATED, &fixture.dram_b_ports);
|
|
/* The partitioner that refuses a run of consecutive live indices which wraps a row or
|
|
* crosses the non-Tensix column gap at NoC x = 8, 9. Never walk indices by hand. */
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_group_rectangle_coordinates(&session.grid, fixture.first_index,
|
|
fixture.pool_rows, fixture.pool_columns,
|
|
fixture.members, FT_TT_OP_COOP_MAX_MEMBERS);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check("cores and DRAM banks resolved from live telemetry", false,
|
|
ft_tt_error_string(rc));
|
|
goto close_path;
|
|
}
|
|
|
|
rc = ft_tt_firmware_set_load(directory, &firmware);
|
|
if (rc != FT_TT_OK)
|
|
goto close_path;
|
|
fixture.firmware = &firmware;
|
|
rc = ft_tt_program_directory_load(directory, 0x1Fu, &program);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_firmware_set_release(&firmware);
|
|
goto close_path;
|
|
}
|
|
fixture.program = &program;
|
|
|
|
printf(" group: %ux%u rectangle from live index %u, translated x %u..%u, y %u..%u\n",
|
|
fixture.pool_rows, fixture.pool_columns, fixture.first_index, fixture.members[0].x,
|
|
fixture.members[fixture.pool_columns - 1u].x, fixture.members[0].y,
|
|
fixture.members[fixture.member_count - 1u].y);
|
|
|
|
report_the_doors(&fixture);
|
|
|
|
ft_tt_test_step("boot — %u cores, SEQUENTIALLY (concurrent boots at 12-way and above wedge "
|
|
"cores, DEV_NOTES.md §1)", fixture.member_count);
|
|
for (slot = 0u; slot < fixture.member_count; slot++) {
|
|
rc = boot_core(&fixture, slot);
|
|
ft_tt_test_check_fmt("a core booted with the program loaded", rc == FT_TT_OK,
|
|
"slot %u at translated (%u,%u): rc=%d (%s)", slot,
|
|
fixture.members[slot].x, fixture.members[slot].y, rc,
|
|
ft_tt_error_string(rc));
|
|
if (rc != FT_TT_OK)
|
|
goto health;
|
|
}
|
|
|
|
for (shape_index = 0u; shape_index < SHAPE_COUNT; shape_index++) {
|
|
run_one_shape(&fixture, &g_shapes[shape_index], shape_index == 0u);
|
|
if (fixture.spread_rounds != 0u)
|
|
break; /* the spread stage asks about ONE shape at ONE N; more is noise */
|
|
}
|
|
|
|
health:
|
|
run_state_before_reboot(&fixture);
|
|
run_cores_still_boot(&fixture);
|
|
|
|
ft_tt_program_directory_release(&program);
|
|
ft_tt_firmware_set_release(&firmware);
|
|
|
|
close_path:
|
|
(void)ft_tt_noc_path_close(&dram_path);
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|