Foundry/tt/tests/test_phase2_vector_add.c

605 lines
25 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* test_phase2_vector_add.c — checklist steps 11 and 12 from docs/tt_launch_notes.md §6.
*
* The first arithmetic Foundry asks a Tensix to do, in two stages that differ by
* exactly one thing: who moves the data.
*
* 11 HOST-STAGED. The host writes both input tiles straight into the circular-buffer
* FIFOs, primes the CB config, and hands the compute triad a full input CB by
* writing the CBs' page counters itself. Those counters are NoC-addressable
* overlay-stream registers on the target core (ft_tt_memmap.h), so the host can
* play producer without any data-movement kernel existing. THE DEVICE ISSUES NO
* NoC TRANSACTION AT ALL, so the blast radius is one core.
*
* 12 FULL TRIAD. Add vector_add_reader.c on BRISC and vector_add_writer.c on
* NCRISC. This is the first time the DEVICE issues NoC traffic. Both kernels are
* pointed at this core's own L1 — a self-addressed transaction — because the one
* endpoint we have proven exists in this coordinate space is the core we have
* been reading and writing all along (spec §7.3: a wrong endpoint hangs the chip).
*
* Both stages are checked byte-exact against a C reference. Inputs are small integers
* so every sum is exactly representable in bfloat16 and "byte-exact" is a fair demand
* rather than a rounding lottery.
*
* Build:
* cc -std=c99 -O1 -Wall -Wextra -Werror -pedantic -I.. -DFT_TT_PHASE1B_NOC_ENABLED \
* ../ft_tt_kmd.c ../ft_tt_device.c ../ft_tt_tlb.c ../ft_tt_arc_telemetry.c \
* ../ft_tt_coord.c ../ft_tt_elf.c ../ft_tt_kernel_blob.c ../ft_tt_launch.c \
* ft_tt_test_report.c ft_tt_hardware_session.c test_phase2_vector_add.c \
* -o build/test_phase2_vector_add
*/
#define _POSIX_C_SOURCE 200809L
#include "ft_tt_hardware_session.h"
#include "ft_tt_kernels.h"
#include "ft_tt_phase2_l1_layout.h"
#include "ft_tt_test_report.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#define RUN_TIMEOUT_MS 5000u
#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)
/** Tiles the full-triad run moves. Two, so the two-page CB ring wraps exactly once —
* a CB that is really a single slot passes a one-tile test. */
#define FULL_TRIAD_TILE_COUNT 2u
/** Step 12's acceptance: byte-exact, repeated. */
#define FULL_TRIAD_REPEAT_COUNT 1000u
/** How long the host will keep re-asserting a CB page counter against TRISC0's
* stock sync init before giving up. */
#define COUNTER_STAGE_ATTEMPTS 64u
/** 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 uint16_t tile_in0[TILE_ELEMENTS];
static uint16_t tile_in1[TILE_ELEMENTS];
static uint16_t tile_expected[TILE_ELEMENTS];
static uint16_t tile_readback[TILE_ELEMENTS];
/** bfloat16 is the top 16 bits of the IEEE-754 binary32 pattern, truncated. Every value
* this test uses is a small integer, so truncation is exact and so is the sum. */
static uint16_t bfloat16_from_float(float value)
{
uint32_t bits;
memcpy(&bits, &value, sizeof(bits));
return (uint16_t)(bits >> 16);
}
/**
* Build the two input tiles and the reference result.
*
* Element ORDER does not have to match the Tensix's 4-face tile layout: an elementwise
* add preserves position whatever the permutation, as long as both operands use the
* same one. What does matter is that every sum lands exactly on a bfloat16.
*/
static void build_reference_tiles(unsigned tile_index)
{
unsigned element;
for (element = 0; element < TILE_ELEMENTS; element++) {
float left = (float)((element + tile_index) % 16u + 1u);
float right = (float)((element * 3u + tile_index) % 16u + 1u);
tile_in0[element] = bfloat16_from_float(left);
tile_in1[element] = bfloat16_from_float(right);
tile_expected[element] = bfloat16_from_float(left + right);
}
}
/** Index of the first differing element, or TILE_ELEMENTS when they match. */
static unsigned first_difference(const uint16_t *left, const uint16_t *right)
{
unsigned element;
for (element = 0; element < TILE_ELEMENTS; element++) {
if (left[element] != right[element])
return element;
}
return TILE_ELEMENTS;
}
/** Describe the three circular buffers this program uses. */
static void describe_circular_buffers(ft_tt_launch_cb_entry entries[3])
{
const uint32_t addresses[3] = {g_layout.cb_in0_address, g_layout.cb_in1_address,
g_layout.cb_out_address};
static const unsigned indices[3] = {FT_TT_PHASE2_CB_IN0, FT_TT_PHASE2_CB_IN1,
FT_TT_PHASE2_CB_OUT};
unsigned entry;
for (entry = 0; entry < 3u; entry++) {
entries[entry].index = indices[entry];
entries[entry].address = addresses[entry];
entries[entry].size_bytes = FT_TT_PHASE2_CB_BYTES;
entries[entry].page_size_bytes = FT_TT_PHASE2_TILE_BYTES;
entries[entry].page_count = FT_TT_PHASE2_CB_PAGE_COUNT;
}
}
/** Write the CB config array into L1 and hand back the mask for the launch message. */
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));
}
/**
* Set one CB's page counters and make them stick.
*
* TRISC0 re-zeroes all 64 counter pairs whenever BRISC asks it to, and BRISC asks after
* every run — so a plain write here races that and loses silently, which shows up as a
* hang, not an error. Write, read back, repeat.
*/
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("vector_add: CB %u counters would not hold their value", cb_index);
return FT_TT_EIO;
}
/** Common launch config for the vector_add program. */
static void build_config(const ft_tt_image_placement placements[FT_TT_PROC_COUNT],
uint32_t enables, 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 = enables;
out_config->local_cb_config_address = g_layout.cb_config_address;
out_config->local_cb_mask = cb_mask;
/* vector_add_reader.c is compiled with NOC_INDEX=0 and the firmware re-reads this
* at run time. The two MUST agree. */
out_config->brisc_noc_id = 0u;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
if ((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);
}
}
static int launch_and_wait(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_launch_config *config, uint32_t timeout_ms)
{
int rc = ft_tt_launch_write_launch_message(noc, x, y, config);
if (rc != FT_TT_OK)
return rc;
rc = ft_tt_launch_send_go(noc, x, y);
if (rc != FT_TT_OK)
return rc;
return ft_tt_launch_wait_done(noc, x, y, timeout_ms, NULL);
}
/* ------------------------------------------------------------------------- */
/* Step 11 — host-staged */
/* ------------------------------------------------------------------------- */
static void run_step_11_host_staged(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_image_placement placements[FT_TT_PROC_COUNT])
{
ft_tt_launch_config config;
uint64_t cb_mask = 0;
uint32_t tile_count = 1u;
uint32_t received_after = 0;
unsigned processor;
unsigned difference;
int rc;
ft_tt_test_step("step 11 — vector add with the HOST staging both input tiles "
"(no device NoC traffic at all)");
build_reference_tiles(0u);
rc = write_cb_config(noc, x, y, &cb_mask);
ft_tt_test_check_fmt("CB config written", rc == FT_TT_OK, "mask=0x%llx at 0x%05x",
(unsigned long long)cb_mask, g_layout.cb_config_address);
if (rc != FT_TT_OK)
return;
build_config(placements, 0x1cu, cb_mask, &config);
ft_tt_test_check_fmt("enables covers the compute triad only", config.enables == 0x1cu,
"0x%02x", config.enables);
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],
&tile_count, 1u);
if (rc != FT_TT_OK)
break;
}
ft_tt_test_check_fmt("tile count planted for each TRISC", rc == FT_TT_OK, "%u tile", tile_count);
if (rc != FT_TT_OK)
return;
/* Both input tiles straight into page 0 of their FIFOs, and a wiped output FIFO so
* a stale result cannot pass for a computed one. */
memset(tile_readback, 0, sizeof(tile_readback));
rc = noc->write(noc->user, x, y, g_layout.cb_in0_address, tile_in0, sizeof(tile_in0));
if (rc == FT_TT_OK)
rc = noc->write(noc->user, x, y, g_layout.cb_in1_address, tile_in1, sizeof(tile_in1));
if (rc == FT_TT_OK)
rc = noc->write(noc->user, x, y, g_layout.cb_out_address, tile_readback,
sizeof(tile_readback));
ft_tt_test_check_fmt("input tiles staged, output FIFO wiped", rc == FT_TT_OK, "%u B each",
FT_TT_PHASE2_TILE_BYTES);
if (rc != FT_TT_OK)
return;
rc = ft_tt_core_wait_sync_registers_ready(noc, x, y, 1000u);
ft_tt_test_check_fmt("TRISC0 finished re-initialising the CB counters", rc == FT_TT_OK,
"rc=%d", rc);
if (rc != FT_TT_OK)
return;
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);
ft_tt_test_check_fmt("the host pushed one page into each input CB", rc == FT_TT_OK,
"tiles_received[%u]@0x%08x = 1", FT_TT_PHASE2_CB_IN0,
FT_TT_CB_TILES_RECEIVED_ADDR(FT_TT_PHASE2_CB_IN0));
if (rc != FT_TT_OK)
return;
rc = launch_and_wait(noc, x, y, &config, RUN_TIMEOUT_MS);
ft_tt_test_check_fmt("compute triad completed", rc == FT_TT_OK, "rc=%d (%s)", rc,
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
return;
/* Informational, not a check. The output CB's counter reads 0 here and that is
* correct: BRISC asks TRISC0 to re-zero all 64 counter pairs as part of finishing a
* run, and it does so before the host can observe the packer's push.
* The evidence that the packer really wrote is the output FIFO itself, which was
* wiped to zeros before the launch and is compared below. */
rc = noc->read(noc->user, x, y, FT_TT_CB_TILES_RECEIVED_ADDR(FT_TT_PHASE2_CB_OUT),
&received_after, sizeof(received_after));
printf(" output CB counter after completion: tiles_received[%u] = %u (rc=%d) — "
"TRISC0 re-zeroes every counter at end of run, so 0 is expected\n",
FT_TT_PHASE2_CB_OUT, received_after & 0xFFFFu, rc);
rc = noc->read(noc->user, x, y, g_layout.cb_out_address, tile_readback,
sizeof(tile_readback));
if (rc != FT_TT_OK) {
ft_tt_test_check("output tile read back", false, ft_tt_error_string(rc));
return;
}
difference = first_difference(tile_readback, tile_expected);
if (difference != TILE_ELEMENTS) {
/* WHICH elements are wrong, and how many, is the whole diagnosis: a handful of
* stray words is a different fault from half a tile, and step 11 has no NoC
* traffic at all, so anything wrong here is the compute. */
unsigned index;
unsigned differing = 0;
unsigned shown = 0;
for (index = 0; index < TILE_ELEMENTS; index++) {
if (tile_readback[index] == tile_expected[index])
continue;
differing++;
if (shown < 24u) {
printf("%s%u(%04x!=%04x)", shown == 0u ? " differing: " : " ", index,
tile_readback[index], tile_expected[index]);
shown++;
}
}
printf("%s%u of %u differ\n", differing > 24u ? " ... " : " — ", differing,
TILE_ELEMENTS);
}
ft_tt_test_check_fmt("output tile is byte-exact against the C reference",
difference == TILE_ELEMENTS,
"%u of %u bfloat16 elements matched before the first difference",
difference, TILE_ELEMENTS);
if (difference != TILE_ELEMENTS)
printf(" element %u: in0=0x%04x in1=0x%04x got=0x%04x expected=0x%04x\n",
difference, tile_in0[difference], tile_in1[difference], tile_readback[difference],
tile_expected[difference]);
}
/* ------------------------------------------------------------------------- */
/* Step 12 — full triad */
/* ------------------------------------------------------------------------- */
/** Fill both source buffers and the reference for a multi-tile run. */
static int stage_source_buffers(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
unsigned tile;
for (tile = 0; tile < FULL_TRIAD_TILE_COUNT; tile++) {
uint32_t offset = tile * FT_TT_PHASE2_TILE_BYTES;
int rc;
build_reference_tiles(tile);
rc = noc->write(noc->user, x, y, g_layout.source0_address + offset, tile_in0,
sizeof(tile_in0));
if (rc != FT_TT_OK)
return rc;
rc = noc->write(noc->user, x, y, g_layout.source1_address + offset, tile_in1,
sizeof(tile_in1));
if (rc != FT_TT_OK)
return rc;
}
return FT_TT_OK;
}
/** Wipe the destination, so a repeat cannot pass on the previous run's answer. */
static int wipe_destination(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
unsigned tile;
memset(tile_readback, 0, sizeof(tile_readback));
for (tile = 0; tile < FULL_TRIAD_TILE_COUNT; tile++) {
int rc = noc->write(noc->user, x, y,
g_layout.destination_address + tile * FT_TT_PHASE2_TILE_BYTES,
tile_readback, sizeof(tile_readback));
if (rc != FT_TT_OK)
return rc;
}
return FT_TT_OK;
}
/** Compare every destination tile against the reference. Returns FT_TT_OK on a match. */
static int check_destination(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
unsigned *out_bad_tile, unsigned *out_bad_element)
{
unsigned tile;
for (tile = 0; tile < FULL_TRIAD_TILE_COUNT; tile++) {
unsigned difference;
int rc = noc->read(noc->user, x, y,
g_layout.destination_address + tile * FT_TT_PHASE2_TILE_BYTES,
tile_readback, sizeof(tile_readback));
if (rc != FT_TT_OK)
return rc;
build_reference_tiles(tile);
difference = first_difference(tile_readback, tile_expected);
if (difference != TILE_ELEMENTS) {
*out_bad_tile = tile;
*out_bad_element = difference;
return FT_TT_ERROR;
}
}
*out_bad_tile = FULL_TRIAD_TILE_COUNT;
*out_bad_element = TILE_ELEMENTS;
return FT_TT_OK;
}
static uint64_t monotonic_milliseconds(void)
{
struct timespec now;
(void)clock_gettime(CLOCK_MONOTONIC, &now);
return (uint64_t)now.tv_sec * 1000u + (uint64_t)(now.tv_nsec / 1000000L);
}
static void run_step_12_full_triad(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_image_placement placements[FT_TT_PROC_COUNT])
{
ft_tt_launch_config config;
uint64_t cb_mask = 0;
uint32_t reader_arguments[7];
uint32_t writer_arguments[4];
uint32_t tile_count = FULL_TRIAD_TILE_COUNT;
uint64_t started;
uint64_t total_ms;
unsigned bad_tile = 0;
unsigned bad_element = 0;
unsigned completed = 0;
unsigned processor;
int rc;
ft_tt_test_step("step 12 — full triad: reader on BRISC (NoC 0), writer on NCRISC "
"(NoC 1), both self-addressed to this core");
rc = write_cb_config(noc, x, y, &cb_mask);
ft_tt_test_check_fmt("CB config written", rc == FT_TT_OK, "mask=0x%llx",
(unsigned long long)cb_mask);
if (rc != FT_TT_OK)
return;
build_config(placements, 0x1fu, cb_mask, &config);
ft_tt_test_check_fmt("enables covers all five processors", config.enables == 0x1fu, "0x%02x",
config.enables);
ft_tt_test_check_fmt("brisc_noc_id matches the NOC the reader was built for",
config.brisc_noc_id == 0u, "%u", config.brisc_noc_id);
reader_arguments[0] = tile_count;
reader_arguments[1] = x;
reader_arguments[2] = y;
reader_arguments[3] = g_layout.source0_address;
reader_arguments[4] = x;
reader_arguments[5] = y;
reader_arguments[6] = g_layout.source1_address;
writer_arguments[0] = tile_count;
writer_arguments[1] = x;
writer_arguments[2] = y;
writer_arguments[3] = g_layout.destination_address;
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);
ft_tt_test_check_fmt("runtime args planted", rc == FT_TT_OK,
"reader src (%u,%u) 0x%05x / 0x%05x, writer dst (%u,%u) 0x%05x", x, y,
g_layout.source0_address, g_layout.source1_address, x, y,
g_layout.destination_address);
if (rc != FT_TT_OK)
return;
rc = stage_source_buffers(noc, x, y);
if (rc == FT_TT_OK)
rc = wipe_destination(noc, x, y);
ft_tt_test_check_fmt("source buffers staged, destination wiped", rc == FT_TT_OK, "%u tiles",
FULL_TRIAD_TILE_COUNT);
if (rc != FT_TT_OK)
return;
rc = launch_and_wait(noc, x, y, &config, RUN_TIMEOUT_MS);
ft_tt_test_check_fmt("full triad completed", rc == FT_TT_OK, "rc=%d (%s)", rc,
ft_tt_error_string(rc));
if (rc != FT_TT_OK)
return;
rc = check_destination(noc, x, y, &bad_tile, &bad_element);
ft_tt_test_check_fmt("every destination tile is byte-exact against the C reference",
rc == FT_TT_OK,
"%u tiles x %u elements; matched up to tile %u element %u",
FULL_TRIAD_TILE_COUNT, TILE_ELEMENTS, bad_tile, bad_element);
if (rc != FT_TT_OK)
return;
/* Acceptance: repeatable. Each repeat wipes the destination first, so a run that
* silently did nothing shows up as zeros rather than as the previous answer. */
ft_tt_test_step("step 12 — %u repeats, destination wiped and checked every time",
FULL_TRIAD_REPEAT_COUNT);
started = monotonic_milliseconds();
while (completed < FULL_TRIAD_REPEAT_COUNT) {
rc = wipe_destination(noc, x, y);
if (rc != FT_TT_OK)
break;
rc = launch_and_wait(noc, x, y, &config, RUN_TIMEOUT_MS);
if (rc != FT_TT_OK)
break;
rc = check_destination(noc, x, y, &bad_tile, &bad_element);
if (rc != FT_TT_OK)
break;
completed++;
}
total_ms = monotonic_milliseconds() - started;
ft_tt_test_check_fmt("every repeat was byte-exact", completed == FULL_TRIAD_REPEAT_COUNT,
"%u/%u in %llu ms (%.2f ms each), last rc=%d", completed,
FULL_TRIAD_REPEAT_COUNT, (unsigned long long)total_ms,
completed ? (double)total_ms / (double)completed : 0.0, rc);
}
/* ------------------------------------------------------------------------- */
int main(void)
{
ft_tt_hardware_session session;
ft_tt_firmware_set firmware;
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
uint16_t x = 0;
uint16_t y = 0;
int rc;
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 vector_add 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_core_load_program(&session.noc, x, y, &ft_tt_kernel_vector_add, placements);
ft_tt_test_check_fmt("all five vector_add images loaded", rc == FT_TT_OK, "%u images",
ft_tt_kernel_vector_add.image_count);
if (rc != FT_TT_OK)
goto done;
run_step_11_host_staged(&session.noc, x, y, placements);
if (ft_tt_test_failure_count() != 0)
goto done;
run_step_12_full_triad(&session.noc, x, y, placements);
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();
}