1581 lines
73 KiB
C
1581 lines
73 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_phase5_resident.c
|
|
* @brief Phase 5 on the card: a RESIDENT kernel polling an L1 work queue, the dispatch
|
|
* A/B against the Phase 4 slow-dispatch arm, two independent workloads on disjoint core
|
|
* groups, a cross-group semaphore, and a DELIBERATE DEADLOCK the watchdog catches with
|
|
* the card still usable afterwards.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT MAKES THE A/B HONEST
|
|
* ---------------------------------------------------------------------------
|
|
* Both arms run `kernels/resident_queue.c`. The only difference is compile-time arg 0:
|
|
* 0 is RESIDENT (the loop), 1 is ONE-SHOT (the loop stopped after one descriptor). So the
|
|
* per-descriptor code is the same source, the same opcode and the same stores, and the
|
|
* measured difference is the DISPATCH — not two implementations of the work.
|
|
*
|
|
* arm A, slow dispatch: per descriptor, plant it, write the launch message, send GO,
|
|
* poll for RUN_MSG_DONE. This is the Phase 4 shape, and Phase 2
|
|
* measured it at 16 us over 1000 launches.
|
|
* arm B, queue dispatch: enqueue a batch, and poll ONCE at the end. The host never
|
|
* round-trips the card between descriptors.
|
|
*
|
|
* **The arms are interleaved.** Two 1.8 GHz cores also driving eight K80s drift enough
|
|
* over ten seconds to swamp a microsecond, and measuring one arm to completion and then
|
|
* the other once reported a NEGATIVE cost for CONFIGURE_TLB (dev notes §8).
|
|
*
|
|
* A third number is reported and is the one comparable to upstream's 2.08 us op-to-op
|
|
* golden: DEVICE-SIDE op-to-op, measured by filling the ring, bumping the head ONCE, and
|
|
* timing until the completion count arrives. No host work is inside that window at all.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT MAKES THE COMPLETION CHECKS HONEST
|
|
* ---------------------------------------------------------------------------
|
|
* Nothing here retires work on `ft_tt_launch_wait_done_all()`. That call reported eight
|
|
* cores complete with one never sent a go message, because `RUN_MSG_DONE` is 0 and is
|
|
* what a core's own boot leaves in the mailbox (dev notes §3). Completion is
|
|
* `completed_count` plus `completed_token`, both written by the resident kernel AFTER the
|
|
* descriptor's work, with the token a per-run nonce — and stage 1 runs the negative case,
|
|
* checking that an unlaunched core FAILS.
|
|
*
|
|
* Per-group data is checked PAIRWISE DISTINCT as well as against a reference, because the
|
|
* multicore seed once collided mod 16 and four of eight cores passed against their own
|
|
* equally-wrong reference (dev notes §3).
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* SAFETY
|
|
* ---------------------------------------------------------------------------
|
|
* Every coordinate comes from `ft_tt_coord` against live telemetry and is classified
|
|
* before it can enter a launch (inside `ft_tt_launch_core_set_add_tensix()`), because a
|
|
* wrong NoC endpoint hangs the chip and costs a physical power cycle (spec §7.3).
|
|
*
|
|
* NOTHING IN THIS FILE ASSERTS RESET ON A BLOCKED KERNEL. The deadlock stage recovers by
|
|
* writing `stop_request` and waiting for the kernel to say EXIT. `assert_reset` on a
|
|
* kernel blocked in a wait is what wedged Tensix 0 (dev notes §1), and in this phase every
|
|
* resident kernel is blocked in a wait by design. Cores are parked only after their
|
|
* kernels have reported EXIT.
|
|
*
|
|
* Usage:
|
|
* FT_TT_FIRMWARE_DIR=../programs/resident \
|
|
* FT_TT_TENSIX_INDICES=71,72,73,74 \
|
|
* ./build-phase5/test_phase5_resident ../programs/resident ../programs/oneshot
|
|
*
|
|
* FT_TT_TENSIX_INDICES exists so the SECOND, DISJOINT core set is the same binary rather
|
|
* than an edited source: a result believed on one set of cores does not generalise, and
|
|
* recompiling to move cores makes the two runs differ by more than the cores.
|
|
*
|
|
* @status Green on the card, 0 failures, on TWO disjoint core sets: 71-74 and 76-79,
|
|
* 2026-08-12. Four mutations recorded red (docs/tt_bringup_log.md 17.7), two of them
|
|
* rebuilt KERNELS driven through this same binary.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* clock_gettime and CLOCK_MONOTONIC are POSIX.1-2008; -std=c99 turns off glibc's
|
|
* default feature set, so ask for them by name or the Linux card host's gcc hides them. */
|
|
#ifndef _POSIX_C_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
|
|
#include "ft_tt_group.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_op_dispatch.h"
|
|
#include "ft_tt_op_l1_layout.h"
|
|
#include "ft_tt_program_directory.h"
|
|
#include "ft_tt_queue.h"
|
|
#include "ft_tt_test_report.h"
|
|
#include "ft_tt_watchdog.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/** Cores this test drives, and the two groups they split into. Both are DERIVED from the
|
|
* index list rather than fixed: the partitioner is handed a count and a group count. */
|
|
#define CORE_COUNT 4u
|
|
#define GROUP_COUNT 2u
|
|
|
|
#define QUEUE_CAPACITY 1024u
|
|
#define IDLE_SPIN_LIMIT 400000000u /* the host-died safety net, not flow control */
|
|
#define RUN_TIMEOUT_MS 5000u
|
|
|
|
/** Where a stage's verifiable output lands. Inside the CB arena, which no CB is using
|
|
* here, and comfortably clear of the queue region at the top of L1. */
|
|
#define SCRATCH_BASE FT_TT_OP_CB_ARENA_BASE
|
|
#define SEMAPHORE_BASE (FT_TT_OP_CB_ARENA_BASE + 0x1000u)
|
|
|
|
#define CORE_INDICES_ENVIRONMENT "FT_TT_TENSIX_INDICES"
|
|
#define AB_ITERATIONS_ENVIRONMENT "FT_TT_QUEUE_AB_ITERATIONS"
|
|
|
|
static uint32_t g_core_indices[CORE_COUNT] = { 71u, 72u, 73u, 74u };
|
|
static uint32_t g_ab_iterations = 256u;
|
|
|
|
static ft_tt_launch_core_placement g_placements[CORE_COUNT];
|
|
static ft_tt_queue g_queues[CORE_COUNT];
|
|
static ft_tt_core_group g_groups[GROUP_COUNT];
|
|
|
|
/** The two builds of kernels/resident_queue.c. */
|
|
static ft_tt_program_directory g_resident_program;
|
|
static ft_tt_program_directory g_oneshot_program;
|
|
static ft_tt_launch_config g_resident_config[CORE_COUNT];
|
|
static ft_tt_launch_config g_oneshot_config[CORE_COUNT];
|
|
|
|
/** A BRISC-only program: nothing but the data-movement processor is enabled, so nothing
|
|
* in this test can block in a circular-buffer wait — which is the one wait the host has
|
|
* no way to break. */
|
|
#define BRISC_ONLY_MASK (1u << FT_TT_PROC_DM0)
|
|
|
|
/** No circular buffers. The descriptor array still has to be written, because the launch
|
|
* message points at it and firmware reads it. */
|
|
static uint32_t g_cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * 4u];
|
|
|
|
/** The flat slot of a (group, slot) pair, which is the index into g_resident_config and
|
|
* g_core_indices. Derived from the group sizes rather than written out, so it stays right
|
|
* if the group count changes. */
|
|
static unsigned flat_index(unsigned group, unsigned slot)
|
|
{
|
|
unsigned flat = 0;
|
|
unsigned index;
|
|
|
|
for (index = 0; index < group; index++)
|
|
flat += ft_tt_launch_core_set_count(&g_groups[index].cores);
|
|
return flat + slot;
|
|
}
|
|
|
|
/** Group sizes, derived the same way ft_tt_group_partition() derives them: the base share
|
|
* plus one extra to the first `total % groups` of them, so nothing is left unassigned. */
|
|
static uint32_t derived_group_size(unsigned group, unsigned total, unsigned groups)
|
|
{
|
|
return total / groups + (group < total % groups ? 1u : 0u);
|
|
}
|
|
|
|
static uint64_t monotonic_microseconds(void)
|
|
{
|
|
struct timespec now;
|
|
|
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
|
|
return 0u;
|
|
return (uint64_t)now.tv_sec * 1000000u + (uint64_t)(now.tv_nsec / 1000);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Environment */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static bool load_environment(void)
|
|
{
|
|
const char *indices = getenv(CORE_INDICES_ENVIRONMENT);
|
|
const char *iterations = getenv(AB_ITERATIONS_ENVIRONMENT);
|
|
|
|
if (indices && indices[0] != '\0') {
|
|
unsigned count = 0;
|
|
const char *cursor = indices;
|
|
|
|
while (*cursor != '\0') {
|
|
char *end = NULL;
|
|
unsigned long value = strtoul(cursor, &end, 10);
|
|
|
|
if (end == cursor || count >= CORE_COUNT) {
|
|
ft_tt_test_check_fmt(CORE_INDICES_ENVIRONMENT " parses", false, "at \"%s\"",
|
|
cursor);
|
|
return false;
|
|
}
|
|
g_core_indices[count++] = (uint32_t)value;
|
|
cursor = (*end == ',') ? end + 1 : end;
|
|
}
|
|
if (count != CORE_COUNT) {
|
|
ft_tt_test_check_fmt(CORE_INDICES_ENVIRONMENT " names every core", false,
|
|
"got %u, need %u", count, CORE_COUNT);
|
|
return false;
|
|
}
|
|
}
|
|
if (iterations && iterations[0] != '\0')
|
|
g_ab_iterations = (uint32_t)strtoul(iterations, NULL, 10);
|
|
if (g_ab_iterations == 0u || g_ab_iterations > QUEUE_CAPACITY)
|
|
g_ab_iterations = 256u;
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Setup */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Partition the four chosen indices into two groups.
|
|
*
|
|
* The indices are a LIST, not a range, so the partitioner is applied to a contiguous run
|
|
* and then each placement is replaced by the one the list actually names. Doing it this
|
|
* way keeps ft_tt_group_partition() the only thing that decides SIZES — the derived part
|
|
* — while the test still gets to pick discontiguous cores, which is what stops a bug that
|
|
* walks a contiguous run from passing.
|
|
*/
|
|
static bool build_groups(const ft_tt_hardware_session *session)
|
|
{
|
|
unsigned group;
|
|
unsigned assigned = 0;
|
|
const char *reason = NULL;
|
|
|
|
ft_tt_test_step("groups: partition %u cores into %u disjoint groups, sizes derived",
|
|
CORE_COUNT, GROUP_COUNT);
|
|
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
uint32_t size = derived_group_size(group, CORE_COUNT, GROUP_COUNT);
|
|
unsigned slot;
|
|
int rc;
|
|
|
|
memset(&g_groups[group], 0, sizeof(g_groups[group]));
|
|
g_groups[group].id = group;
|
|
g_groups[group].first_tensix_index = g_core_indices[assigned];
|
|
g_groups[group].tensix_index_count = size;
|
|
g_groups[group].queues = g_queues + assigned;
|
|
g_groups[group].dram_bank_first = group % session->grid.dram_bank_count;
|
|
g_groups[group].dram_bank_stride = GROUP_COUNT;
|
|
g_groups[group].dram_bank_count =
|
|
(session->grid.dram_bank_count - g_groups[group].dram_bank_first + GROUP_COUNT - 1u) /
|
|
GROUP_COUNT;
|
|
|
|
rc = ft_tt_launch_core_set_init(&g_groups[group].cores, g_placements + assigned, size);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (slot = 0; slot < size; slot++) {
|
|
const ft_tt_launch_core_placement *placement;
|
|
|
|
rc = ft_tt_launch_core_set_add_tensix(&g_groups[group].cores, &session->grid,
|
|
g_core_indices[assigned + slot], NULL);
|
|
placement = ft_tt_launch_core_set_at(&g_groups[group].cores, slot);
|
|
ft_tt_test_check_fmt("coordinate derives and classifies as a Tensix",
|
|
rc == FT_TT_OK && placement != NULL,
|
|
"group %u slot %u: index %u -> translated (%u, %u), rc=%d", group,
|
|
slot, g_core_indices[assigned + slot],
|
|
placement ? placement->x : 0u, placement ? placement->y : 0u, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
assigned += size;
|
|
}
|
|
|
|
{
|
|
bool disjoint = ft_tt_group_check_disjoint(g_groups, GROUP_COUNT, &reason) == FT_TT_OK;
|
|
|
|
ft_tt_test_check("the two groups share no core", disjoint, reason);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/** Boot Tenstorrent's firmware on every core in the group, and load both kernel builds. */
|
|
static bool boot_and_load(const ft_tt_hardware_session *session,
|
|
const ft_tt_firmware_set *firmware)
|
|
{
|
|
unsigned group;
|
|
unsigned flat = 0;
|
|
|
|
ft_tt_test_step("boot firmware on all %u cores and load both kernel builds", CORE_COUNT);
|
|
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
unsigned count = ft_tt_launch_core_set_count(&g_groups[group].cores);
|
|
unsigned slot;
|
|
|
|
for (slot = 0; slot < count; slot++, flat++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[group].cores, slot);
|
|
int rc = ft_tt_core_boot_firmware(&session->noc, placement->x, placement->y, firmware,
|
|
NULL);
|
|
|
|
ft_tt_test_check_fmt("firmware booted", rc == FT_TT_OK, "(%u, %u) rc=%d",
|
|
placement->x, placement->y, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
rc = ft_tt_op_program_load(&session->noc, placement->x, placement->y,
|
|
&g_resident_program.program, NULL, &g_resident_config[flat]);
|
|
if (rc == FT_TT_OK) {
|
|
g_resident_config[flat].local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_resident_config[flat].local_cb_mask = 0u;
|
|
g_resident_config[flat].brisc_noc_id = 0u;
|
|
}
|
|
ft_tt_test_check_fmt("resident kernel image loaded", rc == FT_TT_OK, "(%u, %u) rc=%d",
|
|
placement->x, placement->y, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* The one-shot build is loaded into the SAME slab, so only one of the two can
|
|
* be resident on a core at a time. Staging the other one is a reload, which
|
|
* is exactly what the A/B's arm A is paying for. */
|
|
g_oneshot_config[flat] = g_resident_config[flat];
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Put the one-shot image into a core's BRISC slab, replacing whatever is there.
|
|
*
|
|
* Straight to ft_tt_launch_load_image(): there is no separate "relocated kernel" kind,
|
|
* because the ELF's own text address says which of the two legal regions it was linked
|
|
* into and the ELF is the only party that cannot be wrong about that. */
|
|
static int load_oneshot_image(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
|
|
{
|
|
return ft_tt_launch_load_image(noc, x, y, FT_TT_PROC_DM0, FT_TT_IMAGE_KERNEL,
|
|
g_oneshot_program.program.images[0].elf_data,
|
|
g_oneshot_program.program.images[0].elf_size, NULL);
|
|
}
|
|
|
|
static int load_resident_image(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
|
|
{
|
|
return ft_tt_launch_load_image(noc, x, y, FT_TT_PROC_DM0, FT_TT_IMAGE_KERNEL,
|
|
g_resident_program.program.images[0].elf_data,
|
|
g_resident_program.program.images[0].elf_size, NULL);
|
|
}
|
|
|
|
/** Stage and KICK a resident kernel, without waiting for it. */
|
|
/** The binding every core in this test uses: bounded residency, its own BRISC status slot,
|
|
* and no group block (stage 3 checks groups through per-core completion, which is what
|
|
* this BRISC-only kernel publishes; group-level completion is tested against the triad in
|
|
* test_phase6_triad.c, where a group has more than one processor to disagree about). */
|
|
static void fill_binding(const ft_tt_queue *queue, ft_tt_queue_kernel_binding *binding)
|
|
{
|
|
memset(binding, 0, sizeof(*binding));
|
|
binding->idle_spin_limit = IDLE_SPIN_LIMIT;
|
|
binding->cb_wait_spin_limit = IDLE_SPIN_LIMIT;
|
|
binding->status_address =
|
|
FT_TT_QUEUE_STATUS_ADDRESS(queue->control_address, FT_TT_PROC_DM0);
|
|
}
|
|
|
|
static int start_resident(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
|
|
const ft_tt_launch_config *config, const ft_tt_queue *queue)
|
|
{
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
ft_tt_queue_kernel_binding binding;
|
|
ft_tt_op_args block;
|
|
int rc;
|
|
|
|
fill_binding(queue, &binding);
|
|
rc = ft_tt_queue_kernel_args(queue, &binding, args);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
block.processor = FT_TT_PROC_DM0;
|
|
block.values = args;
|
|
block.count = FT_TT_QUEUE_KERNEL_ARG_COUNT;
|
|
|
|
rc = ft_tt_op_stage(noc, x, y, config, g_cb_buffer, sizeof(g_cb_buffer), &block, 1u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_send_go(noc, x, y);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 1 — the resident kernel runs, and completion is produced by the work */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Poison a scratch word so a "result" that is really untouched memory is visible. */
|
|
static int poison_scratch(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t address,
|
|
uint32_t count)
|
|
{
|
|
uint32_t pattern[8];
|
|
uint32_t index;
|
|
|
|
for (index = 0; index < 8u; index++)
|
|
pattern[index] = 0xDEAD0000u + index;
|
|
for (index = 0; index < count; index += 8u) {
|
|
uint32_t words = (count - index < 8u) ? count - index : 8u;
|
|
int rc = noc->write(noc->user, x, y, address + index * 4u, pattern, words * 4u);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static uint32_t stage1_expected_value(uint32_t core_seed, uint32_t index)
|
|
{
|
|
/* The per-core seed is an OFFSET, never inside a modulus: `(element + index * 7) % 16`
|
|
* made any two indices congruent mod 16 byte-identical and four of eight cores passed
|
|
* against their own wrong reference (dev notes §3). */
|
|
return 0x51000000u + core_seed * 0x10000u + index;
|
|
}
|
|
|
|
static bool stage_1_resident_runs(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[0].cores, 0u);
|
|
ft_tt_queue *queue = &g_groups[0].queues[0];
|
|
ft_tt_queue_descriptor descriptors[8];
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
uint32_t readback[8];
|
|
const uint32_t items = 8u;
|
|
const char *reason = NULL;
|
|
uint32_t index;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 1 — a resident kernel executes descriptors, and says so itself");
|
|
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), queue);
|
|
ft_tt_test_check_fmt("queue control block written", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
(void)poison_scratch(&session->noc, placement->x, placement->y, SCRATCH_BASE, items);
|
|
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
for (index = 0; index < items; index++) {
|
|
descriptors[index].opcode = FT_TT_QUEUE_OP_WRITE_WORD;
|
|
descriptors[index].a0 = SCRATCH_BASE + index * 4u;
|
|
descriptors[index].a1 = stage1_expected_value(0u, index);
|
|
}
|
|
rc = ft_tt_queue_enqueue(&session->noc, queue, descriptors, items);
|
|
ft_tt_test_check_fmt("eight descriptors enqueued", rc == FT_TT_OK, "rc=%d", rc);
|
|
|
|
/* THE NEGATIVE CASE, before the kernel is started. This is exactly the situation
|
|
* ft_tt_launch_wait_done_all() reported as complete on 2026-08-12. */
|
|
(void)ft_tt_queue_read_control(&session->noc, queue, fields);
|
|
{
|
|
/* The condition is evaluated into a local FIRST. C leaves the order of a call's
|
|
* arguments unspecified, and gcc evaluates the format arguments before the
|
|
* condition — which prints the PREVIOUS value of `reason` next to a check that
|
|
* passed, and reads a stale `readback`. The check was right and the report was a
|
|
* lie, which is the one combination this project cannot afford. */
|
|
bool refused =
|
|
ft_tt_queue_check_completion(queue, fields, items, &reason) == FT_TT_ERROR;
|
|
|
|
/* The REASON is asserted, not just the refusal. Without this the case is passed by
|
|
* the count check alone (0 != 8) and the state poison is never exercised — a
|
|
* mutation that stopped writing the poison produced ZERO failures here until the
|
|
* assertion was tightened to name the reason. A check that would pass for a
|
|
* different reason than the one it is testing is a vacuous check. */
|
|
ft_tt_test_check_fmt("with nothing launched, the completion check REFUSES", refused, "%s",
|
|
reason ? reason : "(it passed, which is the bug)");
|
|
ft_tt_test_check_fmt("and it refuses BECAUSE the kernel never ran, not merely on the count",
|
|
reason != NULL && strstr(reason, "never ran") != NULL, "%s",
|
|
reason ? reason : "(no reason given)");
|
|
}
|
|
{
|
|
bool still_poison =
|
|
session->noc.read(session->noc.user, placement->x, placement->y, SCRATCH_BASE,
|
|
readback, sizeof(uint32_t)) == FT_TT_OK &&
|
|
readback[0] == 0xDEAD0000u;
|
|
|
|
ft_tt_test_check_fmt("and the destination is still the poison", still_poison, "0x%08x",
|
|
readback[0]);
|
|
}
|
|
|
|
rc = start_resident(&session->noc, placement->x, placement->y, &g_resident_config[0], queue);
|
|
ft_tt_test_check_fmt("the resident kernel is staged and kicked", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
{
|
|
uint32_t completed = 0u;
|
|
|
|
rc = ft_tt_queue_wait_completed(&session->noc, queue, items, RUN_TIMEOUT_MS, &completed);
|
|
ft_tt_test_check_fmt("all eight descriptors completed", rc == FT_TT_OK, "completed=%u/%u",
|
|
completed, items);
|
|
}
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, queue, fields);
|
|
{
|
|
bool passed = ft_tt_queue_check_completion(queue, fields, items, &reason) == FT_TT_OK;
|
|
|
|
ft_tt_test_check_fmt("the completion check now passes", passed, "%s",
|
|
reason ? reason : "ok");
|
|
}
|
|
ft_tt_test_check_fmt("and the state is no longer the host poison",
|
|
fields[FT_TT_QUEUE_FIELD_STATE] !=
|
|
(uint32_t)FT_TT_QUEUE_STATE_HOST_POISON,
|
|
"state=%s", ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]));
|
|
ft_tt_test_check_fmt("the token is THIS run's eighth",
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ==
|
|
ft_tt_queue_token_at(queue, items - 1u),
|
|
"0x%08x vs 0x%08x", fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN],
|
|
ft_tt_queue_token_at(queue, items - 1u));
|
|
|
|
/* And the WORK, independently of the completion counters. Two checks that can fail
|
|
* separately is the point: a protocol that says "done" and a destination that says
|
|
* nothing happened is the failure this phase has to be able to see. */
|
|
rc = session->noc.read(session->noc.user, placement->x, placement->y, SCRATCH_BASE, readback,
|
|
sizeof(readback));
|
|
ft_tt_test_check_fmt("the destination reads back", rc == FT_TT_OK, "rc=%d", rc);
|
|
for (index = 0; index < items; index++) {
|
|
ft_tt_test_check_fmt("word matches, and is not the poison",
|
|
readback[index] == stage1_expected_value(0u, index),
|
|
"[%u] 0x%08x want 0x%08x", index, readback[index],
|
|
stage1_expected_value(0u, index));
|
|
}
|
|
|
|
/* The kernel is STILL RUNNING — that is what resident means. Prove it by enqueueing
|
|
* more work without launching anything. */
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
for (index = 0; index < 4u; index++) {
|
|
descriptors[index].opcode = FT_TT_QUEUE_OP_WRITE_WORD;
|
|
descriptors[index].a0 = SCRATCH_BASE + index * 4u;
|
|
descriptors[index].a1 = 0xC0FFEE00u + index;
|
|
}
|
|
rc = ft_tt_queue_enqueue(&session->noc, queue, descriptors, 4u);
|
|
ft_tt_test_check_fmt("four more enqueued with NO launch of any kind", rc == FT_TT_OK, "rc=%d",
|
|
rc);
|
|
{
|
|
uint32_t completed = 0u;
|
|
|
|
rc = ft_tt_queue_wait_completed(&session->noc, queue, items + 4u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("and the still-resident kernel executed them", rc == FT_TT_OK,
|
|
"completed=%u/%u", completed, items + 4u);
|
|
}
|
|
(void)session->noc.read(session->noc.user, placement->x, placement->y, SCRATCH_BASE, readback,
|
|
4u * sizeof(uint32_t));
|
|
ft_tt_test_check_fmt("the second batch's result is present too", readback[3] == 0xC0FFEE03u,
|
|
"0x%08x", readback[3]);
|
|
|
|
/* And it retires by ASKING. */
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
|
|
solo.cores.count = 1u;
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("the kernel stops when asked, with no reset anywhere",
|
|
rc == FT_TT_OK && unstopped == 0u, "rc=%d unstopped=%u", rc,
|
|
unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 2 — the dispatch A/B */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
|
|
/** The full Phase 4 op-dispatch shape for ONE descriptor: plant it, write the CB
|
|
* descriptor array, write every enabled processor's runtime-arg block, write the launch
|
|
* message, GO, poll for RUN_MSG_DONE. This is exactly what ft_tt_op_launch() does. */
|
|
static int slow_dispatch_full(const ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
const ft_tt_launch_config *config, ft_tt_queue *queue)
|
|
{
|
|
ft_tt_queue_descriptor descriptor;
|
|
int rc;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_NOP;
|
|
rc = ft_tt_queue_enqueue(&session->noc, queue, &descriptor, 1u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = start_resident(&session->noc, x, y, config, queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_wait_done(&session->noc, x, y, RUN_TIMEOUT_MS, NULL);
|
|
}
|
|
|
|
/** The LEANEST slow dispatch there is: the launch message and the descriptors are already
|
|
* in L1, so a descriptor costs one head bump, one GO and the completion poll. This is the
|
|
* floor of the go/done model, and it is the number Phase 2's 16 us over 1000 launches is
|
|
* comparable to — quoting only the full shape would flatter this phase. */
|
|
static int slow_dispatch_go_only(const ft_tt_hardware_session *session, uint16_t x, uint16_t y,
|
|
ft_tt_queue *queue)
|
|
{
|
|
int rc = ft_tt_queue_publish(&session->noc, queue);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_launch_send_go(&session->noc, x, y);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_wait_done(&session->noc, x, y, RUN_TIMEOUT_MS, NULL);
|
|
}
|
|
|
|
/** Fill a batch with NOPs. NOP is the right op for a dispatch measurement: its wall time
|
|
* IS the protocol's, with no work to hide behind. */
|
|
static void fill_nop_batch(ft_tt_queue_descriptor *batch, uint32_t count)
|
|
{
|
|
uint32_t index;
|
|
|
|
memset(batch, 0, (size_t)count * sizeof(batch[0]));
|
|
for (index = 0; index < count; index++)
|
|
batch[index].opcode = FT_TT_QUEUE_OP_NOP;
|
|
}
|
|
|
|
static ft_tt_queue_descriptor g_batch[QUEUE_CAPACITY];
|
|
|
|
/** Arm B at one batch size: enqueue and poll once. Returns microseconds per descriptor. */
|
|
static double measure_queue_dispatch(const ft_tt_hardware_session *session, uint16_t x,
|
|
uint16_t y, const ft_tt_launch_config *config,
|
|
uint32_t batch, uint32_t batches, int *out_rc)
|
|
{
|
|
ft_tt_queue queue;
|
|
uint64_t elapsed = 0u;
|
|
uint32_t round;
|
|
int rc;
|
|
|
|
rc = load_resident_image(&session->noc, x, y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, x, y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, x, y, config, &queue);
|
|
if (rc != FT_TT_OK) {
|
|
*out_rc = rc;
|
|
return 0.0;
|
|
}
|
|
|
|
for (round = 0; round < batches; round++) {
|
|
uint64_t started;
|
|
|
|
fill_nop_batch(g_batch, batch);
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_queue_enqueue(&session->noc, &queue, g_batch, batch);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, (round + 1u) * batch,
|
|
RUN_TIMEOUT_MS, NULL);
|
|
elapsed += monotonic_microseconds() - started;
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &queue;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
*out_rc = rc;
|
|
return (double)elapsed / (double)(batch * batches);
|
|
}
|
|
|
|
static bool stage_2_dispatch_ab(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[0].cores, 0u);
|
|
ft_tt_queue queue;
|
|
uint64_t full_total = 0u;
|
|
uint64_t go_only_total = 0u;
|
|
uint64_t queue_total = 0u;
|
|
uint64_t device_total = 0u;
|
|
const uint32_t rounds = 8u;
|
|
const uint32_t per_round = (g_ab_iterations / rounds) ? (g_ab_iterations / rounds) : 1u;
|
|
uint32_t round;
|
|
int rc = FT_TT_OK;
|
|
|
|
ft_tt_test_step("stage 2 - dispatch A/B/C/D, interleaved, %u descriptors per arm",
|
|
rounds * per_round);
|
|
|
|
for (round = 0; round < rounds; round++) {
|
|
uint64_t started;
|
|
uint32_t index;
|
|
|
|
/* ---- arm A1: the full Phase 4 op dispatch, one host round trip per op ---- */
|
|
rc = load_oneshot_image(&session->noc, placement->x, placement->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &queue);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
started = monotonic_microseconds();
|
|
for (index = 0; index < per_round; index++) {
|
|
rc = slow_dispatch_full(session, placement->x, placement->y, &g_oneshot_config[0],
|
|
&queue);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
full_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm A1 completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ---- arm A2: go/done only. Launch message and descriptors pre-written ---- */
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK) {
|
|
fill_nop_batch(g_batch, per_round);
|
|
rc = ft_tt_queue_stage(&session->noc, &queue, g_batch, per_round);
|
|
}
|
|
if (rc == FT_TT_OK) {
|
|
/* Stage the launch ONCE; from here a descriptor is a head bump and a GO. */
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
ft_tt_queue_kernel_binding binding;
|
|
ft_tt_op_args block;
|
|
|
|
fill_binding(&queue, &binding);
|
|
(void)ft_tt_queue_kernel_args(&queue, &binding, args);
|
|
block.processor = FT_TT_PROC_DM0;
|
|
block.values = args;
|
|
block.count = FT_TT_QUEUE_KERNEL_ARG_COUNT;
|
|
rc = ft_tt_op_stage(&session->noc, placement->x, placement->y, &g_oneshot_config[0],
|
|
g_cb_buffer, sizeof(g_cb_buffer), &block, 1u);
|
|
}
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
started = monotonic_microseconds();
|
|
for (index = 0; index < per_round; index++) {
|
|
/* One descriptor becomes visible per iteration: publish covers everything
|
|
* staged, so the head is bumped to the full batch on the first pass and the
|
|
* one-shot kernel takes exactly one item per GO. */
|
|
rc = slow_dispatch_go_only(session, placement->x, placement->y, &queue);
|
|
if (rc != FT_TT_OK)
|
|
break;
|
|
}
|
|
go_only_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm A2 completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ---- arm B: queue dispatch, one enqueue and one completion poll ---- */
|
|
rc = load_resident_image(&session->noc, placement->x, placement->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, placement->x, placement->y, &g_resident_config[0],
|
|
&queue);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
fill_nop_batch(g_batch, per_round);
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_queue_enqueue(&session->noc, &queue, g_batch, per_round);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, per_round, RUN_TIMEOUT_MS, NULL);
|
|
queue_total += monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("arm B completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ---- arm C: DEVICE-SIDE op-to-op. The descriptors are STAGED first, so the only
|
|
* host action inside the window is one 4-byte head write; everything after that is
|
|
* the resident kernel executing. This is the number comparable to upstream's
|
|
* 2.08 us per-core op-to-op golden. ---- */
|
|
fill_nop_batch(g_batch, per_round);
|
|
rc = ft_tt_queue_stage(&session->noc, &queue, g_batch, per_round);
|
|
if (rc == FT_TT_OK) {
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_queue_publish(&session->noc, &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, 2u * per_round,
|
|
RUN_TIMEOUT_MS, NULL);
|
|
device_total += monotonic_microseconds() - started;
|
|
}
|
|
ft_tt_test_check_fmt("arm C completed its round", rc == FT_TT_OK, "round %u rc=%d", round,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &queue;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
}
|
|
|
|
{
|
|
double total = (double)(rounds * per_round);
|
|
double full_us = (double)full_total / total;
|
|
double go_only_us = (double)go_only_total / total;
|
|
double queue_us = (double)queue_total / total;
|
|
double device_us = (double)device_total / total;
|
|
|
|
printf("\n DISPATCH COST PER DESCRIPTOR (NOP), %u per arm, arms interleaved\n",
|
|
rounds * per_round);
|
|
printf(" A1 Phase 4 op dispatch: CB config + args + launch msg + GO + poll : %8.3f us\n",
|
|
full_us);
|
|
printf(" A2 go/done only: launch msg pre-written, head bump + GO + poll : %8.3f us\n",
|
|
go_only_us);
|
|
printf(" B queue dispatch: batch of %-4u enqueued, ONE completion poll : %8.3f us\n",
|
|
per_round, queue_us);
|
|
printf(" C device-side: descriptors pre-staged, one head bump, no polling : %8.3f us\n",
|
|
device_us);
|
|
printf(" --- reference points ---\n");
|
|
printf(" Phase 2 slow-dispatch baseline (1000 launches, noop compute) : 16.000 us\n");
|
|
printf(" upstream bh_p150 op-to-op golden, per core : 2.080 us\n");
|
|
printf(" speedup A1 -> B %6.2fx A2 -> B %6.2fx\n", queue_us > 0.0 ? full_us / queue_us : 0.0,
|
|
queue_us > 0.0 ? go_only_us / queue_us : 0.0);
|
|
|
|
ft_tt_test_check_fmt("queue dispatch beats the full Phase 4 op dispatch",
|
|
queue_us < full_us, "%.3f us vs %.3f us", queue_us, full_us);
|
|
ft_tt_test_check_fmt("queue dispatch beats the leanest go/done dispatch",
|
|
queue_us < go_only_us, "%.3f us vs %.3f us", queue_us, go_only_us);
|
|
ft_tt_test_check_fmt("and it beats upstream's 2.08 us per-core op-to-op golden",
|
|
queue_us < 2.08, "%.3f us", queue_us);
|
|
ft_tt_test_check_fmt("the device-side op-to-op number is smaller still",
|
|
device_us < queue_us, "%.3f us vs %.3f us", device_us, queue_us);
|
|
}
|
|
|
|
/* The batch-size sweep. Measured separately rather than interleaved, and labelled as
|
|
* such: what it shows is the SHAPE of the model's cost — a fixed per-batch cost of two
|
|
* device accesses plus one completion poll, divided by the batch, over a per-descriptor
|
|
* device cost that does not depend on the batch at all. */
|
|
{
|
|
static const uint32_t sweep[] = { 1u, 4u, 16u, 64u, 256u };
|
|
unsigned index;
|
|
|
|
printf("\n ARM B BY BATCH SIZE (measured separately, not interleaved)\n");
|
|
for (index = 0; index < sizeof(sweep) / sizeof(sweep[0]); index++) {
|
|
uint32_t batch = sweep[index];
|
|
uint32_t batches = (256u / batch) ? (256u / batch) : 1u;
|
|
int sweep_rc = FT_TT_OK;
|
|
double us = measure_queue_dispatch(session, placement->x, placement->y,
|
|
&g_resident_config[0], batch, batches, &sweep_rc);
|
|
|
|
printf(" batch %4u x %-4u : %8.3f us per descriptor%s\n", batch, batches, us,
|
|
sweep_rc == FT_TT_OK ? "" : " (FAILED)");
|
|
ft_tt_test_check_fmt("the sweep point ran", sweep_rc == FT_TT_OK, "batch %u rc=%d",
|
|
batch, sweep_rc);
|
|
}
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static bool stage_3_two_groups(const ft_tt_hardware_session *session)
|
|
{
|
|
static ft_tt_queue_descriptor batch[64];
|
|
const uint32_t items = 16u;
|
|
uint32_t observed[CORE_COUNT][16];
|
|
uint32_t nonce = ft_tt_queue_make_nonce();
|
|
unsigned group;
|
|
unsigned flat = 0;
|
|
bool all_started = true;
|
|
|
|
ft_tt_test_step("stage 3 — two workloads on disjoint groups, neither stalling the other");
|
|
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
unsigned count = ft_tt_launch_core_set_count(&g_groups[group].cores);
|
|
unsigned slot;
|
|
int rc = ft_tt_group_queues_init(&session->noc, &g_groups[group], FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, nonce + group);
|
|
|
|
ft_tt_test_check_fmt("group queues initialised", rc == FT_TT_OK, "group %u rc=%d", group,
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (slot = 0; slot < count; slot++, flat++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[group].cores, slot);
|
|
uint32_t index;
|
|
|
|
rc = load_resident_image(&session->noc, placement->x, placement->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_scratch(&session->noc, placement->x, placement->y, SCRATCH_BASE, items);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, placement->x, placement->y,
|
|
&g_resident_config[flat], &g_groups[group].queues[slot]);
|
|
if (rc != FT_TT_OK) {
|
|
all_started = false;
|
|
break;
|
|
}
|
|
|
|
memset(batch, 0, sizeof(batch));
|
|
for (index = 0; index < items; index++) {
|
|
batch[index].opcode = FT_TT_QUEUE_OP_WRITE_WORD;
|
|
batch[index].a0 = SCRATCH_BASE + index * 4u;
|
|
/* The seed is the CORE INDEX as an offset, so a core reading another
|
|
* core's L1 produces a wrong answer rather than a coincidentally right
|
|
* one, and no two cores can collide however the indices are chosen. */
|
|
batch[index].a1 = stage1_expected_value(g_core_indices[flat], index);
|
|
}
|
|
rc = ft_tt_queue_enqueue(&session->noc, &g_groups[group].queues[slot], batch, items);
|
|
if (rc != FT_TT_OK)
|
|
all_started = false;
|
|
}
|
|
}
|
|
ft_tt_test_check("every core in both groups is resident and fed", all_started, NULL);
|
|
if (!all_started)
|
|
return false;
|
|
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
ft_tt_watchdog_config config;
|
|
ft_tt_watchdog_report report;
|
|
int rc;
|
|
|
|
config.stall_timeout_ms = 1000u;
|
|
config.overall_timeout_ms = RUN_TIMEOUT_MS;
|
|
rc = ft_tt_watchdog_wait(&session->noc, &g_groups[group], items, &config, &report);
|
|
ft_tt_test_check_fmt("group finished its own work", rc == FT_TT_OK,
|
|
"group %u: %u/%u in %u ms", group, report.completed_total,
|
|
report.completed_target, report.elapsed_ms);
|
|
}
|
|
|
|
flat = 0;
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
unsigned count = ft_tt_launch_core_set_count(&g_groups[group].cores);
|
|
unsigned slot;
|
|
|
|
for (slot = 0; slot < count; slot++, flat++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[group].cores, slot);
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
uint32_t index;
|
|
bool matched = true;
|
|
|
|
bool evidenced;
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_groups[group].queues[slot], fields);
|
|
evidenced = ft_tt_queue_check_completion(&g_groups[group].queues[slot], fields, items,
|
|
&reason) == FT_TT_OK;
|
|
ft_tt_test_check_fmt("completion is evidenced by the kernel", evidenced,
|
|
"group %u slot %u: %s", group, slot, reason ? reason : "ok");
|
|
|
|
(void)session->noc.read(session->noc.user, placement->x, placement->y, SCRATCH_BASE,
|
|
observed[flat], items * sizeof(uint32_t));
|
|
for (index = 0; index < items; index++) {
|
|
if (observed[flat][index] != stage1_expected_value(g_core_indices[flat], index))
|
|
matched = false;
|
|
}
|
|
ft_tt_test_check_fmt("and every word matches this core's own reference", matched,
|
|
"core index %u, first word 0x%08x", g_core_indices[flat],
|
|
observed[flat][0]);
|
|
}
|
|
}
|
|
|
|
/* PAIRWISE DISTINCT, checked against each OTHER and not only against the model. This
|
|
* is the check that caught the mod-16 seed collision, and it does not depend on the
|
|
* reference being right (dev notes §3). */
|
|
{
|
|
unsigned a;
|
|
bool all_distinct = true;
|
|
|
|
for (a = 0; a < CORE_COUNT; a++) {
|
|
unsigned b;
|
|
|
|
for (b = a + 1u; b < CORE_COUNT; b++) {
|
|
if (memcmp(observed[a], observed[b], items * sizeof(uint32_t)) == 0)
|
|
all_distinct = false;
|
|
}
|
|
}
|
|
ft_tt_test_check("the four cores' results are pairwise distinct", all_distinct, NULL);
|
|
}
|
|
|
|
for (group = 0; group < GROUP_COUNT; group++) {
|
|
uint32_t unstopped = 0u;
|
|
int rc = ft_tt_watchdog_shutdown(&session->noc, &g_groups[group], RUN_TIMEOUT_MS,
|
|
&unstopped);
|
|
|
|
ft_tt_test_check_fmt("group retired cleanly", rc == FT_TT_OK && unstopped == 0u,
|
|
"group %u rc=%d unstopped=%u", group, rc, unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 4 — a cross-group semaphore */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static bool stage_4_cross_group_semaphore(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *waiter =
|
|
ft_tt_launch_core_set_at(&g_groups[1].cores, 0u);
|
|
const ft_tt_launch_core_placement *poster =
|
|
ft_tt_launch_core_set_at(&g_groups[0].cores, 0u);
|
|
ft_tt_queue waiter_queue;
|
|
ft_tt_queue poster_queue;
|
|
ft_tt_queue_descriptor descriptors[4];
|
|
uint32_t zero = 0u;
|
|
uint32_t completed = 0u;
|
|
uint32_t readback = 0xFFFFFFFFu;
|
|
uint32_t nonce = ft_tt_queue_make_nonce();
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 4 — group 1 waits on a semaphore that group 0 posts");
|
|
|
|
(void)session->noc.write(session->noc.user, waiter->x, waiter->y, SEMAPHORE_BASE, &zero,
|
|
sizeof(zero));
|
|
(void)poison_scratch(&session->noc, waiter->x, waiter->y, SCRATCH_BASE, 1u);
|
|
|
|
rc = ft_tt_queue_init(&session->noc, waiter->x, waiter->y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
nonce, &waiter_queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, poster->x, poster->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, nonce + 1u, &poster_queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_resident_image(&session->noc, waiter->x, waiter->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_resident_image(&session->noc, poster->x, poster->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, waiter->x, waiter->y, &g_resident_config[flat_index(1u, 0u)],
|
|
&waiter_queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, poster->x, poster->y, &g_resident_config[flat_index(0u, 0u)],
|
|
&poster_queue);
|
|
ft_tt_test_check_fmt("both cores are resident", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* The waiter goes first and BLOCKS: it cannot proceed until the other group posts. */
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_SEM_WAIT;
|
|
descriptors[0].a0 = SEMAPHORE_BASE;
|
|
descriptors[0].a1 = 1u;
|
|
descriptors[0].a2 = 200000000u;
|
|
descriptors[1].opcode = FT_TT_QUEUE_OP_WRITE_WORD;
|
|
descriptors[1].a0 = SCRATCH_BASE;
|
|
descriptors[1].a1 = 0x5E4A0001u;
|
|
(void)ft_tt_queue_enqueue(&session->noc, &waiter_queue, descriptors, 2u);
|
|
|
|
(void)ft_tt_queue_read_field(&session->noc, &waiter_queue, FT_TT_QUEUE_FIELD_COMPLETED_COUNT,
|
|
&completed);
|
|
ft_tt_test_check_fmt("the waiter has not run past the semaphore", completed == 0u,
|
|
"completed=%u", completed);
|
|
(void)session->noc.read(session->noc.user, waiter->x, waiter->y, SCRATCH_BASE, &readback,
|
|
sizeof(readback));
|
|
ft_tt_test_check_fmt("and its destination is still the poison", readback == 0xDEAD0000u,
|
|
"0x%08x", readback);
|
|
|
|
/* Now group 0 posts to group 1's semaphore. */
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_SEM_POST;
|
|
descriptors[0].a0 = waiter->x;
|
|
descriptors[0].a1 = waiter->y;
|
|
descriptors[0].a2 = SEMAPHORE_BASE;
|
|
descriptors[0].a3 = 1u;
|
|
(void)ft_tt_queue_enqueue(&session->noc, &poster_queue, descriptors, 1u);
|
|
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &poster_queue, 1u, RUN_TIMEOUT_MS, &completed);
|
|
ft_tt_test_check_fmt("the poster completed its post", rc == FT_TT_OK, "completed=%u", completed);
|
|
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &waiter_queue, 2u, RUN_TIMEOUT_MS, &completed);
|
|
ft_tt_test_check_fmt("and the waiter then finished BOTH its descriptors", rc == FT_TT_OK,
|
|
"completed=%u/2", completed);
|
|
(void)session->noc.read(session->noc.user, waiter->x, waiter->y, SCRATCH_BASE, &readback,
|
|
sizeof(readback));
|
|
ft_tt_test_check_fmt("the work behind the semaphore ran", readback == 0x5E4A0001u, "0x%08x",
|
|
readback);
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
ft_tt_core_group other = g_groups[1];
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &poster_queue;
|
|
other.cores.count = 1u;
|
|
other.queues = &waiter_queue;
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &other, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 5 — a deliberate deadlock, and the card surviving it */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static bool stage_5_deadlock_and_recovery(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&g_groups[1].cores, 0u);
|
|
ft_tt_core_group solo = g_groups[1];
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[2];
|
|
ft_tt_watchdog_config config;
|
|
ft_tt_watchdog_report report;
|
|
uint32_t zero = 0u;
|
|
uint32_t unstopped = 0u;
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 5 — a deliberate deadlock: the watchdog catches it and the card lives");
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &queue;
|
|
|
|
(void)session->noc.write(session->noc.user, placement->x, placement->y, SEMAPHORE_BASE, &zero,
|
|
sizeof(zero));
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_resident_image(&session->noc, placement->x, placement->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, placement->x, placement->y,
|
|
&g_resident_config[flat_index(1u, 0u)], &queue);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* A wait nobody will ever satisfy. This is the hang the plan asks for, and on this
|
|
* host an unrecoverable one is a walk to the machine. */
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_SEM_WAIT;
|
|
descriptors[0].a0 = SEMAPHORE_BASE;
|
|
descriptors[0].a1 = 0xFFFFFFFFu;
|
|
descriptors[0].a2 = 0u; /* fall back to the idle bound, i.e. effectively forever */
|
|
(void)ft_tt_queue_enqueue(&session->noc, &queue, descriptors, 1u);
|
|
|
|
config.stall_timeout_ms = 500u;
|
|
config.overall_timeout_ms = 3000u;
|
|
rc = ft_tt_watchdog_wait(&session->noc, &solo, 1u, &config, &report);
|
|
ft_tt_test_check_fmt("the watchdog returns instead of hanging the session", rc == FT_TT_EIO,
|
|
"rc=%d after %u ms", rc, report.elapsed_ms);
|
|
ft_tt_test_check_fmt("it says the stalled core is ALIVE, not dead", report.stalled_core_is_alive,
|
|
"state=%s", ft_tt_queue_state_string(report.stalled_state));
|
|
ft_tt_test_check_fmt("and names the state it is blocked in",
|
|
report.stalled_state == (uint32_t)FT_TT_QUEUE_STATE_SEM_WAIT, "%s",
|
|
ft_tt_queue_state_string(report.stalled_state));
|
|
|
|
/* Recovery is a WRITE. No reset is asserted anywhere in this path — that is what
|
|
* wedged Tensix 0, and every resident kernel is blocked in a wait by design. */
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("the deadlocked kernel is retired by ASKING, not by reset",
|
|
rc == FT_TT_OK && unstopped == 0u, "rc=%d unstopped=%u", rc, unstopped);
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &queue, fields);
|
|
ft_tt_test_check_fmt("it reached EXIT",
|
|
fields[FT_TT_QUEUE_FIELD_STATE] == (uint32_t)FT_TT_QUEUE_STATE_EXIT, "%s",
|
|
ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]));
|
|
ft_tt_test_check_fmt("and the abandoned descriptor was NOT retired",
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] == 0u, "completed=%u",
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT]);
|
|
|
|
/* THE ACCEPTANCE: the same core runs a fresh resident kernel afterwards. If this
|
|
* needed a power cycle, the watchdog does not work. */
|
|
{
|
|
uint32_t readback = 0u;
|
|
|
|
(void)poison_scratch(&session->noc, placement->x, placement->y, SCRATCH_BASE, 1u);
|
|
rc = ft_tt_queue_init(&session->noc, placement->x, placement->y, FT_TT_OP_QUEUE_BASE,
|
|
QUEUE_CAPACITY, ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK) {
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_WRITE_WORD;
|
|
descriptors[0].a0 = SCRATCH_BASE;
|
|
descriptors[0].a1 = 0xA11E0001u;
|
|
rc = ft_tt_queue_enqueue(&session->noc, &queue, descriptors, 1u);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, placement->x, placement->y,
|
|
&g_resident_config[flat_index(1u, 0u)], &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, 1u, RUN_TIMEOUT_MS, NULL);
|
|
(void)session->noc.read(session->noc.user, placement->x, placement->y, SCRATCH_BASE,
|
|
&readback, sizeof(readback));
|
|
ft_tt_test_check_fmt("THE CARD SURVIVED: the same core runs a fresh kernel correctly",
|
|
rc == FT_TT_OK && readback == 0xA11E0001u, "rc=%d readback=0x%08x", rc,
|
|
readback);
|
|
(void)ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 6 — the two opcodes that were implemented and never run */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* `FT_TT_QUEUE_OP_L1_COPY` and `FT_TT_QUEUE_OP_DELAY` shipped with Phase 5 and neither had
|
|
* ever executed on the card: only NOP, WRITE_WORD, SEM_POST, SEM_WAIT and STOP had. An
|
|
* opcode nobody has run is a claim, not a feature.
|
|
*
|
|
* L1_COPY is checked the way dev notes §3 says a transfer has to be: the destination is
|
|
* POISONED first, and the payload is VARIED across three patterns including an ALL-ZERO
|
|
* one. Zero earns its place because it is the pattern that catches "the copy did nothing"
|
|
* against a non-zero poison — and it is also the one a WRONG SOURCE passes for free, since
|
|
* uninitialised L1 is usually zero too, so a set that is only zero proves the opposite of
|
|
* what it looks like. The copy is also CROSS-CORE, from group 0's core into group 1's,
|
|
* because a same-core copy cannot tell a NoC write from a local store.
|
|
*
|
|
* DELAY is checked as a RATIO, not against a wall-clock constant: two descriptors whose
|
|
* iteration counts differ 8x must differ in elapsed time by roughly 8x. An absolute
|
|
* threshold on a 2-core host also driving eight K80s measures the host's mood.
|
|
*/
|
|
static bool stage_6_unexercised_opcodes(const ft_tt_hardware_session *session)
|
|
{
|
|
const ft_tt_launch_core_placement *source =
|
|
ft_tt_launch_core_set_at(&g_groups[0].cores, 0u);
|
|
const ft_tt_launch_core_placement *destination =
|
|
ft_tt_launch_core_set_at(&g_groups[1].cores, 0u);
|
|
static const uint32_t patterns[3][4] = {
|
|
{ 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u },
|
|
{ 0u, 0u, 0u, 0u },
|
|
{ 0xFEEDFACEu, 0x00000001u, 0xFFFFFFFFu, 0x5A5A5A5Au }
|
|
};
|
|
const uint32_t copy_source = SCRATCH_BASE + 0x800u;
|
|
const uint32_t copy_destination = SCRATCH_BASE + 0xC00u;
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[2];
|
|
uint32_t completed = 0u;
|
|
unsigned pattern;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 6 — L1_COPY and DELAY, the two opcodes that had never run");
|
|
|
|
rc = ft_tt_queue_init(&session->noc, source->x, source->y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = load_resident_image(&session->noc, source->x, source->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, source->x, source->y,
|
|
&g_resident_config[flat_index(0u, 0u)], &queue);
|
|
ft_tt_test_check_fmt("the copying core is resident", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (pattern = 0; pattern < 3u; pattern++) {
|
|
uint32_t readback[4] = { 0u, 0u, 0u, 0u };
|
|
|
|
/* Poison the DESTINATION on the other core, and plant the payload on this one. */
|
|
(void)poison_scratch(&session->noc, destination->x, destination->y, copy_destination, 4u);
|
|
(void)session->noc.write(session->noc.user, source->x, source->y, copy_source,
|
|
patterns[pattern], sizeof(patterns[pattern]));
|
|
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_L1_COPY;
|
|
descriptors[0].a0 = destination->x;
|
|
descriptors[0].a1 = destination->y;
|
|
descriptors[0].a2 = copy_destination;
|
|
descriptors[0].a3 = copy_source;
|
|
descriptors[0].a4 = sizeof(patterns[pattern]);
|
|
rc = ft_tt_queue_enqueue(&session->noc, &queue, descriptors, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, pattern + 1u, RUN_TIMEOUT_MS,
|
|
&completed);
|
|
ft_tt_test_check_fmt("L1_COPY completed", rc == FT_TT_OK, "pattern %u completed=%u", pattern,
|
|
completed);
|
|
|
|
(void)session->noc.read(session->noc.user, destination->x, destination->y, copy_destination,
|
|
readback, sizeof(readback));
|
|
ft_tt_test_check_fmt("and the OTHER core's L1 now holds this pattern, not the poison",
|
|
memcmp(readback, patterns[pattern], sizeof(readback)) == 0,
|
|
"pattern %u: [0]=0x%08x want 0x%08x, [3]=0x%08x want 0x%08x", pattern,
|
|
readback[0], patterns[pattern][0], readback[3], patterns[pattern][3]);
|
|
}
|
|
|
|
/* DELAY. Timed on the DEVICE-SIDE path — the descriptors are staged first and the
|
|
* window contains one head write — so the number is the kernel's spin and not the
|
|
* host's enqueue. */
|
|
{
|
|
static const uint32_t iterations[2] = { 250000u, 2000000u };
|
|
uint64_t elapsed[2] = { 0u, 0u };
|
|
unsigned index;
|
|
|
|
for (index = 0; index < 2u; index++) {
|
|
uint64_t started;
|
|
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_DELAY;
|
|
descriptors[0].a0 = iterations[index];
|
|
rc = ft_tt_queue_stage(&session->noc, &queue, descriptors, 1u);
|
|
started = monotonic_microseconds();
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_publish(&session->noc, &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, 3u + index + 1u,
|
|
RUN_TIMEOUT_MS, &completed);
|
|
elapsed[index] = monotonic_microseconds() - started;
|
|
ft_tt_test_check_fmt("DELAY completed", rc == FT_TT_OK, "%u iterations, %llu us",
|
|
iterations[index], (unsigned long long)elapsed[index]);
|
|
}
|
|
|
|
printf("\n DELAY: %u iterations in %llu us, %u iterations in %llu us\n", iterations[0],
|
|
(unsigned long long)elapsed[0], iterations[1], (unsigned long long)elapsed[1]);
|
|
|
|
/* The 8x request must show up as a time ratio in a generous band. A DELAY that did
|
|
* nothing would land at 1.0, and one that ignored its argument would too — which is
|
|
* exactly what an unexercised opcode most plausibly does. */
|
|
ft_tt_test_check_fmt("DELAY takes measurably longer than a NOP", elapsed[0] > 100u,
|
|
"%llu us for %u iterations", (unsigned long long)elapsed[0],
|
|
iterations[0]);
|
|
ft_tt_test_check_fmt("and 8x the iterations takes 4x-16x the time, so a0 is read",
|
|
elapsed[0] > 0u && elapsed[1] >= elapsed[0] * 4u &&
|
|
elapsed[1] <= elapsed[0] * 16u,
|
|
"ratio %.2f", elapsed[0] ? (double)elapsed[1] / (double)elapsed[0] : 0.0);
|
|
}
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &queue;
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("the copying core retires by asking", rc == FT_TT_OK && unstopped == 0u,
|
|
"rc=%d unstopped=%u", rc, unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Stage 7 — PHASE D's gate: an ON-CARD dispatcher against the host round trip */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** The dispatcher program (kernels/dispatch_gate.c), loaded on the SECOND core. */
|
|
static ft_tt_program_directory g_dispatch_program;
|
|
static bool g_dispatch_loaded;
|
|
|
|
#define GATE_TABLE_ADDRESS (FT_TT_OP_CB_ARENA_BASE + 0x4000u) /* 64-aligned, clear of scratch */
|
|
#define GATE_MAGIC 0x47505344u
|
|
#define GATE_ENTRY_WORDS 16u
|
|
#define GATE_FIRST_ENTRY 32u
|
|
#define GATE_MAX_ENTRIES 1024u
|
|
#define GATE_POLL_BOUND 2000000u
|
|
|
|
static uint32_t g_gate_table[GATE_FIRST_ENTRY + GATE_MAX_ENTRIES * GATE_ENTRY_WORDS];
|
|
|
|
/**
|
|
* Write the table for `entries` NOP descriptors into the dispatcher core's L1. Tokens are the
|
|
* TARGET queue's own, stamped from the host's copy of its run nonce and issued counter — the
|
|
* host owns the nonce, which is the PHASE D design. `corrupt` (>= 0) stamps a wrong token on
|
|
* that entry's DESCRIPTOR while its expected token stays right: the red arm.
|
|
*/
|
|
static int gate_write_table(const ft_tt_noc_access *noc, uint16_t dx, uint16_t dy,
|
|
const ft_tt_queue *target, uint32_t entries, int corrupt)
|
|
{
|
|
uint32_t i;
|
|
|
|
memset(g_gate_table, 0, sizeof(g_gate_table));
|
|
g_gate_table[0] = GATE_MAGIC;
|
|
g_gate_table[1] = entries;
|
|
g_gate_table[2] = target->x;
|
|
g_gate_table[3] = target->y;
|
|
g_gate_table[4] = target->control_address;
|
|
g_gate_table[5] = target->ring_address;
|
|
g_gate_table[6] = target->capacity - 1u;
|
|
g_gate_table[7] = target->issued;
|
|
g_gate_table[8] = GATE_POLL_BOUND;
|
|
for (i = 0; i < entries; i++) {
|
|
uint32_t *record = g_gate_table + GATE_FIRST_ENTRY + i * GATE_ENTRY_WORDS;
|
|
const uint32_t token = ft_tt_queue_token_at(target, target->issued + i);
|
|
|
|
record[0] = FT_TT_QUEUE_OP_NOP;
|
|
record[1] = ((int)i == corrupt) ? (token ^ 0x00F0F0F0u) : token;
|
|
record[8] = token;
|
|
}
|
|
return noc->write(noc->user, dx, dy, GATE_TABLE_ADDRESS, g_gate_table,
|
|
(size_t)(GATE_FIRST_ENTRY + entries * GATE_ENTRY_WORDS) * 4u);
|
|
}
|
|
|
|
/** Load, stage and kick the dispatcher, then poll its state word. Returns elapsed us. */
|
|
static double gate_run_dispatcher(const ft_tt_noc_access *noc, uint16_t dx, uint16_t dy,
|
|
const ft_tt_launch_config *config, uint32_t *out_status,
|
|
int *out_rc)
|
|
{
|
|
uint32_t args[1] = { GATE_TABLE_ADDRESS };
|
|
ft_tt_op_args block;
|
|
uint64_t started, deadline;
|
|
int rc;
|
|
|
|
rc = ft_tt_launch_load_image(noc, dx, dy, FT_TT_PROC_DM0, FT_TT_IMAGE_KERNEL,
|
|
g_dispatch_program.program.images[0].elf_data,
|
|
g_dispatch_program.program.images[0].elf_size, NULL);
|
|
block.processor = FT_TT_PROC_DM0;
|
|
block.values = args;
|
|
block.count = 1u;
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_stage(noc, dx, dy, config, g_cb_buffer, sizeof(g_cb_buffer), &block, 1u);
|
|
if (rc != FT_TT_OK) {
|
|
*out_rc = rc;
|
|
return 0.0;
|
|
}
|
|
started = monotonic_microseconds();
|
|
rc = ft_tt_launch_send_go(noc, dx, dy);
|
|
deadline = started + (uint64_t)RUN_TIMEOUT_MS * 1000u;
|
|
while (rc == FT_TT_OK) {
|
|
rc = noc->read(noc->user, dx, dy, GATE_TABLE_ADDRESS + 24u * 4u, out_status, 24u);
|
|
if (rc != FT_TT_OK || out_status[0] == 2u || out_status[0] == 3u)
|
|
break;
|
|
if (monotonic_microseconds() > deadline) {
|
|
rc = FT_TT_EIO;
|
|
break;
|
|
}
|
|
}
|
|
*out_rc = rc;
|
|
return (double)(monotonic_microseconds() - started);
|
|
}
|
|
|
|
static bool stage_7_dispatch_gate(const ft_tt_hardware_session *session)
|
|
{
|
|
const unsigned target_flat = 0u, dispatcher_flat = 1u;
|
|
const ft_tt_launch_core_placement *tp = ft_tt_launch_core_set_at(&g_groups[0].cores, 0u);
|
|
const ft_tt_launch_core_placement *dp = ft_tt_launch_core_set_at(&g_groups[0].cores, 1u);
|
|
const uint32_t sizes[3] = { 64u, 256u, 1024u };
|
|
ft_tt_queue queue;
|
|
uint32_t status[6];
|
|
unsigned round, size;
|
|
int rc;
|
|
|
|
ft_tt_test_step("PHASE D gate: core (%u, %u) dispatches NOPs into core (%u, %u)'s queue "
|
|
"on-card, against the host issuing and awaiting each one",
|
|
dp->x, dp->y, tp->x, tp->y);
|
|
rc = load_resident_image(&session->noc, tp->x, tp->y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_init(&session->noc, tp->x, tp->y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &queue);
|
|
if (rc == FT_TT_OK)
|
|
rc = start_resident(&session->noc, tp->x, tp->y, &g_resident_config[target_flat], &queue);
|
|
ft_tt_test_check_fmt("the target residency is up", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (size = 0; size < 3u; size++) {
|
|
for (round = 0; round < 2u; round++) {
|
|
const uint32_t n = sizes[size];
|
|
uint64_t started = monotonic_microseconds();
|
|
uint32_t i;
|
|
double host_us, card_us;
|
|
|
|
/* ARM H — the host's own shape today: one descriptor, one await, n times. */
|
|
for (i = 0; i < n && rc == FT_TT_OK; i++) {
|
|
fill_nop_batch(g_batch, 1u);
|
|
rc = ft_tt_queue_enqueue(&session->noc, &queue, g_batch, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, queue.issued,
|
|
RUN_TIMEOUT_MS, NULL);
|
|
}
|
|
host_us = (double)(monotonic_microseconds() - started) / (double)n;
|
|
ft_tt_test_check_fmt("host arm completed", rc == FT_TT_OK, "n=%u rc=%d", n, rc);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
/* ARM D — the dispatcher issues and awaits the same n on-card. */
|
|
rc = gate_write_table(&session->noc, dp->x, dp->y, &queue, n, -1);
|
|
card_us = (rc == FT_TT_OK)
|
|
? gate_run_dispatcher(&session->noc, dp->x, dp->y,
|
|
&g_resident_config[dispatcher_flat], status, &rc)
|
|
: 0.0;
|
|
ft_tt_test_check_fmt("dispatcher completed every entry",
|
|
rc == FT_TT_OK && status[0] == 2u && status[1] == n,
|
|
"rc=%d state %u done %u error %u count %u", rc, status[0],
|
|
status[1], status[2], status[3]);
|
|
if (rc != FT_TT_OK || status[0] != 2u)
|
|
return false;
|
|
/* THE HOST TAKES ITS COUNTERS BACK, advanced by what the card issued. */
|
|
queue.issued += n;
|
|
queue.head += n;
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &queue, queue.issued, RUN_TIMEOUT_MS,
|
|
NULL);
|
|
ft_tt_test_check_fmt("the target's completion agrees with the host's resync",
|
|
rc == FT_TT_OK, "issued %u rc=%d", queue.issued, rc);
|
|
printf("GATE n=%u round %u: host %.2f us/descriptor, on-card %.2f us/descriptor "
|
|
"(doorbell to done %.0f us)\n", n, round, host_us, card_us / (double)n,
|
|
card_us);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* RED: one entry's descriptor carries a wrong token. The target executes it and echoes
|
|
* that token, so the dispatcher must stop AT that entry with error 3 (token mismatch). */
|
|
rc = gate_write_table(&session->noc, dp->x, dp->y, &queue, 64u, 17);
|
|
if (rc == FT_TT_OK)
|
|
(void)gate_run_dispatcher(&session->noc, dp->x, dp->y,
|
|
&g_resident_config[dispatcher_flat], status, &rc);
|
|
ft_tt_test_check_fmt("RED: a wrong token stops the dispatcher at that entry, error 3",
|
|
rc == FT_TT_OK && status[0] == 3u && status[1] == 17u &&
|
|
status[2] == 3u,
|
|
"state %u done %u error %u", status[0], status[1], status[2]);
|
|
queue.issued += 18u;
|
|
queue.head += 18u;
|
|
|
|
{
|
|
uint32_t unstopped = 0u;
|
|
ft_tt_core_group solo = g_groups[0];
|
|
|
|
solo.cores.count = 1u;
|
|
solo.queues = &queue;
|
|
rc = ft_tt_watchdog_shutdown(&session->noc, &solo, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("the target retires by asking", rc == FT_TT_OK && unstopped == 0u,
|
|
"rc=%d unstopped %u", rc, unstopped);
|
|
}
|
|
return ft_tt_test_failure_count() == 0;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_firmware_set firmware;
|
|
const char *resident_directory = (argc > 1) ? argv[1] : ft_tt_firmware_directory();
|
|
const char *oneshot_directory = (argc > 2) ? argv[2] : NULL;
|
|
unsigned group;
|
|
int rc;
|
|
|
|
if (!oneshot_directory) {
|
|
fprintf(stderr, "usage: %s <resident-program-dir> <oneshot-program-dir>\n", argv[0]);
|
|
return 2;
|
|
}
|
|
if (!load_environment())
|
|
return ft_tt_test_finish();
|
|
|
|
printf("Phase 5 resident dispatch — cores %u, %u, %u, %u\n", g_core_indices[0],
|
|
g_core_indices[1], g_core_indices[2], g_core_indices[3]);
|
|
|
|
rc = ft_tt_hardware_session_open(&session);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
|
|
/* Own the four cores for the whole run, per-core rather than card-wide, and
|
|
* refuse with the holder's name if any of them is taken. `retire` is NULL:
|
|
* this suite boots firmware from scratch on every core it claims, which is
|
|
* the one case where accepting whatever state a core is in is defensible.
|
|
* Anything RESUMING a residency must pass a real retirement instead — a
|
|
* released claim is not a retired kernel. */
|
|
rc = ft_tt_hardware_session_claim_cores(&session, g_core_indices, CORE_COUNT,
|
|
"phase5 resident dispatch", NULL, NULL);
|
|
ft_tt_test_check_fmt("all four cores are claimed for this run", rc == FT_TT_OK, "%s",
|
|
ft_tt_error_string(rc));
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|
|
|
|
rc = ft_tt_firmware_set_load(ft_tt_firmware_directory(), &firmware);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
rc = ft_tt_program_directory_load(resident_directory, BRISC_ONLY_MASK, &g_resident_program);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_program_directory_load(oneshot_directory, BRISC_ONLY_MASK, &g_oneshot_program);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_firmware_set_release(&firmware);
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
|
|
if (argc > 3 && ft_tt_program_directory_load(argv[3], BRISC_ONLY_MASK,
|
|
&g_dispatch_program) == FT_TT_OK)
|
|
g_dispatch_loaded = true;
|
|
if (build_groups(&session) && boot_and_load(&session, &firmware)) {
|
|
/* `FT_TT_PHASE5_GATE_ONLY=1` runs PHASE D's gate alone. */
|
|
const char *gate_only = getenv("FT_TT_PHASE5_GATE_ONLY");
|
|
|
|
if (gate_only && gate_only[0] == '1') {
|
|
if (g_dispatch_loaded)
|
|
(void)stage_7_dispatch_gate(&session);
|
|
else
|
|
ft_tt_test_check("the dispatcher program loaded (argv[3])", false, "missing");
|
|
} else if (stage_1_resident_runs(&session) && stage_2_dispatch_ab(&session) &&
|
|
stage_3_two_groups(&session)) {
|
|
(void)(stage_4_cross_group_semaphore(&session) &&
|
|
stage_5_deadlock_and_recovery(&session) &&
|
|
stage_6_unexercised_opcodes(&session) &&
|
|
(!g_dispatch_loaded || stage_7_dispatch_gate(&session)));
|
|
}
|
|
}
|
|
|
|
/* Park on EVERY exit path. By here every kernel has reported EXIT, so this is a park
|
|
* of an idle core rather than a reset of a blocked one. */
|
|
for (group = 0; group < GROUP_COUNT; group++)
|
|
(void)ft_tt_launch_park_all(&session.noc, &g_groups[group].cores);
|
|
|
|
if (g_dispatch_loaded)
|
|
ft_tt_program_directory_release(&g_dispatch_program);
|
|
ft_tt_program_directory_release(&g_oneshot_program);
|
|
ft_tt_program_directory_release(&g_resident_program);
|
|
ft_tt_firmware_set_release(&firmware);
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|