Foundry/tt/tests/test_phase2_reaim_cost.c

440 lines
18 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* test_phase2_reaim_cost.c — what the launch path's window-aim skip is worth.
*
* Plan Phase 2 follow-up: "measure what the launch path's re-aim skip is worth. It used
* to call CONFIGURE_TLB on every 4-byte access; now the shared ft_tt_noc path skips the
* aim when the window is already on that (x, y, base). Launch cost was 16 µs with the
* old behaviour — re-measure."
*
* The two arms differ by ONE line. `ft_tt_noc_path` remembers the (x, y, base) its
* window is aimed at and skips the ioctl when the next access matches; clearing that flag
* before every access reproduces the old behaviour EXACTLY, through the same code, with
* no second implementation to be wrong. Nothing else changes: same firmware, same kernel,
* same launch message, same order, same process.
*
* 1 per-access cost — N reads of the soft-reset register, one 4-byte access each.
* This isolates CONFIGURE_TLB from everything else and is what the launch delta
* has to be divided by to mean anything.
* 2 per-launch cost — N launches of the do-nothing kernel: write the launch message,
* send the go message, poll for DONE. Step 9's measurement, unchanged, so the
* forced arm is directly comparable to the 16 µs the plan recorded.
*
* **The arm that must be able to fail is the forced one.** If poking the flag did not
* actually force a re-aim, both arms would run identical code and report the same number
* — a green run that measures nothing. So the aim COUNTER
* (ft_tt_noc_path_aim_count()) is checked, not just the clock: the forced arm must aim
* once per access and the skipping arm must aim far less often than that. A test that
* only printed two timings would be exactly the "harness that measures nothing" this
* repo keeps catching.
*
* Usage: test_phase2_reaim_cost
* Firmware comes from FT_TT_FIRMWARE_DIR and the kernel is `ft_tt_kernel_noop` out of
* the committed ft_tt_kernels.h — deliberately the SAME pair step 9 launched a
* thousand times, because the number being re-measured is step 9's.
*
* Build:
* make -f Makefile.phase2_followups PHASE1B=1 build-phase2b/test_phase2_reaim_cost
*/
#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
/** Register reads per arm of measurement 1. Large enough that the clock's resolution is
* irrelevant, small enough that the slow arm still finishes in a couple of seconds. */
#define REGISTER_READ_COUNT 20000u
/** Launches per arm of measurement 2 — step 9's count, so the numbers are comparable. */
#define LAUNCH_COUNT 1000u
/** Launches per block when the two arms are INTERLEAVED. The card host also runs eight K80s and
* other agents, and the host is two 1.8 GHz cores: measuring one arm to completion and
* then the other put a several-microsecond drift straight into the difference, and once
* produced a NEGATIVE cost for CONFIGURE_TLB. Alternating in short blocks makes both
* arms see the same load. */
#define LAUNCH_BLOCK 25u
/** Same idea for the register reads. */
#define READ_BLOCK 500u
/** Discarded iterations before each timed run, so the first-touch cost of a cold window
* or a cold page does not land in the average. */
#define WARMUP_COUNT 200u
static ft_tt_phase2_l1_layout g_layout;
/**
* An accessor that wraps another one, counts accesses, and optionally invalidates the
* path's "already aimed" memo first.
*
* Reaching into ft_tt_noc_path is deliberate and is confined to this file: it is the
* only way to run the OLD behaviour through the CURRENT code. Re-aiming by bouncing
* between two endpoints would double the transaction count and measure something else.
*/
typedef struct reaim_probe {
const ft_tt_noc_access *inner;
ft_tt_noc_window_binding *binding;
bool force_reaim;
uint64_t accesses;
} reaim_probe;
static void probe_before_access(reaim_probe *probe)
{
probe->accesses++;
if (probe->force_reaim)
probe->binding->path.aimed = false;
}
static int probe_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src,
size_t bytes)
{
reaim_probe *probe = (reaim_probe *)user;
probe_before_access(probe);
return probe->inner->write(probe->inner->user, x, y, addr, src, bytes);
}
static int probe_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes)
{
reaim_probe *probe = (reaim_probe *)user;
probe_before_access(probe);
return probe->inner->read(probe->inner->user, x, y, addr, dst, bytes);
}
static void probe_init(reaim_probe *probe, ft_tt_hardware_session *session, bool force_reaim,
ft_tt_noc_access *out_access)
{
memset(probe, 0, sizeof(*probe));
probe->inner = &session->noc;
probe->binding = &session->runtime.binding;
probe->force_reaim = force_reaim;
out_access->write = probe_write;
out_access->read = probe_read;
out_access->user = probe;
}
static uint64_t monotonic_nanoseconds(void)
{
struct timespec now;
(void)clock_gettime(CLOCK_MONOTONIC, &now);
return (uint64_t)now.tv_sec * 1000000000uLL + (uint64_t)now.tv_nsec;
}
/** One arm's numbers. Kept together so the two arms are printed with the same fields. */
typedef struct arm_result {
uint64_t elapsed_ns;
uint64_t accesses;
uint64_t aims;
unsigned completed;
} arm_result;
static double nanoseconds_each(const arm_result *result, unsigned operations)
{
return operations ? (double)result->elapsed_ns / (double)operations : 0.0;
}
/* ------------------------------------------------------------------------- */
/* 1 — per-access cost */
/* ------------------------------------------------------------------------- */
/** Time one block of register reads through `probe`, accumulating into `result`. */
static int timed_read_block(reaim_probe *probe, const ft_tt_noc_access *access,
ft_tt_hardware_session *session, uint16_t x, uint16_t y,
unsigned count, arm_result *result)
{
uint64_t aims_before = ft_tt_noc_path_aim_count(&session->runtime.binding.path);
uint64_t accesses_before = probe->accesses;
uint64_t started = monotonic_nanoseconds();
unsigned iteration;
for (iteration = 0; iteration < count; iteration++) {
uint32_t value = 0;
int rc = ft_tt_launch_read_reset(access, x, y, &value);
if (rc != FT_TT_OK)
return rc;
result->completed++;
}
result->elapsed_ns += monotonic_nanoseconds() - started;
result->aims += ft_tt_noc_path_aim_count(&session->runtime.binding.path) - aims_before;
result->accesses += probe->accesses - accesses_before;
return FT_TT_OK;
}
/** Both arms, interleaved, for the same reason measure_launches() interleaves. */
static int measure_register_reads(ft_tt_hardware_session *session, uint16_t x, uint16_t y,
arm_result *out_skipping, arm_result *out_forced)
{
ft_tt_noc_access skipping_access;
ft_tt_noc_access forced_access;
reaim_probe skipping_probe;
reaim_probe forced_probe;
unsigned block;
unsigned iteration;
probe_init(&skipping_probe, session, false, &skipping_access);
probe_init(&forced_probe, session, true, &forced_access);
memset(out_skipping, 0, sizeof(*out_skipping));
memset(out_forced, 0, sizeof(*out_forced));
for (iteration = 0; iteration < WARMUP_COUNT; iteration++) {
uint32_t value = 0;
int rc = ft_tt_launch_read_reset(&skipping_access, x, y, &value);
if (rc != FT_TT_OK)
return rc;
}
for (block = 0; block * READ_BLOCK < REGISTER_READ_COUNT; block++) {
int rc = timed_read_block(&skipping_probe, &skipping_access, session, x, y, READ_BLOCK,
out_skipping);
if (rc == FT_TT_OK)
rc = timed_read_block(&forced_probe, &forced_access, session, x, y, READ_BLOCK,
out_forced);
if (rc != FT_TT_OK)
return rc;
}
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* 2 — per-launch cost */
/* ------------------------------------------------------------------------- */
/** One launch: stage the message, kick, poll for DONE. Step 9's loop body exactly. */
static int one_launch(const ft_tt_noc_access *access, uint16_t x, uint16_t y,
const ft_tt_launch_config *config)
{
int rc = ft_tt_launch_write_launch_message(access, x, y, config);
if (rc == FT_TT_OK)
rc = ft_tt_launch_send_go(access, x, y);
if (rc == FT_TT_OK)
rc = ft_tt_launch_wait_done(access, x, y, RUN_TIMEOUT_MS, NULL);
return rc;
}
/** Time one block of launches through `probe`, accumulating into `result`. */
static int timed_block(reaim_probe *probe, const ft_tt_noc_access *access,
ft_tt_hardware_session *session, uint16_t x, uint16_t y,
const ft_tt_launch_config *config, unsigned count, arm_result *result)
{
uint64_t aims_before = ft_tt_noc_path_aim_count(&session->runtime.binding.path);
uint64_t accesses_before = probe->accesses;
uint64_t started = monotonic_nanoseconds();
unsigned iteration;
for (iteration = 0; iteration < count; iteration++) {
int rc = one_launch(access, x, y, config);
if (rc != FT_TT_OK)
return rc;
result->completed++;
}
result->elapsed_ns += monotonic_nanoseconds() - started;
result->aims += ft_tt_noc_path_aim_count(&session->runtime.binding.path) - aims_before;
result->accesses += probe->accesses - accesses_before;
return FT_TT_OK;
}
/**
* Both arms, interleaved in blocks, so a host that gets busier halfway through the
* measurement slows BOTH of them rather than only the one that ran second.
*/
static int measure_launches(ft_tt_hardware_session *session, uint16_t x, uint16_t y,
const ft_tt_launch_config *config, arm_result *out_skipping,
arm_result *out_forced)
{
ft_tt_noc_access skipping_access;
ft_tt_noc_access forced_access;
reaim_probe skipping_probe;
reaim_probe forced_probe;
unsigned block;
unsigned iteration;
probe_init(&skipping_probe, session, false, &skipping_access);
probe_init(&forced_probe, session, true, &forced_access);
memset(out_skipping, 0, sizeof(*out_skipping));
memset(out_forced, 0, sizeof(*out_forced));
for (iteration = 0; iteration < WARMUP_COUNT / 10u; iteration++) {
int rc = one_launch(&skipping_access, x, y, config);
if (rc != FT_TT_OK)
return rc;
}
for (block = 0; block * LAUNCH_BLOCK < LAUNCH_COUNT; block++) {
int rc = timed_block(&skipping_probe, &skipping_access, session, x, y, config,
LAUNCH_BLOCK, out_skipping);
if (rc == FT_TT_OK)
rc = timed_block(&forced_probe, &forced_access, session, x, y, config, LAUNCH_BLOCK,
out_forced);
if (rc != FT_TT_OK)
return rc;
}
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
static void report_arm(const char *what, const arm_result *result, unsigned operations,
const char *unit)
{
printf(" %-22s %8.2f µs/%s %llu accesses, %llu window aims (%.2f per access)\n", what,
nanoseconds_each(result, operations) / 1000.0, unit,
(unsigned long long)result->accesses, (unsigned long long)result->aims,
result->accesses ? (double)result->aims / (double)result->accesses : 0.0);
}
int main(void)
{
ft_tt_hardware_session session;
ft_tt_firmware_set firmware;
ft_tt_image_placement firmware_placements[FT_TT_PROC_COUNT];
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
ft_tt_launch_config config;
arm_result skipping = {0, 0, 0, 0};
arm_result forced = {0, 0, 0, 0};
unsigned processor;
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 from %s and load the do-nothing kernel",
ft_tt_firmware_directory());
rc = ft_tt_core_boot_firmware(&session.noc, x, y, &firmware, firmware_placements);
if (rc == FT_TT_OK)
rc = ft_tt_phase2_l1_layout_derive(firmware_placements, FT_TT_PROC_COUNT, &g_layout);
ft_tt_test_check_fmt("firmware booted and the L1 map derived", rc == FT_TT_OK,
"base 0x%05x, rc=%d (%s)", g_layout.kernel_config_base, 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_noop, placements);
ft_tt_test_check_fmt("program loaded", rc == FT_TT_OK, "%u images",
ft_tt_kernel_noop.image_count);
if (rc != FT_TT_OK)
goto done;
memset(&config, 0, sizeof(config));
config.kernel_config_base = g_layout.kernel_config_base;
config.enables = ft_tt_kernel_program_enables(&ft_tt_kernel_noop);
config.brisc_noc_id = 0u;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
uint32_t zero = 0;
if (placements[processor].bytes_written == 0u)
continue;
config.kernel_text_address[processor] = placements[processor].entry_address;
config.kernel_text_bytes[processor] = placements[processor].text_bytes;
/* A VALID runtime-arg block for every enabled processor, even though noop reads
* none. The sentinel is not safer than a real one: a TRISC that finds whatever
* the previous program left at its block has stalled a core here before (dev
* notes §5), and this program runs after 20,000 unrelated device accesses. */
config.runtime_args_address[processor] =
ft_tt_phase2_runtime_args_address(&g_layout, processor);
rc = ft_tt_launch_plant_runtime_args(&session.noc, x, y,
config.runtime_args_address[processor], &zero, 1u);
if (rc != FT_TT_OK)
break;
}
ft_tt_test_check_fmt("a valid runtime-arg block planted for every enabled processor",
rc == FT_TT_OK, "rc=%d (%s)", rc, ft_tt_error_string(rc));
if (rc != FT_TT_OK)
goto done;
/* --- 2: per-launch ---------------------------------------------------- */
ft_tt_test_step("2 — %u launches of the do-nothing kernel per arm, interleaved in blocks "
"of %u", LAUNCH_COUNT, LAUNCH_BLOCK);
rc = measure_launches(&session, x, y, &config, &skipping, &forced);
ft_tt_test_check_fmt("both arms completed every launch",
rc == FT_TT_OK && skipping.completed == LAUNCH_COUNT &&
forced.completed == LAUNCH_COUNT,
"%u and %u of %u, interleaved %u at a time", skipping.completed,
forced.completed, LAUNCH_COUNT, LAUNCH_BLOCK);
if (rc != FT_TT_OK)
goto done;
report_arm("aim skipped (today)", &skipping, LAUNCH_COUNT, "launch");
report_arm("aim forced (before)", &forced, LAUNCH_COUNT, "launch");
ft_tt_test_check_fmt("the forced arm really re-aimed on every access",
forced.aims == forced.accesses, "%llu aims for %llu accesses",
(unsigned long long)forced.aims, (unsigned long long)forced.accesses);
ft_tt_test_check_fmt("skipping the aim made a launch cheaper",
skipping.elapsed_ns < forced.elapsed_ns, "%.2f µs vs %.2f µs per launch",
nanoseconds_each(&skipping, LAUNCH_COUNT) / 1000.0,
nanoseconds_each(&forced, LAUNCH_COUNT) / 1000.0);
printf(" the skip is worth %.2f µs per launch (%.2f accesses per launch, "
"%.2f of them aimed before)\n",
(nanoseconds_each(&forced, LAUNCH_COUNT) -
nanoseconds_each(&skipping, LAUNCH_COUNT)) / 1000.0,
(double)forced.accesses / (double)LAUNCH_COUNT,
(double)forced.aims / (double)LAUNCH_COUNT);
/* --- 1: per-access ---------------------------------------------------- */
ft_tt_test_step("1 — %u reads of the soft-reset register per arm, one 4-byte access "
"each, interleaved in blocks of %u", REGISTER_READ_COUNT, READ_BLOCK);
rc = measure_register_reads(&session, x, y, &skipping, &forced);
ft_tt_test_check_fmt("both arms completed", rc == FT_TT_OK,
"rc=%d (%s), interleaved %u reads at a time", rc,
ft_tt_error_string(rc), READ_BLOCK);
if (rc != FT_TT_OK)
goto done;
report_arm("aim skipped (today)", &skipping, REGISTER_READ_COUNT, "read");
report_arm("aim forced (before)", &forced, REGISTER_READ_COUNT, "read");
/* These two are the reason the timings mean anything. */
ft_tt_test_check_fmt("the forced arm really re-aimed on every access",
forced.aims == forced.accesses, "%llu aims for %llu accesses",
(unsigned long long)forced.aims, (unsigned long long)forced.accesses);
ft_tt_test_check_fmt("the skipping arm re-aimed far less often",
skipping.aims * 10u < skipping.accesses, "%llu aims for %llu accesses",
(unsigned long long)skipping.aims,
(unsigned long long)skipping.accesses);
printf(" one CONFIGURE_TLB costs %.2f µs (the difference of the two arms, "
"%.2f aims apart per access)\n",
(nanoseconds_each(&forced, REGISTER_READ_COUNT) -
nanoseconds_each(&skipping, REGISTER_READ_COUNT)) / 1000.0,
(double)(forced.aims - skipping.aims) / (double)REGISTER_READ_COUNT);
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();
}