Foundry/tt/tests/test_phase2_mop_expander.c

873 lines
36 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* test_phase2_mop_expander.c — WHAT DOES THE TENSIX MOP EXPANDER DO WITH
* `loop0_last_instr` / `loop1_last_instr`?
*
* The nine-word double-loop template (ported in
* kernels/ft_tensix_compute.h) carries two "last instruction" words. Everything this
* backend counts — 32 ELWMUL per mul_tiles, 32 MVMUL per reduce_tile, 64 MVMUL per HiFi4
* matmul tile — rests on a rule that was inferred from a self-consistent story and never
* measured: that the substitution REPLACES rather than ADDS. A C-vs-C++ instruction diff
* cannot settle it, because both models program the SAME nine words.
*
* THE MEASUREMENT. mop_expander_probe_compute.c turns DST into a TAPE: every issued op writes
* one 8-row DST slot and the ADDR_MOD advances DST by 8, so the k-th instruction the expander
* issues lands in slot k. With SrcA = 5.0 and SrcB = 2.0 uniformly, ELWADD writes 7, ELWSUB
* writes +/-3 and ELWMUL writes 10 — so one packed 32x32 output tile reads back as the
* LITERAL SEQUENCE of ops the MOP expanded to, eight slots at a time, and the tape LENGTH is
* the instruction COUNT. DST is zeroed by the math thread before every trial, so an unwritten
* slot reads 0 and a count is a count rather than a maximum.
*
* THE FOUR CANDIDATE RULES, all of which program identical nine words:
*
* H1 REPLACE-LAST the substitution replaces the LAST instruction of the inner body:
* loop_op1 when loop_op1 is a real op, loop_op0 when loop_op1 is NOP.
* (This is what the backend assumes; it is the only rule under which BOTH
* upstream constructors are an identity.)
* H2 REPLACE-OP0 the substitution always replaces the loop_op0 slot.
* H3 REPLACE-OP1 the substitution always replaces the loop_op1 slot. This is the LITERAL
* pseudocode in the upstream class comment.
* H4 ADD the substitution is APPENDED; both the body and the last-instr issue.
*
* H1 and H3 agree whenever loop_op1 is a real op; H1 and H2 agree whenever loop_op1 is NOP.
* So it takes BOTH kinds of trial to separate all four, and the sweep runs both.
*
* Predictions are not typed in per trial: expand_template() below IS each model, and the
* expected tape for every trial is computed from it. A trial can therefore come out matching
* a model nobody wanted, which is the point.
*
* Build (on the card host):
* cc -std=c99 -O1 -g -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion \
* -Wstrict-prototypes -Wformat=2 -I.. -DFT_TT_PHASE1B_NOC_ENABLED \
* ../ft_tt_kmd.c ../ft_tt_arch.c ../ft_tt_arch_blackhole.c ../ft_tt_device.c \
* ../ft_tt_tlb.c ../ft_tt_arc_telemetry.c ../ft_tt_coord.c ../ft_tt_noc.c \
* ../ft_tt_elf.c ../ft_tt_kernel_blob.c ../ft_tt_launch.c \
* ft_tt_test_report.c ft_tt_hardware_session.c ft_tt_phase2_l1_layout.c \
* ft_tt_program_directory.c test_phase2_mop_expander.c \
* -o build/test_phase2_mop_expander
*
* Run:
* FT_TT_TENSIX_INDEX=n FT_TT_FIRMWARE_DIR=<program dir> \
* ./build/test_phase2_mop_expander <program dir>
*/
#define _POSIX_C_SOURCE 200809L
#include "ft_tt_hardware_session.h"
#include "ft_tt_phase2_l1_layout.h"
#include "ft_tt_program_directory.h"
#include "ft_tt_test_report.h"
#include <stdio.h>
#include <string.h>
#define RUN_TIMEOUT_MS 10000u
#define TILE_ELEMENTS (FT_TT_PHASE2_TILE_BYTES / 2u)
#define CB_CONFIG_BYTES (FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4u)
#define COUNTER_STAGE_ATTEMPTS 64u
/** Rows a tape slot covers, and therefore elements: MAX_FPU_ROWS(8) * 16 datums. */
#define SLOT_ELEMENTS 128u
/** Slots in one packed 32x32 tile. The tape is read one output tile per trial. */
#define TAPE_SLOTS (TILE_ELEMENTS / SLOT_ELEMENTS)
/** Output CB pages, and so trials per launch: 0x6000 B between CB_OUT and SOURCE0. */
#define MAX_TRIALS 12u
/** The two operands. Chosen so ADD/SUB/MUL land on three distinct exact bfloat16 values. */
#define OPERAND_A 5.0f
#define OPERAND_B 2.0f
/* Symbols, matching the kernel's trial encoding. */
#define SYM_NOP 0u
#define SYM_ADD 1u
#define SYM_SUB 2u
#define SYM_MUL 3u
#define SYM_NONE 7u /**< host-only: "no substitution applies here" */
/* Hypotheses. */
#define H_REPLACE_LAST 0u
#define H_REPLACE_OP0 1u
#define H_REPLACE_OP1 2u
#define H_ADD 3u
#define HYPOTHESIS_COUNT 4u
static const char *const hypothesis_name[HYPOTHESIS_COUNT] = {
"H1 REPLACE-last-of-body", "H2 REPLACE-op0-slot", "H3 REPLACE-op1-slot", "H4 ADD-append"};
/* Kernel trial modes. */
#define MODE_MOP 0u
#define MODE_LADDER_UNIFORM 1u
#define MODE_LADDER_CYCLE 2u
#define MODE_SILENT 3u
typedef struct trial {
const char *name;
unsigned mode;
unsigned outer;
unsigned inner;
unsigned ladder;
unsigned addr_mod; /**< 1 = every op writes slot 0; the deliberately-wrong control */
unsigned start0;
unsigned end0;
unsigned end1;
unsigned op0;
unsigned op1;
unsigned l0;
unsigned l1;
} trial;
static ft_tt_phase2_l1_layout g_layout;
/* ------------------------------------------------------------------------- */
/* The models */
/* ------------------------------------------------------------------------- */
/** Append one symbol to a tape, counting past the end rather than writing past it. */
static void tape_emit(unsigned symbol, unsigned char *tape, unsigned capacity, unsigned *length)
{
if (symbol == SYM_NOP || symbol == SYM_NONE) {
return; /* a NOP issues but writes no DST slot, so it leaves no mark on the tape */
}
if (*length < capacity)
tape[*length] = (unsigned char)symbol;
(*length)++;
}
/**
* Expand one trial under one hypothesis into the symbol sequence the DST tape would show.
*
* Returns the FULL length; only the first `capacity` symbols are written. The outer-loop
* shape is the upstream class comment's, which is not in dispute:
* LOOP_OUTER: { START_OP; LOOP_INNER: { LOOP_OP0; LOOP_OP1 }; END_OP0; END_OP1 }
* The hypotheses differ only in what the last inner iteration does with loop0/loop1_last.
*/
static unsigned expand_template(const trial *t, unsigned hypothesis, unsigned char *tape,
unsigned capacity)
{
unsigned length = 0;
unsigned k;
unsigned j;
if (t->mode == MODE_SILENT)
return 0;
if (t->mode == MODE_LADDER_UNIFORM) {
for (k = 0; k < t->ladder; k++)
tape_emit(t->op0, tape, capacity, &length);
return length;
}
if (t->mode == MODE_LADDER_CYCLE) {
const unsigned cycle[3] = {t->op0, t->op1, t->l0};
for (k = 0; k < t->ladder; k++)
tape_emit(cycle[k % 3u], tape, capacity, &length);
return length;
}
for (k = 0; k < t->outer; k++) {
tape_emit(t->start0, tape, capacity, &length);
for (j = 0; j < t->inner; j++) {
int last_inner = (j + 1u == t->inner);
/* The class comment's own rule for WHICH last-instr applies, which this
* experiment also gets to check: loop0_last on the final inner iteration of the
* final outer iteration, loop1_last on every other final inner iteration. */
unsigned substitution =
last_inner ? ((k + 1u == t->outer) ? t->l0 : t->l1) : SYM_NONE;
switch (hypothesis) {
case H_REPLACE_LAST:
if (t->op1 != SYM_NOP) {
tape_emit(t->op0, tape, capacity, &length);
tape_emit(last_inner ? substitution : t->op1, tape, capacity, &length);
} else {
tape_emit(last_inner ? substitution : t->op0, tape, capacity, &length);
tape_emit(t->op1, tape, capacity, &length);
}
break;
case H_REPLACE_OP0:
tape_emit(last_inner ? substitution : t->op0, tape, capacity, &length);
tape_emit(t->op1, tape, capacity, &length);
break;
case H_REPLACE_OP1:
tape_emit(t->op0, tape, capacity, &length);
tape_emit(last_inner ? substitution : t->op1, tape, capacity, &length);
break;
default: /* H_ADD */
tape_emit(t->op0, tape, capacity, &length);
tape_emit(t->op1, tape, capacity, &length);
if (last_inner)
tape_emit(substitution, tape, capacity, &length);
break;
}
}
tape_emit(t->end0, tape, capacity, &length);
tape_emit(t->end1, tape, capacity, &length);
}
return length;
}
/**
* What the OUTPUT TILE should show for a trial under a hypothesis.
*
* Identical to the expansion except when the trial names the zero-increment ADDR_MOD: then
* every op overwrites slot 0, so all that survives is the last symbol issued. That is the
* deliberately-wrong control, and its predicted observation is a wrong-looking one slot.
*/
static unsigned predict_observation(const trial *t, unsigned hypothesis, unsigned char *tape,
unsigned capacity)
{
unsigned char full[64];
unsigned length = expand_template(t, hypothesis, full, 64u);
if (t->addr_mod == 0u) {
unsigned copy = (length < capacity) ? length : capacity;
memcpy(tape, full, copy);
return (length < capacity) ? length : capacity;
}
if (length == 0u)
return 0u;
if (length > 64u)
length = 64u;
if (capacity > 0u)
tape[0] = full[length - 1u];
return 1u;
}
/* ------------------------------------------------------------------------- */
/* Trial encoding */
/* ------------------------------------------------------------------------- */
static uint32_t trial_shape_word(const trial *t)
{
return (t->mode & 0xFu) | ((t->outer & 0xFu) << 4) | ((t->inner & 0xFu) << 8) |
((t->ladder & 0x1Fu) << 12) | ((t->addr_mod & 0x3u) << 17);
}
static uint32_t trial_ops_word(const trial *t)
{
return (t->start0 & 7u) | ((t->end0 & 7u) << 3) | ((t->end1 & 7u) << 6) |
((t->op0 & 7u) << 9) | ((t->op1 & 7u) << 12) | ((t->l0 & 7u) << 15) |
((t->l1 & 7u) << 18);
}
/* ------------------------------------------------------------------------- */
/* Tiles */
/* ------------------------------------------------------------------------- */
static uint16_t tile_a[TILE_ELEMENTS];
static uint16_t tile_b[TILE_ELEMENTS];
static uint16_t tile_zero[TILE_ELEMENTS];
static uint16_t tile_readback[TILE_ELEMENTS];
static uint16_t bfloat16_from_float(float value)
{
uint32_t bits;
memcpy(&bits, &value, sizeof(bits));
return (uint16_t)(bits >> 16);
}
static float float_from_bfloat16(uint16_t value)
{
uint32_t bits = (uint32_t)value << 16;
float result;
memcpy(&result, &bits, sizeof(result));
return result;
}
static void build_operand_tiles(void)
{
unsigned element;
for (element = 0; element < TILE_ELEMENTS; element++) {
tile_a[element] = bfloat16_from_float(OPERAND_A);
tile_b[element] = bfloat16_from_float(OPERAND_B);
tile_zero[element] = 0u;
}
}
/* ------------------------------------------------------------------------- */
/* Calibration: which bfloat16 pattern each op family writes */
/* ------------------------------------------------------------------------- */
/** Filled by the calibration trial. 0 means "not yet known". */
static uint16_t g_symbol_pattern[4];
static const char *symbol_text(unsigned symbol)
{
switch (symbol) {
case SYM_ADD: return "A";
case SYM_SUB: return "S";
case SYM_MUL: return "M";
case SYM_NOP: return "n";
default: return "?";
}
}
/** Decode one slot of a packed tile: its uniform value, or a refusal. */
static int slot_value(const uint16_t *tile, unsigned slot, uint16_t *out_value)
{
unsigned base = slot * SLOT_ELEMENTS;
uint16_t first = tile[base];
unsigned index;
for (index = 1; index < SLOT_ELEMENTS; index++) {
if (tile[base + index] != first)
return 0; /* not uniform: the slot is not a clean symbol */
}
*out_value = first;
return 1;
}
/** Map a slot's value to a symbol using the calibration; SYM_NONE if unrecognised. */
static unsigned symbol_of_value(uint16_t value)
{
unsigned symbol;
if (value == 0u)
return SYM_NOP; /* an untouched slot */
for (symbol = SYM_ADD; symbol <= SYM_MUL; symbol++) {
if (g_symbol_pattern[symbol] != 0u && g_symbol_pattern[symbol] == value)
return symbol;
}
return SYM_NONE;
}
/**
* Read a tape out of a packed tile.
*
* The tape ENDS at the first zero slot: a written slot is never zero (7, +/-3 and 10 are all
* nonzero) and the math thread zeroed DST before the trial, so a zero slot is an instruction
* that was never issued.
*/
static unsigned read_tape(const uint16_t *tile, unsigned char *tape, uint16_t *values,
int *out_clean)
{
unsigned length = 0;
unsigned slot;
*out_clean = 1;
for (slot = 0; slot < TAPE_SLOTS; slot++) {
uint16_t value = 0;
if (!slot_value(tile, slot, &value)) {
*out_clean = 0;
values[slot] = 0xFFFFu;
tape[slot] = (unsigned char)SYM_NONE;
length = slot + 1u;
continue;
}
values[slot] = value;
tape[slot] = (unsigned char)symbol_of_value(value);
if (value != 0u)
length = slot + 1u;
}
return length;
}
/* ------------------------------------------------------------------------- */
/* Device plumbing */
/* ------------------------------------------------------------------------- */
static int boot_and_derive_layout(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_firmware_set *firmware)
{
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
int rc = ft_tt_core_boot_firmware(noc, x, y, firmware, placements);
if (rc != FT_TT_OK)
return rc;
return ft_tt_phase2_l1_layout_derive(placements, FT_TT_PROC_COUNT, &g_layout);
}
static void describe_circular_buffers(ft_tt_launch_cb_entry entries[3])
{
entries[0].index = FT_TT_PHASE2_CB_IN0;
entries[0].address = g_layout.cb_in0_address;
entries[0].size_bytes = 2u * FT_TT_PHASE2_TILE_BYTES;
entries[0].page_size_bytes = FT_TT_PHASE2_TILE_BYTES;
entries[0].page_count = 2u;
entries[1].index = FT_TT_PHASE2_CB_IN1;
entries[1].address = g_layout.cb_in1_address;
entries[1].size_bytes = 2u * FT_TT_PHASE2_TILE_BYTES;
entries[1].page_size_bytes = FT_TT_PHASE2_TILE_BYTES;
entries[1].page_count = 2u;
/* One output page per trial: the tape for trial i is output tile i. */
entries[2].index = FT_TT_PHASE2_CB_OUT;
entries[2].address = g_layout.cb_out_address;
entries[2].size_bytes = MAX_TRIALS * FT_TT_PHASE2_TILE_BYTES;
entries[2].page_size_bytes = FT_TT_PHASE2_TILE_BYTES;
entries[2].page_count = MAX_TRIALS;
}
static int write_cb_config(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint64_t *out_mask)
{
static uint8_t buffer[CB_CONFIG_BYTES];
ft_tt_launch_cb_entry entries[3];
int rc;
describe_circular_buffers(entries);
rc = ft_tt_launch_encode_cb_config(entries, 3u, buffer, sizeof(buffer), out_mask);
if (rc != FT_TT_OK)
return rc;
return noc->write(noc->user, x, y, g_layout.cb_config_address, buffer, sizeof(buffer));
}
static int stage_cb_counters(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
unsigned cb_index, uint32_t received, uint32_t acked)
{
uint32_t received_address = FT_TT_CB_TILES_RECEIVED_ADDR(cb_index);
uint32_t acked_address = FT_TT_CB_TILES_ACKED_ADDR(cb_index);
unsigned attempt;
for (attempt = 0; attempt < COUNTER_STAGE_ATTEMPTS; attempt++) {
uint32_t observed_received = 0;
uint32_t observed_acked = 0;
int rc = noc->write(noc->user, x, y, acked_address, &acked, sizeof(acked));
if (rc != FT_TT_OK)
return rc;
rc = noc->write(noc->user, x, y, received_address, &received, sizeof(received));
if (rc != FT_TT_OK)
return rc;
rc = noc->read(noc->user, x, y, received_address, &observed_received,
sizeof(observed_received));
if (rc != FT_TT_OK)
return rc;
rc = noc->read(noc->user, x, y, acked_address, &observed_acked, sizeof(observed_acked));
if (rc != FT_TT_OK)
return rc;
if (observed_received == received && observed_acked == acked)
return FT_TT_OK;
}
ft_tt_log_error("mop probe: CB %u counters would not hold their value", cb_index);
return FT_TT_EIO;
}
static void build_config(const ft_tt_image_placement placements[FT_TT_PROC_COUNT],
uint64_t cb_mask, ft_tt_launch_config *out_config)
{
unsigned processor;
memset(out_config, 0, sizeof(*out_config));
out_config->kernel_config_base = g_layout.kernel_config_base;
out_config->enables = 0x1cu; /* the compute triad only — no device NoC traffic at all */
out_config->local_cb_config_address = g_layout.cb_config_address;
out_config->local_cb_mask = cb_mask;
out_config->brisc_noc_id = 0u;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
if ((out_config->enables & (1u << processor)) == 0u)
continue;
out_config->kernel_text_address[processor] = placements[processor].entry_address;
out_config->kernel_text_bytes[processor] = placements[processor].text_bytes;
out_config->runtime_args_address[processor] =
ft_tt_phase2_runtime_args_address(&g_layout, processor);
}
}
/* ------------------------------------------------------------------------- */
/* One batch */
/* ------------------------------------------------------------------------- */
/** Per-hypothesis tally across every scored trial in the whole run. */
static unsigned g_hypothesis_matches[HYPOTHESIS_COUNT];
static unsigned g_hypothesis_scored;
static void print_tape(const char *label, const unsigned char *tape, unsigned length)
{
unsigned index;
printf("%s[", label);
for (index = 0; index < length; index++)
printf("%s", symbol_text(tape[index]));
printf("]%u", length);
}
static int run_batch(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_image_placement placements[FT_TT_PROC_COUNT],
const trial *trials, unsigned trial_count, const char *batch_name)
{
ft_tt_launch_config config;
uint64_t cb_mask = 0;
uint32_t arguments[1u + 2u * MAX_TRIALS];
unsigned processor;
unsigned index;
int rc;
ft_tt_test_step("%s", batch_name);
if (trial_count > MAX_TRIALS) {
ft_tt_test_check("batch fits the output CB", 0, "too many trials");
return FT_TT_EINVAL;
}
rc = write_cb_config(noc, x, y, &cb_mask);
if (rc != FT_TT_OK) {
ft_tt_test_check("CB config written", 0, ft_tt_error_string(rc));
return rc;
}
build_config(placements, cb_mask, &config);
arguments[0] = trial_count;
for (index = 0; index < trial_count; index++) {
arguments[1u + 2u * index] = trial_shape_word(&trials[index]);
arguments[2u + 2u * index] = trial_ops_word(&trials[index]);
}
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
if (config.runtime_args_address[processor] == 0u)
continue;
rc = ft_tt_launch_plant_runtime_args(noc, x, y, config.runtime_args_address[processor],
arguments, 1u + 2u * trial_count);
if (rc != FT_TT_OK)
break;
}
if (rc != FT_TT_OK) {
ft_tt_test_check("trial table planted on each TRISC", 0, ft_tt_error_string(rc));
return rc;
}
rc = noc->write(noc->user, x, y, g_layout.cb_in0_address, tile_a, sizeof(tile_a));
if (rc == FT_TT_OK)
rc = noc->write(noc->user, x, y, g_layout.cb_in1_address, tile_b, sizeof(tile_b));
for (index = 0; index < trial_count && rc == FT_TT_OK; index++) {
rc = noc->write(noc->user, x, y,
g_layout.cb_out_address + index * FT_TT_PHASE2_TILE_BYTES, tile_zero,
sizeof(tile_zero));
}
if (rc != FT_TT_OK) {
ft_tt_test_check("operands staged and output wiped", 0, ft_tt_error_string(rc));
return rc;
}
rc = ft_tt_core_wait_sync_registers_ready(noc, x, y, 1000u);
if (rc == FT_TT_OK)
rc = stage_cb_counters(noc, x, y, FT_TT_PHASE2_CB_IN0, 1u, 0u);
if (rc == FT_TT_OK)
rc = stage_cb_counters(noc, x, y, FT_TT_PHASE2_CB_IN1, 1u, 0u);
if (rc == FT_TT_OK)
rc = stage_cb_counters(noc, x, y, FT_TT_PHASE2_CB_OUT, 0u, 0u);
if (rc != FT_TT_OK) {
ft_tt_test_check("CB page counters primed", 0, ft_tt_error_string(rc));
return rc;
}
rc = ft_tt_launch_write_launch_message(noc, x, y, &config);
if (rc == FT_TT_OK)
rc = ft_tt_launch_send_go(noc, x, y);
if (rc == FT_TT_OK)
rc = ft_tt_launch_wait_done(noc, x, y, RUN_TIMEOUT_MS, NULL);
ft_tt_test_check_fmt("compute triad completed", rc == FT_TT_OK, "rc=%d (%s), %u trials", rc,
ft_tt_error_string(rc), trial_count);
/* Deliberately NOT a `return rc`. A thread that does not retire does not unwrite the tape
* the packer already put in L1: the first run of this experiment hung the UNPACK thread
* with all of MATH's and PACK's work complete and threw the whole measurement away. Read
* the tapes and let each trial be judged on whether its slots are sane. */
if (rc != FT_TT_OK)
printf(" launch did not retire — reading the tapes anyway\n");
for (index = 0; index < trial_count; index++) {
const trial *t = &trials[index];
unsigned char measured[TAPE_SLOTS];
uint16_t values[TAPE_SLOTS];
unsigned measured_length;
unsigned hypothesis;
int clean = 0;
unsigned matches = 0;
unsigned slot;
rc = noc->read(noc->user, x, y,
g_layout.cb_out_address + index * FT_TT_PHASE2_TILE_BYTES, tile_readback,
sizeof(tile_readback));
if (rc != FT_TT_OK) {
ft_tt_test_check_fmt("tape read back", 0, "%s: %s", t->name, ft_tt_error_string(rc));
return rc;
}
/* The calibration trial DEFINES the three signature values; run it first in a batch. */
if (t->mode == MODE_LADDER_CYCLE) {
const unsigned cycle[3] = {t->op0, t->op1, t->l0};
for (slot = 0; slot < 3u && slot < TAPE_SLOTS; slot++) {
uint16_t value = 0;
if (slot_value(tile_readback, slot, &value) && value != 0u &&
cycle[slot] <= SYM_MUL)
g_symbol_pattern[cycle[slot]] = value;
}
}
measured_length = read_tape(tile_readback, measured, values, &clean);
printf(" %-22s ", t->name);
if (t->mode == MODE_MOP)
printf("O=%u I=%u am=%u ops=[s%s e%s/%s | %s %s | L0=%s L1=%s] ", t->outer, t->inner,
t->addr_mod, symbol_text(t->start0), symbol_text(t->end0),
symbol_text(t->end1), symbol_text(t->op0), symbol_text(t->op1),
symbol_text(t->l0), symbol_text(t->l1));
else
printf("mode=%u k=%u ", t->mode, t->ladder);
print_tape("got=", measured, measured_length);
for (hypothesis = 0; hypothesis < HYPOTHESIS_COUNT; hypothesis++) {
unsigned char predicted[TAPE_SLOTS];
unsigned predicted_length =
predict_observation(t, hypothesis, predicted, TAPE_SLOTS);
int same = (predicted_length == measured_length) &&
(memcmp(predicted, measured, predicted_length) == 0);
printf(" ");
print_tape(hypothesis_name[hypothesis], predicted, predicted_length);
printf("%s", same ? "=OK" : "=NO");
if (same)
matches |= 1u << hypothesis;
}
printf("%s\n", clean ? "" : " [SLOT NOT UNIFORM]");
printf(" raw slots:");
for (slot = 0; slot < TAPE_SLOTS; slot++)
printf(" %g", (double)float_from_bfloat16(values[slot]));
printf("\n");
/* Only score a trial that actually discriminates: if every hypothesis predicts the
* same tape, agreement is not evidence. */
{
unsigned char reference[TAPE_SLOTS];
unsigned reference_length = predict_observation(t, 0u, reference, TAPE_SLOTS);
int discriminating = 0;
for (hypothesis = 1; hypothesis < HYPOTHESIS_COUNT; hypothesis++) {
unsigned char other[TAPE_SLOTS];
unsigned other_length = predict_observation(t, hypothesis, other, TAPE_SLOTS);
if (other_length != reference_length ||
memcmp(other, reference, other_length) != 0)
discriminating = 1;
}
if (discriminating) {
g_hypothesis_scored++;
for (hypothesis = 0; hypothesis < HYPOTHESIS_COUNT; hypothesis++) {
if ((matches & (1u << hypothesis)) != 0u)
g_hypothesis_matches[hypothesis]++;
}
}
ft_tt_test_check_fmt(t->name, matches != 0u, "%s, matched mask 0x%x",
discriminating ? "discriminating" : "control (all models agree)",
matches);
}
}
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* The sweep */
/* ------------------------------------------------------------------------- */
#define MOP(nm, o, i, s, e0, e1, a, b, la, lb) \
{nm, MODE_MOP, (o), (i), 0u, 0u, (s), (e0), (e1), (a), (b), (la), (lb)}
static const trial batch_one[] = {
/* Calibration FIRST: it defines what bfloat16 pattern each op family writes, and it uses
* no MOP at all, so it cannot be contaminated by the question under test. */
{"cal-cycle-k6", MODE_LADDER_CYCLE, 0u, 0u, 6u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_SUB, SYM_MUL, SYM_NOP},
/* The harness must be able to count. Hand-issued ladders, no MOP. */
{"ctl-ladder-k1", MODE_LADDER_UNIFORM, 0u, 0u, 1u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_NOP, SYM_NOP, SYM_NOP},
{"ctl-ladder-k3", MODE_LADDER_UNIFORM, 0u, 0u, 3u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_NOP, SYM_NOP, SYM_NOP},
{"ctl-ladder-k6", MODE_LADDER_UNIFORM, 0u, 0u, 6u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_NOP, SYM_NOP, SYM_NOP},
{"ctl-ladder-k8", MODE_LADDER_UNIFORM, 0u, 0u, 8u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_NOP, SYM_NOP, SYM_NOP},
/* Nothing is issued at all: the tape must be empty, which is what proves the math thread's
* ZEROACC really established the initial state and a count is not a leftover. */
{"ctl-silent", MODE_SILENT, 0u, 0u, 0u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_NOP, SYM_NOP,
SYM_NOP, SYM_NOP},
/* Deliberately wrong: the zero-increment ADDR_MOD, so four issued ops pile into slot 0.
* Predicted, and wanted, answer: ONE slot. */
{"ctl-destincr0", MODE_MOP, 1u, 4u, 0u, 1u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP,
SYM_ADD, SYM_ADD},
/* The 1-argument constructor, swept in the inner loop. */
MOP("1arg-O1-I1", 1u, 1u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O1-I2", 1u, 2u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O1-I4", 1u, 4u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O1-I7", 1u, 7u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O2-I1", 2u, 1u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
};
static const trial batch_two[] = {
/* Re-run the calibration so the symbol map is established in this launch too. */
{"cal-cycle-k6", MODE_LADDER_CYCLE, 0u, 0u, 6u, 0u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD,
SYM_SUB, SYM_MUL, SYM_NOP},
/* 1-argument constructor, swept in the OUTER loop: the two models diverge by O here, not
* by 1, so the two predictions are different LINES and not one offset. */
MOP("1arg-O4-I1", 4u, 1u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O2-I2", 2u, 2u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
MOP("1arg-O2-I3", 2u, 3u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD, SYM_ADD),
/* The 2-argument constructor, where the substitution is claimed to be an IDENTITY by
* construction. Separates REPLACE-op0 and ADD from the other two. */
MOP("2arg-O1-I3", 1u, 3u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_SUB, SYM_SUB, SYM_SUB),
MOP("2arg-O2-I2", 2u, 2u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_SUB, SYM_SUB, SYM_SUB),
/* Two real inner ops with the substitution NOT an identity: this is the trial that
* separates REPLACE-op0 from REPLACE-op1 outright. */
MOP("2arg-subst-A", 1u, 3u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_SUB, SYM_ADD, SYM_ADD),
MOP("2arg-subst-M", 2u, 2u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_SUB, SYM_MUL, SYM_MUL),
/* Distinct loop0_last and loop1_last: also checks the class comment's claim that the
* OUTER one wins on the final inner iteration of the final outer iteration. */
MOP("outer-vs-inner-O3", 3u, 2u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_MUL,
SYM_SUB),
MOP("outer-vs-inner-O1", 1u, 3u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_MUL,
SYM_SUB),
/* NEGATIVE CONTROL WITH TEETH. Both last-instr words are NOP. Under REPLACE the template
* LOSES one op per outer iteration (2*(3-1) = 4); under ADD nothing changes (2*3 = 6).
* 4 is a number nobody wants, and it is what this backend's own model predicts. */
MOP("ctl-lastinstr-NOP", 2u, 3u, SYM_NOP, SYM_NOP, SYM_NOP, SYM_ADD, SYM_NOP, SYM_NOP,
SYM_NOP),
/* Where START_OP and END_OP sit relative to the loops — free, and Phase 7/8 needs it. */
MOP("start-end-place", 2u, 2u, SYM_MUL, SYM_SUB, SYM_NOP, SYM_ADD, SYM_NOP, SYM_ADD,
SYM_ADD),
};
/* ------------------------------------------------------------------------- */
/**
* Print what each model predicts, WITHOUT touching the card.
*
* Run this before the run and keep the output: it is the same expand_template() that scores
* the measurement, so the predictions cannot be adjusted after seeing the data.
*/
static void print_predictions(const trial *trials, unsigned trial_count, const char *batch_name)
{
unsigned index;
unsigned hypothesis;
printf("=== PREDICTIONS: %s ===\n", batch_name);
for (index = 0; index < trial_count; index++) {
const trial *t = &trials[index];
printf(" %-22s ", t->name);
if (t->mode == MODE_MOP)
printf("O=%u I=%u am=%u [s%s e%s/%s | %s %s | L0=%s L1=%s]", t->outer, t->inner,
t->addr_mod, symbol_text(t->start0), symbol_text(t->end0),
symbol_text(t->end1), symbol_text(t->op0), symbol_text(t->op1),
symbol_text(t->l0), symbol_text(t->l1));
else
printf("mode=%u k=%u", t->mode, t->ladder);
for (hypothesis = 0; hypothesis < HYPOTHESIS_COUNT; hypothesis++) {
unsigned char predicted[TAPE_SLOTS];
unsigned predicted_length =
predict_observation(t, hypothesis, predicted, TAPE_SLOTS);
printf(" ");
print_tape(hypothesis_name[hypothesis], predicted, predicted_length);
}
printf("\n");
}
}
int main(int argc, char **argv)
{
ft_tt_hardware_session session;
ft_tt_firmware_set firmware;
ft_tt_program_directory loaded;
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
const char *directory = (argc > 1) ? argv[1] : NULL;
uint16_t x = 0;
uint16_t y = 0;
unsigned hypothesis;
int rc;
if (directory == NULL) {
fprintf(stderr, "usage: %s <program-directory> | --predict\n", argv[0]);
return 2;
}
if (strcmp(directory, "--predict") == 0) {
print_predictions(batch_one, (unsigned)(sizeof(batch_one) / sizeof(batch_one[0])),
"batch 1");
print_predictions(batch_two, (unsigned)(sizeof(batch_two) / sizeof(batch_two[0])),
"batch 2");
return 0;
}
build_operand_tiles();
rc = ft_tt_hardware_session_open(&session);
if (rc != FT_TT_OK)
return 1;
rc = ft_tt_hardware_session_tensix_from_environment(&session, &x, &y, NULL);
if (rc != FT_TT_OK) {
ft_tt_hardware_session_close(&session);
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;
}
ft_tt_test_step("setup — boot the firmware and load the MOP-expander probe triad");
rc = boot_and_derive_layout(&session.noc, x, y, &firmware);
ft_tt_test_check_fmt("firmware booted", rc == FT_TT_OK, "rc=%d (%s)", rc,
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto done;
rc = ft_tt_program_directory_load(directory, 0x1cu, &loaded);
ft_tt_test_check_fmt("probe triad images read", rc == FT_TT_OK, "%s: %s", directory,
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto done;
rc = ft_tt_core_load_program(&session.noc, x, y, &loaded.program, placements);
ft_tt_test_check_fmt("probe triad loaded", rc == FT_TT_OK, "%u images",
loaded.program.image_count);
if (rc != FT_TT_OK)
goto done;
rc = run_batch(&session.noc, x, y, placements, batch_one,
(unsigned)(sizeof(batch_one) / sizeof(batch_one[0])),
"batch 1 — calibration, harness controls, and the inner-loop sweep");
if (rc != FT_TT_OK)
goto done;
rc = run_batch(&session.noc, x, y, placements, batch_two,
(unsigned)(sizeof(batch_two) / sizeof(batch_two[0])),
"batch 2 — outer-loop sweep, the 2-argument form, and the negative control");
if (rc != FT_TT_OK)
goto done;
ft_tt_test_step("verdict");
printf(" symbol calibration: ADD=%g SUB=%g MUL=%g\n",
(double)float_from_bfloat16(g_symbol_pattern[SYM_ADD]),
(double)float_from_bfloat16(g_symbol_pattern[SYM_SUB]),
(double)float_from_bfloat16(g_symbol_pattern[SYM_MUL]));
for (hypothesis = 0; hypothesis < HYPOTHESIS_COUNT; hypothesis++)
printf(" %-26s %u / %u discriminating trials\n", hypothesis_name[hypothesis],
g_hypothesis_matches[hypothesis], g_hypothesis_scored);
{
unsigned survivors = 0;
for (hypothesis = 0; hypothesis < HYPOTHESIS_COUNT; hypothesis++) {
if (g_hypothesis_matches[hypothesis] == g_hypothesis_scored && g_hypothesis_scored > 0u)
survivors++;
}
ft_tt_test_check_fmt("exactly one rule survives every discriminating trial",
survivors == 1u, "%u survivor(s) of %u over %u trials", survivors,
HYPOTHESIS_COUNT, g_hypothesis_scored);
}
done:
(void)ft_tt_core_park(&session.noc, x, y);
ft_tt_firmware_set_release(&firmware);
ft_tt_hardware_session_close(&session);
return ft_tt_test_finish();
}