Foundry/tt/tests/test_fidelity_sweep.c

683 lines
30 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file test_fidelity_sweep.c
* @brief Explains the docs/tt_bringup_log.md §16.3 inversion (LoFi beats HiFi4 on every
* long-K MiniCPM5-1B matmul shape, reverses on the one short-K shape) instead of leaving
* it as the INFERRED "probably 16-bit DST accumulation" note it was left at.
*
* ---------------------------------------------------------------------------
* THE TWO DISCRIMINATING MEASUREMENTS
* ---------------------------------------------------------------------------
* 1. **K sweep, fidelity fixed at each of HiFi4/LoFi, DST_ACCUM_MODE fixed at 0 (the
* shipped setting).** If DST accumulator rounding dominates and scales with the number
* of K-tile write-backs, the HiFi4-vs-LoFi gap should move systematically with Kt
* rather than jump straight from "HiFi4 better" to "LoFi better" at some arbitrary
* shape. Every Kt point in the sweep re-reads DST's actual state after exactly that
* many accumulation steps — `matmul_tiles()` accumulates into DST across the K loop
* (matmul_compute.c) and `pack_tile()` reads DST out after the loop ends, so running
* the SAME op with tile_inner = Kt for increasing Kt on a fixed operand PREFIX is a
* direct, non-invasive read of the accumulator's trajectory — no debug hook needed.
*
* 2. **DST_ACCUM_MODE toggled, at each of HiFi4/LoFi, K fixed.** dst_accum_mode is a
* compile-time cbtable setting (tools/emit_tensix_cb_formats.py) that reaches the
* kernel as the constexpr `DST_ACCUM_MODE` ft_tensix_cb_formats.h defines, and controls
* whether the destination register holds Float16_b (16-bit) or Float32 values end to
* end (tech_reports/matrix_engine/matrix_engine.md "Fp32 Dest Acc"). This is the ONLY
* other lever that changes accumulation width without touching product precision, so a
* 2x2 design over {HiFi4, LoFi} x {dst16, dst32} is what tells "the accumulator is the
* dominant term" apart from "fidelity phases are doing something unexpected": if the
* accumulator dominates, both dst32 arms should converge near each other regardless of
* fidelity, and the inversion should shrink or vanish at dst32.
*
* Four cbtables drive this, all in kernels/cb_tables/, all diagnostic-only (never named by
* Makefile.ops or any shipped program directory):
* matmul_float16b.cbtable HiFi4 dst16 (the shipped configuration)
* matmul_float16b_lofi.cbtable LoFi dst16
* matmul_float16b_fp32acc.cbtable HiFi4 dst32
* matmul_float16b_lofi_fp32acc.cbtable LoFi dst32
* All four are built from the UNCHANGED matmul_reader.c / matmul_compute.c /
* vector_add_writer.c sources — nothing about the kernel changes, only the compile-time
* descriptor set, so a difference between arms cannot be a kernel-logic difference.
*
* ---------------------------------------------------------------------------
* WHAT THIS DOES NOT SETTLE, AND WHERE THAT IS CHECKED INSTEAD
* ---------------------------------------------------------------------------
* Whether the CPU fp32 reference's own summation order could be biasing the comparison —
* a classic false signal for exactly this shape of result — was checked OFF-HARDWARE (host
* only, no card, on the Mac per the "CPU work on the Mac" rule) with a standalone scratch
* program, not committed here since it is a property of the reference FUNCTION alone and
* needs no device: same M=32/K=4608/N=32 shape, same bf16-rounded random operands,
* reference_matmul's actual ascending-K double-accumulator order vs a descending order vs
* pairwise (tree) summation, all in double. RESULT: bit-identical (max|difference| = 0.0)
* across all three orders and 6 seeds. The reference's term count and dynamic range are
* nowhere near double's ~52-bit mantissa budget, so order cannot be a contributor at any
* magnitude, let alone the ~0.1-1 magnitude gaps this file measures on the card. MEASURED,
* not assumed — see this agent's final report for the numbers.
*
* ---------------------------------------------------------------------------
* ANTI-VACUITY GUARDS (dev notes §3 — a green result here has to be able to go red)
* ---------------------------------------------------------------------------
* Same discipline as test_ops.c: output poisoned to 0xA5 before every launch and checked
* clear after; full-mantissa random operands, never small integers (a bf16-exact array is
* bit-identical to a wrong tiling of itself); non-finite counts reported explicitly rather
* than let a `-nan` PCC silently mean "identical to garbage". Unlike test_ops.c this file
* does not gate on PCC >= 0.99 — the point is to READ max-abs error and its trend, not to
* pass/fail a threshold — but every row still asserts the output is finite and unpoisoned
* before its numbers are trusted.
*
* Environment:
* FT_TT_TENSIX_INDEX which enabled Tensix to drive (default 101 — inside this file's
* owner's assigned 100-110 range; 100 is left free of historical
* runs from other sessions)
* FT_TT_PROGRAM_DIR parent of the four arm directories (see ARM_SUBDIR below)
*
* @status Run on Tensix 101, 2026-08-12, 0 checks failed. RESULT: at DST_ACCUM_MODE=0 (the
* shipped setting) HiFi4's max-abs error grows ~159x over the K=32->4608 sweep while
* LoFi's grows only ~38x, so the two curves cross between Kt=32 and Kt=48 — HiFi4 ahead
* at every Kt <= 32, LoFi ahead at every Kt >= 48, matching the shipped-config finding's
* short-K/long-K split. At DST_ACCUM_MODE=1, HiFi4's error stays flat and near the
* sqrt(K) floor (0.0039 -> 0.052, ~13x) and beats LoFi at EVERY Kt with no crossover.
* PCC goes from visibly K-dependent (0.999992 -> 0.998962 for HiFi4 dst16) to flat at
* 0.999999 for HiFi4 dst32. The off-hardware reference-order check (see file header)
* found summation order contributes exactly 0.0 difference. Full numbers, MEASURED vs
* INFERRED marked, are in this agent's final report — not written into
* docs/tt_bringup_log.md by this agent (shared file; another agent integrates it).
* @issues None
* @todo None
*/
#define _POSIX_C_SOURCE 200809L
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../convert/ft_float_convert.h"
#include "../ft_tt_op_l1_layout.h"
#include "../ft_tt_op_matmul.h"
#include "../ft_tt_tilize.h"
#include "ft_tt_hardware_session.h"
#include "ft_tt_program_directory.h"
#include "ft_tt_test_report.h"
#define TILE_DIM FT_TT_OP_TILE_DIM
#define TILE_ELEMENTS (TILE_DIM * TILE_DIM)
#define POISON_BYTE 0xA5u
#define LAUNCH_TIMEOUT 20000u
#define M_ROWS 32u /**< one M-tile: the accumulation trajectory is per output
* element and does not depend on M or N, so the smallest
* legal shape is the cheapest full probe. */
#define N_COLS 32u
#define K_MAX_TILES 144u /**< ffn_down's real Kt — the longest K MiniCPM5-1B uses. */
#define K_MAX_ELEMENTS (K_MAX_TILES * TILE_DIM)
/** DRAM banks 6/7, deliberately away from test_ops.c's 2-5, so a concurrent run of that
* suite on the same card cannot collide with this one's buffers. */
#define DRAM_BANK_ACTIVATION 6u
#define DRAM_BANK_WEIGHT 6u
#define DRAM_BANK_OUTPUT 7u
#define ACTIVATION_BASE 0x00100000u
#define WEIGHT_BASE 0x00300000u
#define OUTPUT_BASE 0x00100000u
typedef struct op_context {
ft_tt_hardware_session session;
uint16_t core_x;
uint16_t core_y;
ft_tt_noc_coord dram[8];
} op_context;
/* ================================================================
* Deterministic full-mantissa operands (dev notes §3: never small integers)
* ================================================================ */
static uint64_t random_state = 0x9E3779B97F4A7C15ull;
static void random_seed(uint64_t seed) { random_state = seed ? seed : 1u; }
static float random_value(void)
{
random_state ^= random_state >> 12;
random_state ^= random_state << 25;
random_state ^= random_state >> 27;
return (float)((double)(random_state * 0x2545F4914F6CDD1Dull >> 11) /
(double)(1ull << 53) * 2.0 -
1.0);
}
static void round_to_bf16(float *matrix, size_t count)
{
size_t index;
for (index = 0; index < count; index++)
matrix[index] = ft_float_from_bf16(ft_bf16_from_float(matrix[index]));
}
/* ================================================================
* Scoring — duplicated from test_ops.c rather than shared, because these are file-local
* statics there and this file must not edit test_ops.c to export them.
* ================================================================ */
static double pearson(const float *a, const float *b, size_t count)
{
double mean_a = 0.0, mean_b = 0.0, cov = 0.0, var_a = 0.0, var_b = 0.0;
size_t index;
for (index = 0; index < count; index++) {
mean_a += (double)a[index];
mean_b += (double)b[index];
}
mean_a /= (double)count;
mean_b /= (double)count;
for (index = 0; index < count; index++) {
double da = (double)a[index] - mean_a;
double db = (double)b[index] - mean_b;
cov += da * db;
var_a += da * da;
var_b += db * db;
}
if (var_a <= 0.0 || var_b <= 0.0)
return 0.0 / 0.0;
return cov / sqrt(var_a * var_b);
}
static double max_abs_difference(const float *a, const float *b, size_t count)
{
double worst = 0.0;
size_t index;
for (index = 0; index < count; index++) {
double difference = fabs((double)a[index] - (double)b[index]);
if (difference > worst)
worst = difference;
}
return worst;
}
static size_t count_non_finite(const float *values, size_t count)
{
size_t bad = 0;
size_t index;
for (index = 0; index < count; index++)
if (!isfinite((double)values[index]))
bad++;
return bad;
}
/**
* C = A x B in fp32, accumulated in double, ascending K order — bit-identical to
* test_ops.c's reference_matmul (same loop nest, same accumulation order). Kept as a
* separate copy so this file has no dependency on test_ops.c's internals; the order
* itself, and whether it could be biasing the comparison, was checked independently
* off-hardware and found not to matter at all — see the file header.
*/
static void reference_matmul(const float *a, const float *b, uint32_t m, uint32_t k,
uint32_t n, float *out)
{
double *accumulator = malloc((size_t)n * sizeof(double));
uint32_t row;
if (!accumulator)
return;
for (row = 0; row < m; row++) {
uint32_t column, inner;
for (column = 0; column < n; column++)
accumulator[column] = 0.0;
for (inner = 0; inner < k; inner++) {
double scale = (double)a[(size_t)row * k + inner];
const float *b_row = b + (size_t)inner * n;
for (column = 0; column < n; column++)
accumulator[column] += scale * (double)b_row[column];
}
for (column = 0; column < n; column++)
out[(size_t)row * n + column] = (float)accumulator[column];
}
free(accumulator);
}
/* ================================================================
* Device plumbing — poison/write/read, same shape as test_ops.c's
* ================================================================ */
static int poison_buffer(const op_context *context, ft_tt_noc_coord where, uint32_t address,
uint32_t bytes)
{
unsigned char *pattern = malloc(bytes);
int rc;
if (!pattern)
return FT_TT_ENOMEM;
memset(pattern, (int)POISON_BYTE, bytes);
rc = context->session.noc.write(context->session.noc.user, (uint16_t)where.x,
(uint16_t)where.y, (uint64_t)address, pattern, bytes);
free(pattern);
return rc;
}
static size_t poison_remaining(const unsigned char *data, size_t count)
{
size_t remaining = 0;
size_t index;
for (index = 0; index < count; index++)
if (data[index] == POISON_BYTE)
remaining++;
return remaining;
}
/* Chunked at 64 KiB, matching test_ops.c's device_write/device_read exactly — a proven
* pattern on this hardware rather than a new untested one. */
static int device_write(const op_context *context, ft_tt_noc_coord where, uint32_t address,
const void *source, size_t bytes)
{
const unsigned char *cursor = source;
size_t offset = 0;
while (offset < bytes) {
size_t chunk = bytes - offset;
int rc;
if (chunk > 0x10000u)
chunk = 0x10000u;
rc = context->session.noc.write(context->session.noc.user, (uint16_t)where.x,
(uint16_t)where.y, (uint64_t)address + offset,
cursor + offset, chunk);
if (rc != FT_TT_OK)
return rc;
offset += chunk;
}
return FT_TT_OK;
}
static int device_read(const op_context *context, ft_tt_noc_coord where, uint32_t address,
void *destination, size_t bytes)
{
unsigned char *cursor = destination;
size_t offset = 0;
while (offset < bytes) {
size_t chunk = bytes - offset;
int rc;
if (chunk > 0x10000u)
chunk = 0x10000u;
rc = context->session.noc.read(context->session.noc.user, (uint16_t)where.x,
(uint16_t)where.y, (uint64_t)address + offset,
cursor + offset, chunk);
if (rc != FT_TT_OK)
return rc;
offset += chunk;
}
return FT_TT_OK;
}
/* ================================================================
* One K point: run tile_inner = kt on a fixed operand PREFIX, i.e. read DST's actual
* state after exactly kt accumulation steps.
* ================================================================ */
static void run_k_point(op_context *context, const ft_tt_kernel_program *program,
const char *arm_label, const float *base_a, const float *base_b,
uint32_t kt, double *out_pcc, double *out_max_abs)
{
uint32_t k = kt * TILE_DIM;
float *expected = malloc((size_t)M_ROWS * N_COLS * sizeof(float));
float *actual = malloc((size_t)M_ROWS * N_COLS * sizeof(float));
uint16_t *a_tiles = malloc((size_t)kt * TILE_ELEMENTS * sizeof(uint16_t));
uint16_t *b_tiles = malloc((size_t)kt * TILE_ELEMENTS * sizeof(uint16_t));
uint16_t *out_tiles = malloc((size_t)FT_TT_OP_TILE_BYTES_BF16);
size_t out_bytes = FT_TT_OP_TILE_BYTES_BF16;
ft_tt_op_matmul_request request;
int rc;
*out_pcc = 0.0 / 0.0;
*out_max_abs = -1.0;
if (!expected || !actual || !a_tiles || !b_tiles || !out_tiles) {
ft_tt_test_check_fmt("fidelity sweep allocation", false, "%s kt=%u: out of memory",
arm_label, kt);
goto cleanup;
}
/* base_a is M_ROWS x K_MAX_ELEMENTS row-major fp32; the first `k` columns of every
* row are the prefix this kt point uses, so it is first squeezed into a DENSE M_ROWS
* x k copy — reference_matmul indexes its `a` argument with a row stride of `k`, and
* base_a's real row stride is K_MAX_ELEMENTS. base_b is K_MAX_ELEMENTS x N_COLS with
* N_COLS elements per row, so its first `k` rows are ALREADY a dense, contiguous
* prefix and need no such copy. Both prefixes are the SAME terms every larger kt in
* the sweep will also use — that is what makes the sweep a trajectory instead of a
* series of unrelated draws. ft_tt_tilize_strip only rearranges bytes, it does not
* convert format, so both prefixes are rendered to bf16 separately for the tile
* upload — exactly what the card will read (base_a/base_b are pre-rounded to bf16
* already, so this second rounding is exact, matching test_ops.c's convention). */
{
float *a_dense = malloc((size_t)M_ROWS * k * sizeof(float));
uint16_t *a_bf16 = malloc((size_t)M_ROWS * k * sizeof(uint16_t));
uint16_t *b_bf16 = malloc((size_t)k * N_COLS * sizeof(uint16_t));
uint32_t row, inner;
if (!a_dense || !a_bf16 || !b_bf16) {
ft_tt_test_check_fmt("fidelity sweep allocation", false,
"%s kt=%u: out of memory (staging)", arm_label, kt);
free(a_dense);
free(a_bf16);
free(b_bf16);
goto cleanup;
}
for (row = 0; row < M_ROWS; row++)
memcpy(a_dense + (size_t)row * k, base_a + (size_t)row * K_MAX_ELEMENTS,
(size_t)k * sizeof(float));
reference_matmul(a_dense, base_b, M_ROWS, k, N_COLS, expected);
for (row = 0; row < M_ROWS; row++) {
uint32_t column;
for (column = 0; column < k; column++)
a_bf16[(size_t)row * k + column] = ft_bf16_from_float(a_dense[(size_t)row * k +
column]);
}
ft_tt_tilize_strip(a_bf16, sizeof(uint16_t), M_ROWS, k, k * sizeof(uint16_t), a_tiles);
for (row = 0; row < k; row++) {
uint32_t column;
for (column = 0; column < N_COLS; column++)
b_bf16[(size_t)row * N_COLS + column] =
ft_bf16_from_float(base_b[(size_t)row * N_COLS + column]);
}
for (inner = 0; inner < kt; inner++)
ft_tt_tilize_strip(b_bf16 + (size_t)inner * TILE_DIM * N_COLS, sizeof(uint16_t),
TILE_DIM, N_COLS, N_COLS * sizeof(uint16_t),
b_tiles + (size_t)inner * TILE_ELEMENTS);
free(a_dense);
free(a_bf16);
free(b_bf16);
}
rc = device_write(context, context->dram[DRAM_BANK_ACTIVATION], ACTIVATION_BASE, a_tiles,
(size_t)kt * FT_TT_OP_TILE_BYTES_BF16);
rc |= device_write(context, context->dram[DRAM_BANK_WEIGHT], WEIGHT_BASE, b_tiles,
(size_t)kt * FT_TT_OP_TILE_BYTES_BF16);
rc |= poison_buffer(context, context->dram[DRAM_BANK_OUTPUT], OUTPUT_BASE,
(uint32_t)out_bytes);
if (rc != FT_TT_OK) {
ft_tt_test_check_fmt("fidelity sweep upload", false, "%s kt=%u: %s", arm_label, kt,
ft_tt_error_string(rc));
goto cleanup;
}
memset(&request, 0, sizeof(request));
request.tile_rows = 1u;
request.tile_inner = kt;
request.tile_cols = 1u;
request.prefetch_tiles = 1u;
request.activation.x = (uint16_t)context->dram[DRAM_BANK_ACTIVATION].x;
request.activation.y = (uint16_t)context->dram[DRAM_BANK_ACTIVATION].y;
request.activation.address = ACTIVATION_BASE;
request.activation.tile_bytes = FT_TT_OP_TILE_BYTES_BF16;
request.weight = request.activation;
request.weight.x = (uint16_t)context->dram[DRAM_BANK_WEIGHT].x;
request.weight.y = (uint16_t)context->dram[DRAM_BANK_WEIGHT].y;
request.weight.address = WEIGHT_BASE;
request.output = request.activation;
request.output.x = (uint16_t)context->dram[DRAM_BANK_OUTPUT].x;
request.output.y = (uint16_t)context->dram[DRAM_BANK_OUTPUT].y;
request.output.address = OUTPUT_BASE;
rc = ft_tt_op_matmul_run(&context->session.noc, context->core_x, context->core_y, program,
&request, LAUNCH_TIMEOUT);
if (rc != FT_TT_OK) {
ft_tt_test_check_fmt("fidelity sweep run", false, "%s kt=%u: %s", arm_label, kt,
ft_tt_error_string(rc));
goto cleanup;
}
rc = device_read(context, context->dram[DRAM_BANK_OUTPUT], OUTPUT_BASE, out_tiles,
out_bytes);
if (rc != FT_TT_OK) {
ft_tt_test_check_fmt("fidelity sweep read back", false, "%s kt=%u: %s", arm_label, kt,
ft_tt_error_string(rc));
goto cleanup;
}
{
size_t poison_bytes = poison_remaining((const unsigned char *)out_tiles, out_bytes);
uint32_t element;
for (element = 0; element < M_ROWS * N_COLS; element++)
actual[element] = ft_float_from_bf16(
out_tiles[ft_tt_tile_face_index(element / N_COLS, element % N_COLS)]);
{
size_t bad_expected = count_non_finite(expected, M_ROWS * N_COLS);
size_t bad_actual = count_non_finite(actual, M_ROWS * N_COLS);
/* A handful of the 2048 output BYTES legitimately equal the 0xA5 poison byte
* by chance in real bf16 data (~1/256 of bytes, so ~8 of 2048 expected) — this
* is a byte-level scan, not a word-level one, so "still poisoned" is a RATE,
* not a count, exactly test_ops.c's poison_bytes*100 < out_bytes convention.
* The first cut of this file used `poison_bytes == 0u`, which failed on
* essentially every real run for this reason alone — caught by inspecting the
* very first sweep's log rather than trusting a wall of FAILs. */
int poison_cleared = poison_bytes * 100u < out_bytes;
ft_tt_test_check_fmt("fidelity sweep output is real",
poison_cleared && bad_expected == 0u && bad_actual == 0u,
"%s kt=%u: %zu/%zu poison bytes left, %zu/%u non-finite "
"expected, %zu/%u non-finite actual", arm_label, kt,
poison_bytes, out_bytes, bad_expected, M_ROWS * N_COLS,
bad_actual, M_ROWS * N_COLS);
if (!poison_cleared || bad_expected != 0u || bad_actual != 0u)
goto cleanup;
}
*out_pcc = pearson(expected, actual, M_ROWS * N_COLS);
*out_max_abs = max_abs_difference(expected, actual, M_ROWS * N_COLS);
ft_tt_test_step("%s kt=%3u (K=%4u): PCC %.9f max-abs %.6g", arm_label, kt, k,
*out_pcc, *out_max_abs);
}
cleanup:
free(expected);
free(actual);
free(a_tiles);
free(b_tiles);
free(out_tiles);
}
/* ================================================================
* One arm: boot ITS OWN firmware+kernel directory (dst_accum_mode changes what the
* firmware's math/pack startup programs, so firmware and kernel must come from the same
* build — ft_tt_program_directory.h), run every Kt point, park, release.
* ================================================================ */
static const uint32_t KT_POINTS[] = {1u, 2u, 3u, 4u, 6u, 8u, 12u, 16u, 24u,
32u, 48u, 64u, 96u, 128u, 144u};
#define KT_POINT_COUNT (sizeof(KT_POINTS) / sizeof(KT_POINTS[0]))
typedef struct fidelity_arm {
const char *label;
const char *subdirectory; /**< under FT_TT_PROGRAM_DIR */
} fidelity_arm;
static const fidelity_arm ARMS[] = {
{"HiFi4 dst16 (shipped)", "matmul_fidelity_hifi4_dst16"},
{"LoFi dst16", "matmul_fidelity_lofi_dst16"},
{"HiFi4 dst32", "matmul_fidelity_hifi4_dst32"},
{"LoFi dst32", "matmul_fidelity_lofi_dst32"},
};
#define ARM_COUNT (sizeof(ARMS) / sizeof(ARMS[0]))
static double MAX_ABS[ARM_COUNT][KT_POINT_COUNT];
static double PCC[ARM_COUNT][KT_POINT_COUNT];
static void run_arm(op_context *context, size_t arm_index, const char *program_root,
const float *base_a, const float *base_b)
{
char directory[512];
ft_tt_firmware_set firmware; /* local, not context->firmware — each arm boots its OWN
* firmware+kernel pair from its OWN directory
* (ft_tt_program_directory.h: mixing is a wedged core,
* not a configuration), so nothing carries over between
* arms in the loop that calls this. */
ft_tt_program_directory loaded;
int rc;
size_t point;
memset(&firmware, 0, sizeof(firmware));
snprintf(directory, sizeof(directory), "%s/%s", program_root, ARMS[arm_index].subdirectory);
rc = ft_tt_firmware_set_load(directory, &firmware);
ft_tt_test_check_fmt("fidelity sweep firmware loads", rc == FT_TT_OK, "%s: %s from %s",
ARMS[arm_index].label, ft_tt_error_string(rc), directory);
if (rc != FT_TT_OK)
return;
rc = ft_tt_core_boot_firmware(&context->session.noc, context->core_x, context->core_y,
&firmware, NULL);
ft_tt_test_check_fmt("fidelity sweep firmware boots", rc == FT_TT_OK, "%s: %s",
ARMS[arm_index].label, ft_tt_error_string(rc));
if (rc != FT_TT_OK) {
ft_tt_firmware_set_release(&firmware);
return;
}
rc = ft_tt_program_directory_load(directory, 0x1Fu, &loaded);
ft_tt_test_check_fmt("fidelity sweep program loads", rc == FT_TT_OK, "%s: %s",
ARMS[arm_index].label, ft_tt_error_string(rc));
if (rc == FT_TT_OK) {
for (point = 0; point < KT_POINT_COUNT; point++)
run_k_point(context, &loaded.program, ARMS[arm_index].label, base_a, base_b,
KT_POINTS[point], &PCC[arm_index][point], &MAX_ABS[arm_index][point]);
ft_tt_program_directory_release(&loaded);
}
ft_tt_core_park(&context->session.noc, context->core_x, context->core_y);
ft_tt_firmware_set_release(&firmware);
}
/**
* The summary table this whole file exists to print: every arm's max-abs at every Kt,
* side by side, so the K-scaling question and the 2x2 DST_ACCUM_MODE question can both be
* read off one table instead of grepped out of interleaved per-run lines.
*/
static void print_summary_table(void)
{
size_t point, arm;
printf("\n=== max-abs error, all arms, all Kt (K = Kt*32) ===\n");
printf("%6s", "Kt");
for (arm = 0; arm < ARM_COUNT; arm++)
printf(" %-22s", ARMS[arm].label);
printf("\n");
for (point = 0; point < KT_POINT_COUNT; point++) {
printf("%6u", KT_POINTS[point]);
for (arm = 0; arm < ARM_COUNT; arm++)
printf(" %-22.6g", MAX_ABS[arm][point]);
printf("\n");
}
printf("\n=== PCC, all arms, all Kt ===\n");
printf("%6s", "Kt");
for (arm = 0; arm < ARM_COUNT; arm++)
printf(" %-22s", ARMS[arm].label);
printf("\n");
for (point = 0; point < KT_POINT_COUNT; point++) {
printf("%6u", KT_POINTS[point]);
for (arm = 0; arm < ARM_COUNT; arm++)
printf(" %-22.9f", PCC[arm][point]);
printf("\n");
}
}
/** FT_TT_PROGRAM_DIR, else tt/programs (where build_tensix_program.sh trees go) relative to
* tt/tests, the directory these tests are run from. */
static const char *program_root(void)
{
const char *value = getenv("FT_TT_PROGRAM_DIR");
return value ? value : "../programs";
}
int main(void)
{
op_context context;
float *base_a = malloc((size_t)M_ROWS * K_MAX_ELEMENTS * sizeof(float));
float *base_b = malloc((size_t)K_MAX_ELEMENTS * N_COLS * sizeof(float));
uint32_t tensix_index = 101u;
uint32_t bank;
size_t index;
int rc;
memset(&context, 0, sizeof(context));
if (!base_a || !base_b) {
ft_tt_test_check_fmt("fidelity sweep allocation", false, "out of memory");
return ft_tt_test_finish();
}
ft_tt_test_step("Fidelity/DST_ACCUM_MODE explanatory sweep — %zu arms x %zu Kt points",
ARM_COUNT, KT_POINT_COUNT);
/* ONE draw of operands, shared by every arm and every Kt point (the prefix of it).
* Scaled *0.5 and bf16-rounded, same convention as test_ops.c, so the values fed to
* the card are exactly what the reference is scored against. */
random_seed(0xF1DE71Au);
for (index = 0; index < (size_t)M_ROWS * K_MAX_ELEMENTS; index++)
base_a[index] = random_value() * 0.5f;
for (index = 0; index < (size_t)K_MAX_ELEMENTS * N_COLS; index++)
base_b[index] = random_value() * 0.5f;
round_to_bf16(base_a, (size_t)M_ROWS * K_MAX_ELEMENTS);
round_to_bf16(base_b, (size_t)K_MAX_ELEMENTS * N_COLS);
rc = ft_tt_hardware_session_open(&context.session);
ft_tt_test_check_fmt("card opens", rc == FT_TT_OK, "%s", ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto done;
(void)ft_tt_tensix_index_from_environment(&tensix_index);
rc = ft_tt_hardware_session_tensix(&context.session, tensix_index, &context.core_x,
&context.core_y);
ft_tt_test_check_fmt("Tensix resolves from live telemetry", rc == FT_TT_OK,
"index %u -> translated (%u, %u): %s", tensix_index,
(unsigned)context.core_x, (unsigned)context.core_y,
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto close;
/* A harness that names its core owns it — per-core, so this sweep runs
* alongside any other suite on a different Tensix. */
rc = ft_tt_hardware_session_claim_cores(&context.session, &tensix_index, 1u,
"fidelity sweep", NULL, NULL);
ft_tt_test_check_fmt("the Tensix is claimed for this run", rc == FT_TT_OK, "index %u: %s",
tensix_index, ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto close;
for (bank = 0; bank < 8u; bank++) {
rc = ft_tt_coord_dram_bank(&context.session.grid, bank, FT_TT_COORD_TRANSLATED,
&context.dram[bank]);
if (rc != FT_TT_OK)
break;
}
ft_tt_test_check_fmt("DRAM bank coordinates derive", rc == FT_TT_OK, "%s",
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto close;
for (index = 0; index < ARM_COUNT; index++)
run_arm(&context, index, program_root(), base_a, base_b);
print_summary_table();
close:
ft_tt_hardware_session_close(&context.session);
done:
free(base_a);
free(base_b);
return ft_tt_test_finish();
}