1717 lines
73 KiB
C
1717 lines
73 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_phase6_fusion.c
|
|
* @brief ON-CARD MEASUREMENT for Phase 6 item 8 — op fusions. It measures where the real
|
|
* MiniCPM5-1B layer actually round-trips data, and what each round trip costs, so a
|
|
* fusion can be ranked by a number instead of by how natural it looks.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE LICENCE THIS SUITE EXISTS TO TEST AGAINST
|
|
* ---------------------------------------------------------------------------
|
|
* The plan's Phase 6 item 8 licenses a fusion on ONE ground: it avoids a round trip
|
|
* through L1 or DRAM. **Never dispatch cost** — dispatch is 1.363 us batched and 0.39 us
|
|
* device-side (docs/tt_bringup_log.md §17, §20.4), so a fusion sold on descriptor count is
|
|
* porting a K80 workaround for a problem this backend does not have. This suite therefore
|
|
* measures ROUND TRIPS and reports the dispatch floor beside them only so it can be
|
|
* SUBTRACTED, never claimed.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT IT MEASURES, AND WHY EACH ARM IS THE ARM IT IS
|
|
* ---------------------------------------------------------------------------
|
|
* 1. **The delay ratio, first.** FT_TT_QUEUE_OP_DELAY at N and 8N iterations. If the
|
|
* harness cannot see a factor of eight it cannot see a fusion, and every later number
|
|
* in the run would be noise wearing a decimal point. This is the timing harness's own
|
|
* red/green: it is a check that CAN fail, run before anything it protects.
|
|
* 2. **The dispatch floor.** A batch of NOPs on the same residency, so the per-descriptor
|
|
* protocol cost is measured in THIS run, on THIS core, and can be subtracted from every
|
|
* other arm rather than inherited from another session's table.
|
|
* 3. **The round-trip matrix.** The SAME elementwise op with only its operand ADDRESSES
|
|
* changed: operands in DRAM, in this core's own L1, or in a neighbour core's L1. The
|
|
* ops already take a NoC (x, y, address) triple per operand, so "keep the intermediate
|
|
* in L1" is a placement, not a kernel — which means the DRAM-versus-L1 difference is
|
|
* measurable TODAY, without building the fusion that would exploit it.
|
|
* 4. **The removable op.** SILU_MUL at 144 tiles is the FFN gate exactly as MiniCPM5-1B
|
|
* runs it, and ADD at 48 tiles is the residual. A fusion that absorbs one of these
|
|
* removes its whole wall time, so its whole wall time is the UPPER BOUND of that
|
|
* fusion's saving and is measured directly rather than modelled.
|
|
* 5. **The matmul denominators**, at every real shape, so a saving can be quoted as a
|
|
* fraction of the layer it sits in — and ffn_gate/up against ffn_down, which have the
|
|
* SAME tile-pair count and 144 against 48 output tiles, so the difference between them
|
|
* is what one output tile write costs inside a matmul.
|
|
* 6. **The binary switch.** Five kernel images over the x1 link. Not a fusion and not
|
|
* dispatch — a third thing, and on the real layer sequence it is larger than either.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* INTERLEAVED, ALWAYS
|
|
* ---------------------------------------------------------------------------
|
|
* Two 1.8 GHz host cores also driving eight K80s drift enough over ten seconds to swamp a
|
|
* microsecond — measuring one arm to completion and then the other once reported a NEGATIVE
|
|
* cost for CONFIGURE_TLB (dev notes §8). Every arm here runs once per round, in the same
|
|
* order, and the rounds are averaged.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* AND THE ANSWERS ARE COMPARED AS BYTES
|
|
* ---------------------------------------------------------------------------
|
|
* A placement change moves where data lives and must not move what it is. PCC is blind to
|
|
* a consistent permutation and has already failed twice as a gate on this project (dev
|
|
* notes §3), so the DRAM arm and the L1 arm are compared BYTE FOR BYTE, and a NEGATIVE
|
|
* CONTROL — the same comparison against a deliberately different computation — is asserted
|
|
* to come back DIFFERENT in the same run, so the comparison is known to be load-bearing
|
|
* rather than assumed to be.
|
|
*
|
|
* Environment:
|
|
* FT_TT_TENSIX_INDEX the core to drive (default 0; use 100-119 for this worker)
|
|
* FT_TT_FUSION_SCRATCH_INDEX a second, IDLE core whose L1 is the neighbour-L1 arm
|
|
* FT_TT_FIRMWARE_DIR the program directory whose fw_*.elf to boot
|
|
* FT_TT_FUSION_ROUNDS rounds per arm (default 5)
|
|
*
|
|
* Usage — the last two directories are OPTIONAL writer arms; give both to get the
|
|
* three-arm barrier A/B of stage 3b:
|
|
* FT_TT_FIRMWARE_DIR=../programs/eltwise FT_TT_TENSIX_INDEX=100 \
|
|
* FT_TT_FUSION_SCRATCH_INDEX=119 \
|
|
* ./build-fusion-card/test_phase6_fusion ../programs/eltwise ../programs/reduce \
|
|
* ../programs/rtriad ../programs/eltwise_blocked ../programs/eltwise_flushed
|
|
*
|
|
* @status GREEN on silicon, 0 checks failed, on TWO disjoint cores — Tensix 100 (scratch
|
|
* 119) and Tensix 110 (scratch 118) — every figure reproducing within ~1%. All six cores
|
|
* touched probe PASS 4/4 afterwards. It builds no fusion; the kernels it adds
|
|
* (kernels/resident_vector_writer_blocked.c and _flushed.c) are measurement arms for the
|
|
* shipped writer's per-tile barrier and nothing depends on them.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* clock_gettime(CLOCK_MONOTONIC) is POSIX.1-2008, not C99. */
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "../convert/ft_float_convert.h"
|
|
#include "../ft_tt_coord.h"
|
|
#include "../ft_tt_op_dispatch.h"
|
|
#include "../ft_tt_op_l1_layout.h"
|
|
#include "../ft_tt_op_matmul.h"
|
|
#include "../ft_tt_op_vector.h"
|
|
#include "../ft_tt_queue.h"
|
|
#include "../ft_tt_tilize.h"
|
|
#include "../ft_tt_watchdog.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_program_directory.h"
|
|
#include "ft_tt_test_report.h"
|
|
|
|
#define TILE_BYTES FT_TT_OP_TILE_BYTES_BF16
|
|
#define TILE_DIM FT_TT_OP_TILE_DIM
|
|
#define RUN_TIMEOUT_MS 60000u
|
|
#define QUEUE_CAPACITY 64u
|
|
#define POISON_BYTE 0x5A
|
|
|
|
/** DRAM banks 2..5, as every other op suite here uses. */
|
|
#define DRAM_BANK_A 2u
|
|
#define DRAM_BANK_B 3u
|
|
#define DRAM_BANK_C 4u
|
|
#define DRAM_BANK_CONST 5u
|
|
#define DRAM_BANK_COUNT 6u
|
|
#define DRAM_BUFFER_BASE 0x00100000u
|
|
|
|
/** Scratch bases inside the CB arena, above anything ft_tt_op_cb_place() hands out at this
|
|
* prefetch depth (139,264 B from 0x26000, i.e. up to ~0x48000) and below the work queue at
|
|
* 0x170000. Each holds MAX_TILES tiles = 0x48000 bytes, so the three do not overlap. */
|
|
#define L1_SCRATCH_A 0x00060000u
|
|
#define L1_SCRATCH_B 0x000B0000u
|
|
#define L1_SCRATCH_C 0x00100000u
|
|
|
|
/** The widest run any arm uses: MiniCPM5-1B's FFN width, 4608 / 32. */
|
|
#define MAX_TILES 144u
|
|
/** MiniCPM5-1B's hidden width, 1536 / 32 — the residual and norm width. */
|
|
#define HIDDEN_TILES 48u
|
|
|
|
/** One prefetch depth for every elementwise arm, so depth is never a variable between
|
|
* arms. 16 divides both 48 and 144, and the CBs it places (2 blocks each on two input
|
|
* streams) cost 139,264 B of a 1,351,680 B arena. */
|
|
#define ELTWISE_DEPTH 24u
|
|
/** The matmul arms' requested depth — the same 48 the published per-shape figures were
|
|
* taken at, so these denominators sit beside them rather than beside nothing. Shrunk per
|
|
* shape to a divisor of Kt by ft_tt_op_block_depth(); the CBs are placed for 2x48 pages,
|
|
* which every shrunk value (48, 32, 8) divides. */
|
|
#define MATMUL_DEPTH 48u
|
|
|
|
/** Descriptors per timed window. Batching amortizes the host's completion poll so the
|
|
* number is the DEVICE's throughput and not the PCIe poll granularity. */
|
|
#define BATCH 8u
|
|
|
|
#define IDLE_SPIN_LIMIT 400000000u
|
|
#define CB_SPIN_LIMIT 400000000u
|
|
|
|
#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))
|
|
|
|
/** One Tensix's derived peak at 800 MHz, HiFi4 — the denominator every per-core figure in
|
|
* this project is quoted against (dev notes §6). */
|
|
#define TENSIX_PEAK_GFLOPS 1638.4
|
|
|
|
static ft_tt_noc_coord g_dram[DRAM_BANK_COUNT];
|
|
static ft_tt_launch_core_placement g_placement_storage[1];
|
|
static ft_tt_core_group g_group;
|
|
static ft_tt_program_directory g_eltwise;
|
|
static ft_tt_program_directory g_reduce;
|
|
static ft_tt_program_directory g_rtriad;
|
|
static ft_tt_launch_config g_config;
|
|
static ft_tt_queue g_queue;
|
|
static ft_tt_launch_cb_entry g_cb_placed[FT_TT_OP_MAX_CB];
|
|
static unsigned char g_cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4];
|
|
static uint32_t g_rounds = 5u;
|
|
|
|
/** This core, and the idle core whose L1 the neighbour arm uses. */
|
|
static uint16_t g_self_x;
|
|
static uint16_t g_self_y;
|
|
static uint16_t g_peer_x;
|
|
static uint16_t g_peer_y;
|
|
|
|
/* ================================================================
|
|
* 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 int device_write(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, 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, x, 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, uint16_t x, uint16_t y, 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, x, y, (uint64_t)address + offset, cursor + offset, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int fill_pattern(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t address,
|
|
size_t bytes, unsigned char byte)
|
|
{
|
|
unsigned char pattern[0x1000];
|
|
size_t offset = 0;
|
|
|
|
memset(pattern, byte, sizeof(pattern));
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > sizeof(pattern))
|
|
chunk = sizeof(pattern);
|
|
rc = device_write(noc, x, y, address + (uint32_t)offset, pattern, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* How many whole TILES came back still entirely poison.
|
|
*
|
|
* Counting poison BYTES is the obvious version and it is wrong: 0x5A is a legal byte of a
|
|
* bf16 result, and a correct 294,912-byte answer contained 519 of them — so a
|
|
* "no poison survives" check on bytes fails on a kernel that worked perfectly. The
|
|
* question the poison is there to answer is "did the kernel write this region at all",
|
|
* and a whole tile of unbroken poison is what that looks like. One misleading counter,
|
|
* caught by demanding the check go red for a reason (dev notes §3).
|
|
*/
|
|
static size_t poison_tiles_remaining(const void *data, size_t bytes)
|
|
{
|
|
const unsigned char *cursor = data;
|
|
const size_t tiles = bytes / TILE_BYTES;
|
|
size_t untouched = 0;
|
|
size_t tile;
|
|
|
|
for (tile = 0; tile < tiles; tile++) {
|
|
size_t index;
|
|
bool all_poison = true;
|
|
|
|
for (index = 0; index < TILE_BYTES; index++) {
|
|
if (cursor[tile * TILE_BYTES + index] != POISON_BYTE) {
|
|
all_poison = false;
|
|
break;
|
|
}
|
|
}
|
|
if (all_poison)
|
|
untouched++;
|
|
}
|
|
return untouched;
|
|
}
|
|
|
|
/** A cheap full-mantissa generator — the operands must carry low mantissa bits or a wrong
|
|
* format reproduces the right answer (dev notes §3). */
|
|
static uint64_t g_random_state = 0x9E3779B97F4A7C15ULL;
|
|
|
|
static void random_seed(uint64_t seed)
|
|
{
|
|
g_random_state = seed ? seed : 1u;
|
|
}
|
|
|
|
static float random_value(void)
|
|
{
|
|
g_random_state ^= g_random_state << 13;
|
|
g_random_state ^= g_random_state >> 7;
|
|
g_random_state ^= g_random_state << 17;
|
|
return ((float)(g_random_state >> 40) / 8388608.0f) * 2.0f - 1.0f;
|
|
}
|
|
|
|
static int upload_random_tiles(const ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
uint32_t address, uint32_t tiles, uint64_t seed)
|
|
{
|
|
const size_t elements = (size_t)tiles * TILE_DIM * TILE_DIM;
|
|
uint16_t *plain = malloc(elements * sizeof(uint16_t));
|
|
uint16_t *tiled = malloc(elements * sizeof(uint16_t));
|
|
size_t index;
|
|
int rc = FT_TT_ENOMEM;
|
|
|
|
if (!plain || !tiled)
|
|
goto done;
|
|
random_seed(seed);
|
|
for (index = 0; index < elements; index++)
|
|
plain[index] = ft_bf16_from_float(random_value());
|
|
/* The values are what matters here, not the row geometry: every arm reads the same
|
|
* bytes, so a flat tile stream is a legitimate operand for a timing comparison and for
|
|
* a byte-for-byte equality between two placements of the same op. */
|
|
rc = ft_tt_tilize_matrix(plain, sizeof(uint16_t), TILE_DIM, tiles * TILE_DIM, tiled) == 0
|
|
? FT_TT_OK
|
|
: FT_TT_EINVAL;
|
|
if (rc == FT_TT_OK)
|
|
rc = device_write(&session->noc, x, y, address, tiled,
|
|
(size_t)tiles * TILE_BYTES);
|
|
done:
|
|
free(plain);
|
|
free(tiled);
|
|
return rc;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Bringing a resident triad up
|
|
* ================================================================ */
|
|
|
|
static int load_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;
|
|
}
|
|
|
|
static int start_triad(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
|
|
{
|
|
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 };
|
|
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;
|
|
|
|
memset(&binding, 0, sizeof(binding));
|
|
binding.idle_spin_limit = IDLE_SPIN_LIMIT;
|
|
binding.cb_wait_spin_limit = CB_SPIN_LIMIT;
|
|
binding.status_address =
|
|
FT_TT_QUEUE_STATUS_ADDRESS(g_queue.control_address, processors[index]);
|
|
rc = ft_tt_queue_kernel_args(&g_queue, &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, x, y, &g_config, g_cb_buffer, sizeof(g_cb_buffer), blocks, 5u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_send_go(noc, x, y);
|
|
}
|
|
|
|
/** NEVER a park — on a five-processor resident program that is the hazard, twice, on cores
|
|
* that had retired cleanly (dev notes §1). Asking is the whole protocol. */
|
|
static void retire_triad(const ft_tt_noc_access *noc, const char *label)
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
int rc = ft_tt_watchdog_shutdown(noc, &g_group, RUN_TIMEOUT_MS, &unstopped);
|
|
|
|
ft_tt_test_check_fmt(label, rc == FT_TT_OK && unstopped == 0u, "rc=%d unstopped=0x%X", rc,
|
|
unstopped);
|
|
}
|
|
|
|
/* ================================================================
|
|
* The timed window
|
|
* ================================================================ */
|
|
|
|
/** How many descriptors have been retired on the live residency, so every enqueue knows
|
|
* what completion count to wait for without threading a counter through every arm. */
|
|
static uint32_t g_completed;
|
|
|
|
/**
|
|
* Enqueue `count` copies of `descriptor` and wait for all of them, returning the elapsed
|
|
* microseconds.
|
|
*
|
|
* The window is one host head-write, `count` device-side executions and one completion
|
|
* poll — so per-descriptor cost is the device's, not the PCIe poll's, which is the whole
|
|
* reason to batch rather than time one descriptor at a time.
|
|
*/
|
|
static uint64_t time_batch(const ft_tt_hardware_session *session,
|
|
const ft_tt_queue_descriptor *descriptor, uint32_t count, int *out_rc)
|
|
{
|
|
ft_tt_queue_descriptor batch[BATCH];
|
|
uint64_t started;
|
|
uint64_t elapsed;
|
|
uint32_t index;
|
|
int rc;
|
|
|
|
if (count > BATCH) {
|
|
*out_rc = FT_TT_EINVAL;
|
|
return 0u;
|
|
}
|
|
for (index = 0; index < count; index++)
|
|
batch[index] = *descriptor;
|
|
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_queue_enqueue(&session->noc, &g_queue, batch, count);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queue, g_completed + count,
|
|
RUN_TIMEOUT_MS, NULL);
|
|
elapsed = monotonic_microseconds() - started;
|
|
if (rc == FT_TT_OK)
|
|
g_completed += count;
|
|
*out_rc = rc;
|
|
return elapsed;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The arm table
|
|
* ================================================================ */
|
|
|
|
/** Where an operand lives. The op takes a NoC (x, y, address) triple, so this is the whole
|
|
* of "keep the intermediate in L1 instead of DRAM" — a placement, not a kernel. */
|
|
typedef enum operand_place {
|
|
PLACE_DRAM = 0,
|
|
PLACE_L1_SELF = 1,
|
|
PLACE_L1_PEER = 2
|
|
} operand_place;
|
|
|
|
typedef struct eltwise_arm {
|
|
const char *label;
|
|
ft_tt_op_vector_function function;
|
|
uint32_t tiles;
|
|
operand_place inputs;
|
|
operand_place output;
|
|
/** Point input B at input A's buffer instead of B's — the NEGATIVE CONTROL, which must
|
|
* produce a DIFFERENT answer and so proves the byte comparison can fail. */
|
|
bool alias_b_onto_a;
|
|
uint64_t total_microseconds;
|
|
} eltwise_arm;
|
|
|
|
static void place_operand(ft_tt_op_operand *operand, operand_place place, uint32_t dram_bank,
|
|
uint32_t l1_address)
|
|
{
|
|
switch (place) {
|
|
case PLACE_L1_SELF:
|
|
operand->x = g_self_x;
|
|
operand->y = g_self_y;
|
|
operand->address = l1_address;
|
|
break;
|
|
case PLACE_L1_PEER:
|
|
operand->x = g_peer_x;
|
|
operand->y = g_peer_y;
|
|
operand->address = l1_address;
|
|
break;
|
|
case PLACE_DRAM:
|
|
default:
|
|
operand->x = (uint16_t)g_dram[dram_bank].x;
|
|
operand->y = (uint16_t)g_dram[dram_bank].y;
|
|
operand->address = DRAM_BUFFER_BASE;
|
|
break;
|
|
}
|
|
operand->tile_bytes = TILE_BYTES;
|
|
}
|
|
|
|
static void fill_eltwise_request(ft_tt_op_eltwise_request *request, const eltwise_arm *arm)
|
|
{
|
|
memset(request, 0, sizeof(*request));
|
|
request->function = arm->function;
|
|
request->tile_count = arm->tiles;
|
|
request->prefetch_tiles = ELTWISE_DEPTH;
|
|
place_operand(&request->input_a, arm->inputs, DRAM_BANK_A, L1_SCRATCH_A);
|
|
if (ft_tt_op_vector_function_is_binary(arm->function)) {
|
|
if (arm->alias_b_onto_a)
|
|
place_operand(&request->input_b, arm->inputs, DRAM_BANK_A, L1_SCRATCH_A);
|
|
else
|
|
place_operand(&request->input_b, arm->inputs, DRAM_BANK_B, L1_SCRATCH_B);
|
|
}
|
|
place_operand(&request->output, arm->output, DRAM_BANK_C, L1_SCRATCH_C);
|
|
}
|
|
|
|
/** Read one arm's output back, wherever it landed. */
|
|
static int read_arm_output(const ft_tt_hardware_session *session, const eltwise_arm *arm,
|
|
void *destination)
|
|
{
|
|
const size_t bytes = (size_t)arm->tiles * TILE_BYTES;
|
|
|
|
switch (arm->output) {
|
|
case PLACE_L1_SELF:
|
|
return device_read(&session->noc, g_self_x, g_self_y, L1_SCRATCH_C, destination, bytes);
|
|
case PLACE_L1_PEER:
|
|
return device_read(&session->noc, g_peer_x, g_peer_y, L1_SCRATCH_C, destination, bytes);
|
|
case PLACE_DRAM:
|
|
default:
|
|
return device_read(&session->noc, (uint16_t)g_dram[DRAM_BANK_C].x,
|
|
(uint16_t)g_dram[DRAM_BANK_C].y, DRAM_BUFFER_BASE, destination, bytes);
|
|
}
|
|
}
|
|
|
|
static int poison_arm_output(const ft_tt_hardware_session *session, const eltwise_arm *arm)
|
|
{
|
|
const size_t bytes = (size_t)arm->tiles * TILE_BYTES;
|
|
|
|
switch (arm->output) {
|
|
case PLACE_L1_SELF:
|
|
return fill_pattern(&session->noc, g_self_x, g_self_y, L1_SCRATCH_C, bytes, POISON_BYTE);
|
|
case PLACE_L1_PEER:
|
|
return fill_pattern(&session->noc, g_peer_x, g_peer_y, L1_SCRATCH_C, bytes, POISON_BYTE);
|
|
case PLACE_DRAM:
|
|
default:
|
|
return fill_pattern(&session->noc, (uint16_t)g_dram[DRAM_BANK_C].x,
|
|
(uint16_t)g_dram[DRAM_BANK_C].y, DRAM_BUFFER_BASE, bytes, POISON_BYTE);
|
|
}
|
|
}
|
|
|
|
/* The arms. Order is the interleave order and does not change between rounds. */
|
|
static eltwise_arm g_arms[] = {
|
|
{ "MUL 144 A,B DRAM C DRAM", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES, PLACE_DRAM, PLACE_DRAM,
|
|
false, 0u },
|
|
{ "MUL 144 A,B L1self C DRAM", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES, PLACE_L1_SELF, PLACE_DRAM,
|
|
false, 0u },
|
|
{ "MUL 144 A,B DRAM C L1self", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES, PLACE_DRAM,
|
|
PLACE_L1_SELF, false, 0u },
|
|
{ "MUL 144 A,B L1self C L1self", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES, PLACE_L1_SELF,
|
|
PLACE_L1_SELF, false, 0u },
|
|
{ "MUL 144 A,B L1peer C L1peer", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES, PLACE_L1_PEER,
|
|
PLACE_L1_PEER, false, 0u },
|
|
{ "SILU 144 A DRAM C DRAM", FT_TT_OP_VECTOR_FN_SILU, MAX_TILES, PLACE_DRAM, PLACE_DRAM,
|
|
false, 0u },
|
|
{ "SILU 144 A L1self C L1self", FT_TT_OP_VECTOR_FN_SILU, MAX_TILES, PLACE_L1_SELF,
|
|
PLACE_L1_SELF, false, 0u },
|
|
{ "SILUMUL 144 A,B DRAM C DRAM", FT_TT_OP_VECTOR_FN_SILU_MUL, MAX_TILES, PLACE_DRAM,
|
|
PLACE_DRAM, false, 0u },
|
|
{ "MUL 48 A,B DRAM C DRAM", FT_TT_OP_VECTOR_FN_MUL, HIDDEN_TILES, PLACE_DRAM, PLACE_DRAM,
|
|
false, 0u },
|
|
{ "SILU 48 A DRAM C DRAM", FT_TT_OP_VECTOR_FN_SILU, HIDDEN_TILES, PLACE_DRAM,
|
|
PLACE_DRAM, false, 0u },
|
|
{ "ADD 48 A,B DRAM C DRAM", FT_TT_OP_VECTOR_FN_ADD, HIDDEN_TILES, PLACE_DRAM, PLACE_DRAM,
|
|
false, 0u },
|
|
{ "MUL 144 NEGATIVE CONTROL (B aliased onto A)", FT_TT_OP_VECTOR_FN_MUL, MAX_TILES,
|
|
PLACE_DRAM, PLACE_DRAM, true, 0u }
|
|
};
|
|
|
|
#define ARM_COUNT (sizeof(g_arms) / sizeof(g_arms[0]))
|
|
|
|
/* Indices into g_arms that the report and the byte comparisons name. */
|
|
#define ARM_MUL144_DD 0u
|
|
#define ARM_MUL144_LD 1u
|
|
#define ARM_MUL144_DL 2u
|
|
#define ARM_MUL144_LL 3u
|
|
#define ARM_MUL144_PEER 4u
|
|
#define ARM_SILU144_DD 5u
|
|
#define ARM_SILU144_LL 6u
|
|
#define ARM_SILUMUL144 7u
|
|
#define ARM_MUL48_DD 8u
|
|
#define ARM_SILU48_DD 9u
|
|
#define ARM_ADD48_DD 10u
|
|
#define ARM_NEGATIVE 11u
|
|
|
|
/* ================================================================
|
|
* Stage 0 — the transfer path, before anything is scored
|
|
* ================================================================ */
|
|
|
|
static void stage_0_round_trip(const ft_tt_hardware_session *session)
|
|
{
|
|
uint16_t written[TILE_DIM * TILE_DIM];
|
|
uint16_t read_back[TILE_DIM * TILE_DIM];
|
|
size_t index;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 0 — host->DRAM->host and host->peer L1->host, so the transfer path "
|
|
"is a LINE and not an assumption");
|
|
|
|
random_seed(0xF05E0001ULL);
|
|
for (index = 0; index < TILE_DIM * TILE_DIM; index++)
|
|
written[index] = ft_bf16_from_float(random_value());
|
|
|
|
memset(read_back, 0xA5, sizeof(read_back));
|
|
rc = device_write(&session->noc, (uint16_t)g_dram[DRAM_BANK_A].x,
|
|
(uint16_t)g_dram[DRAM_BANK_A].y, DRAM_BUFFER_BASE, written, sizeof(written));
|
|
if (rc == FT_TT_OK)
|
|
rc = device_read(&session->noc, (uint16_t)g_dram[DRAM_BANK_A].x,
|
|
(uint16_t)g_dram[DRAM_BANK_A].y, DRAM_BUFFER_BASE, read_back,
|
|
sizeof(read_back));
|
|
ft_tt_test_check_fmt("a tile round-trips through DRAM", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check("and it comes back byte-exact",
|
|
memcmp(written, read_back, sizeof(written)) == 0, "");
|
|
|
|
memset(read_back, 0xA5, sizeof(read_back));
|
|
rc = device_write(&session->noc, g_peer_x, g_peer_y, L1_SCRATCH_A, written, sizeof(written));
|
|
if (rc == FT_TT_OK)
|
|
rc = device_read(&session->noc, g_peer_x, g_peer_y, L1_SCRATCH_A, read_back,
|
|
sizeof(read_back));
|
|
ft_tt_test_check_fmt("a tile round-trips through the PEER core's L1", rc == FT_TT_OK, "rc=%d",
|
|
rc);
|
|
ft_tt_test_check("and it comes back byte-exact",
|
|
memcmp(written, read_back, sizeof(written)) == 0, "");
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 1 — the timing harness's own red/green, before anything it protects
|
|
* ================================================================ */
|
|
|
|
static double arm_microseconds(uint32_t arm);
|
|
|
|
/** The dispatch floor, measured by stage 2 in this run, on this core. It exists to be
|
|
* SUBTRACTED — never to license a fusion (plan Phase 6 item 8). */
|
|
static double g_nop_microseconds;
|
|
|
|
/**
|
|
* The timing harness's OWN red/green, and it is deliberately run on the very op the rest
|
|
* of the suite measures rather than on a synthetic one.
|
|
*
|
|
* FT_TT_QUEUE_OP_DELAY was the obvious instrument and it is NOT available here: a resident
|
|
* binary is bound to one slab, the elementwise triad implements ELTWISE, NOP and STOP, and
|
|
* it refuses everything else with kErrBadOpcode — correctly. So the known ratio comes from
|
|
* the work instead: three times the tiles, net of the dispatch floor measured in the same
|
|
* run, must cost about three times the wall clock. A harness that measured nothing would
|
|
* report 1.0, and one that measured only the host's poll would too.
|
|
*/
|
|
static bool check_timer_resolves_work(void)
|
|
{
|
|
const double mul144 = arm_microseconds(ARM_MUL144_DD) - g_nop_microseconds;
|
|
const double mul48 = arm_microseconds(ARM_MUL48_DD) - g_nop_microseconds;
|
|
const double silu144 = arm_microseconds(ARM_SILU144_DD) - g_nop_microseconds;
|
|
const double silu48 = arm_microseconds(ARM_SILU48_DD) - g_nop_microseconds;
|
|
const double mul_ratio = (mul48 > 0.0) ? mul144 / mul48 : 0.0;
|
|
const double silu_ratio = (silu48 > 0.0) ? silu144 / silu48 : 0.0;
|
|
|
|
printf("\n timer resolution, from the work itself (net of the %.3f us dispatch floor):\n",
|
|
g_nop_microseconds);
|
|
printf(" MUL 144 / MUL 48 = %.3f (144/48 = 3.000)\n", mul_ratio);
|
|
printf(" SILU 144 / SILU 48 = %.3f\n", silu_ratio);
|
|
|
|
ft_tt_test_check_fmt("3x the tiles costs 2.7-3.3x the time on BOTH functions, so this timer "
|
|
"resolves work and not host noise",
|
|
mul_ratio > 2.7 && mul_ratio < 3.3 && silu_ratio > 2.7 &&
|
|
silu_ratio < 3.3,
|
|
"MUL %.3f SILU %.3f", mul_ratio, silu_ratio);
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 2 — the dispatch floor, measured in THIS run so it can be subtracted
|
|
* ================================================================ */
|
|
|
|
static bool stage_2_dispatch_floor(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_queue_descriptor descriptor;
|
|
uint64_t total = 0u;
|
|
uint32_t round;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 2 — the DISPATCH FLOOR on this core, this run: a batch of NOPs. It is "
|
|
"here to be SUBTRACTED, never to license a fusion (plan Phase 6 item 8)");
|
|
|
|
for (round = 0; round < g_rounds; round++) {
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_NOP;
|
|
total += time_batch(session, &descriptor, BATCH, &rc);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
ft_tt_test_check_fmt("the NOP arm ran", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
g_nop_microseconds = (double)total / (double)(g_rounds * BATCH);
|
|
printf("\n NOP, batch %u, %u rounds : %.3f us per descriptor\n", BATCH, g_rounds,
|
|
g_nop_microseconds);
|
|
ft_tt_test_check_fmt("the dispatch floor is microseconds, not tens of them — so a fusion "
|
|
"sold on descriptor count would be selling nothing",
|
|
g_nop_microseconds < 10.0, "%.3f us", g_nop_microseconds);
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 3 — the round-trip matrix
|
|
* ================================================================ */
|
|
|
|
static int start_eltwise_residency(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_op_eltwise_request request;
|
|
eltwise_arm widest = g_arms[ARM_MUL144_DD];
|
|
uint64_t mask = 0;
|
|
int rc;
|
|
|
|
fill_eltwise_request(&request, &widest);
|
|
rc = load_images(&session->noc, g_self_x, g_self_y, &g_eltwise);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_program_load(&session->noc, g_self_x, g_self_y, &g_eltwise.program, NULL,
|
|
&g_config);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_eltwise_place_cbs(&request, g_cb_placed, g_cb_buffer, sizeof(g_cb_buffer),
|
|
&mask);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
g_config.local_cb_mask = mask;
|
|
g_config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_config.brisc_noc_id = 0u;
|
|
rc = ft_tt_queue_init(&session->noc, g_self_x, g_self_y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &g_queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
g_completed = 0u;
|
|
return start_triad(&session->noc, g_self_x, g_self_y);
|
|
}
|
|
|
|
/** Every operand buffer, in every place an arm can read it from, filled with the SAME
|
|
* values — which is what makes the DRAM arm and the L1 arm comparable byte for byte. */
|
|
static int upload_all_operands(const ft_tt_hardware_session *session)
|
|
{
|
|
int rc;
|
|
|
|
rc = upload_random_tiles(session, (uint16_t)g_dram[DRAM_BANK_A].x,
|
|
(uint16_t)g_dram[DRAM_BANK_A].y, DRAM_BUFFER_BASE, MAX_TILES,
|
|
0xA11CE0001ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, (uint16_t)g_dram[DRAM_BANK_B].x,
|
|
(uint16_t)g_dram[DRAM_BANK_B].y, DRAM_BUFFER_BASE, MAX_TILES,
|
|
0xB0B0B0002ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, g_self_x, g_self_y, L1_SCRATCH_A, MAX_TILES,
|
|
0xA11CE0001ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, g_self_x, g_self_y, L1_SCRATCH_B, MAX_TILES,
|
|
0xB0B0B0002ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, g_peer_x, g_peer_y, L1_SCRATCH_A, MAX_TILES,
|
|
0xA11CE0001ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, g_peer_x, g_peer_y, L1_SCRATCH_B, MAX_TILES,
|
|
0xB0B0B0002ULL);
|
|
return rc;
|
|
}
|
|
|
|
static bool stage_3_round_trip_matrix(const ft_tt_hardware_session *session)
|
|
{
|
|
const size_t widest_bytes = (size_t)MAX_TILES * TILE_BYTES;
|
|
unsigned char *dram_answer = malloc(widest_bytes);
|
|
unsigned char *other_answer = malloc(widest_bytes);
|
|
uint32_t slot_base = 0u;
|
|
uint32_t round;
|
|
uint32_t arm;
|
|
bool ok = true;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 3 — the ROUND-TRIP MATRIX: one op, one residency, only the operand "
|
|
"ADDRESSES change. %u arms x %u rounds x %u descriptors, interleaved",
|
|
(unsigned)ARM_COUNT, g_rounds, BATCH);
|
|
|
|
if (!dram_answer || !other_answer) {
|
|
ft_tt_test_check("scratch allocates", false, "");
|
|
free(dram_answer);
|
|
free(other_answer);
|
|
return false;
|
|
}
|
|
|
|
/* One parameter block per arm, written once. Enqueueing eight descriptors that all
|
|
* point at the same block is what keeps the timed window free of host work. */
|
|
for (arm = 0; arm < ARM_COUNT; arm++) {
|
|
ft_tt_op_eltwise_request request;
|
|
uint32_t words[FT_TT_OP_VECTOR_WORDS];
|
|
uint32_t address = 0u;
|
|
|
|
fill_eltwise_request(&request, &g_arms[arm]);
|
|
rc = ft_tt_op_eltwise_parameters(&request, words);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_write_op_parameters(&session->noc, &g_queue, slot_base + arm, words,
|
|
&address);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("every arm's parameter block writes", false, "arm %u rc=%d", arm,
|
|
rc);
|
|
ok = false;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
for (round = 0; round < g_rounds; round++) {
|
|
for (arm = 0; arm < ARM_COUNT; arm++) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ELTWISE;
|
|
descriptor.a0 = FT_TT_QUEUE_PARAM_ADDRESS(g_queue.control_address, slot_base + arm);
|
|
g_arms[arm].total_microseconds += time_batch(session, &descriptor, BATCH, &rc);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("every arm completes its batch", false,
|
|
"round %u arm %u (%s) rc=%d", round, arm, g_arms[arm].label,
|
|
rc);
|
|
ok = false;
|
|
goto done;
|
|
}
|
|
}
|
|
}
|
|
ft_tt_test_check("every arm completed every batch on ONE residency, with no launch and no "
|
|
"image load between them",
|
|
true, "");
|
|
|
|
/* --- the answers, as BYTES, and a negative control that proves the comparison can fail */
|
|
rc = poison_arm_output(session, &g_arms[ARM_MUL144_DD]);
|
|
if (rc == FT_TT_OK) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ELTWISE;
|
|
descriptor.a0 =
|
|
FT_TT_QUEUE_PARAM_ADDRESS(g_queue.control_address, slot_base + ARM_MUL144_DD);
|
|
(void)time_batch(session, &descriptor, 1u, &rc);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = read_arm_output(session, &g_arms[ARM_MUL144_DD], dram_answer);
|
|
ft_tt_test_check_fmt("the DRAM arm's answer reads back", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK) {
|
|
ok = false;
|
|
goto done;
|
|
}
|
|
ft_tt_test_check_fmt("and NO TILE came back untouched, so the kernel wrote the whole output "
|
|
"rather than the previous round's bytes surviving",
|
|
poison_tiles_remaining(dram_answer, widest_bytes) == 0,
|
|
"%zu of %u tiles still all-poison",
|
|
poison_tiles_remaining(dram_answer, widest_bytes), MAX_TILES);
|
|
|
|
{
|
|
static const uint32_t compared[] = { ARM_MUL144_LL, ARM_MUL144_PEER, ARM_MUL144_DL,
|
|
ARM_MUL144_LD };
|
|
size_t index;
|
|
|
|
for (index = 0; index < sizeof(compared) / sizeof(compared[0]); index++) {
|
|
const uint32_t which = compared[index];
|
|
char label[160];
|
|
|
|
rc = poison_arm_output(session, &g_arms[which]);
|
|
if (rc == FT_TT_OK) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ELTWISE;
|
|
descriptor.a0 =
|
|
FT_TT_QUEUE_PARAM_ADDRESS(g_queue.control_address, slot_base + which);
|
|
(void)time_batch(session, &descriptor, 1u, &rc);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = read_arm_output(session, &g_arms[which], other_answer);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("a placement arm's answer reads back", false, "arm %u rc=%d",
|
|
which, rc);
|
|
ok = false;
|
|
goto done;
|
|
}
|
|
snprintf(label, sizeof(label),
|
|
"[%s] is BYTE-IDENTICAL to the all-DRAM arm — a placement moves where data "
|
|
"lives, never what it is",
|
|
g_arms[which].label);
|
|
ft_tt_test_check_fmt(label, memcmp(dram_answer, other_answer, widest_bytes) == 0,
|
|
"%zu of %u tiles still all-poison",
|
|
poison_tiles_remaining(other_answer, widest_bytes), MAX_TILES);
|
|
}
|
|
}
|
|
|
|
/* THE NEGATIVE CONTROL. Same comparison, same code path, against an arm whose operands
|
|
* are deliberately wrong — so "byte-identical" is known to be a claim this suite can
|
|
* refuse, and not a comparison that would pass on anything (dev notes §3). */
|
|
rc = poison_arm_output(session, &g_arms[ARM_NEGATIVE]);
|
|
if (rc == FT_TT_OK) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ELTWISE;
|
|
descriptor.a0 = FT_TT_QUEUE_PARAM_ADDRESS(g_queue.control_address, slot_base + ARM_NEGATIVE);
|
|
(void)time_batch(session, &descriptor, 1u, &rc);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = read_arm_output(session, &g_arms[ARM_NEGATIVE], other_answer);
|
|
ft_tt_test_check_fmt("the negative control ran", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc == FT_TT_OK)
|
|
ft_tt_test_check("NEGATIVE CONTROL: the byte comparison REFUSES an arm computing a*a "
|
|
"instead of a*b — so the four greens above are a detection, not a "
|
|
"tautology",
|
|
memcmp(dram_answer, other_answer, widest_bytes) != 0, "");
|
|
|
|
done:
|
|
free(dram_answer);
|
|
free(other_answer);
|
|
return ok;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 3b — is the DRAM write premium a LATENCY? Batch the barrier and see
|
|
* ================================================================ */
|
|
|
|
/** Tiles per NoC write barrier in the blocked writer, and the pages the output CB is given
|
|
* so that a block is contiguous. 8 divides both 48 and 144, and 16 % 8 == 0. */
|
|
#define WRITE_BLOCK 8u
|
|
#define WRITE_BLOCK_OUT_PAGES 16u
|
|
|
|
static ft_tt_program_directory g_eltwise_blocked;
|
|
static ft_tt_program_directory g_eltwise_flushed;
|
|
static bool g_have_blocked_writer;
|
|
static bool g_have_flushed_writer;
|
|
|
|
/** The writer arms, in interleave order. `resident_vector_writer.c` is the shipped one and
|
|
* is always arm 0, so every other arm's ratio is against what ships. */
|
|
enum { WRITER_PER_TILE = 0, WRITER_BLOCKED = 1, WRITER_FLUSHED = 2, WRITER_ARM_COUNT = 3 };
|
|
|
|
static const char *const g_writer_arm_names[WRITER_ARM_COUNT] = {
|
|
"barrier/tile", "barrier/block", "flush/tile"
|
|
};
|
|
|
|
static uint64_t g_writer_ab_total[WRITER_ARM_COUNT][3];
|
|
|
|
/** The three arms of the writer A/B: the two ops the round-trip matrix found EXPOSED to
|
|
* the write path, plus the one it found hidden behind the SFPU. */
|
|
static const uint32_t g_writer_ab_arms[3] = { ARM_MUL144_DD, ARM_ADD48_DD, ARM_SILU144_DD };
|
|
|
|
/** CBs for the writer A/B — placed BY HAND rather than by ft_tt_op_eltwise_place_cbs(),
|
|
* because the blocked writer needs an output CB deep enough to hold a whole block and
|
|
* that is a host placement, not a change to anything shipped. BOTH arms get the identical
|
|
* placement: an A/B with two variables measures neither. */
|
|
static int start_eltwise_writer_arm(const ft_tt_hardware_session *session,
|
|
const ft_tt_program_directory *program)
|
|
{
|
|
ft_tt_op_cb_request requests[4];
|
|
uint64_t mask = 0;
|
|
int rc = load_images(&session->noc, g_self_x, g_self_y, program);
|
|
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_program_load(&session->noc, g_self_x, g_self_y, &program->program, NULL,
|
|
&g_config);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
requests[0].index = FT_TT_OP_ELTWISE_CB_IN0;
|
|
requests[0].page_bytes = TILE_BYTES;
|
|
requests[0].page_count = ELTWISE_DEPTH * 2u;
|
|
requests[1].index = FT_TT_OP_ELTWISE_CB_IN1;
|
|
requests[1].page_bytes = TILE_BYTES;
|
|
requests[1].page_count = ELTWISE_DEPTH * 2u;
|
|
requests[2].index = FT_TT_OP_ELTWISE_CB_SCRATCH;
|
|
requests[2].page_bytes = TILE_BYTES;
|
|
requests[2].page_count = 2u;
|
|
requests[3].index = FT_TT_OP_ELTWISE_CB_OUT;
|
|
requests[3].page_bytes = TILE_BYTES;
|
|
requests[3].page_count = WRITE_BLOCK_OUT_PAGES;
|
|
rc = ft_tt_op_cb_place(requests, 4u, g_cb_placed, g_cb_buffer, sizeof(g_cb_buffer), &mask);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
g_config.local_cb_mask = mask;
|
|
g_config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_config.brisc_noc_id = 0u;
|
|
rc = ft_tt_queue_init(&session->noc, g_self_x, g_self_y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &g_queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
g_completed = 0u;
|
|
return start_triad(&session->noc, g_self_x, g_self_y);
|
|
}
|
|
|
|
/** One arm's three ops, on an already-started residency. `answer` receives the MUL 144
|
|
* result so the two writers can be compared as BYTES. */
|
|
static int run_writer_arm(const ft_tt_hardware_session *session, uint64_t out_totals[3],
|
|
unsigned char *answer)
|
|
{
|
|
uint32_t index;
|
|
int rc = FT_TT_OK;
|
|
|
|
for (index = 0; index < 3u; index++) {
|
|
const uint32_t arm = g_writer_ab_arms[index];
|
|
ft_tt_op_eltwise_request request;
|
|
ft_tt_queue_descriptor descriptor;
|
|
uint32_t words[FT_TT_OP_VECTOR_WORDS];
|
|
uint32_t address = 0u;
|
|
|
|
fill_eltwise_request(&request, &g_arms[arm]);
|
|
/* POISON FIRST for the arm whose bytes are compared. Without this the "no tile came
|
|
* back untouched" line scores the PREVIOUS stage's leftovers and cannot fail — the
|
|
* @issues note this file already carried. */
|
|
if (index == 0u && answer)
|
|
rc = poison_arm_output(session, &g_arms[arm]);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_eltwise_parameters(&request, words);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_write_op_parameters(&session->noc, &g_queue, index, words, &address);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ELTWISE;
|
|
descriptor.a0 = address;
|
|
out_totals[index] += time_batch(session, &descriptor, BATCH, &rc);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
if (index == 0u && answer)
|
|
rc = read_arm_output(session, &g_arms[arm], answer);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** The three writer program directories, indexed by the WRITER_* enum. An absent arm is
|
|
* NULL and is skipped, so the suite still runs when only two were built. */
|
|
static const ft_tt_program_directory *writer_arm_program(uint32_t arm)
|
|
{
|
|
switch (arm) {
|
|
case WRITER_PER_TILE:
|
|
return &g_eltwise;
|
|
case WRITER_BLOCKED:
|
|
return g_have_blocked_writer ? &g_eltwise_blocked : NULL;
|
|
case WRITER_FLUSHED:
|
|
return g_have_flushed_writer ? &g_eltwise_flushed : NULL;
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static bool stage_3b_writer_barrier(const ft_tt_hardware_session *session)
|
|
{
|
|
const size_t widest_bytes = (size_t)MAX_TILES * TILE_BYTES;
|
|
unsigned char *answers[WRITER_ARM_COUNT] = { NULL, NULL, NULL };
|
|
uint32_t round;
|
|
uint32_t index;
|
|
uint32_t arm;
|
|
bool ok = true;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 3b — is the DRAM write premium a LATENCY? THREE writers off one "
|
|
"source: a barrier per tile (ships), a barrier per %u tiles, and upstream's "
|
|
"flush-per-tile with ONE barrier per descriptor. Arms interleaved by ROUND",
|
|
WRITE_BLOCK);
|
|
|
|
for (arm = 0; arm < WRITER_ARM_COUNT; arm++) {
|
|
if (writer_arm_program(arm))
|
|
answers[arm] = malloc(widest_bytes);
|
|
}
|
|
if (!answers[WRITER_PER_TILE]) {
|
|
ft_tt_test_check("scratch allocates", false, "");
|
|
goto done;
|
|
}
|
|
|
|
for (round = 0; round < g_rounds && rc == FT_TT_OK; round++) {
|
|
for (arm = 0; arm < WRITER_ARM_COUNT && rc == FT_TT_OK; arm++) {
|
|
const ft_tt_program_directory *program = writer_arm_program(arm);
|
|
|
|
if (!program || !answers[arm])
|
|
continue;
|
|
rc = start_eltwise_writer_arm(session, program);
|
|
if (rc == FT_TT_OK)
|
|
rc = run_writer_arm(session, g_writer_ab_total[arm], answers[arm]);
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &g_group, RUN_TIMEOUT_MS, NULL);
|
|
}
|
|
}
|
|
ft_tt_test_check_fmt("every writer arm ran every round", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK) {
|
|
ok = false;
|
|
goto done;
|
|
}
|
|
|
|
/* A BARRIER-PLACEMENT CHANGE MAY NOT MOVE A VALUE. Each arm is compared against the
|
|
* shipped writer's bytes, and the destination was poisoned before each arm's own run,
|
|
* so "no tile came back untouched" is a line that can fail. */
|
|
for (arm = 1u; arm < WRITER_ARM_COUNT; arm++) {
|
|
char label[160];
|
|
|
|
if (!writer_arm_program(arm) || !answers[arm])
|
|
continue;
|
|
snprintf(label, sizeof(label),
|
|
"the %s writer produces BYTE-IDENTICAL output to the shipped per-tile "
|
|
"barrier — a barrier-placement change that moved a value would be a bug, "
|
|
"not a tradeoff",
|
|
g_writer_arm_names[arm]);
|
|
ft_tt_test_check(label, memcmp(answers[WRITER_PER_TILE], answers[arm], widest_bytes) == 0,
|
|
"");
|
|
snprintf(label, sizeof(label), "and the %s writer left no tile untouched",
|
|
g_writer_arm_names[arm]);
|
|
ft_tt_test_check_fmt(label, poison_tiles_remaining(answers[arm], widest_bytes) == 0,
|
|
"%zu of %u tiles still all-poison",
|
|
poison_tiles_remaining(answers[arm], widest_bytes), MAX_TILES);
|
|
}
|
|
|
|
printf("\n THE WRITER'S BARRIER — three arms, identical CBs, %u tiles a block\n",
|
|
WRITE_BLOCK);
|
|
printf(" op ");
|
|
for (arm = 0; arm < WRITER_ARM_COUNT; arm++)
|
|
printf(" %13s", writer_arm_program(arm) ? g_writer_arm_names[arm] : "(not built)");
|
|
printf(" best vs ships\n");
|
|
for (index = 0; index < 3u; index++) {
|
|
const uint32_t which = g_writer_ab_arms[index];
|
|
const double shipped =
|
|
(double)g_writer_ab_total[WRITER_PER_TILE][index] / (double)(g_rounds * BATCH);
|
|
double best = shipped;
|
|
|
|
printf(" %-30s", g_arms[which].label);
|
|
for (arm = 0; arm < WRITER_ARM_COUNT; arm++) {
|
|
double value;
|
|
|
|
if (!writer_arm_program(arm)) {
|
|
printf(" %13s", "-");
|
|
continue;
|
|
}
|
|
value = (double)g_writer_ab_total[arm][index] / (double)(g_rounds * BATCH);
|
|
if (value > 0.0 && value < best)
|
|
best = value;
|
|
printf(" %10.2f us", value);
|
|
}
|
|
printf(" %8.3fx %8.1f ns/tile\n", (best > 0.0) ? shipped / best : 0.0,
|
|
(shipped - best) * 1000.0 / (double)g_arms[which].tiles);
|
|
}
|
|
|
|
done:
|
|
for (arm = 0; arm < WRITER_ARM_COUNT; arm++)
|
|
free(answers[arm]);
|
|
return ok;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 4 — the matmul denominators, and what one output tile write costs
|
|
* ================================================================ */
|
|
|
|
typedef struct matmul_shape {
|
|
const char *label;
|
|
uint32_t tile_rows;
|
|
uint32_t tile_inner;
|
|
uint32_t tile_cols;
|
|
uint64_t total_microseconds;
|
|
} matmul_shape;
|
|
|
|
/* MiniCPM5-1B at Mt = 1 (32 rows), the orientation ft_tt_op_matmul.h settled: A is
|
|
* [Mt*32 x Kt*32], B is K x N. Hidden 1536, q 2048, kv 256, FFN 4608 — from the
|
|
* checkpoint's own metadata, not from a table. */
|
|
static matmul_shape g_shapes[] = {
|
|
{ "attn_q 1536x2048", 1u, 48u, 64u, 0u },
|
|
{ "attn_k/v 1536x256", 1u, 48u, 8u, 0u },
|
|
{ "attn_output 2048x1536", 1u, 64u, 48u, 0u },
|
|
{ "ffn_gate/up 1536x4608", 1u, 48u, 144u, 0u },
|
|
{ "ffn_down 4608x1536", 1u, 144u, 48u, 0u }
|
|
};
|
|
|
|
#define SHAPE_COUNT (sizeof(g_shapes) / sizeof(g_shapes[0]))
|
|
#define SHAPE_FFN_GATE 3u
|
|
#define SHAPE_FFN_DOWN 4u
|
|
|
|
static int start_matmul_residency(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_op_cb_request requests[3];
|
|
uint64_t mask = 0;
|
|
int rc;
|
|
|
|
rc = load_images(&session->noc, g_self_x, g_self_y, &g_rtriad);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_program_load(&session->noc, g_self_x, g_self_y, &g_rtriad.program, NULL,
|
|
&g_config);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
requests[0].index = 0u;
|
|
requests[0].page_bytes = TILE_BYTES;
|
|
requests[0].page_count = MATMUL_DEPTH * 2u;
|
|
requests[1].index = 1u;
|
|
requests[1].page_bytes = TILE_BYTES;
|
|
requests[1].page_count = MATMUL_DEPTH * 2u;
|
|
requests[2].index = 16u;
|
|
requests[2].page_bytes = TILE_BYTES;
|
|
requests[2].page_count = 2u;
|
|
rc = ft_tt_op_cb_place(requests, 3u, g_cb_placed, g_cb_buffer, sizeof(g_cb_buffer), &mask);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
g_config.local_cb_mask = mask;
|
|
g_config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_config.brisc_noc_id = 0u;
|
|
rc = ft_tt_queue_init(&session->noc, g_self_x, g_self_y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &g_queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
g_completed = 0u;
|
|
return start_triad(&session->noc, g_self_x, g_self_y);
|
|
}
|
|
|
|
static bool stage_4_matmul_shapes(const ft_tt_hardware_session *session)
|
|
{
|
|
uint32_t round;
|
|
uint32_t index;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 4 — every real MiniCPM5-1B matmul shape at Mt=1 on a resident triad, "
|
|
"interleaved. These are the DENOMINATORS a fusion's saving is a fraction of");
|
|
|
|
for (index = 0; index < SHAPE_COUNT; index++) {
|
|
uint32_t words[FT_TT_QUEUE_MATMUL_WORDS];
|
|
uint32_t address = 0u;
|
|
|
|
memset(words, 0, sizeof(words));
|
|
words[FT_TT_QUEUE_MATMUL_TILE_ROWS] = g_shapes[index].tile_rows;
|
|
words[FT_TT_QUEUE_MATMUL_TILE_INNER] = g_shapes[index].tile_inner;
|
|
words[FT_TT_QUEUE_MATMUL_TILE_COLS] = g_shapes[index].tile_cols;
|
|
words[FT_TT_QUEUE_MATMUL_A_X] = (uint32_t)g_dram[DRAM_BANK_A].x;
|
|
words[FT_TT_QUEUE_MATMUL_A_Y] = (uint32_t)g_dram[DRAM_BANK_A].y;
|
|
words[FT_TT_QUEUE_MATMUL_A_BASE] = DRAM_BUFFER_BASE;
|
|
words[FT_TT_QUEUE_MATMUL_B_X] = (uint32_t)g_dram[DRAM_BANK_B].x;
|
|
words[FT_TT_QUEUE_MATMUL_B_Y] = (uint32_t)g_dram[DRAM_BANK_B].y;
|
|
words[FT_TT_QUEUE_MATMUL_B_BASE] = DRAM_BUFFER_BASE;
|
|
words[FT_TT_QUEUE_MATMUL_C_X] = (uint32_t)g_dram[DRAM_BANK_C].x;
|
|
words[FT_TT_QUEUE_MATMUL_C_Y] = (uint32_t)g_dram[DRAM_BANK_C].y;
|
|
words[FT_TT_QUEUE_MATMUL_C_BASE] = DRAM_BUFFER_BASE;
|
|
/* SHRINK IT HOST-SIDE. resident_matmul_reader.c:84 collapses a depth that does
|
|
* not DIVIDE Kt to 1 — always correct, and 5.9x slower (docs/tt_kernel_model.md
|
|
* §12), with nothing anywhere reporting it. Writing the parameter block by hand,
|
|
* which the resident queue path invites, skips the host-side shrink that is the
|
|
* only thing standing between a real shape and that fallback: attn_output (Kt=64)
|
|
* at a requested 48 measured 2.850 ms / 70.6 GFLOP/s here before this line existed,
|
|
* against 0.35 ms with the shrink. A wrong SPEED with no error anywhere. */
|
|
words[FT_TT_QUEUE_MATMUL_DEPTH] =
|
|
ft_tt_op_matmul_prefetch_depth(g_shapes[index].tile_inner, MATMUL_DEPTH);
|
|
words[FT_TT_QUEUE_MATMUL_B_N_MAJOR] = 0u;
|
|
|
|
rc = ft_tt_queue_write_op_parameters(&session->noc, &g_queue, index, words, &address);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("every shape's parameter block writes", false,
|
|
"shape %u rc=%d", index, rc);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (round = 0; round < g_rounds; round++) {
|
|
for (index = 0; index < SHAPE_COUNT; index++) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_MATMUL;
|
|
descriptor.a0 = FT_TT_QUEUE_PARAM_ADDRESS(g_queue.control_address, index);
|
|
g_shapes[index].total_microseconds += time_batch(session, &descriptor, 1u, &rc);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("every shape completes", false, "round %u shape %s rc=%d",
|
|
round, g_shapes[index].label, rc);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
ft_tt_test_check("every MiniCPM5-1B matmul shape ran on one residency", true, "");
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 4b — the RMS norm, which a MiniCPM5-1B layer runs twice
|
|
* ================================================================ */
|
|
|
|
static double g_rms_norm_microseconds;
|
|
|
|
static int start_reduce_residency(const ft_tt_hardware_session *session,
|
|
const ft_tt_op_reduce_residency *residency)
|
|
{
|
|
uint64_t mask = 0;
|
|
int rc = load_images(&session->noc, g_self_x, g_self_y, &g_reduce);
|
|
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_program_load(&session->noc, g_self_x, g_self_y, &g_reduce.program, NULL,
|
|
&g_config);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_reduce_place_cbs(residency, g_cb_placed, g_cb_buffer, sizeof(g_cb_buffer),
|
|
&mask);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
g_config.local_cb_mask = mask;
|
|
g_config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_config.brisc_noc_id = 0u;
|
|
rc = ft_tt_queue_init(&session->noc, g_self_x, g_self_y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &g_queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
g_completed = 0u;
|
|
return start_triad(&session->noc, g_self_x, g_self_y);
|
|
}
|
|
|
|
static bool stage_4b_rms_norm(const ft_tt_hardware_session *session)
|
|
{
|
|
const uint32_t cols = HIDDEN_TILES * TILE_DIM;
|
|
const ft_tt_op_reduce_function member = FT_TT_OP_REDUCE_FN_RMS_NORM;
|
|
ft_tt_op_reduce_residency residency;
|
|
ft_tt_op_reduce_request request;
|
|
uint32_t words[FT_TT_OP_VECTOR_WORDS];
|
|
uint32_t address = 0u;
|
|
uint64_t total = 0u;
|
|
unsigned char *constants = NULL;
|
|
float *gamma = NULL;
|
|
size_t constant_bytes;
|
|
uint32_t round;
|
|
uint32_t index;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 4b — RMS norm over 32x1536, which a MiniCPM5-1B layer runs TWICE. "
|
|
"The producer of every matmul's activation operand");
|
|
|
|
rc = ft_tt_op_reduce_residency_for(&member, 1u, HIDDEN_TILES, TILE_BYTES, &residency);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_reduce_residency(session, &residency);
|
|
ft_tt_test_check_fmt("the reduce residency starts", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
constant_bytes = ft_tt_op_reduce_constants_bytes(member, HIDDEN_TILES, TILE_BYTES);
|
|
constants = malloc(constant_bytes);
|
|
gamma = malloc((size_t)cols * sizeof(float));
|
|
if (!constants || !gamma) {
|
|
ft_tt_test_check("the norm's constants allocate", false, "");
|
|
goto done;
|
|
}
|
|
random_seed(0x6A11AULL);
|
|
for (index = 0; index < cols; index++)
|
|
gamma[index] = 0.5f + random_value() * 0.25f;
|
|
rc = ft_tt_op_reduce_build_constants(member, cols, 1e-6f, gamma, NULL,
|
|
FT_TT_OP_TILE_BYTES_BF16, constants, constant_bytes);
|
|
if (rc == FT_TT_OK)
|
|
rc = device_write(&session->noc, (uint16_t)g_dram[DRAM_BANK_CONST].x,
|
|
(uint16_t)g_dram[DRAM_BANK_CONST].y, DRAM_BUFFER_BASE, constants,
|
|
constant_bytes);
|
|
if (rc == FT_TT_OK)
|
|
rc = upload_random_tiles(session, (uint16_t)g_dram[DRAM_BANK_A].x,
|
|
(uint16_t)g_dram[DRAM_BANK_A].y, DRAM_BUFFER_BASE, HIDDEN_TILES,
|
|
0x1401110001ULL);
|
|
if (rc == FT_TT_OK)
|
|
rc = fill_pattern(&session->noc, (uint16_t)g_dram[DRAM_BANK_C].x,
|
|
(uint16_t)g_dram[DRAM_BANK_C].y, DRAM_BUFFER_BASE,
|
|
(size_t)HIDDEN_TILES * TILE_BYTES, POISON_BYTE);
|
|
ft_tt_test_check_fmt("the norm's operands and a poisoned output upload", rc == FT_TT_OK,
|
|
"rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
goto done;
|
|
|
|
memset(&request, 0, sizeof(request));
|
|
request.function = member;
|
|
request.row_blocks = 1u;
|
|
request.tile_cols = HIDDEN_TILES;
|
|
request.prefetch_tiles = HIDDEN_TILES;
|
|
place_operand(&request.input, PLACE_DRAM, DRAM_BANK_A, 0u);
|
|
place_operand(&request.constants, PLACE_DRAM, DRAM_BANK_CONST, 0u);
|
|
place_operand(&request.output, PLACE_DRAM, DRAM_BANK_C, 0u);
|
|
|
|
rc = ft_tt_op_reduce_parameters(&request, words);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_write_op_parameters(&session->noc, &g_queue, 0u, words, &address);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the norm's parameter block writes", false, "rc=%d", rc);
|
|
goto done;
|
|
}
|
|
|
|
for (round = 0; round < g_rounds; round++) {
|
|
ft_tt_queue_descriptor descriptor;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_REDUCE;
|
|
descriptor.a0 = address;
|
|
total += time_batch(session, &descriptor, BATCH, &rc);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
ft_tt_test_check_fmt("the norm arm ran", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
goto done;
|
|
g_rms_norm_microseconds = (double)total / (double)(g_rounds * BATCH);
|
|
|
|
{
|
|
unsigned char *answer = malloc((size_t)HIDDEN_TILES * TILE_BYTES);
|
|
|
|
if (answer) {
|
|
rc = device_read(&session->noc, (uint16_t)g_dram[DRAM_BANK_C].x,
|
|
(uint16_t)g_dram[DRAM_BANK_C].y, DRAM_BUFFER_BASE, answer,
|
|
(size_t)HIDDEN_TILES * TILE_BYTES);
|
|
ft_tt_test_check_fmt("the norm wrote every tile of its output", rc == FT_TT_OK &&
|
|
poison_tiles_remaining(answer, (size_t)HIDDEN_TILES *
|
|
TILE_BYTES) == 0,
|
|
"rc=%d", rc);
|
|
free(answer);
|
|
}
|
|
}
|
|
printf("\n RMS_NORM 32x1536 (Wt=%u) : %.2f us per descriptor\n", HIDDEN_TILES,
|
|
g_rms_norm_microseconds);
|
|
|
|
done:
|
|
free(constants);
|
|
free(gamma);
|
|
retire_triad(&session->noc, "the reduce residency retires by ASKING, never a park");
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Stage 5 — what a binary switch costs
|
|
* ================================================================ */
|
|
|
|
static double g_switch_microseconds;
|
|
|
|
static bool stage_5_binary_switch(const ft_tt_hardware_session *session)
|
|
{
|
|
uint64_t total = 0u;
|
|
uint32_t round;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 5 — what a BINARY SWITCH costs: five kernel images over the x1 link. "
|
|
"Not a fusion and not dispatch — a third thing, and the layer sequence hits "
|
|
"it a dozen times");
|
|
|
|
for (round = 0; round < g_rounds; round++) {
|
|
uint64_t started = monotonic_microseconds();
|
|
|
|
rc = load_images(&session->noc, g_self_x, g_self_y, &g_eltwise);
|
|
total += monotonic_microseconds() - started;
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
started = monotonic_microseconds();
|
|
rc = load_images(&session->noc, g_self_x, g_self_y, &g_rtriad);
|
|
total += monotonic_microseconds() - started;
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
ft_tt_test_check_fmt("both image sets loaded every round", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
g_switch_microseconds = (double)total / (double)(g_rounds * 2u);
|
|
printf("\n five kernel images, host -> L1 : %.3f ms per switch\n",
|
|
g_switch_microseconds / 1000.0);
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The report
|
|
* ================================================================ */
|
|
|
|
static double arm_microseconds(uint32_t arm)
|
|
{
|
|
return (double)g_arms[arm].total_microseconds / (double)(g_rounds * BATCH);
|
|
}
|
|
|
|
static double shape_microseconds(uint32_t index)
|
|
{
|
|
return (double)g_shapes[index].total_microseconds / (double)g_rounds;
|
|
}
|
|
|
|
static void report(void)
|
|
{
|
|
const double mul144 = arm_microseconds(ARM_MUL144_DD);
|
|
const double silu144 = arm_microseconds(ARM_SILU144_DD);
|
|
const double mul48 = arm_microseconds(ARM_MUL48_DD);
|
|
const double silu48 = arm_microseconds(ARM_SILU48_DD);
|
|
/* The SFPU's share, per tile. MUL and SILU move the same output bytes and differ by one
|
|
* input stream and one SFPU verb, and the measured gap is far too large to be an input
|
|
* stream — which is what makes it a statement about the SFPU. The first version of this
|
|
* report divided that gap by the tile count and called it "one DRAM tile read", which
|
|
* produced a NEGATIVE cost. The difference between those two arms is not a read. */
|
|
const double silu_premium_144 = (silu144 - mul144) * 1000.0 / (double)MAX_TILES;
|
|
const double silu_premium_48 = (silu48 - mul48) * 1000.0 / (double)HIDDEN_TILES;
|
|
/* What DRAM costs over L1, per tile, on each side. THIS is the round trip the licence is
|
|
* about, and it is measured by MOVING ADDRESSES rather than by subtracting two ops. */
|
|
const double read_premium_ns =
|
|
(arm_microseconds(ARM_MUL144_DD) - arm_microseconds(ARM_MUL144_LD)) * 1000.0 /
|
|
(2.0 * (double)MAX_TILES);
|
|
const double write_premium_ns =
|
|
(arm_microseconds(ARM_MUL144_DD) - arm_microseconds(ARM_MUL144_DL)) * 1000.0 /
|
|
(double)MAX_TILES;
|
|
uint32_t index;
|
|
double layer_matmul_us;
|
|
|
|
printf("\n");
|
|
printf(" ============================================================================\n");
|
|
printf(" THE ROUND-TRIP MATRIX — one elementwise op, only the operand ADDRESSES differ\n");
|
|
printf(" ============================================================================\n");
|
|
printf(" arm us/descriptor ns/tile\n");
|
|
for (index = 0; index < ARM_COUNT; index++) {
|
|
const double us = arm_microseconds(index);
|
|
|
|
printf(" %-46s %10.2f %9.1f\n", g_arms[index].label, us,
|
|
(us - g_nop_microseconds) * 1000.0 / (double)g_arms[index].tiles);
|
|
}
|
|
printf("\n dispatch floor (NOP, same run, same core) %10.3f us <- SUBTRACTED above\n",
|
|
g_nop_microseconds);
|
|
printf(" DRAM over L1(self), per tile READ : %10.1f ns <- THE READ ROUND TRIP\n",
|
|
read_premium_ns);
|
|
printf(" DRAM over L1(self), per tile WRITE : %10.1f ns <- THE WRITE ROUND TRIP\n",
|
|
write_premium_ns);
|
|
printf(" the SFPU's share (SILU over MUL), per tile : %10.1f ns (144), %.1f ns (48)\n",
|
|
silu_premium_144, silu_premium_48);
|
|
|
|
printf("\n");
|
|
printf(" ============================================================================\n");
|
|
printf(" THE DENOMINATORS — MiniCPM5-1B matmul, Mt=1, bf16, depth %u, one core\n",
|
|
MATMUL_DEPTH);
|
|
printf(" ============================================================================\n");
|
|
printf(" shape wall (ms) GFLOP/s %% of one Tensix out tiles\n");
|
|
layer_matmul_us = 0.0;
|
|
for (index = 0; index < SHAPE_COUNT; index++) {
|
|
const double us = shape_microseconds(index);
|
|
const double flops = 2.0 * (double)g_shapes[index].tile_rows * TILE_DIM *
|
|
(double)g_shapes[index].tile_inner * TILE_DIM *
|
|
(double)g_shapes[index].tile_cols * TILE_DIM;
|
|
const double gflops = flops / (us * 1000.0);
|
|
|
|
printf(" %-28s %9.3f %8.1f %14.2f%% %9u\n", g_shapes[index].label, us / 1000.0,
|
|
gflops, 100.0 * gflops / TENSIX_PEAK_GFLOPS,
|
|
g_shapes[index].tile_rows * g_shapes[index].tile_cols);
|
|
}
|
|
/* One MiniCPM5-1B layer: q + k + v + attn_output + gate + up + down. */
|
|
layer_matmul_us = shape_microseconds(0) + 2.0 * shape_microseconds(1) +
|
|
shape_microseconds(2) + 2.0 * shape_microseconds(3) +
|
|
shape_microseconds(4);
|
|
printf(" ONE LAYER's seven matmuls %9.3f ms\n", layer_matmul_us / 1000.0);
|
|
printf(" ffn_gate/up vs ffn_down: SAME %u tile-pairs, 144 vs 48 output tiles -> "
|
|
"%.1f ns per output tile write inside a matmul\n",
|
|
48u * 144u,
|
|
(shape_microseconds(SHAPE_FFN_GATE) - shape_microseconds(SHAPE_FFN_DOWN)) * 1000.0 /
|
|
96.0);
|
|
|
|
printf("\n");
|
|
printf(" ============================================================================\n");
|
|
printf(" WHAT A FUSION WOULD REMOVE, per layer, per 32-row block\n");
|
|
printf(" ============================================================================\n");
|
|
printf(" A matmul gate + matmul up + SILU_MUL -> one op\n");
|
|
printf(" removes the whole SILU_MUL descriptor %9.3f ms (measured, arm 8)\n",
|
|
arm_microseconds(ARM_SILUMUL144) / 1000.0);
|
|
printf(" plus 288 matmul output-tile writes %9.3f ms (measured, gate vs down)\n",
|
|
288.0 *
|
|
((shape_microseconds(SHAPE_FFN_GATE) - shape_microseconds(SHAPE_FFN_DOWN)) / 96.0) /
|
|
1000.0);
|
|
printf(" B matmul down + residual ADD, and matmul attn_output + residual ADD\n");
|
|
printf(" removes two ADD descriptors %9.3f ms (measured, arm 11)\n",
|
|
2.0 * arm_microseconds(ARM_ADD48_DD) / 1000.0);
|
|
printf(" C residual ADD + RMS norm -> one op\n");
|
|
printf(" removes the norm's 48 tile READS only %9.3f ms (the ADD's write stays: "
|
|
"the residual is needed downstream)\n",
|
|
2.0 * 48.0 * read_premium_ns / 1000000.0);
|
|
printf(" for scale, a whole RMS norm descriptor is %9.3f ms\n",
|
|
g_rms_norm_microseconds / 1000.0);
|
|
printf(" D ONE binary switch %9.3f ms (measured, stage 5)\n",
|
|
g_switch_microseconds / 1000.0);
|
|
printf(" — NOT a fusion and NOT dispatch. It is what a MERGE removes, and a core that\n");
|
|
printf(" runs a whole layer pays it once per binary change\n");
|
|
printf("\n layer matmul budget for scale %9.3f ms\n",
|
|
layer_matmul_us / 1000.0);
|
|
printf(" ============================================================================\n\n");
|
|
}
|
|
|
|
/* ================================================================
|
|
* main
|
|
* ================================================================ */
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_firmware_set firmware;
|
|
uint32_t tensix_index = 0;
|
|
uint32_t peer_index = 0;
|
|
const char *peer_text;
|
|
uint32_t bank;
|
|
int rc;
|
|
|
|
if (argc < 4) {
|
|
fprintf(stderr,
|
|
"usage: %s <eltwise-dir> <reduce-dir> <rtriad-dir> [blocked-writer-dir] "
|
|
"[flushed-writer-dir]\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
if (getenv("FT_TT_FUSION_ROUNDS"))
|
|
g_rounds = (uint32_t)strtoul(getenv("FT_TT_FUSION_ROUNDS"), NULL, 10);
|
|
if (g_rounds == 0u)
|
|
g_rounds = 1u;
|
|
|
|
ft_tt_test_step("Phase 6 item 8 — OP FUSION MEASUREMENT (no fusion is built here)");
|
|
|
|
rc = ft_tt_hardware_session_open(&session);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
rc = ft_tt_firmware_set_load(ft_tt_firmware_directory(), &firmware);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
rc = ft_tt_hardware_session_tensix_from_environment(&session, &g_self_x, &g_self_y,
|
|
&tensix_index);
|
|
if (rc != FT_TT_OK)
|
|
goto release;
|
|
|
|
/* The peer is an IDLE core whose L1 is a scratch buffer. Its coordinate is DERIVED from
|
|
* the live grid, never guessed — a wrong NoC endpoint is a hung chip (spec §7.3). */
|
|
peer_text = getenv("FT_TT_FUSION_SCRATCH_INDEX");
|
|
peer_index = peer_text ? (uint32_t)strtoul(peer_text, NULL, 10) : tensix_index + 1u;
|
|
{
|
|
ft_tt_noc_coord peer;
|
|
|
|
rc = ft_tt_coord_tensix_by_index(&session.grid, peer_index, FT_TT_COORD_TRANSLATED, &peer);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the peer core's coordinate derives", false, "index %u rc=%d",
|
|
peer_index, rc);
|
|
goto release;
|
|
}
|
|
g_peer_x = (uint16_t)peer.x;
|
|
g_peer_y = (uint16_t)peer.y;
|
|
}
|
|
ft_tt_test_check_fmt("the compute core and the L1-scratch core are DIFFERENT cores",
|
|
peer_index != tensix_index, "compute %u scratch %u", tensix_index,
|
|
peer_index);
|
|
|
|
for (bank = 0; bank < DRAM_BANK_COUNT; 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 release;
|
|
}
|
|
}
|
|
|
|
rc = ft_tt_program_directory_load(argv[1], TRIAD_MASK, &g_eltwise);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(argv[2], TRIAD_MASK, &g_reduce);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(argv[3], TRIAD_MASK, &g_rtriad);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("all three program directories load", false, "rc=%d", rc);
|
|
goto release;
|
|
}
|
|
if (argc > 4) {
|
|
int blocked_rc = ft_tt_program_directory_load(argv[4], TRIAD_MASK, &g_eltwise_blocked);
|
|
|
|
g_have_blocked_writer = (blocked_rc == FT_TT_OK);
|
|
ft_tt_test_check_fmt("the blocked-writer program directory loads", g_have_blocked_writer,
|
|
"rc=%d", blocked_rc);
|
|
}
|
|
if (argc > 5) {
|
|
int flushed_rc = ft_tt_program_directory_load(argv[5], TRIAD_MASK, &g_eltwise_flushed);
|
|
|
|
g_have_flushed_writer = (flushed_rc == FT_TT_OK);
|
|
ft_tt_test_check_fmt("the flushed-writer program directory loads", g_have_flushed_writer,
|
|
"rc=%d", flushed_rc);
|
|
}
|
|
|
|
rc = ft_tt_launch_core_set_init(&g_group.cores, g_placement_storage, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_core_set_add_tensix(&g_group.cores, &session.grid, tensix_index, NULL);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the core set derives and classifies the target", false,
|
|
"index %u rc=%d", tensix_index, rc);
|
|
goto release_programs;
|
|
}
|
|
g_group.id = 0u;
|
|
g_group.first_tensix_index = tensix_index;
|
|
g_group.tensix_index_count = 1u;
|
|
g_group.queues = &g_queue;
|
|
g_group.resident_processor_mask = TRIAD_MASK;
|
|
|
|
rc = ft_tt_core_boot_firmware(&session.noc, g_self_x, g_self_y, &firmware, NULL);
|
|
ft_tt_test_check_fmt("firmware boots on the target core", rc == FT_TT_OK,
|
|
"index %u -> (%u, %u) rc=%d", tensix_index, g_self_x, g_self_y, rc);
|
|
if (rc != FT_TT_OK)
|
|
goto release_programs;
|
|
|
|
stage_0_round_trip(&session);
|
|
|
|
rc = start_eltwise_residency(&session);
|
|
ft_tt_test_check_fmt("the elementwise residency starts", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
goto release_programs;
|
|
|
|
if (stage_2_dispatch_floor(&session)) {
|
|
rc = upload_all_operands(&session);
|
|
ft_tt_test_check_fmt("every operand buffer is filled, in DRAM and in both L1s, with the "
|
|
"SAME values",
|
|
rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc == FT_TT_OK && stage_3_round_trip_matrix(&session))
|
|
(void)check_timer_resolves_work();
|
|
}
|
|
retire_triad(&session.noc, "the elementwise residency retires by ASKING, never a park");
|
|
|
|
if (g_have_blocked_writer || g_have_flushed_writer)
|
|
(void)stage_3b_writer_barrier(&session);
|
|
|
|
rc = start_matmul_residency(&session);
|
|
ft_tt_test_check_fmt("the matmul residency starts", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc == FT_TT_OK)
|
|
(void)stage_4_matmul_shapes(&session);
|
|
retire_triad(&session.noc, "the matmul residency retires by ASKING, never a park");
|
|
|
|
(void)stage_4b_rms_norm(&session);
|
|
(void)stage_5_binary_switch(&session);
|
|
report();
|
|
|
|
release_programs:
|
|
ft_tt_program_directory_release(&g_eltwise);
|
|
ft_tt_program_directory_release(&g_reduce);
|
|
ft_tt_program_directory_release(&g_rtriad);
|
|
if (g_have_blocked_writer)
|
|
ft_tt_program_directory_release(&g_eltwise_blocked);
|
|
if (g_have_flushed_writer)
|
|
ft_tt_program_directory_release(&g_eltwise_flushed);
|
|
release:
|
|
ft_tt_firmware_set_release(&firmware);
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|