1541 lines
70 KiB
C
1541 lines
70 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_phase6_triad.c
|
|
* @brief The RESIDENT TRIAD on the card: a real matmul running through a reader/compute/
|
|
* writer triad that never returns between ops, that can be asked to stop while it is
|
|
* blocked in `cb_wait_front`, and that leaves the core reusable afterwards.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT THIS FILE HAS TO SETTLE, AND HOW EACH STAGE SETTLES IT
|
|
* ---------------------------------------------------------------------------
|
|
* Phase 5 left one design question: its resident kernel is BRISC-only and CB-free on
|
|
* purpose, because `cb_wait_front` is the one wait the host cannot break and killing a
|
|
* kernel blocked in it is how Tensix 0 was wedged (dev notes §1). A real op needs a triad.
|
|
* So a resident triad either makes the CB wait stop-aware or accepts `assert_reset` as its
|
|
* only exit, and the second is not acceptable.
|
|
*
|
|
* stage 1 the triad computes a REAL matmul, byte-for-byte identical to the Phase 4
|
|
* slow-dispatch arm on the same inputs, over three different input sets with the
|
|
* destination poisoned before each — so "identical" cannot be stale DRAM
|
|
* stage 2 the dispatch A/B on a REAL op. Every Phase 5 figure is for a NOP payload; this
|
|
* is what a resident triad does to a matmul's 0.245 ms fixed cost, in GFLOP/s
|
|
* and in % of one Tensix's 1.64 TFLOP/s derived peak
|
|
* stage 3 what the stoppable wait COSTS: the same triad built with upstream's own
|
|
* blocking waits versus the stoppable ones, interleaved, same shape
|
|
* stage 4 the acceptance. A DELIBERATE CB DEADLOCK — the reader withholds the last block
|
|
* of a real matmul, so the compute thread is stuck in the op's own
|
|
* `cb_wait_front` — caught by the watchdog, which names WHICH PROCESSOR and
|
|
* WHICH WAIT; retired by ONE WRITE; and then the SAME CORE runs a fresh matmul
|
|
* correctly
|
|
* stage 5 the RED PROOF for stage 4: the same deadlock against a build whose stop check
|
|
* is deliberately removed. It must NOT stop when asked. This is safe to run only
|
|
* because every wait is bounded independently of the ask, so the worst case is
|
|
* `kErrCbWaitTimeout` and not a wedged core
|
|
* stage 6 group-level completion: two cores, a sequence number the KERNELS write, and
|
|
* the check refusing when one member is behind
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY BYTE-EXACTNESS AND NOT PCC
|
|
* ---------------------------------------------------------------------------
|
|
* PCC is blind to a consistent permutation and degrades gently under a smooth corruption
|
|
* (dev notes §3), and both arms here run the SAME compute source — so a difference between
|
|
* them is a plumbing bug, and the sensitive instrument for a plumbing bug is a byte
|
|
* comparison, not a correlation. The Phase 4 arm is already PCC-validated against a CPU
|
|
* fp32 reference (`tests/test_ops.c`, bring-up log §14); this file's job is to show the
|
|
* resident path does not change the answer. "Nine identical digits is evidence; an
|
|
* identical file is proof."
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* SAFETY
|
|
* ---------------------------------------------------------------------------
|
|
* NOTHING HERE ASSERTS RESET. Every recovery is `stop_request`. Cores are parked only after
|
|
* every processor has reported EXIT — and for a triad that means all five status slots, not
|
|
* the control block's single `state` word, which has one writer and would happily read EXIT
|
|
* while a peer was still spinning.
|
|
*
|
|
* The kWaitUpstream build is NEVER deadlocked. It has no escape and no bound; deadlocking it
|
|
* is exactly the wedge this phase exists to prevent, and it exists only as the cost arm.
|
|
*
|
|
* Usage:
|
|
* FT_TT_FIRMWARE_DIR=../programs/rtriad FT_TT_TENSIX_INDICES=60,61 \
|
|
* ./build-phase6/test_phase6_triad ../programs/rtriad ../programs/rtriad_upstream \
|
|
* ../programs/rtriad_boundonly ../programs/matmul
|
|
*
|
|
* @status See docs/tt_bringup_log.md.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* clock_gettime and CLOCK_MONOTONIC are POSIX.1-2008; -std=c99 turns off glibc's default
|
|
* feature set, so ask for them by name or the Linux card host's gcc hides them. */
|
|
#ifndef _POSIX_C_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
|
|
#include "ft_tt_group.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_op_dispatch.h"
|
|
#include "ft_tt_op_l1_layout.h"
|
|
#include "ft_tt_op_matmul.h"
|
|
#include "ft_tt_program_directory.h"
|
|
#include "ft_tt_queue.h"
|
|
#include "ft_tt_test_report.h"
|
|
#include "ft_tt_tilize.h"
|
|
#include "ft_tt_watchdog.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* ================================================================
|
|
* The shape, and the constants the numbers are quoted against
|
|
* ================================================================ */
|
|
|
|
/** One Tensix's derived peak at this card's measured 800 MHz busy clock, HiFi4. Every
|
|
* figure in this file is PER CORE and a fraction of THIS — which is what makes it portable
|
|
* to a 120-core p150 (dev notes §6). */
|
|
#define TENSIX_PEAK_GFLOPS 1640.0
|
|
|
|
/** MiniCPM5-1B's `ffn_gate/up` family: 32 tokens, 1536 in, 4608 out. Chosen because it is
|
|
* the shape §16 and §18 measured, so the numbers here sit beside those rather than beside
|
|
* nothing. depth 48 divides Kt exactly, which the reader requires. */
|
|
#define TILE_ROWS 1u
|
|
#define TILE_INNER 48u
|
|
#define TILE_COLS 144u
|
|
#define PREFETCH_DEPTH 48u
|
|
|
|
#define TILE_BYTES FT_TT_OP_TILE_BYTES_BF16
|
|
#define A_TILES (TILE_ROWS * TILE_INNER)
|
|
#define B_TILES (TILE_INNER * TILE_COLS)
|
|
#define C_TILES (TILE_ROWS * TILE_COLS)
|
|
|
|
/** Where the operands live. Separate banks so a read and a write cannot alias. */
|
|
#define DRAM_BANK_A 0u
|
|
#define DRAM_BANK_B 1u
|
|
#define DRAM_BANK_C 2u
|
|
#define DRAM_BUFFER_BASE 0x00100000u
|
|
|
|
#define CORE_COUNT 2u
|
|
#define QUEUE_CAPACITY 64u
|
|
#define RUN_TIMEOUT_MS 20000u
|
|
#define POISON_BYTE 0x5Au
|
|
|
|
/** The two bounds.
|
|
*
|
|
* The red-proof bound has to be LONGER than the window the ask is given, not shorter, and
|
|
* getting that backwards made the first red run green for the wrong reason: at 40M spins
|
|
* (~0.4 s here) every processor had already retired ITSELF on the bound before the shutdown
|
|
* was even called, so `unstopped` came back 0 and the case passed against a build whose ask
|
|
* does nothing. A red proof that the bound can satisfy is not a proof of the ask.
|
|
*
|
|
* So: ~400M spins is ~10 s of blocked polling, the ask is given 3 s, and the two outcomes
|
|
* are then distinguishable — "it stopped in milliseconds because it was asked" versus "the
|
|
* shutdown timed out and the BOUND cleaned up afterwards". */
|
|
#define IDLE_SPIN_LIMIT 400000000u
|
|
#define CB_SPIN_LIMIT 400000000u
|
|
#define RED_PROOF_SHUTDOWN_MS 3000u
|
|
|
|
/** Every processor of the triad is resident. */
|
|
#define TRIAD_MASK \
|
|
((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) | (1u << FT_TT_PROC_MATH0) | \
|
|
(1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2))
|
|
|
|
#define CB_IN0 0u
|
|
#define CB_IN1 1u
|
|
#define CB_OUT 16u
|
|
|
|
#define CORE_INDICES_ENVIRONMENT "FT_TT_TENSIX_INDICES"
|
|
#define AB_ITERATIONS_ENVIRONMENT "FT_TT_TRIAD_AB_ITERATIONS"
|
|
|
|
static uint32_t g_core_indices[CORE_COUNT] = { 60u, 61u };
|
|
static uint32_t g_ab_iterations = 8u;
|
|
|
|
static ft_tt_launch_core_placement g_placements[CORE_COUNT];
|
|
static ft_tt_queue g_queues[CORE_COUNT];
|
|
static ft_tt_core_group g_group;
|
|
static ft_tt_noc_coord g_dram[8];
|
|
|
|
static ft_tt_program_directory g_triad; /* stoppable — what ships */
|
|
static ft_tt_program_directory g_triad_upstream; /* upstream's blocking waits — cost arm */
|
|
static ft_tt_program_directory g_triad_boundonly;/* the ask removed — the red proof */
|
|
static ft_tt_program_directory g_matmul; /* the Phase 4 slow-dispatch arm */
|
|
|
|
static ft_tt_launch_config g_triad_config[CORE_COUNT];
|
|
static unsigned char g_cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4];
|
|
static ft_tt_launch_cb_entry g_cb_placed[3];
|
|
|
|
static uint16_t *g_a_tiles;
|
|
static uint16_t *g_b_tiles;
|
|
static uint16_t *g_reference; /* the Phase 4 arm's output, byte-for-byte */
|
|
static uint16_t *g_observed;
|
|
|
|
/* ================================================================
|
|
* Small helpers
|
|
* ================================================================ */
|
|
|
|
static uint64_t monotonic_microseconds(void)
|
|
{
|
|
struct timespec now;
|
|
|
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
|
|
return 0u;
|
|
return (uint64_t)now.tv_sec * 1000000u + (uint64_t)(now.tv_nsec / 1000);
|
|
}
|
|
|
|
static uint32_t g_random_state = 1u;
|
|
|
|
static void random_seed(uint32_t seed)
|
|
{
|
|
g_random_state = seed ? seed : 1u;
|
|
}
|
|
|
|
/** A full-mantissa value. Small integers are the WRONG choice for a layout or plumbing
|
|
* test: a bf16-exact array is bit-identical to several wrong renderings of itself, which
|
|
* made two deliberately-different builds byte-exact once (dev notes §3). */
|
|
static float random_value(void)
|
|
{
|
|
g_random_state = g_random_state * 1664525u + 1013904223u;
|
|
return ((float)(g_random_state >> 8) / (float)(1u << 24)) * 2.0f - 1.0f;
|
|
}
|
|
|
|
static uint16_t bf16_from_float(float value)
|
|
{
|
|
union {
|
|
float f;
|
|
uint32_t u;
|
|
} bits;
|
|
|
|
bits.f = value;
|
|
/* Round-to-nearest-even on the truncated 16 bits, the same rule the converter uses. */
|
|
return (uint16_t)((bits.u + 0x7FFFu + ((bits.u >> 16) & 1u)) >> 16);
|
|
}
|
|
|
|
static int device_write(const ft_tt_noc_access *noc, ft_tt_noc_coord where, uint32_t address,
|
|
const void *source, size_t bytes)
|
|
{
|
|
const unsigned char *cursor = source;
|
|
size_t offset = 0;
|
|
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > 0x10000u)
|
|
chunk = 0x10000u;
|
|
rc = noc->write(noc->user, (uint16_t)where.x, (uint16_t)where.y,
|
|
(uint64_t)address + offset, cursor + offset, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int device_read(const ft_tt_noc_access *noc, ft_tt_noc_coord where, uint32_t address,
|
|
void *destination, size_t bytes)
|
|
{
|
|
unsigned char *cursor = destination;
|
|
size_t offset = 0;
|
|
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > 0x10000u)
|
|
chunk = 0x10000u;
|
|
rc = noc->read(noc->user, (uint16_t)where.x, (uint16_t)where.y,
|
|
(uint64_t)address + offset, cursor + offset, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** Poison the output bank, so an arm that computed nothing is visible as such rather than
|
|
* as whatever the previous arm left there. */
|
|
static int poison_output(const ft_tt_noc_access *noc)
|
|
{
|
|
unsigned char pattern[0x1000];
|
|
size_t bytes = (size_t)C_TILES * TILE_BYTES;
|
|
size_t offset = 0;
|
|
|
|
memset(pattern, POISON_BYTE, sizeof(pattern));
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > sizeof(pattern))
|
|
chunk = sizeof(pattern);
|
|
rc = noc->write(noc->user, (uint16_t)g_dram[DRAM_BANK_C].x, (uint16_t)g_dram[DRAM_BANK_C].y,
|
|
(uint64_t)DRAM_BUFFER_BASE + offset, pattern, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** GFLOP/s for this shape at a measured wall time. 2 flops per multiply-accumulate. */
|
|
static double gflops_at(uint64_t microseconds)
|
|
{
|
|
const double flops = 2.0 * (double)(TILE_ROWS * FT_TT_OP_TILE_DIM) *
|
|
(double)(TILE_INNER * FT_TT_OP_TILE_DIM) *
|
|
(double)(TILE_COLS * FT_TT_OP_TILE_DIM);
|
|
|
|
if (microseconds == 0u)
|
|
return 0.0;
|
|
return flops / ((double)microseconds * 1e3);
|
|
}
|
|
|
|
/* ================================================================
|
|
* Operands
|
|
* ================================================================ */
|
|
|
|
/** Upload a fresh pair of operands, seeded so each round is a DIFFERENT matrix. Byte
|
|
* equality between two arms means nothing if both arms could be reading the same stale
|
|
* DRAM as the previous round — so the data has to move. */
|
|
static int upload_operands(const ft_tt_noc_access *noc, uint32_t seed)
|
|
{
|
|
const size_t a_elements = (size_t)TILE_ROWS * FT_TT_OP_TILE_DIM * TILE_INNER * FT_TT_OP_TILE_DIM;
|
|
const size_t b_elements = (size_t)TILE_INNER * FT_TT_OP_TILE_DIM * TILE_COLS * FT_TT_OP_TILE_DIM;
|
|
uint16_t *row_major = malloc((a_elements > b_elements ? b_elements : b_elements) *
|
|
sizeof(uint16_t));
|
|
size_t index;
|
|
int rc;
|
|
|
|
if (!row_major)
|
|
return FT_TT_ENOMEM;
|
|
|
|
random_seed(seed);
|
|
for (index = 0; index < a_elements; index++)
|
|
row_major[index] = bf16_from_float(random_value());
|
|
rc = ft_tt_tilize_matrix(row_major, sizeof(uint16_t), TILE_ROWS * FT_TT_OP_TILE_DIM,
|
|
TILE_INNER * FT_TT_OP_TILE_DIM, g_a_tiles);
|
|
if (rc != FT_TT_OK)
|
|
goto cleanup;
|
|
|
|
for (index = 0; index < b_elements; index++)
|
|
row_major[index] = bf16_from_float(random_value());
|
|
rc = ft_tt_tilize_matrix(row_major, sizeof(uint16_t), TILE_INNER * FT_TT_OP_TILE_DIM,
|
|
TILE_COLS * FT_TT_OP_TILE_DIM, g_b_tiles);
|
|
if (rc != FT_TT_OK)
|
|
goto cleanup;
|
|
|
|
rc = device_write(noc, g_dram[DRAM_BANK_A], DRAM_BUFFER_BASE, g_a_tiles,
|
|
(size_t)A_TILES * TILE_BYTES);
|
|
if (rc == FT_TT_OK)
|
|
rc = device_write(noc, g_dram[DRAM_BANK_B], DRAM_BUFFER_BASE, g_b_tiles,
|
|
(size_t)B_TILES * TILE_BYTES);
|
|
|
|
cleanup:
|
|
free(row_major);
|
|
return rc;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The Phase 4 arm
|
|
* ================================================================ */
|
|
|
|
static void fill_matmul_request(ft_tt_op_matmul_request *request)
|
|
{
|
|
memset(request, 0, sizeof(*request));
|
|
request->tile_rows = TILE_ROWS;
|
|
request->tile_inner = TILE_INNER;
|
|
request->tile_cols = TILE_COLS;
|
|
request->prefetch_tiles = PREFETCH_DEPTH;
|
|
/* K-major, because upload_operands() tilizes B in its natural [K x N] order. The field
|
|
* is a STATEMENT ABOUT DRAM, not a request: both index formulas are in range, so a
|
|
* wrong value here is a wrong answer with no error anywhere. */
|
|
request->weight_n_major = 0u;
|
|
request->activation.x = (uint16_t)g_dram[DRAM_BANK_A].x;
|
|
request->activation.y = (uint16_t)g_dram[DRAM_BANK_A].y;
|
|
request->activation.address = DRAM_BUFFER_BASE;
|
|
request->activation.tile_bytes = TILE_BYTES;
|
|
request->weight.x = (uint16_t)g_dram[DRAM_BANK_B].x;
|
|
request->weight.y = (uint16_t)g_dram[DRAM_BANK_B].y;
|
|
request->weight.address = DRAM_BUFFER_BASE;
|
|
request->weight.tile_bytes = TILE_BYTES;
|
|
request->output.x = (uint16_t)g_dram[DRAM_BANK_C].x;
|
|
request->output.y = (uint16_t)g_dram[DRAM_BANK_C].y;
|
|
request->output.address = DRAM_BUFFER_BASE;
|
|
request->output.tile_bytes = TILE_BYTES;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The resident triad
|
|
* ================================================================ */
|
|
|
|
/** Place the triad's three circular buffers, exactly as ft_tt_op_matmul.c does: two blocks
|
|
* of `depth` pages on each input so the reader fills one while the compute thread consumes
|
|
* the other, and two pages out. */
|
|
static int place_circular_buffers(ft_tt_launch_config *config)
|
|
{
|
|
ft_tt_op_cb_request requests[3];
|
|
|
|
requests[0].index = CB_IN0;
|
|
requests[0].page_bytes = TILE_BYTES;
|
|
requests[0].page_count = PREFETCH_DEPTH * 2u;
|
|
requests[1].index = CB_IN1;
|
|
requests[1].page_bytes = TILE_BYTES;
|
|
requests[1].page_count = PREFETCH_DEPTH * 2u;
|
|
requests[2].index = CB_OUT;
|
|
requests[2].page_bytes = TILE_BYTES;
|
|
requests[2].page_count = 2u;
|
|
return ft_tt_op_cb_place(requests, 3u, g_cb_placed, g_cb_buffer, sizeof(g_cb_buffer),
|
|
&config->local_cb_mask);
|
|
}
|
|
|
|
/**
|
|
* Stage and KICK a resident triad: five images already in L1, five runtime-arg blocks, the
|
|
* CB descriptor array, the launch message, GO — and then the host walks away. Everything
|
|
* after this is `ft_tt_queue_enqueue()`.
|
|
*/
|
|
static int start_triad(const ft_tt_noc_access *noc, const ft_tt_core_group *group, uint32_t slot,
|
|
const ft_tt_launch_config *config, uint32_t cb_spin_limit)
|
|
{
|
|
static const unsigned processors[5] = { FT_TT_PROC_DM0, FT_TT_PROC_DM1, FT_TT_PROC_MATH0,
|
|
FT_TT_PROC_MATH1, FT_TT_PROC_MATH2 };
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&group->cores, slot);
|
|
uint32_t args[5][FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
ft_tt_op_args blocks[5];
|
|
unsigned index;
|
|
int rc;
|
|
|
|
for (index = 0; index < 5u; index++) {
|
|
ft_tt_queue_kernel_binding binding;
|
|
|
|
/* DM1 is the publisher: the writer owns completion, per core and per group. Giving
|
|
* a second processor the group address would give one slot two writers, which is
|
|
* the one property the slot array exists to have. */
|
|
rc = ft_tt_group_kernel_binding(group, slot, processors[index], FT_TT_PROC_DM1,
|
|
IDLE_SPIN_LIMIT, cb_spin_limit, &binding);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_queue_kernel_args(&group->queues[slot], &binding, args[index]);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
blocks[index].processor = processors[index];
|
|
blocks[index].values = args[index];
|
|
blocks[index].count = FT_TT_QUEUE_KERNEL_ARG_COUNT;
|
|
}
|
|
|
|
rc = ft_tt_op_stage(noc, placement->x, placement->y, config, g_cb_buffer, sizeof(g_cb_buffer),
|
|
blocks, 5u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_send_go(noc, placement->x, placement->y);
|
|
}
|
|
|
|
/** Load all five images of one program build into a core, replacing whatever is there. */
|
|
static int load_triad_images(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
|
|
const ft_tt_program_directory *program)
|
|
{
|
|
unsigned index;
|
|
|
|
for (index = 0; index < program->program.image_count; index++) {
|
|
const ft_tt_kernel_image *image = &program->program.images[index];
|
|
int rc = ft_tt_launch_load_image(noc, x, y, image->processor, FT_TT_IMAGE_KERNEL,
|
|
image->elf_data, image->elf_size, NULL);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** Build one matmul descriptor plus the parameter block it points at, at an EXPLICIT
|
|
* output-tile shape. `enqueue_matmul()` is this at the shipped shape; the shape is a
|
|
* parameter only so the CB-cycle red proof can name one the shipped path never uses. */
|
|
static int enqueue_matmul_shaped(const ft_tt_noc_access *noc, ft_tt_queue *queue, uint32_t slot,
|
|
uint32_t starve_blocks, uint32_t group_sequence,
|
|
uint32_t tile_rows, uint32_t tile_cols)
|
|
{
|
|
uint32_t parameters[FT_TT_QUEUE_MATMUL_WORDS];
|
|
ft_tt_queue_descriptor descriptor;
|
|
uint32_t address = 0u;
|
|
int rc;
|
|
|
|
memset(parameters, 0, sizeof(parameters));
|
|
parameters[FT_TT_QUEUE_MATMUL_TILE_ROWS] = tile_rows;
|
|
parameters[FT_TT_QUEUE_MATMUL_TILE_INNER] = TILE_INNER;
|
|
parameters[FT_TT_QUEUE_MATMUL_TILE_COLS] = tile_cols;
|
|
parameters[FT_TT_QUEUE_MATMUL_A_X] = g_dram[DRAM_BANK_A].x;
|
|
parameters[FT_TT_QUEUE_MATMUL_A_Y] = g_dram[DRAM_BANK_A].y;
|
|
parameters[FT_TT_QUEUE_MATMUL_A_BASE] = DRAM_BUFFER_BASE;
|
|
parameters[FT_TT_QUEUE_MATMUL_B_X] = g_dram[DRAM_BANK_B].x;
|
|
parameters[FT_TT_QUEUE_MATMUL_B_Y] = g_dram[DRAM_BANK_B].y;
|
|
parameters[FT_TT_QUEUE_MATMUL_B_BASE] = DRAM_BUFFER_BASE;
|
|
parameters[FT_TT_QUEUE_MATMUL_C_X] = g_dram[DRAM_BANK_C].x;
|
|
parameters[FT_TT_QUEUE_MATMUL_C_Y] = g_dram[DRAM_BANK_C].y;
|
|
parameters[FT_TT_QUEUE_MATMUL_C_BASE] = DRAM_BUFFER_BASE;
|
|
parameters[FT_TT_QUEUE_MATMUL_DEPTH] = PREFETCH_DEPTH;
|
|
parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] = 0u;
|
|
|
|
rc = ft_tt_queue_write_matmul_parameters(noc, queue, slot, parameters, &address);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_MATMUL;
|
|
descriptor.a0 = address;
|
|
descriptor.a1 = starve_blocks;
|
|
descriptor.a5 = group_sequence;
|
|
return ft_tt_queue_enqueue(noc, queue, &descriptor, 1u);
|
|
}
|
|
|
|
/** Build one matmul descriptor at the shipped shape. */
|
|
static int enqueue_matmul(const ft_tt_noc_access *noc, ft_tt_queue *queue, uint32_t slot,
|
|
uint32_t starve_blocks, uint32_t group_sequence)
|
|
{
|
|
return enqueue_matmul_shaped(noc, queue, slot, starve_blocks, group_sequence, TILE_ROWS,
|
|
TILE_COLS);
|
|
}
|
|
|
|
/* ================================================================
|
|
* Setup
|
|
* ================================================================ */
|
|
|
|
static bool load_environment(void)
|
|
{
|
|
const char *indices = getenv(CORE_INDICES_ENVIRONMENT);
|
|
const char *iterations = getenv(AB_ITERATIONS_ENVIRONMENT);
|
|
|
|
if (indices && indices[0] != '\0') {
|
|
unsigned count = 0;
|
|
const char *cursor = indices;
|
|
|
|
while (*cursor != '\0') {
|
|
char *end = NULL;
|
|
unsigned long value = strtoul(cursor, &end, 10);
|
|
|
|
if (end == cursor || count >= CORE_COUNT) {
|
|
ft_tt_test_check_fmt(CORE_INDICES_ENVIRONMENT " parses", false, "at \"%s\"", cursor);
|
|
return false;
|
|
}
|
|
g_core_indices[count++] = (uint32_t)value;
|
|
cursor = (*end == ',') ? end + 1 : end;
|
|
}
|
|
if (count != CORE_COUNT) {
|
|
ft_tt_test_check_fmt(CORE_INDICES_ENVIRONMENT " names every core", false,
|
|
"got %u, need %u", count, CORE_COUNT);
|
|
return false;
|
|
}
|
|
}
|
|
if (iterations && iterations[0] != '\0')
|
|
g_ab_iterations = (uint32_t)strtoul(iterations, NULL, 10);
|
|
if (g_ab_iterations == 0u || g_ab_iterations > 64u)
|
|
g_ab_iterations = 8u;
|
|
return true;
|
|
}
|
|
|
|
static bool build_group(const ft_tt_hardware_session *session)
|
|
{
|
|
unsigned slot;
|
|
int rc;
|
|
|
|
memset(&g_group, 0, sizeof(g_group));
|
|
g_group.id = 0u;
|
|
g_group.first_tensix_index = g_core_indices[0];
|
|
g_group.tensix_index_count = CORE_COUNT;
|
|
g_group.queues = g_queues;
|
|
g_group.resident_processor_mask = TRIAD_MASK;
|
|
|
|
rc = ft_tt_launch_core_set_init(&g_group.cores, g_placements, CORE_COUNT);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (slot = 0; slot < CORE_COUNT; slot++) {
|
|
const ft_tt_launch_core_placement *placement;
|
|
|
|
rc = ft_tt_launch_core_set_add_tensix(&g_group.cores, &session->grid, g_core_indices[slot],
|
|
NULL);
|
|
placement = ft_tt_launch_core_set_at(&g_group.cores, slot);
|
|
ft_tt_test_check_fmt("coordinate derives and classifies as a Tensix",
|
|
rc == FT_TT_OK && placement != NULL, "index %u -> (%u, %u), rc=%d",
|
|
g_core_indices[slot], placement ? placement->x : 0u,
|
|
placement ? placement->y : 0u, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool boot_and_place(const ft_tt_hardware_session *session,
|
|
const ft_tt_firmware_set *firmware)
|
|
{
|
|
unsigned slot;
|
|
|
|
ft_tt_test_step("boot firmware, place the circular buffers, load the triad");
|
|
|
|
for (slot = 0; slot < CORE_COUNT; slot++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_group.cores, slot);
|
|
int rc = ft_tt_core_boot_firmware(&session->noc, placement->x, placement->y, firmware, NULL);
|
|
|
|
ft_tt_test_check_fmt("firmware booted", rc == FT_TT_OK, "(%u, %u) rc=%d", placement->x,
|
|
placement->y, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = ft_tt_op_program_load(&session->noc, placement->x, placement->y, &g_triad.program,
|
|
NULL, &g_triad_config[slot]);
|
|
ft_tt_test_check_fmt("all five triad images load into their slabs", rc == FT_TT_OK,
|
|
"(%u, %u) rc=%d", placement->x, placement->y, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = place_circular_buffers(&g_triad_config[slot]);
|
|
ft_tt_test_check_fmt("the three circular buffers place in the arena", rc == FT_TT_OK,
|
|
"rc=%d, depth %u", rc, PREFETCH_DEPTH);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
g_triad_config[slot].local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_triad_config[slot].brisc_noc_id = 0u;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 1 — the triad computes a real matmul, byte-identical
|
|
* ================================================================ */
|
|
|
|
static bool stage_1_real_op(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&g_group.cores, 0u);
|
|
ft_tt_op_matmul_request request;
|
|
unsigned round;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 1 — the resident triad runs a REAL matmul, byte-identical to the "
|
|
"Phase 4 arm (Mt=%u Kt=%u Nt=%u, depth %u)",
|
|
TILE_ROWS, TILE_INNER, TILE_COLS, PREFETCH_DEPTH);
|
|
|
|
fill_matmul_request(&request);
|
|
|
|
for (round = 0; round < 3u; round++) {
|
|
size_t bytes = (size_t)C_TILES * TILE_BYTES;
|
|
uint32_t completed = 0u;
|
|
|
|
/* Fresh operands every round: byte equality between two arms proves nothing if a
|
|
* stale DRAM buffer could satisfy both. */
|
|
rc = upload_operands(&session->noc, 0xA5000000u + round);
|
|
ft_tt_test_check_fmt("operands uploaded", rc == FT_TT_OK, "round %u rc=%d", round, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* --- the Phase 4 arm, which test_ops.c has already scored against a CPU fp32
|
|
* reference. Its output is this round's reference. --- */
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_matmul_run(&session->noc, placement->x, placement->y, &g_matmul.program,
|
|
&request, RUN_TIMEOUT_MS);
|
|
ft_tt_test_check_fmt("the Phase 4 slow-dispatch arm completes", rc == FT_TT_OK,
|
|
"round %u rc=%d", round, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_C], DRAM_BUFFER_BASE, g_reference, bytes);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
{
|
|
size_t poisoned = 0;
|
|
size_t index;
|
|
|
|
for (index = 0; index < bytes; index++)
|
|
if (((const unsigned char *)g_reference)[index] == POISON_BYTE)
|
|
poisoned++;
|
|
ft_tt_test_check_fmt("and it wrote over the poison", poisoned * 100u < bytes,
|
|
"round %u: %zu of %zu bytes still poison", round, poisoned, bytes);
|
|
}
|
|
|
|
/* --- the resident triad, on the SAME core and the SAME inputs --- */
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
ft_tt_test_check_fmt("the triad is staged and kicked", rc == FT_TT_OK, "round %u rc=%d",
|
|
round, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("the triad completed the matmul descriptor", rc == FT_TT_OK,
|
|
"round %u completed=%u", round, completed);
|
|
|
|
{
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
bool evidenced;
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queues[0], fields);
|
|
evidenced = ft_tt_queue_check_completion(&g_queues[0], fields, 1u, &reason) == FT_TT_OK;
|
|
ft_tt_test_check_fmt("completion is evidenced by the WRITER, not by a run message",
|
|
evidenced, "round %u: %s", round, reason ? reason : "ok");
|
|
}
|
|
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_C], DRAM_BUFFER_BASE, g_observed, bytes);
|
|
ft_tt_test_check_fmt("the triad's output reads back", rc == FT_TT_OK, "round %u rc=%d",
|
|
round, rc);
|
|
|
|
/* THE CHECK. Not PCC: both arms run the same compute source, so a difference is a
|
|
* plumbing bug and a byte comparison is the instrument for one. */
|
|
{
|
|
size_t index;
|
|
size_t first = bytes;
|
|
size_t mismatches = 0;
|
|
|
|
for (index = 0; index < bytes; index++) {
|
|
if (((const unsigned char *)g_observed)[index] !=
|
|
((const unsigned char *)g_reference)[index]) {
|
|
mismatches++;
|
|
if (first == bytes)
|
|
first = index;
|
|
}
|
|
}
|
|
ft_tt_test_check_fmt("and it is BYTE-IDENTICAL to the Phase 4 arm", mismatches == 0,
|
|
"round %u: %zu of %zu bytes differ, first at %zu", round,
|
|
mismatches, bytes, first);
|
|
}
|
|
|
|
/* The triad is STILL RESIDENT — that is what resident means. A second descriptor
|
|
* with no launch of any kind is the proof. */
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 1u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 2u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("a SECOND matmul runs with no launch of any kind", rc == FT_TT_OK,
|
|
"round %u completed=%u", round, completed);
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_group;
|
|
|
|
solo.cores.count = 1u;
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("all five processors retire when asked, with no reset anywhere",
|
|
rc == FT_TT_OK && unstopped == 0u, "round %u rc=%d unstopped=%u",
|
|
round, rc, unstopped);
|
|
}
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 2 — the REAL-OP dispatch A/B
|
|
* ================================================================ */
|
|
|
|
/** The Phase 4 shape without the image reload: CB config + five arg blocks + launch message
|
|
* + GO + poll DONE. Splitting it out is what separates "the images crossed the x1 link"
|
|
* from "the launch protocol ran", and those are very different fractions of 0.245 ms. */
|
|
static int slow_dispatch_staged(const ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
const ft_tt_launch_config *config, const ft_tt_op_args *args,
|
|
unsigned arg_count)
|
|
{
|
|
int rc = ft_tt_op_stage(&session->noc, x, y, config, g_cb_buffer, sizeof(g_cb_buffer), args,
|
|
arg_count);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_launch_send_go(&session->noc, x, y);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_wait_done(&session->noc, x, y, RUN_TIMEOUT_MS, NULL);
|
|
}
|
|
|
|
static bool stage_2_real_op_dispatch(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&g_group.cores, 0u);
|
|
ft_tt_op_matmul_request request;
|
|
uint64_t full_total = 0u;
|
|
uint64_t staged_total = 0u;
|
|
uint64_t queue_total = 0u;
|
|
uint32_t round;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 2 — dispatch on a REAL op, %u matmuls per arm, arms interleaved",
|
|
g_ab_iterations);
|
|
|
|
fill_matmul_request(&request);
|
|
rc = upload_operands(&session->noc, 0xBEEF0001u);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (round = 0; round < g_ab_iterations; round++) {
|
|
uint64_t started;
|
|
|
|
/* ---- arm A1: the whole Phase 4 op call, images and all ---- */
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_op_matmul_run(&session->noc, placement->x, placement->y, &g_matmul.program,
|
|
&request, RUN_TIMEOUT_MS);
|
|
full_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm A1 completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ---- arm A2: the same launch protocol with the images already in L1 ---- */
|
|
{
|
|
uint32_t reader_args[11];
|
|
uint32_t writer_args[4];
|
|
uint32_t compute_args[3];
|
|
ft_tt_op_args args[5];
|
|
ft_tt_launch_config config;
|
|
|
|
rc = ft_tt_op_program_load(&session->noc, placement->x, placement->y,
|
|
&g_matmul.program, NULL, &config);
|
|
if (rc == FT_TT_OK)
|
|
rc = place_circular_buffers(&config);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
config.brisc_noc_id = 0u;
|
|
|
|
reader_args[0] = TILE_ROWS;
|
|
reader_args[1] = TILE_INNER;
|
|
reader_args[2] = TILE_COLS;
|
|
reader_args[3] = g_dram[DRAM_BANK_A].x;
|
|
reader_args[4] = g_dram[DRAM_BANK_A].y;
|
|
reader_args[5] = DRAM_BUFFER_BASE;
|
|
reader_args[6] = g_dram[DRAM_BANK_B].x;
|
|
reader_args[7] = g_dram[DRAM_BANK_B].y;
|
|
reader_args[8] = DRAM_BUFFER_BASE;
|
|
reader_args[9] = PREFETCH_DEPTH;
|
|
reader_args[10] = 0u;
|
|
writer_args[0] = TILE_ROWS * TILE_COLS;
|
|
writer_args[1] = g_dram[DRAM_BANK_C].x;
|
|
writer_args[2] = g_dram[DRAM_BANK_C].y;
|
|
writer_args[3] = DRAM_BUFFER_BASE;
|
|
compute_args[0] = TILE_ROWS;
|
|
compute_args[1] = TILE_INNER;
|
|
compute_args[2] = TILE_COLS;
|
|
args[0].processor = FT_TT_PROC_DM0;
|
|
args[0].values = reader_args;
|
|
args[0].count = 11u;
|
|
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;
|
|
|
|
started = monotonic_microseconds();
|
|
rc = slow_dispatch_staged(session, placement->x, placement->y, &config, args, 5u);
|
|
staged_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm A2 completed its round", rc == FT_TT_OK, "round %u rc=%d",
|
|
round, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
|
|
/* ---- arm B: the resident triad. One parameter block, one descriptor, one head
|
|
* write, one completion poll. No images, no CB config, no launch message. ---- */
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
started = monotonic_microseconds();
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RUN_TIMEOUT_MS, NULL);
|
|
queue_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm B completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_group;
|
|
|
|
solo.cores.count = 1u;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
}
|
|
|
|
{
|
|
double rounds = (double)g_ab_iterations;
|
|
uint64_t full_us = (uint64_t)((double)full_total / rounds);
|
|
uint64_t staged_us = (uint64_t)((double)staged_total / rounds);
|
|
uint64_t queue_us = (uint64_t)((double)queue_total / rounds);
|
|
|
|
printf("\n ONE REAL MATMUL, Mt=%u Kt=%u Nt=%u bf16, depth %u, %u per arm, interleaved\n",
|
|
TILE_ROWS, TILE_INNER, TILE_COLS, PREFETCH_DEPTH, g_ab_iterations);
|
|
printf(" arm wall (ms) GFLOP/s %% of one Tensix\n");
|
|
printf(" A1 Phase 4 op call, images reloaded : %9.3f %8.1f %6.2f%%\n",
|
|
(double)full_us / 1000.0, gflops_at(full_us),
|
|
100.0 * gflops_at(full_us) / TENSIX_PEAK_GFLOPS);
|
|
printf(" A2 same, images already in L1 : %9.3f %8.1f %6.2f%%\n",
|
|
(double)staged_us / 1000.0, gflops_at(staged_us),
|
|
100.0 * gflops_at(staged_us) / TENSIX_PEAK_GFLOPS);
|
|
printf(" B resident triad, one enqueue : %9.3f %8.1f %6.2f%%\n",
|
|
(double)queue_us / 1000.0, gflops_at(queue_us),
|
|
100.0 * gflops_at(queue_us) / TENSIX_PEAK_GFLOPS);
|
|
printf(" fixed cost removed: A1 - B = %.3f ms, A2 - B = %.3f ms\n",
|
|
(double)(full_us - queue_us) / 1000.0, (double)(staged_us - queue_us) / 1000.0);
|
|
printf(" (peak is one Tensix at 800 MHz HiFi4 = %.0f GFLOP/s; every figure here is "
|
|
"PER CORE)\n", TENSIX_PEAK_GFLOPS);
|
|
|
|
ft_tt_test_check_fmt("the resident triad beats the full Phase 4 op call",
|
|
queue_us < full_us, "%.3f ms vs %.3f ms", (double)queue_us / 1000.0,
|
|
(double)full_us / 1000.0);
|
|
ft_tt_test_check_fmt("and beats it with the images already loaded too",
|
|
queue_us < staged_us, "%.3f ms vs %.3f ms", (double)queue_us / 1000.0,
|
|
(double)staged_us / 1000.0);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 3 — what the stoppable wait costs
|
|
* ================================================================ */
|
|
|
|
/** Run `count` matmuls on a resident triad built from `program`, and return the total. */
|
|
static uint64_t time_resident_matmuls(const ft_tt_hardware_session *session,
|
|
const ft_tt_program_directory *program, uint32_t count,
|
|
int *out_rc)
|
|
{
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&g_group.cores, 0u);
|
|
uint64_t started;
|
|
uint64_t elapsed = 0u;
|
|
uint32_t index;
|
|
int rc;
|
|
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, program);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
if (rc != FT_TT_OK) {
|
|
*out_rc = rc;
|
|
return 0u;
|
|
}
|
|
|
|
started = monotonic_microseconds();
|
|
for (index = 0; index < count; index++) {
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], index % 8u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], index + 1u, RUN_TIMEOUT_MS,
|
|
NULL);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
elapsed = monotonic_microseconds() - started;
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_group;
|
|
|
|
solo.cores.count = 1u;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
*out_rc = rc;
|
|
return elapsed;
|
|
}
|
|
|
|
static bool stage_3_stoppable_cost(const ft_tt_hardware_session *session)
|
|
{
|
|
uint64_t upstream_total = 0u;
|
|
uint64_t stoppable_total = 0u;
|
|
uint32_t round;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 3 — what the stoppable CB wait COSTS: upstream's blocking waits "
|
|
"versus ours, same source, same shape, interleaved");
|
|
|
|
rc = upload_operands(&session->noc, 0xC0DE0002u);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* Interleaved, because two 1.8 GHz cores also driving eight K80s drift enough over ten
|
|
* seconds to swamp the difference being measured (dev notes §8). */
|
|
for (round = 0; round < g_ab_iterations; round++) {
|
|
int arm_rc = FT_TT_OK;
|
|
|
|
upstream_total += time_resident_matmuls(session, &g_triad_upstream, 1u, &arm_rc);
|
|
ft_tt_test_check_fmt("the upstream-wait arm ran", arm_rc == FT_TT_OK, "round %u rc=%d",
|
|
round, arm_rc);
|
|
if (arm_rc != FT_TT_OK)
|
|
return false;
|
|
|
|
stoppable_total += time_resident_matmuls(session, &g_triad, 1u, &arm_rc);
|
|
ft_tt_test_check_fmt("the stoppable-wait arm ran", arm_rc == FT_TT_OK, "round %u rc=%d",
|
|
round, arm_rc);
|
|
if (arm_rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
|
|
{
|
|
double rounds = (double)g_ab_iterations;
|
|
uint64_t upstream_us = (uint64_t)((double)upstream_total / rounds);
|
|
uint64_t stoppable_us = (uint64_t)((double)stoppable_total / rounds);
|
|
double cost = upstream_us ? 100.0 * ((double)stoppable_us - (double)upstream_us) /
|
|
(double)upstream_us
|
|
: 0.0;
|
|
|
|
printf("\n THE COST OF A STOPPABLE WAIT, one real matmul, %u per arm, interleaved\n",
|
|
g_ab_iterations);
|
|
printf(" upstream cb_wait_front / cb_reserve_back : %9.3f ms %8.1f GFLOP/s\n",
|
|
(double)upstream_us / 1000.0, gflops_at(upstream_us));
|
|
printf(" stoppable (ask + bound) : %9.3f ms %8.1f GFLOP/s\n",
|
|
(double)stoppable_us / 1000.0, gflops_at(stoppable_us));
|
|
printf(" cost of the escape: %+.2f%%\n", cost);
|
|
printf(" (the escape sits INSIDE the loop body, which only runs while BLOCKED, so a\n"
|
|
" wait that is already satisfied pays one predictable branch and nothing else)\n");
|
|
|
|
/* A band, not a point: this host cannot resolve better than a few percent, and a
|
|
* tighter assertion would be measuring its mood. What would fail is a REGRESSION —
|
|
* an escape that polled somewhere it should not. */
|
|
ft_tt_test_check_fmt("the stoppable wait costs less than 5%", cost < 5.0, "%+.2f%%", cost);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 4 — the acceptance: a deliberate CB deadlock
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* The deadlock is a REAL matmul with its last block withheld.
|
|
*
|
|
* That matters. A synthetic wait built to be easy to break proves nothing about
|
|
* `cb_wait_front`; here the compute thread is blocked in the op's own
|
|
* `cb_wait_front(cb_in0, 1)` inside the real K loop, the writer is blocked in its own
|
|
* `cb_wait_front(cb_out, 1)`, and the reader is idle at the queue poll — which is exactly
|
|
* the state a starved pipeline reaches in production.
|
|
*/
|
|
static bool stage_4_deadlock_and_recovery(const ft_tt_hardware_session *session,
|
|
const ft_tt_program_directory *program,
|
|
uint32_t shutdown_ms, bool expect_stop_honoured)
|
|
{
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&g_group.cores, 0u);
|
|
ft_tt_core_group solo = g_group;
|
|
ft_tt_watchdog_config config;
|
|
ft_tt_watchdog_report report;
|
|
uint32_t unstopped = 0u;
|
|
uint64_t asked_at;
|
|
uint64_t stopped_at;
|
|
int rc;
|
|
|
|
solo.cores.count = 1u;
|
|
|
|
rc = upload_operands(&session->noc, 0xDEAD0003u);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, program);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
ft_tt_test_check_fmt("the triad is resident", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* One block withheld: every tile pair but the last arrives, and the compute thread waits
|
|
* for a tile that will never be pushed. */
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 1u, 0u);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
config.stall_timeout_ms = 1000u;
|
|
config.overall_timeout_ms = 5000u;
|
|
rc = ft_tt_watchdog_wait(&session->noc, &solo, 1u, &config, &report);
|
|
ft_tt_test_check_fmt("the watchdog returns instead of hanging the session", rc == FT_TT_EIO,
|
|
"rc=%d after %u ms", rc, report.elapsed_ms);
|
|
ft_tt_test_check_fmt("it says the stalled core is ALIVE, not dead", report.stalled_core_is_alive,
|
|
"state=%s", ft_tt_queue_state_string(report.stalled_state));
|
|
/* THE DIAGNOSIS. Five processors share this queue; "the core is blocked" is not an
|
|
* answer. Both halves are asserted separately so a green run cannot be carried by one. */
|
|
ft_tt_test_check_fmt("and it NAMES THE PROCESSOR that is blocked",
|
|
report.stalled_processor < FT_TT_PROC_COUNT, "processor=%u",
|
|
report.stalled_processor);
|
|
/* Asserted only in the arm where the ask works. In the red-proof arm the bound is
|
|
* running out underneath the dump, so which wait a processor is in at the moment the
|
|
* watchdog looks is a race — asserting it there would make the RED case fail for a
|
|
* reason that has nothing to do with what it is testing. */
|
|
if (expect_stop_honoured)
|
|
ft_tt_test_check_fmt("and NAMES THE WAIT it is blocked in",
|
|
report.stalled_processor_state ==
|
|
(uint32_t)FT_TT_QUEUE_STATE_CB_WAIT_FRONT ||
|
|
report.stalled_processor_state ==
|
|
(uint32_t)FT_TT_QUEUE_STATE_CB_RESERVE_BACK,
|
|
"%s in %s", ft_tt_processor_name(report.stalled_processor),
|
|
ft_tt_queue_state_string(report.stalled_processor_state));
|
|
|
|
/* Recovery is a WRITE. No reset is asserted anywhere on this path. */
|
|
asked_at = monotonic_microseconds();
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, shutdown_ms, &unstopped);
|
|
stopped_at = monotonic_microseconds();
|
|
|
|
if (expect_stop_honoured) {
|
|
ft_tt_test_check_fmt("the deadlocked TRIAD is retired by ASKING, not by reset",
|
|
rc == FT_TT_OK && unstopped == 0u, "rc=%d unstopped=%u", rc, unstopped);
|
|
/* It has to be the ASK that retired it, not the bound quietly expiring: the bound is
|
|
* ~0.5 s and the ask should land in milliseconds. Without this the stage would pass
|
|
* against a kernel whose stop check does nothing, which is the whole point of it. */
|
|
ft_tt_test_check_fmt("and it stopped because it was ASKED, not because a bound expired",
|
|
stopped_at - asked_at < 200000u, "%llu us after the ask",
|
|
(unsigned long long)(stopped_at - asked_at));
|
|
{
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queues[0], fields);
|
|
ft_tt_test_check_fmt("the abandoned descriptor was NOT retired",
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] == 0u, "completed=%u",
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT]);
|
|
}
|
|
} else {
|
|
/* THE RED PROOF. The same deadlock against a build whose stop check is removed: the
|
|
* ask must NOT retire it. It is safe to run only because the bound is independent of
|
|
* the ask, so the worst case is a timeout and not a wedged core. */
|
|
ft_tt_test_check_fmt("WITH THE ASK REMOVED, the shutdown REFUSES instead of passing",
|
|
rc == FT_TT_EIO && unstopped == 1u, "rc=%d unstopped=%u after %llu us",
|
|
rc, unstopped, (unsigned long long)(stopped_at - asked_at));
|
|
/* And it recovers anyway — on the bound. That is rule 4 earning its place: a build
|
|
* with a broken ask still leaves the core usable. */
|
|
{
|
|
uint32_t pending = 0u;
|
|
uint64_t deadline = monotonic_microseconds() + 30000000u;
|
|
|
|
while (monotonic_microseconds() < deadline) {
|
|
if (ft_tt_queue_all_processors_exited(&session->noc, &g_queues[0], TRIAD_MASK,
|
|
&pending) == FT_TT_OK)
|
|
break;
|
|
}
|
|
ft_tt_test_check_fmt("but the BOUND still retires it, so even a broken ask is not a "
|
|
"wedged core",
|
|
pending == 0u, "pending mask 0x%02x", pending);
|
|
}
|
|
{
|
|
uint32_t status[FT_TT_QUEUE_STATUS_WORDS];
|
|
|
|
(void)ft_tt_queue_read_status(&session->noc, &g_queues[0], FT_TT_PROC_MATH0, status);
|
|
ft_tt_test_check_fmt("and it says WHY: the CB wait exhausted its bound",
|
|
status[FT_TT_QUEUE_STATUS_ERROR] ==
|
|
(uint32_t)FT_TT_QUEUE_KERR_CB_WAIT_TIMEOUT,
|
|
"%s",
|
|
ft_tt_queue_kernel_error_string(status[FT_TT_QUEUE_STATUS_ERROR]));
|
|
}
|
|
}
|
|
|
|
/* THE ACCEPTANCE: the SAME core runs a fresh triad afterwards and gets the right answer.
|
|
* If this needed a power cycle, none of the above is worth anything. */
|
|
{
|
|
size_t bytes = (size_t)C_TILES * TILE_BYTES;
|
|
uint32_t completed = 0u;
|
|
ft_tt_op_matmul_request request;
|
|
|
|
fill_matmul_request(&request);
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_matmul_run(&session->noc, placement->x, placement->y, &g_matmul.program,
|
|
&request, RUN_TIMEOUT_MS);
|
|
if (rc == FT_TT_OK)
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_C], DRAM_BUFFER_BASE, g_reference,
|
|
bytes);
|
|
ft_tt_test_check_fmt("the reference arm still runs on this core", rc == FT_TT_OK, "rc=%d",
|
|
rc);
|
|
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
if (rc == FT_TT_OK)
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
if (rc == FT_TT_OK)
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_C], DRAM_BUFFER_BASE, g_observed,
|
|
bytes);
|
|
ft_tt_test_check_fmt("THE CARD SURVIVED: the SAME core runs a fresh resident triad",
|
|
rc == FT_TT_OK, "rc=%d completed=%u", rc, completed);
|
|
ft_tt_test_check_fmt("and its answer is byte-identical to the reference",
|
|
rc == FT_TT_OK && memcmp(g_observed, g_reference, bytes) == 0,
|
|
"%zu bytes", bytes);
|
|
|
|
{
|
|
uint32_t stopped = 0u;
|
|
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &stopped);
|
|
}
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 6 — group-level completion
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* A DESCRIPTOR THIS PROGRAM CANNOT EXECUTE IS REFUSED, ON THE CARD, AND FOR THE STATED
|
|
* REASON.
|
|
*
|
|
* `tile_inner = 0` is the shape every one of the reader's derived lengths collapses on:
|
|
* `depth * IN0_TILE_BYTES` is fine but the block loop never runs, `blocks_per_tile` is a
|
|
* divide by the collapsed depth, and a descriptor that reads nothing still hands the writer
|
|
* a completion. A zero-shaped descriptor used to walk straight through.
|
|
*
|
|
* TWO THINGS ARE ASSERTED AND THEY FAIL DIFFERENTLY: that it is REFUSED, and that it is
|
|
* refused FOR THE STATED REASON (`BAD_PARAMETERS`) rather than by whichever ground is
|
|
* cheapest — a check with several sufficient grounds tests the cheapest one, and the one
|
|
* you care about may never run (DEV_NOTES.md §3).
|
|
*
|
|
* **This is NOT the red proof for the CB block-grid guard.** That guard cannot be exercised
|
|
* on this op: its circular buffers hold `2 * depth` pages, every legal depth divides both
|
|
* Kt = 48 and 96, and the write pointer is therefore always on the grid of whatever depth
|
|
* the next descriptor asks for. The op that CAN express it is the memory family, where one
|
|
* residency serves a 4-tile KV group and a 48-tile embedding group through one CB_OUT — and
|
|
* that is where the guard was proved, on silicon, refusing a real off-grid descriptor with
|
|
* `BAD_PARAMETERS` while the pre-round control passed the same stage beside it.
|
|
*
|
|
* The shipped shape is then run on a FRESH residency of the same core, so "the guard
|
|
* refuses" and "the guard refuses everything" are distinguishable.
|
|
*/
|
|
static bool stage_7_zero_shape_refusal(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement = ft_tt_launch_core_set_at(&g_group.cores, 0u);
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
uint32_t completed = 0u;
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_group;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 7 — a zero-shaped descriptor is REFUSED on the card, with a reason");
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &g_queues[0];
|
|
|
|
rc = upload_operands(&session->noc, 0xC0FFEE01u);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
ft_tt_test_check_fmt("a fresh residency for the bad descriptor", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = enqueue_matmul_shaped(&session->noc, &g_queues[0], 0u, 0u, 0u, 0u, TILE_COLS);
|
|
ft_tt_test_check_fmt("the zero-shaped descriptor enqueues — the HOST does not catch it",
|
|
rc == FT_TT_OK, "rc=%d", rc);
|
|
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RED_PROOF_SHUTDOWN_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("and the kernel REFUSES it rather than retiring empty work",
|
|
rc != FT_TT_OK && completed == 0u, "rc=%d completed=%u", rc, completed);
|
|
|
|
memset(fields, 0, sizeof(fields));
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queues[0], fields);
|
|
ft_tt_test_check_fmt("...for the STATED reason, not whichever ground was cheapest",
|
|
fields[FT_TT_QUEUE_FIELD_ERROR] ==
|
|
(uint32_t)FT_TT_QUEUE_KERR_BAD_PARAMETERS,
|
|
"error=%u (%s)", fields[FT_TT_QUEUE_FIELD_ERROR],
|
|
ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR]));
|
|
ft_tt_test_check_fmt("...and it left the loop rather than blocking in it",
|
|
fields[FT_TT_QUEUE_FIELD_STATE] == (uint32_t)FT_TT_QUEUE_STATE_EXIT,
|
|
"state=0x%08x", fields[FT_TT_QUEUE_FIELD_STATE]);
|
|
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
|
|
/* AND THE GOOD PATH STILL PASSES. A guard that refuses everything is not a guard. */
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &g_queues[0]);
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(&session->noc);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, 0u, &g_triad_config[0], CB_SPIN_LIMIT);
|
|
if (rc == FT_TT_OK)
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 0u, 0u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("the SHIPPED shape still completes on the same core", rc == FT_TT_OK,
|
|
"rc=%d completed=%u", rc, completed);
|
|
|
|
memset(fields, 0, sizeof(fields));
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queues[0], fields);
|
|
ft_tt_test_check_fmt("...with no error recorded", fields[FT_TT_QUEUE_FIELD_ERROR] == 0u,
|
|
"error=%u", fields[FT_TT_QUEUE_FIELD_ERROR]);
|
|
|
|
unstopped = 0u;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
return true;
|
|
}
|
|
|
|
static bool stage_6_group_completion(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_group_member_completion members[CORE_COUNT];
|
|
const char *reason = NULL;
|
|
uint32_t short_slot = 0u;
|
|
uint32_t reached = 0u;
|
|
uint32_t group_nonce;
|
|
unsigned slot;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 6 — group-level completion: a sequence number the KERNELS write");
|
|
|
|
rc = upload_operands(&session->noc, 0x6E000004u);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = ft_tt_group_completion_init(&session->noc, &g_group,
|
|
FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET);
|
|
ft_tt_test_check_fmt("the group completion block is written on the leader", rc == FT_TT_OK,
|
|
"rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ONE NONCE FOR THE WHOLE GROUP, which is what ft_tt_group_queues_init() does and why it
|
|
* takes a single one. A group-level token check asks "did every member finish item N",
|
|
* and that only has an answer if item N has ONE token across the group — per-member
|
|
* nonces give per-member tokens and the check can never pass. Getting this wrong is a
|
|
* green-looking failure: every member posts, every sequence is right, and the check
|
|
* refuses on the token. */
|
|
group_nonce = ft_tt_queue_make_nonce();
|
|
for (slot = 0; slot < CORE_COUNT; slot++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_group.cores, slot);
|
|
|
|
rc = load_triad_images(&session->noc, placement->x, placement->y, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, group_nonce, &g_queues[slot]);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_triad(&session->noc, &g_group, slot, &g_triad_config[slot], CB_SPIN_LIMIT);
|
|
ft_tt_test_check_fmt("member is resident", rc == FT_TT_OK, "slot %u rc=%d", slot, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
|
|
/* Feed ONE member only. The whole point is that a group-level check must refuse this —
|
|
* and a SUM of completed_count across the group would not, because one member doing the
|
|
* work of two sums to the same number. */
|
|
rc = enqueue_matmul(&session->noc, &g_queues[0], 0u, 0u, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queues[0], 1u, RUN_TIMEOUT_MS, NULL);
|
|
ft_tt_test_check_fmt("member 0 finished item 1", rc == FT_TT_OK, "rc=%d", rc);
|
|
|
|
rc = ft_tt_group_completion_read(&session->noc, &g_group, members, CORE_COUNT);
|
|
ft_tt_test_check_fmt("the group block reads back", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check_fmt("member 0's slot carries sequence 1", members[0].sequence == 1u,
|
|
"sequence=%u token=0x%08x, want sequence 1 token 0x%08x",
|
|
members[0].sequence, members[0].token,
|
|
ft_tt_queue_token_at(&g_queues[0], 0u));
|
|
ft_tt_test_check_fmt("and the token beside it is the one the host stamped, so the pair "
|
|
"landed together",
|
|
members[0].token == ft_tt_queue_token_at(&g_queues[0], 0u), "0x%08x",
|
|
members[0].token);
|
|
ft_tt_test_check_fmt("and member 1's slot is untouched", members[1].sequence == 0u,
|
|
"sequence=%u", members[1].sequence);
|
|
|
|
rc = ft_tt_group_check_completion(&g_group, members, CORE_COUNT, 1u,
|
|
ft_tt_queue_token_at(&g_queues[0], 0u), &reason, &short_slot);
|
|
ft_tt_test_check_fmt("THE GROUP CHECK REFUSES with one member behind", rc == FT_TT_ERROR, "%s",
|
|
reason ? reason : "(it passed, which is the bug)");
|
|
ft_tt_test_check_fmt("and it refuses BECAUSE a member has not reached the sequence",
|
|
reason != NULL && strstr(reason, "has not reached") != NULL, "%s",
|
|
reason ? reason : "(no reason)");
|
|
ft_tt_test_check_fmt("and it names WHICH member", short_slot == 1u, "slot %u", short_slot);
|
|
|
|
/* Now feed the other member, and it must pass. */
|
|
rc = enqueue_matmul(&session->noc, &g_queues[1], 0u, 0u, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_group_wait_completion(&session->noc, &g_group, 1u, RUN_TIMEOUT_MS, &reached);
|
|
ft_tt_test_check_fmt("with both members fed, the group reaches sequence 1", rc == FT_TT_OK,
|
|
"reached=%u rc=%d", reached, rc);
|
|
|
|
rc = ft_tt_group_completion_read(&session->noc, &g_group, members, CORE_COUNT);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_group_check_completion(&g_group, members, CORE_COUNT, 1u,
|
|
ft_tt_queue_token_at(&g_queues[0], 0u), &reason,
|
|
&short_slot);
|
|
ft_tt_test_check_fmt("and the group check now passes", rc == FT_TT_OK, "%s",
|
|
reason ? reason : "ok");
|
|
|
|
/* A WRONG TOKEN must refuse, or the sequence is doing all the work and a previous run's
|
|
* residue would satisfy the check. */
|
|
rc = ft_tt_group_check_completion(&g_group, members, CORE_COUNT, 1u, 0x1234ABCDu, &reason,
|
|
&short_slot);
|
|
ft_tt_test_check_fmt("a group check with the WRONG token refuses", rc == FT_TT_ERROR, "%s",
|
|
reason ? reason : "(it passed, which is the bug)");
|
|
ft_tt_test_check_fmt("and it refuses BECAUSE of the token, not the sequence",
|
|
reason != NULL && strstr(reason, "token") != NULL, "%s",
|
|
reason ? reason : "(no reason)");
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &g_group, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("the whole group retires by asking", rc == FT_TT_OK && unstopped == 0u,
|
|
"rc=%d unstopped=%u", rc, unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ================================================================ */
|
|
|
|
static bool allocate_host_buffers(void)
|
|
{
|
|
g_a_tiles = malloc((size_t)A_TILES * TILE_BYTES);
|
|
g_b_tiles = malloc((size_t)B_TILES * TILE_BYTES);
|
|
g_reference = malloc((size_t)C_TILES * TILE_BYTES);
|
|
g_observed = malloc((size_t)C_TILES * TILE_BYTES);
|
|
return g_a_tiles && g_b_tiles && g_reference && g_observed;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_firmware_set firmware;
|
|
uint32_t bank;
|
|
int rc;
|
|
|
|
if (argc < 5) {
|
|
fprintf(stderr,
|
|
"usage: %s <triad-dir> <triad-upstream-dir> <triad-boundonly-dir> <matmul-dir>\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
if (!load_environment())
|
|
return ft_tt_test_finish();
|
|
if (!allocate_host_buffers()) {
|
|
ft_tt_test_check("host buffers allocate", false, "out of memory");
|
|
return ft_tt_test_finish();
|
|
}
|
|
|
|
printf("Phase 6 resident triad — cores %u, %u\n", g_core_indices[0], g_core_indices[1]);
|
|
|
|
rc = ft_tt_hardware_session_open(&session);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
|
|
/* Per-core, not card-wide: this pair can run while another agent works a
|
|
* different pair. A refusal names the holder's pid instead of blocking. */
|
|
rc = ft_tt_hardware_session_claim_cores(&session, g_core_indices, CORE_COUNT,
|
|
"phase6 resident triad", NULL, NULL);
|
|
ft_tt_test_check_fmt("both cores are claimed for this run", rc == FT_TT_OK, "%s",
|
|
ft_tt_error_string(rc));
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|
|
|
|
rc = ft_tt_firmware_set_load(ft_tt_firmware_directory(), &firmware);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
|
|
for (bank = 0; bank < 3u; bank++) {
|
|
rc = ft_tt_coord_dram_bank(&session.grid, bank, FT_TT_COORD_TRANSLATED, &g_dram[bank]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("DRAM bank coordinates derive", false, "bank %u rc=%d", bank, rc);
|
|
goto shutdown;
|
|
}
|
|
}
|
|
|
|
rc = ft_tt_program_directory_load(argv[1], TRIAD_MASK, &g_triad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(argv[2], TRIAD_MASK, &g_triad_upstream);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(argv[3], TRIAD_MASK, &g_triad_boundonly);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(argv[4], TRIAD_MASK, &g_matmul);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("all four program directories load", false, "rc=%d", rc);
|
|
goto release;
|
|
}
|
|
|
|
if (build_group(&session) && boot_and_place(&session, &firmware)) {
|
|
if (stage_1_real_op(&session) && stage_2_real_op_dispatch(&session) &&
|
|
stage_3_stoppable_cost(&session)) {
|
|
ft_tt_test_step("stage 4 — the acceptance: a REAL matmul deadlocked in cb_wait_front");
|
|
if (stage_4_deadlock_and_recovery(&session, &g_triad, RUN_TIMEOUT_MS, true)) {
|
|
ft_tt_test_step("stage 5 — THE RED PROOF: the same deadlock with the ask removed");
|
|
if (stage_4_deadlock_and_recovery(&session, &g_triad_boundonly,
|
|
RED_PROOF_SHUTDOWN_MS, false)) {
|
|
(void)stage_6_group_completion(&session);
|
|
(void)stage_7_zero_shape_refusal(&session);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* DO NOT PARK. AT ALL.
|
|
*
|
|
* Two versions of this ran and both cost a core. The first parked on every exit path,
|
|
* including the failure one, so a core whose BRISC was still executing had its TRISCs
|
|
* reset underneath it and BRISC hung in the firmware's `tensix_sync()` forever
|
|
* (`soft reset 0x00047000: BRISC=running NCRISC=held TRISC0..2=held`, every later boot
|
|
* timing out). The second parked only after confirming all five processors had reported
|
|
* EXIT — and a core whose kernel exited that way ALSO failed its next
|
|
* `ft_tt_core_boot_firmware` until the host was rebooted.
|
|
*
|
|
* So the measured rule is stronger than "park carefully": on a five-processor resident
|
|
* program, PARKING IS THE HAZARD. There is no need for it either. `ft_tt_watchdog_shutdown()`
|
|
* already leaves the core with its firmware up and ready for the next launch, and its own
|
|
* header says it deliberately does not park because "a parked core needs its firmware
|
|
* re-booted before it can run anything". Retiring the kernel and stopping using the core
|
|
* are different decisions; this test only needs the first.
|
|
*
|
|
* What is left behind is a core sitting in its firmware's GO wait, which is exactly where
|
|
* a healthy idle core sits. */
|
|
{
|
|
uint32_t slot;
|
|
|
|
for (slot = 0u; slot < ft_tt_launch_core_set_count(&g_group.cores); slot++) {
|
|
ft_tt_core_group solo = g_group;
|
|
uint32_t control_state = 0u;
|
|
uint32_t unstopped = 0u;
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &g_queues[slot];
|
|
(void)ft_tt_queue_read_field(&session.noc, &g_queues[slot], FT_TT_QUEUE_FIELD_STATE,
|
|
&control_state);
|
|
if (control_state == (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON)
|
|
continue; /* nothing was ever launched here */
|
|
(void)ft_tt_watchdog_shutdown(&session.noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
}
|
|
|
|
release:
|
|
ft_tt_program_directory_release(&g_matmul);
|
|
ft_tt_program_directory_release(&g_triad_boundonly);
|
|
ft_tt_program_directory_release(&g_triad_upstream);
|
|
ft_tt_program_directory_release(&g_triad);
|
|
ft_tt_firmware_set_release(&firmware);
|
|
shutdown:
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|