471 lines
20 KiB
C
471 lines
20 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* test_phase2_cb_formats.c — a SECOND tile format, end to end on the card.
|
|
*
|
|
* Plan Phase 2 follow-up 5. Until 2026-08-12 build_tensix_kernel.sh emitted one hardcoded
|
|
* CB descriptor set — Float16_b, 32x32, 4 faces, 2048 B — so exactly one tile shape had
|
|
* ever been compiled, and a kernel with any other page size or format got descriptors
|
|
* describing a tile it does not have. That failure is silent: nothing in the build or the
|
|
* launch protocol checks the descriptors against the CB config, because
|
|
* ft_tensix_cb_formats.h IS the contract and there is no runtime negotiation.
|
|
*
|
|
* This runs the same vector_add triad on **Float32** tiles — 4096-byte pages, twice the
|
|
* bytes, a different format code — under two builds that differ in NOTHING except which
|
|
* descriptors were baked in:
|
|
*
|
|
* vector_add_float32 CB_TABLE=kernels/cb_tables/vector_add_float32.cbtable
|
|
* vector_add_float32_legacydesc no CB table, i.e. the old uniform Float16_b set
|
|
*
|
|
* Both are built with the same 4096-byte tile compile-time args, so the data movement is
|
|
* identical and the descriptors are the only variable. Both are booted and run here, in
|
|
* one process, against the same data, and the destinations are compared to each other as
|
|
* BYTES — a pass/fail against a reference could not tell "the descriptors are right" from
|
|
* "the descriptors never mattered".
|
|
*
|
|
* WHAT THE FIRST VERSION OF THIS TEST GOT WRONG, measured on the card 2026-08-12 and
|
|
* worth keeping: with small-integer inputs BOTH builds return a byte-exact result. That
|
|
* is not luck twice, it is arithmetic — a Float32 array whose values are all exactly
|
|
* representable in bfloat16 has zero in every low 16 bits, so it is bit-identical to its
|
|
* own bfloat16 rendering interleaved with zeros, and the wrong descriptors reproduce it
|
|
* exactly. **Integer test data cannot distinguish two descriptor sets on this path.** The
|
|
* fractional pass below is the one that separates them, and the integer pass is kept and
|
|
* reported because the trap is the finding.
|
|
*
|
|
* The correct expectation for Float32 in, with dst_accum_mode off: the source and
|
|
* destination registers hold Float16_b (the build-time generator derives
|
|
* this, nobody chooses it), so every result is a bfloat16 value widened back to Float32,
|
|
* i.e. its low 16 bits are ZERO. That is a rounding-mode-independent assertion, which
|
|
* matters because whether the unpacker truncates or rounds to nearest is documented
|
|
* nowhere we have read.
|
|
*
|
|
* This test uses its OWN L1 layout, not ft_tt_phase2_l1_layout.h's: at 4096-byte pages a
|
|
* two-page CB is 8 KiB and the Phase 2 CBs are 4 KiB apart, so that map would overlap.
|
|
* Only the kernel-config base and the runtime-arg blocks are shared.
|
|
*
|
|
* Usage: test_phase2_cb_formats <table-driven-directory> <legacy-descriptor-directory>
|
|
*
|
|
* Build:
|
|
* make -f Makefile.phase2_followups PHASE1B=1 build-phase2b/test_phase2_cb_formats
|
|
*/
|
|
|
|
#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 5000u
|
|
|
|
/** 32x32 Float32. This is the number the whole test is about. */
|
|
#define FLOAT32_TILE_BYTES 4096u
|
|
#define FLOAT32_TILE_ELEMENTS (FLOAT32_TILE_BYTES / 4u)
|
|
|
|
/** Same CB indices vector_add is always built with: 0 and 1 in, 16 out. */
|
|
#define CB_IN0 0u
|
|
#define CB_IN1 1u
|
|
#define CB_OUT 16u
|
|
|
|
/** Two pages, so the ring wraps once over a two-tile run. */
|
|
#define CB_PAGE_COUNT 2u
|
|
#define CB_BYTES (CB_PAGE_COUNT * FLOAT32_TILE_BYTES)
|
|
|
|
#define CB_IN0_ADDRESS 0x00030000u
|
|
#define CB_IN1_ADDRESS 0x00032000u
|
|
#define CB_OUT_ADDRESS 0x00034000u
|
|
|
|
#define SOURCE0_ADDRESS 0x00040000u
|
|
#define SOURCE1_ADDRESS 0x00044000u
|
|
#define DESTINATION_ADDRESS 0x00048000u
|
|
|
|
#define TILE_COUNT 2u
|
|
#define TOTAL_ELEMENTS (TILE_COUNT * FLOAT32_TILE_ELEMENTS)
|
|
|
|
#define CB_CONFIG_BYTES (FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4u)
|
|
|
|
/** Which input set a run uses. */
|
|
typedef enum input_kind {
|
|
/** Small integers: every value and every sum is exactly a bfloat16. */
|
|
INPUT_BFLOAT16_EXACT = 0,
|
|
/** Values with mantissa bits below the bfloat16 boundary, so the format the
|
|
* descriptors name changes the answer. */
|
|
INPUT_FRACTIONAL = 1
|
|
} input_kind;
|
|
|
|
/** The L1 map, derived from where the firmware images landed rather than chosen. A
|
|
* file-scope object because every helper below needs it and threading one more
|
|
* parameter through all of them would say less than this comment does. Written once,
|
|
* in main(), before anything reads it. */
|
|
static ft_tt_phase2_l1_layout g_layout;
|
|
|
|
/**
|
|
* Boot the firmware and DERIVE the L1 map from where its images landed.
|
|
*
|
|
* The map's base used to be the literal 0x14000. It is now the higher of "the top of the
|
|
* five firmware images the loader just placed" and "the end of the regions the stock memory map
|
|
* reserves above MEM_MAP_END", which is exactly what ft_tt_image_placement_l1_floor()
|
|
* was written to compute and had no caller for.
|
|
*/
|
|
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 float tile_in0[FLOAT32_TILE_ELEMENTS];
|
|
static float tile_in1[FLOAT32_TILE_ELEMENTS];
|
|
static float tile_expected[FLOAT32_TILE_ELEMENTS];
|
|
static float tile_scratch[FLOAT32_TILE_ELEMENTS];
|
|
|
|
/** Whole destinations, one per program, so the two can be compared byte for byte. */
|
|
static uint32_t destination_words[2][TOTAL_ELEMENTS];
|
|
|
|
/**
|
|
* Build both input tiles and the exact-arithmetic reference for one tile.
|
|
*
|
|
* Element ORDER does not have to match the Tensix's 4-face tile layout: an elementwise
|
|
* add preserves position under any permutation applied to both operands.
|
|
*/
|
|
static void build_reference_tiles(unsigned tile_index, input_kind inputs)
|
|
{
|
|
unsigned element;
|
|
|
|
for (element = 0; element < FLOAT32_TILE_ELEMENTS; element++) {
|
|
float left = (float)((element + tile_index) % 16u + 1u);
|
|
float right = (float)((element * 3u + tile_index) % 16u + 1u);
|
|
|
|
if (inputs == INPUT_FRACTIONAL) {
|
|
left += (float)(element % 7u) / 128.0f;
|
|
right += (float)(element % 5u) / 512.0f;
|
|
}
|
|
tile_in0[element] = left;
|
|
tile_in1[element] = right;
|
|
tile_expected[element] = left + right;
|
|
}
|
|
}
|
|
|
|
/** Index of the first differing element, or FLOAT32_TILE_ELEMENTS when they match. */
|
|
static unsigned first_difference(const float *left, const float *right)
|
|
{
|
|
unsigned element;
|
|
|
|
for (element = 0; element < FLOAT32_TILE_ELEMENTS; element++) {
|
|
if (memcmp(&left[element], &right[element], sizeof(float)) != 0)
|
|
return element;
|
|
}
|
|
return FLOAT32_TILE_ELEMENTS;
|
|
}
|
|
|
|
/** Write the three CBs' config into L1 and hand back the launch message's mask. */
|
|
static int write_cb_config(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint64_t *out_mask)
|
|
{
|
|
static const uint32_t addresses[3] = {CB_IN0_ADDRESS, CB_IN1_ADDRESS, CB_OUT_ADDRESS};
|
|
static const unsigned indices[3] = {CB_IN0, CB_IN1, CB_OUT};
|
|
static uint8_t buffer[CB_CONFIG_BYTES];
|
|
ft_tt_launch_cb_entry entries[3];
|
|
unsigned entry;
|
|
int rc;
|
|
|
|
for (entry = 0; entry < 3u; entry++) {
|
|
entries[entry].index = indices[entry];
|
|
entries[entry].address = addresses[entry];
|
|
entries[entry].size_bytes = CB_BYTES;
|
|
entries[entry].page_size_bytes = FLOAT32_TILE_BYTES;
|
|
entries[entry].page_count = CB_PAGE_COUNT;
|
|
}
|
|
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 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 = 0x1fu;
|
|
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++) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/** Plant the reader's, writer's and TRISCs' runtime args for a self-addressed run. */
|
|
static int plant_runtime_args(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
|
|
const ft_tt_launch_config *config)
|
|
{
|
|
uint32_t reader_arguments[7] = {TILE_COUNT, x, y, SOURCE0_ADDRESS, x, y, SOURCE1_ADDRESS};
|
|
uint32_t writer_arguments[4] = {TILE_COUNT, x, y, DESTINATION_ADDRESS};
|
|
uint32_t tile_count = TILE_COUNT;
|
|
unsigned processor;
|
|
int rc;
|
|
|
|
rc = ft_tt_launch_plant_runtime_args(noc, x, y, config->runtime_args_address[FT_TT_PROC_DM0],
|
|
reader_arguments, 7u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_plant_runtime_args(noc, x, y,
|
|
config->runtime_args_address[FT_TT_PROC_DM1],
|
|
writer_arguments, 4u);
|
|
for (processor = FT_TT_PROC_MATH0; processor < FT_TT_PROC_COUNT && rc == FT_TT_OK; processor++)
|
|
rc = ft_tt_launch_plant_runtime_args(noc, x, y, config->runtime_args_address[processor],
|
|
&tile_count, 1u);
|
|
return rc;
|
|
}
|
|
|
|
/** Fill both source buffers in this core's L1 and wipe the destination. */
|
|
static int stage_buffers(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, input_kind inputs)
|
|
{
|
|
unsigned tile;
|
|
|
|
for (tile = 0; tile < TILE_COUNT; tile++) {
|
|
uint32_t offset = tile * FLOAT32_TILE_BYTES;
|
|
int rc;
|
|
|
|
build_reference_tiles(tile, inputs);
|
|
rc = noc->write(noc->user, x, y, SOURCE0_ADDRESS + offset, tile_in0, sizeof(tile_in0));
|
|
if (rc == FT_TT_OK)
|
|
rc = noc->write(noc->user, x, y, SOURCE1_ADDRESS + offset, tile_in1, sizeof(tile_in1));
|
|
if (rc == FT_TT_OK) {
|
|
memset(tile_scratch, 0, sizeof(tile_scratch));
|
|
rc = noc->write(noc->user, x, y, DESTINATION_ADDRESS + offset, tile_scratch,
|
|
sizeof(tile_scratch));
|
|
}
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* Boot one program directory, run the Float32 vector add over `inputs`, and copy the
|
|
* whole destination out.
|
|
*
|
|
* @param out_words TOTAL_ELEMENTS words, the destination exactly as the card left it
|
|
*/
|
|
static int run_one_program(ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
const char *directory, input_kind inputs, uint32_t *out_words)
|
|
{
|
|
ft_tt_firmware_set firmware;
|
|
ft_tt_program_directory program;
|
|
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
|
|
ft_tt_launch_config config;
|
|
uint64_t cb_mask = 0;
|
|
unsigned tile;
|
|
int rc;
|
|
|
|
rc = ft_tt_firmware_set_load(directory, &firmware);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_program_directory_load(directory, 0x1fu, &program);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_firmware_set_release(&firmware);
|
|
return rc;
|
|
}
|
|
|
|
rc = boot_and_derive_layout(&session->noc, x, y, &firmware);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_core_load_program(&session->noc, x, y, &program.program, placements);
|
|
if (rc == FT_TT_OK)
|
|
rc = write_cb_config(&session->noc, x, y, &cb_mask);
|
|
if (rc == FT_TT_OK) {
|
|
build_config(placements, cb_mask, &config);
|
|
rc = plant_runtime_args(&session->noc, x, y, &config);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = stage_buffers(&session->noc, x, y, inputs);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_write_launch_message(&session->noc, x, y, &config);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_send_go(&session->noc, x, y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_wait_done(&session->noc, x, y, RUN_TIMEOUT_MS, NULL);
|
|
|
|
for (tile = 0; tile < TILE_COUNT && rc == FT_TT_OK; tile++)
|
|
rc = session->noc.read(session->noc.user, x, y,
|
|
DESTINATION_ADDRESS + tile * FLOAT32_TILE_BYTES,
|
|
&out_words[tile * FLOAT32_TILE_ELEMENTS], FLOAT32_TILE_BYTES);
|
|
|
|
/* Park between programs: the next boot would otherwise load firmware over a core that
|
|
* is still running the previous one. */
|
|
(void)ft_tt_core_park(&session->noc, x, y);
|
|
ft_tt_program_directory_release(&program);
|
|
ft_tt_firmware_set_release(&firmware);
|
|
return rc;
|
|
}
|
|
|
|
/** How many destination words are NOT a bfloat16 value widened to Float32. */
|
|
static unsigned count_words_with_low_mantissa_bits(const uint32_t *words)
|
|
{
|
|
unsigned element;
|
|
unsigned count = 0;
|
|
|
|
for (element = 0; element < TOTAL_ELEMENTS; element++) {
|
|
if ((words[element] & 0x0000FFFFu) != 0u)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/** First element that differs from the exact-arithmetic reference, and its tile. */
|
|
static unsigned first_reference_difference(const uint32_t *words, input_kind inputs,
|
|
unsigned *out_tile)
|
|
{
|
|
unsigned tile;
|
|
|
|
for (tile = 0; tile < TILE_COUNT; tile++) {
|
|
unsigned difference;
|
|
|
|
build_reference_tiles(tile, inputs);
|
|
memcpy(tile_scratch, &words[tile * FLOAT32_TILE_ELEMENTS], sizeof(tile_scratch));
|
|
difference = first_difference(tile_scratch, tile_expected);
|
|
if (difference != FLOAT32_TILE_ELEMENTS) {
|
|
*out_tile = tile;
|
|
return difference;
|
|
}
|
|
}
|
|
*out_tile = TILE_COUNT;
|
|
return FLOAT32_TILE_ELEMENTS;
|
|
}
|
|
|
|
static void report_head(const char *label, const uint32_t *words)
|
|
{
|
|
printf(" %-14s destination[0..3] = 0x%08x 0x%08x 0x%08x 0x%08x\n", label, words[0],
|
|
words[1], words[2], words[3]);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void run_integer_pass(ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
const char *table_directory, const char *legacy_directory)
|
|
{
|
|
unsigned bad_tile = 0;
|
|
unsigned difference;
|
|
int rc;
|
|
|
|
ft_tt_test_step("Float32 tiles, bfloat16-exact inputs — correctness, and why this pass "
|
|
"cannot be the demonstration");
|
|
|
|
rc = run_one_program(session, x, y, table_directory, INPUT_BFLOAT16_EXACT,
|
|
destination_words[0]);
|
|
ft_tt_test_check_fmt("the table-driven build completed", rc == FT_TT_OK, "rc=%d (%s)", rc,
|
|
ft_tt_error_string(rc));
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
difference = first_reference_difference(destination_words[0], INPUT_BFLOAT16_EXACT, &bad_tile);
|
|
ft_tt_test_check_fmt("every Float32 element is byte-exact against the C reference",
|
|
difference == FLOAT32_TILE_ELEMENTS,
|
|
"%u tiles x %u elements; matched up to tile %u element %u", TILE_COUNT,
|
|
FLOAT32_TILE_ELEMENTS, bad_tile, difference);
|
|
report_head("table-driven", destination_words[0]);
|
|
|
|
rc = run_one_program(session, x, y, legacy_directory, INPUT_BFLOAT16_EXACT,
|
|
destination_words[1]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check("the legacy build completed", false, ft_tt_error_string(rc));
|
|
return;
|
|
}
|
|
report_head("legacy", destination_words[1]);
|
|
printf(" the two destinations are %s on integer data — which is why the fractional\n"
|
|
" pass exists: a bfloat16-exact Float32 array is bit-identical to its own\n"
|
|
" bfloat16 rendering, so WRONG descriptors reproduce it exactly\n",
|
|
memcmp(destination_words[0], destination_words[1], sizeof(destination_words[0])) == 0
|
|
? "IDENTICAL"
|
|
: "different");
|
|
}
|
|
|
|
static void run_fractional_pass(ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
const char *table_directory, const char *legacy_directory)
|
|
{
|
|
unsigned table_low_bits;
|
|
unsigned legacy_low_bits;
|
|
int rc;
|
|
|
|
ft_tt_test_step("Float32 tiles, inputs with mantissa bits below the bfloat16 boundary — "
|
|
"now the descriptors are the only thing that can explain the answer");
|
|
|
|
rc = run_one_program(session, x, y, table_directory, INPUT_FRACTIONAL, destination_words[0]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check("the table-driven build completed", false, ft_tt_error_string(rc));
|
|
return;
|
|
}
|
|
rc = run_one_program(session, x, y, legacy_directory, INPUT_FRACTIONAL, destination_words[1]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check("the legacy build completed", false, ft_tt_error_string(rc));
|
|
return;
|
|
}
|
|
report_head("table-driven", destination_words[0]);
|
|
report_head("legacy", destination_words[1]);
|
|
|
|
ft_tt_test_check("the two descriptor sets produce DIFFERENT device output",
|
|
memcmp(destination_words[0], destination_words[1],
|
|
sizeof(destination_words[0])) != 0,
|
|
"same kernel source, same CB config, same data — only "
|
|
"ft_tensix_cb_formats.h differs");
|
|
|
|
/* Rounding-mode independent: whatever the unpacker does to the low mantissa bits, a
|
|
* correctly described Float32 CB puts a bfloat16 value back into L1, so the low 16
|
|
* bits of every destination word must be zero. */
|
|
table_low_bits = count_words_with_low_mantissa_bits(destination_words[0]);
|
|
legacy_low_bits = count_words_with_low_mantissa_bits(destination_words[1]);
|
|
|
|
ft_tt_test_check_fmt("the table-driven result is a bfloat16 value widened to Float32, "
|
|
"which is what the derived formats require",
|
|
table_low_bits == 0u, "%u of %u words carry low mantissa bits",
|
|
table_low_bits, TOTAL_ELEMENTS);
|
|
ft_tt_test_check_fmt("the legacy descriptors do NOT produce that, so they were really wrong",
|
|
legacy_low_bits > 0u, "%u of %u words carry low mantissa bits",
|
|
legacy_low_bits, TOTAL_ELEMENTS);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
uint16_t x = 0;
|
|
uint16_t y = 0;
|
|
int rc;
|
|
|
|
if (argc < 3) {
|
|
fprintf(stderr, "usage: %s <table-driven-directory> <legacy-descriptor-directory>\n"
|
|
" both are build_tensix_program.sh outputs for the Float32 vector_add\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
|
|
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;
|
|
}
|
|
printf("target Tensix 0 at translated (%u, %u)\n table-driven: %s\n legacy: %s\n",
|
|
(unsigned)x, (unsigned)y, argv[1], argv[2]);
|
|
|
|
run_integer_pass(&session, x, y, argv[1], argv[2]);
|
|
run_fractional_pass(&session, x, y, argv[1], argv[2]);
|
|
|
|
/* run_one_program() parks on every path; do it once more so a failure before the
|
|
* first run still leaves the core in reset. */
|
|
(void)ft_tt_core_park(&session.noc, x, y);
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|