1796 lines
84 KiB
C
1796 lines
84 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_queue.c
|
|
* @brief Phase 5 off-device tests: the work-queue wire protocol, the completion-evidence
|
|
* rules, the shutdown protocol, and the core-group partitioner — including a SYNTHETIC
|
|
* 12-column grid that must partition into 120 cores with no code change.
|
|
*
|
|
* NO DEVICE IS TOUCHED. Cores are host memory behind the same `ft_tt_noc_access` vtable
|
|
* the real TLB path implements, and the resident kernel is a SOFTWARE MODEL stepped by
|
|
* this file.
|
|
*
|
|
* **What a model can and cannot prove.** The model executes the same loop the real
|
|
* kernel does, from the same header constants, so it pins the HOST half: ring wrap,
|
|
* batch writes, head-last ordering, token derivation, the four completion checks, the
|
|
* stop protocol and the watchdog's alive-vs-progressing distinction. It cannot tell you
|
|
* that `kernels/resident_queue.c` compiles to that loop — that is
|
|
* `tests/test_phase5_resident.c` on silicon, and nothing here substitutes for it. Two
|
|
* halves written from one header agreeing with each other is exactly the shape of
|
|
* self-consistency this project has been caught by before (dev notes §3).
|
|
*
|
|
* The negative cases are the point of the file. Each of them asserts that a check
|
|
* REFUSES something, so a check that was quietly weakened goes red here rather than on
|
|
* the card:
|
|
* - a queue whose kernel never ran must fail completion, not pass on a zero;
|
|
* - a completed_count that is right with a stale token must fail;
|
|
* - two groups holding one core must fail the disjointness check;
|
|
* - a set with no storage must refuse an add rather than write through NULL.
|
|
*
|
|
* @status Green, 92/92, on macOS/arm64 and on the Linux card host's gcc under the
|
|
* tests/Makefile.queue flags. Seven mutations recorded red
|
|
* (docs/tt_bringup_log.md 17.7).
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_arch.h"
|
|
#include "ft_tt_coord.h"
|
|
#include "ft_tt_group.h"
|
|
#include "ft_tt_launch.h"
|
|
#include "ft_tt_noc.h"
|
|
#include "ft_tt_queue.h"
|
|
#include "ft_tt_watchdog.h"
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Tiny harness */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static int checks_run;
|
|
static int checks_failed;
|
|
|
|
static void check(int condition, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (condition) {
|
|
printf(" ok %s\n", what);
|
|
} else {
|
|
checks_failed++;
|
|
printf(" FAIL %s\n", what);
|
|
}
|
|
}
|
|
|
|
static void check_u64(uint64_t got, uint64_t want, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (got == want) {
|
|
printf(" ok %s = 0x%" PRIx64 "\n", what, got);
|
|
} else {
|
|
checks_failed++;
|
|
printf(" FAIL %s: got 0x%" PRIx64 ", want 0x%" PRIx64 "\n", what, got, want);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* A simulated card: several cores, each with its own L1 */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#define SIM_MAX_CORES 8u
|
|
#define SIM_L1_BYTES FT_TT_MEM_L1_SIZE
|
|
|
|
typedef struct simulated_core {
|
|
uint16_t x;
|
|
uint16_t y;
|
|
bool present;
|
|
unsigned char *l1;
|
|
/* The model kernel's private registers — the things a real kernel keeps in its own
|
|
* local memory rather than in L1. */
|
|
bool kernel_running;
|
|
uint32_t kernel_tail;
|
|
uint32_t kernel_completed;
|
|
uint32_t kernel_idle_spins;
|
|
uint32_t kernel_heartbeat;
|
|
uint32_t kernel_error;
|
|
uint32_t kernel_control_address;
|
|
uint32_t kernel_capacity_mask;
|
|
uint32_t kernel_ring_address;
|
|
uint32_t kernel_idle_limit;
|
|
} simulated_core;
|
|
|
|
static simulated_core sim_cores[SIM_MAX_CORES];
|
|
static unsigned sim_core_count;
|
|
static uint32_t sim_read_calls;
|
|
static uint32_t sim_write_calls;
|
|
|
|
static simulated_core *sim_find(uint16_t x, uint16_t y)
|
|
{
|
|
unsigned index;
|
|
|
|
for (index = 0; index < sim_core_count; index++) {
|
|
if (sim_cores[index].present && sim_cores[index].x == x && sim_cores[index].y == y)
|
|
return &sim_cores[index];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int simulated_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src,
|
|
size_t bytes)
|
|
{
|
|
simulated_core *core = sim_find(x, y);
|
|
|
|
(void)user;
|
|
sim_write_calls++;
|
|
if (!core)
|
|
return FT_TT_EINVAL;
|
|
if (addr + bytes > SIM_L1_BYTES)
|
|
return FT_TT_EINVAL;
|
|
memcpy(core->l1 + addr, src, bytes);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int simulated_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst,
|
|
size_t bytes)
|
|
{
|
|
simulated_core *core = sim_find(x, y);
|
|
|
|
(void)user;
|
|
sim_read_calls++;
|
|
if (!core)
|
|
return FT_TT_EINVAL;
|
|
if (addr + bytes > SIM_L1_BYTES)
|
|
return FT_TT_EINVAL;
|
|
memcpy(dst, core->l1 + addr, bytes);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static ft_tt_noc_access simulated_access(void)
|
|
{
|
|
ft_tt_noc_access access;
|
|
|
|
memset(&access, 0, sizeof(access));
|
|
access.write = simulated_write;
|
|
access.read = simulated_read;
|
|
access.user = NULL;
|
|
return access;
|
|
}
|
|
|
|
/** Bring up `count` simulated cores at the coordinates a real grid would derive. */
|
|
static void sim_reset(const uint16_t *xs, const uint16_t *ys, unsigned count)
|
|
{
|
|
unsigned index;
|
|
|
|
for (index = 0; index < SIM_MAX_CORES; index++) {
|
|
if (sim_cores[index].l1)
|
|
memset(sim_cores[index].l1, 0, SIM_L1_BYTES);
|
|
sim_cores[index].present = false;
|
|
}
|
|
for (index = 0; index < count && index < SIM_MAX_CORES; index++) {
|
|
if (!sim_cores[index].l1)
|
|
sim_cores[index].l1 = calloc(1, SIM_L1_BYTES);
|
|
sim_cores[index].x = xs[index];
|
|
sim_cores[index].y = ys[index];
|
|
sim_cores[index].present = true;
|
|
sim_cores[index].kernel_running = false;
|
|
sim_cores[index].kernel_tail = 0u;
|
|
sim_cores[index].kernel_completed = 0u;
|
|
sim_cores[index].kernel_idle_spins = 0u;
|
|
sim_cores[index].kernel_heartbeat = 0u;
|
|
sim_cores[index].kernel_error = FT_TT_QUEUE_KERR_NONE;
|
|
}
|
|
sim_core_count = (count < SIM_MAX_CORES) ? count : SIM_MAX_CORES;
|
|
sim_read_calls = 0u;
|
|
sim_write_calls = 0u;
|
|
}
|
|
|
|
static uint32_t sim_l1_u32(const simulated_core *core, uint32_t address)
|
|
{
|
|
uint32_t value;
|
|
|
|
memcpy(&value, core->l1 + address, sizeof(value));
|
|
return value;
|
|
}
|
|
|
|
static void sim_l1_store_u32(simulated_core *core, uint32_t address, uint32_t value)
|
|
{
|
|
memcpy(core->l1 + address, &value, sizeof(value));
|
|
}
|
|
|
|
static uint32_t sim_control(const simulated_core *core, ft_tt_queue_control_field field)
|
|
{
|
|
return sim_l1_u32(core, core->kernel_control_address + (uint32_t)field * 4u);
|
|
}
|
|
|
|
static void sim_set_control(simulated_core *core, ft_tt_queue_control_field field, uint32_t value)
|
|
{
|
|
sim_l1_store_u32(core, core->kernel_control_address + (uint32_t)field * 4u, value);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* The model resident kernel */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Start the model kernel on a core, as a launch would. */
|
|
static void model_kernel_start(simulated_core *core, const uint32_t args[4])
|
|
{
|
|
core->kernel_control_address = args[0];
|
|
core->kernel_capacity_mask = args[1];
|
|
core->kernel_ring_address = args[2];
|
|
core->kernel_idle_limit = args[3];
|
|
core->kernel_tail = sim_control(core, FT_TT_QUEUE_FIELD_TAIL);
|
|
core->kernel_completed = sim_control(core, FT_TT_QUEUE_FIELD_COMPLETED_COUNT);
|
|
core->kernel_heartbeat = sim_control(core, FT_TT_QUEUE_FIELD_HEARTBEAT);
|
|
core->kernel_idle_spins = 0u;
|
|
core->kernel_error = FT_TT_QUEUE_KERR_NONE;
|
|
core->kernel_running = true;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_BOOT);
|
|
if (sim_control(core, FT_TT_QUEUE_FIELD_MAGIC) != FT_TT_QUEUE_MAGIC) {
|
|
core->kernel_error = FT_TT_QUEUE_KERR_BAD_MAGIC;
|
|
core->kernel_running = false;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_ERROR, core->kernel_error);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXIT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One turn of the model kernel's loop. Mirrors kernels/resident_queue.c.
|
|
*
|
|
* The order of the two completion stores is deliberate and is the thing this model is
|
|
* here to pin: the TOKEN is written before the COUNT, so a host that has seen the count
|
|
* is entitled to trust the token.
|
|
*/
|
|
static void model_kernel_step(simulated_core *core)
|
|
{
|
|
uint32_t head;
|
|
uint32_t slot;
|
|
uint32_t base;
|
|
uint32_t opcode;
|
|
uint32_t token;
|
|
bool item_completed = true;
|
|
|
|
if (!core->kernel_running)
|
|
return;
|
|
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_HEARTBEAT, ++core->kernel_heartbeat);
|
|
|
|
if (sim_control(core, FT_TT_QUEUE_FIELD_STOP_REQUEST) != 0u) {
|
|
core->kernel_running = false;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_ERROR, core->kernel_error);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXIT);
|
|
return;
|
|
}
|
|
|
|
head = sim_control(core, FT_TT_QUEUE_FIELD_HEAD);
|
|
if (head == core->kernel_tail) {
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_IDLE);
|
|
core->kernel_idle_spins++;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_IDLE_SPINS, core->kernel_idle_spins);
|
|
if (core->kernel_idle_spins >= core->kernel_idle_limit) {
|
|
core->kernel_error = FT_TT_QUEUE_KERR_IDLE_TIMEOUT;
|
|
core->kernel_running = false;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_ERROR, core->kernel_error);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXIT);
|
|
}
|
|
return;
|
|
}
|
|
|
|
core->kernel_idle_spins = 0u;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXEC);
|
|
|
|
slot = core->kernel_tail & core->kernel_capacity_mask;
|
|
base = core->kernel_ring_address + slot * FT_TT_QUEUE_DESCRIPTOR_BYTES;
|
|
opcode = sim_l1_u32(core, base + 0u);
|
|
token = sim_l1_u32(core, base + 4u);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_LAST_OPCODE, opcode);
|
|
|
|
switch (opcode) {
|
|
case FT_TT_QUEUE_OP_NOP:
|
|
break;
|
|
case FT_TT_QUEUE_OP_WRITE_WORD:
|
|
sim_l1_store_u32(core, sim_l1_u32(core, base + 8u), sim_l1_u32(core, base + 12u));
|
|
break;
|
|
case FT_TT_QUEUE_OP_SEM_WAIT: {
|
|
uint32_t semaphore_address = sim_l1_u32(core, base + 8u);
|
|
uint32_t target = sim_l1_u32(core, base + 12u);
|
|
|
|
/* The model blocks the same way the kernel does: it stays on this descriptor
|
|
* until the semaphore reaches its target or the host asks it to stop, and an
|
|
* abandoned wait is NOT completed. */
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_SEM_WAIT);
|
|
if (sim_l1_u32(core, semaphore_address) < target)
|
|
return; /* still waiting; tail does not advance */
|
|
break;
|
|
}
|
|
case FT_TT_QUEUE_OP_STOP:
|
|
core->kernel_running = false;
|
|
break;
|
|
default:
|
|
core->kernel_error = FT_TT_QUEUE_KERR_BAD_OPCODE;
|
|
item_completed = false;
|
|
core->kernel_running = false;
|
|
break;
|
|
}
|
|
|
|
/* AN ABANDONED DESCRIPTOR ADVANCES NOTHING — not `tail` and not `completed_count`. It
|
|
* was handed to nobody, so the next launch re-attempts it. This is what makes `tail`
|
|
* mean one thing in both kernels: with the resident TRIAD it is the handoff pointer the
|
|
* WRITER's cursor follows, and advancing it for an item the reader abandoned would let
|
|
* the writer retire work that never ran. */
|
|
if (item_completed) {
|
|
core->kernel_tail++;
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_TAIL, core->kernel_tail);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_COMPLETED_TOKEN, token);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_COMPLETED_COUNT, ++core->kernel_completed);
|
|
}
|
|
if (!core->kernel_running) {
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_ERROR, core->kernel_error);
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXIT);
|
|
}
|
|
}
|
|
|
|
static void model_kernel_run(simulated_core *core, unsigned max_steps)
|
|
{
|
|
unsigned step;
|
|
|
|
for (step = 0; step < max_steps && core->kernel_running; step++)
|
|
model_kernel_step(core);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 1. The control block the host writes */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static const uint16_t sim_xs[SIM_MAX_CORES] = { 1, 2, 3, 4, 5, 6, 7, 8 };
|
|
static const uint16_t sim_ys[SIM_MAX_CORES] = { 2, 2, 2, 2, 3, 3, 3, 3 };
|
|
|
|
#define TEST_CAPACITY 16u
|
|
#define TEST_IDLE_LIMIT 1000u
|
|
|
|
/**
|
|
* The kernel binding these off-hardware cases use: both spin bounds set to `spins`, no
|
|
* status slot and no group block.
|
|
*
|
|
* A helper rather than a struct per call site so that a new REQUIRED field in the binding
|
|
* shows up in one place. The two bounds are set together on purpose: `cb_wait_spin_limit`
|
|
* is refused at 0, and a test that left it there would be exercising the refusal instead of
|
|
* the case it names.
|
|
*/
|
|
static const ft_tt_queue_kernel_binding *sim_binding(uint32_t spins)
|
|
{
|
|
static ft_tt_queue_kernel_binding binding;
|
|
|
|
memset(&binding, 0, sizeof(binding));
|
|
binding.idle_spin_limit = spins;
|
|
binding.cb_wait_spin_limit = spins;
|
|
return &binding;
|
|
}
|
|
|
|
static void test_queue_init_writes_a_recognisable_block(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
printf("\nqueue: init writes a block the kernel can recognise, and poisons the rest\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
|
|
check(ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x1234u, &queue) ==
|
|
FT_TT_OK,
|
|
"init");
|
|
check(ft_tt_queue_read_control(&noc, &queue, fields) == FT_TT_OK, "the block reads back");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_MAGIC], FT_TT_QUEUE_MAGIC, "magic");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_CAPACITY], TEST_CAPACITY, "capacity");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_HEAD], 0u, "head");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_TAIL], 0u, "tail");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], 0u, "completed count");
|
|
|
|
/* The two fields that must NOT start at zero. A zero here is what makes "never ran"
|
|
* and "finished" the same bit pattern, which is the trap this whole protocol is
|
|
* shaped around (dev notes §3). */
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_STATE], (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON,
|
|
"state starts poisoned, not zero");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN], (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON,
|
|
"and so does the completed token");
|
|
|
|
check(queue.ring_address == FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_RING_OFFSET,
|
|
"the ring follows the control block");
|
|
check(ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, 15u, 0x1234u, &queue) == FT_TT_EINVAL,
|
|
"a capacity that is not a power of two is refused");
|
|
check(ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE + 1u, TEST_CAPACITY, 0x1234u,
|
|
&queue) == FT_TT_EINVAL,
|
|
"and so is a misaligned control address");
|
|
check(ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0u, &queue) ==
|
|
FT_TT_EINVAL,
|
|
"a zero nonce is refused: it would make every token derivable without the run");
|
|
check(ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
(uint32_t)FT_TT_QUEUE_STATE_HOST_POISON, &queue) == FT_TT_EINVAL,
|
|
"and so is a nonce that collides with the poison");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 2. Enqueue: one device write per batch, head last */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void fill_nops(ft_tt_queue_descriptor *descriptors, uint32_t count)
|
|
{
|
|
uint32_t index;
|
|
|
|
memset(descriptors, 0, (size_t)count * sizeof(*descriptors));
|
|
for (index = 0; index < count; index++)
|
|
descriptors[index].opcode = FT_TT_QUEUE_OP_NOP;
|
|
}
|
|
|
|
static void test_enqueue_is_one_write_plus_a_head_bump(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
simulated_core *core;
|
|
uint32_t before;
|
|
|
|
printf("\nqueue: a batch is one descriptor write and one head bump — no round trip\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0xABCDu, &queue);
|
|
|
|
fill_nops(descriptors, 8u);
|
|
before = sim_write_calls;
|
|
check(ft_tt_queue_enqueue(&noc, &queue, descriptors, 8u) == FT_TT_OK, "eight enqueued");
|
|
check_u64(sim_write_calls - before, 2u,
|
|
"eight descriptors cost TWO device accesses in total, not eight");
|
|
check_u64(sim_read_calls, 0u, "and enqueue never reads the card: there is no round trip");
|
|
check_u64(queue.head, 8u, "the host's head advanced by the batch");
|
|
|
|
/* Tokens are stamped by the enqueue, not by the caller. */
|
|
check_u64(descriptors[0].token, ft_tt_queue_token_at(&queue, 0u), "descriptor 0 got its token");
|
|
check_u64(descriptors[7].token, ft_tt_queue_token_at(&queue, 7u), "and descriptor 7 got its");
|
|
check(ft_tt_queue_token_at(&queue, 0u) != ft_tt_queue_token_at(&queue, 1u),
|
|
"consecutive tokens differ");
|
|
check(ft_tt_queue_token_at(&queue, 0u) != 0u &&
|
|
ft_tt_queue_token_at(&queue, 0u) != (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON,
|
|
"and a token is never zero and never the poison");
|
|
|
|
check_u64(sim_l1_u32(core, queue.ring_address), FT_TT_QUEUE_OP_NOP, "slot 0 holds the opcode");
|
|
check_u64(sim_l1_u32(core, queue.ring_address + 4u), descriptors[0].token,
|
|
"and slot 0 holds its token");
|
|
check_u64(sim_l1_u32(core, queue.control_address + FT_TT_QUEUE_FIELD_HEAD * 4u), 8u,
|
|
"the device head is the last thing written");
|
|
|
|
check(ft_tt_queue_enqueue(&noc, &queue, descriptors, TEST_CAPACITY + 1u) == FT_TT_ENOMEM,
|
|
"a batch bigger than the ring is refused");
|
|
check(ft_tt_queue_enqueue(&noc, &queue, descriptors, 0u) == FT_TT_EINVAL,
|
|
"and so is an empty one");
|
|
}
|
|
|
|
static void test_enqueue_wraps_the_ring(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
simulated_core *core;
|
|
|
|
printf("\nqueue: a batch that crosses the end of the ring lands in both halves\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x55AAu, &queue);
|
|
|
|
/* Push the head to two before the wrap, then enqueue four. */
|
|
fill_nops(descriptors, TEST_CAPACITY - 2u);
|
|
(void)ft_tt_queue_enqueue(&noc, &queue, descriptors, TEST_CAPACITY - 2u);
|
|
|
|
fill_nops(descriptors, 4u);
|
|
descriptors[0].a0 = 0xF00Du;
|
|
descriptors[3].a0 = 0xBEEFu;
|
|
check(ft_tt_queue_enqueue(&noc, &queue, descriptors, 4u) == FT_TT_OK, "four more enqueued");
|
|
check_u64(queue.head, TEST_CAPACITY + 2u, "the head is monotonic, not wrapped");
|
|
check_u64(sim_l1_u32(core, queue.ring_address +
|
|
(TEST_CAPACITY - 2u) * FT_TT_QUEUE_DESCRIPTOR_BYTES + 8u),
|
|
0xF00Du, "the first of the four is in the last-but-one slot");
|
|
check_u64(sim_l1_u32(core, queue.ring_address + 1u * FT_TT_QUEUE_DESCRIPTOR_BYTES + 8u),
|
|
0xBEEFu, "and the fourth wrapped round to slot 1");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 3. Completion is produced by the work item */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void test_completion_cannot_report_work_that_never_ran(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
simulated_core *core;
|
|
|
|
printf("\nqueue: a core that never ran fails the completion check\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x0F0Fu, &queue);
|
|
|
|
fill_nops(descriptors, 4u);
|
|
(void)ft_tt_queue_enqueue(&noc, &queue, descriptors, 4u);
|
|
|
|
/* The kernel is never started. This is the exact case ft_tt_launch_wait_done_all()
|
|
* reported as complete on 2026-08-12. */
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_ERROR,
|
|
"four descriptors enqueued and nothing started is NOT complete");
|
|
check(reason != NULL && strstr(reason, "never ran") != NULL,
|
|
"and the reason says so rather than being a bare failure");
|
|
|
|
/* Now run it and the same four descriptors do pass. */
|
|
{
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
|
|
check(ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args) == FT_TT_OK, "kernel args");
|
|
check_u64(args[1], TEST_CAPACITY - 1u, "the capacity arrives as a MASK");
|
|
check(ft_tt_queue_kernel_args(&queue, sim_binding(0u), args) == FT_TT_EINVAL,
|
|
"an idle limit of 0 is refused: residency is bounded, always");
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 64u);
|
|
}
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_OK,
|
|
"after the kernel runs them, the same four ARE complete");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN], ft_tt_queue_token_at(&queue, 3u),
|
|
"and the token is this run's fourth");
|
|
}
|
|
|
|
static void test_completion_rejects_a_stale_token(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
|
|
printf("\nqueue: the right COUNT with the wrong TOKEN is still a failure\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x77u, &queue);
|
|
|
|
memset(fields, 0, sizeof(fields));
|
|
fields[FT_TT_QUEUE_FIELD_STATE] = FT_TT_QUEUE_STATE_EXIT;
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] = 4u;
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] = ft_tt_queue_token_at(&queue, 3u);
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_OK,
|
|
"the honest case passes");
|
|
|
|
/* A previous run's residue: a plausible non-zero token that this run never issued. */
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] = ft_tt_queue_token_at(&queue, 3u) ^ 1u;
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_ERROR,
|
|
"a token this run never issued fails even with the count right");
|
|
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] = ft_tt_queue_token_at(&queue, 3u);
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] = 3u;
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_ERROR,
|
|
"a short count fails even with a token that is a real one");
|
|
|
|
fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] = 4u;
|
|
fields[FT_TT_QUEUE_FIELD_ERROR] = FT_TT_QUEUE_KERR_BAD_OPCODE;
|
|
check(ft_tt_queue_check_completion(&queue, fields, 4u, &reason) == FT_TT_ERROR,
|
|
"and a kernel that reported an error fails whatever its counters say");
|
|
}
|
|
|
|
static void test_two_runs_cannot_share_a_token(void)
|
|
{
|
|
ft_tt_queue first;
|
|
ft_tt_queue second;
|
|
ft_tt_noc_access noc = simulated_access();
|
|
uint32_t index;
|
|
bool any_collision = false;
|
|
|
|
printf("\nqueue: two runs' token sets are disjoint — residue cannot be mistaken for work\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x1111u, &first);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x1112u, &second);
|
|
|
|
for (index = 0; index < 256u && !any_collision; index++) {
|
|
uint32_t j;
|
|
|
|
for (j = 0; j < 256u; j++) {
|
|
if (ft_tt_queue_token_at(&first, index) == ft_tt_queue_token_at(&second, j)) {
|
|
any_collision = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
check(!any_collision, "no token of run A equals any of the first 256 of run B");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 4. Shutdown by asking, and the deadlock it has to survive */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void test_stop_request_retires_a_blocked_kernel(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[4];
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
simulated_core *core;
|
|
const uint32_t semaphore_address = FT_TT_OP_CB_ARENA_BASE;
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
printf("\nqueue: a kernel blocked on a semaphore nobody will post is retired by ASKING\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x2222u, &queue);
|
|
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_NOP;
|
|
descriptors[1].opcode = FT_TT_QUEUE_OP_SEM_WAIT;
|
|
descriptors[1].a0 = semaphore_address;
|
|
descriptors[1].a1 = 1u; /* nobody ever posts it */
|
|
(void)ft_tt_queue_enqueue(&noc, &queue, descriptors, 2u);
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 64u);
|
|
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], 1u,
|
|
"the NOP completed and the blocked wait did NOT");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_STATE], FT_TT_QUEUE_STATE_SEM_WAIT, "the kernel is blocked");
|
|
check(core->kernel_running, "and it is still running: a stall, not a crash");
|
|
{
|
|
uint32_t heartbeat_before = fields[FT_TT_QUEUE_FIELD_HEARTBEAT];
|
|
|
|
model_kernel_run(core, 4u);
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check(fields[FT_TT_QUEUE_FIELD_HEARTBEAT] > heartbeat_before,
|
|
"the heartbeat still moves — ALIVE and blocked is distinguishable from dead");
|
|
}
|
|
|
|
check(ft_tt_queue_request_stop(&noc, &queue) == FT_TT_OK, "the host asks it to stop");
|
|
model_kernel_run(core, 8u);
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check(!core->kernel_running, "the kernel left its loop");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_STATE], FT_TT_QUEUE_STATE_EXIT, "and says EXIT");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], 1u,
|
|
"the ABANDONED descriptor was not retired — stopping is not finishing");
|
|
}
|
|
|
|
static void test_stop_sentinel_is_ordered_with_the_work(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[4];
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
simulated_core *core;
|
|
|
|
printf("\nqueue: a STOP descriptor runs after the work queued before it\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x3333u, &queue);
|
|
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_NOP;
|
|
descriptors[1].opcode = FT_TT_QUEUE_OP_NOP;
|
|
descriptors[2].opcode = FT_TT_QUEUE_OP_STOP;
|
|
(void)ft_tt_queue_enqueue(&noc, &queue, descriptors, 3u);
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 64u);
|
|
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], 3u,
|
|
"all three retired, the STOP among them");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_STATE], FT_TT_QUEUE_STATE_EXIT, "and the kernel exited");
|
|
}
|
|
|
|
static void test_bounded_residency(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
simulated_core *core;
|
|
|
|
printf("\nqueue: a kernel whose host stopped feeding it returns on its own\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x4444u, &queue);
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(5u), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 100u);
|
|
|
|
(void)ft_tt_queue_read_control(&noc, &queue, fields);
|
|
check(!core->kernel_running, "it is not spinning forever");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_STATE], FT_TT_QUEUE_STATE_EXIT, "it reached EXIT");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_ERROR], FT_TT_QUEUE_KERR_IDLE_TIMEOUT,
|
|
"and it says WHY — the idle bound, not a mystery");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 5. The partitioner — derived, never constant */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static ft_tt_grid_info p150a_grid(void)
|
|
{
|
|
ft_tt_grid_info grid;
|
|
|
|
memset(&grid, 0, sizeof(grid));
|
|
grid.valid = true;
|
|
grid.arch = ft_tt_arch_blackhole();
|
|
grid.noc_translation_enabled = 1u;
|
|
grid.tensix_enabled_col = 16383u;
|
|
grid.tensix_column_count = 14u;
|
|
grid.tensix_core_count = 140u;
|
|
grid.enabled_gddr = 255u;
|
|
grid.dram_bank_count = 8u;
|
|
return grid;
|
|
}
|
|
|
|
/**
|
|
* A SYNTHETIC 12-column part.
|
|
*
|
|
* This is the acceptance criterion, not a nicety: TT's 140→120 reduction is a
|
|
* compatibility measure rather than yield binning, so 12-column p150s are a shipping
|
|
* configuration running this same software. The partitioner has to produce 120 cores here
|
|
* with no code change, and a literal 140 or 14 anywhere in it makes this case red and
|
|
* nothing else.
|
|
*/
|
|
static const uint8_t twelve_columns[12] = { 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14 };
|
|
static const uint8_t twelve_bits[12] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
|
|
static ft_tt_arch twelve_column_arch;
|
|
|
|
static ft_tt_grid_info twelve_column_grid(void)
|
|
{
|
|
ft_tt_grid_info grid;
|
|
|
|
twelve_column_arch = *ft_tt_arch_blackhole();
|
|
twelve_column_arch.tensix.column_count = 12u;
|
|
twelve_column_arch.tensix.noc_x_by_column = twelve_columns;
|
|
twelve_column_arch.tensix.telemetry_bit_by_column = twelve_bits;
|
|
|
|
memset(&grid, 0, sizeof(grid));
|
|
grid.valid = true;
|
|
grid.arch = &twelve_column_arch;
|
|
grid.noc_translation_enabled = 1u;
|
|
grid.tensix_enabled_col = 0xFFFu;
|
|
grid.enabled_gddr = 255u;
|
|
grid.dram_bank_count = 8u;
|
|
return grid;
|
|
}
|
|
|
|
#define PARTITION_STORAGE 160u
|
|
static ft_tt_launch_core_placement partition_placements[PARTITION_STORAGE];
|
|
static ft_tt_queue partition_queues[PARTITION_STORAGE];
|
|
static ft_tt_core_group partition_groups[8];
|
|
|
|
static void test_partition_is_derived_from_the_live_grid(void)
|
|
{
|
|
const ft_tt_grid_info grid = p150a_grid();
|
|
uint32_t live = ft_tt_coord_tensix_core_count(&grid);
|
|
const char *reason = NULL;
|
|
|
|
printf("\ngroups: the partition comes from the LIVE core count\n");
|
|
check_u64(live, 140u, "this fixture is a 14-column part");
|
|
|
|
check(ft_tt_group_partition(&grid, 0u, 0u, 4u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_OK,
|
|
"140 cores partition into four groups");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[0].cores), 35u, "group 0 size");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[3].cores), 35u, "group 3 size");
|
|
check_u64(partition_groups[3].first_tensix_index, 105u, "group 3 starts where group 2 ended");
|
|
check(ft_tt_group_check_disjoint(partition_groups, 4u, &reason) == FT_TT_OK,
|
|
"and no core is in two groups");
|
|
|
|
/* A remainder must be spread, not dropped — a partitioner that leaves cores
|
|
* unassigned is one that silently runs a smaller machine than it was given. */
|
|
check(ft_tt_group_partition(&grid, 0u, 0u, 3u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_OK,
|
|
"140 cores partition into three groups");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[0].cores), 47u, "group 0 takes one extra");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[1].cores), 47u, "so does group 1");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[2].cores), 46u, "group 2 takes the base");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[0].cores) +
|
|
ft_tt_launch_core_set_count(&partition_groups[1].cores) +
|
|
ft_tt_launch_core_set_count(&partition_groups[2].cores),
|
|
live, "and every live core is assigned exactly once");
|
|
}
|
|
|
|
static void test_partition_on_a_synthetic_twelve_column_part(void)
|
|
{
|
|
const ft_tt_grid_info grid = twelve_column_grid();
|
|
uint32_t live = ft_tt_coord_tensix_core_count(&grid);
|
|
const char *reason = NULL;
|
|
|
|
printf("\ngroups: a synthetic 12-column part partitions into 120 with NO code change\n");
|
|
check_u64(live, 120u, "the live core count is 12 x 10, not 140");
|
|
|
|
check(ft_tt_group_partition(&grid, 0u, 0u, 4u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_OK,
|
|
"and it partitions");
|
|
check_u64(ft_tt_launch_core_set_count(&partition_groups[0].cores), 30u,
|
|
"into groups of 120/4, not 140/4");
|
|
check_u64(partition_groups[3].first_tensix_index + partition_groups[3].tensix_index_count, 120u,
|
|
"the last group ends exactly at the live count");
|
|
check(ft_tt_group_check_disjoint(partition_groups, 4u, &reason) == FT_TT_OK, "disjoint");
|
|
|
|
/* And an index the 14-column part has but this one does not must be REFUSED rather
|
|
* than addressed — that is a NoC transaction at a coordinate nobody derived. */
|
|
check(ft_tt_group_partition(&grid, 130u, 4u, 1u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) != FT_TT_OK,
|
|
"an index range past this part's live count is refused, not clamped");
|
|
}
|
|
|
|
static void test_partition_refusals_and_disjointness(void)
|
|
{
|
|
const ft_tt_grid_info grid = p150a_grid();
|
|
const char *reason = NULL;
|
|
|
|
printf("\ngroups: refusals, sub-ranges, and the pairwise check that catches a shared core\n");
|
|
|
|
check(ft_tt_group_partition(&grid, 1u, 8u, 2u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_OK,
|
|
"a sub-range partitions: indices 1..8 into two groups");
|
|
check_u64(partition_groups[0].first_tensix_index, 1u, "group 0 starts at the first index given");
|
|
check_u64(partition_groups[1].first_tensix_index, 5u, "group 1 follows it");
|
|
check(ft_tt_group_check_disjoint(partition_groups, 2u, &reason) == FT_TT_OK, "disjoint");
|
|
|
|
/* DRAM banks are handed out by stride, so two groups never share one while the part
|
|
* has at least as many banks as groups. */
|
|
{
|
|
uint32_t bank_a = 0u;
|
|
uint32_t bank_b = 0u;
|
|
|
|
check(ft_tt_group_dram_bank(&partition_groups[0], 0u, &bank_a) == FT_TT_OK, "group 0 bank 0");
|
|
check(ft_tt_group_dram_bank(&partition_groups[1], 0u, &bank_b) == FT_TT_OK, "group 1 bank 0");
|
|
check(bank_a != bank_b, "and the two groups' first DRAM banks differ");
|
|
check(ft_tt_group_dram_bank(&partition_groups[0], 99u, &bank_a) == FT_TT_EINVAL,
|
|
"a bank index past what the group owns is refused");
|
|
}
|
|
|
|
check(ft_tt_group_partition(&grid, 0u, 4u, 5u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_EINVAL,
|
|
"more groups than cores is refused");
|
|
check(ft_tt_group_partition(&grid, 0u, 0u, 4u, partition_groups, partition_placements,
|
|
partition_queues, 8u) == FT_TT_ENOMEM,
|
|
"storage smaller than the range is refused before anything is placed");
|
|
check(ft_tt_group_partition(&grid, 0u, 0u, 0u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE) == FT_TT_EINVAL,
|
|
"and so is zero groups");
|
|
|
|
/* The negative case for the disjointness check itself: hand it two groups holding the
|
|
* same core and it must say so. A checker nobody has seen refuse is not a checker. */
|
|
(void)ft_tt_group_partition(&grid, 0u, 8u, 2u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE);
|
|
partition_placements[4] = partition_placements[0]; /* group 1 slot 0 := group 0 slot 0 */
|
|
check(ft_tt_group_check_disjoint(partition_groups, 2u, &reason) == FT_TT_ERROR,
|
|
"two groups holding one core is caught");
|
|
check(reason != NULL, "with a reason");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 6. The watchdog */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void test_watchdog_reports_alive_but_blocked(void)
|
|
{
|
|
const ft_tt_grid_info grid = p150a_grid();
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_watchdog_config config;
|
|
ft_tt_watchdog_report report;
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
ft_tt_queue_descriptor descriptors[4];
|
|
unsigned slot;
|
|
|
|
printf("\nwatchdog: a blocked group times out, and the report says ALIVE\n");
|
|
|
|
(void)ft_tt_group_partition(&grid, 0u, 2u, 1u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE);
|
|
{
|
|
uint16_t xs[2];
|
|
uint16_t ys[2];
|
|
|
|
for (slot = 0; slot < 2u; slot++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&partition_groups[0].cores, slot);
|
|
xs[slot] = placement->x;
|
|
ys[slot] = placement->y;
|
|
}
|
|
sim_reset(xs, ys, 2u);
|
|
}
|
|
|
|
check(ft_tt_group_queues_init(&noc, &partition_groups[0], FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
0x9999u) == FT_TT_OK,
|
|
"both cores' queues initialise");
|
|
|
|
/* Core 0 gets a NOP it can finish; core 1 gets a wait nobody will satisfy. */
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_NOP;
|
|
(void)ft_tt_queue_enqueue(&noc, &partition_groups[0].queues[0], descriptors, 1u);
|
|
memset(descriptors, 0, sizeof(descriptors));
|
|
descriptors[0].opcode = FT_TT_QUEUE_OP_SEM_WAIT;
|
|
descriptors[0].a0 = FT_TT_OP_CB_ARENA_BASE;
|
|
descriptors[0].a1 = 1u;
|
|
(void)ft_tt_queue_enqueue(&noc, &partition_groups[0].queues[1], descriptors, 1u);
|
|
|
|
for (slot = 0; slot < 2u; slot++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&partition_groups[0].cores, slot);
|
|
simulated_core *core = sim_find(placement->x, placement->y);
|
|
|
|
(void)ft_tt_queue_kernel_args(&partition_groups[0].queues[slot], sim_binding(1000000u),
|
|
args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 16u);
|
|
}
|
|
|
|
config.stall_timeout_ms = 20u;
|
|
config.overall_timeout_ms = 200u;
|
|
check(ft_tt_watchdog_wait(&noc, &partition_groups[0], 1u, &config, &report) == FT_TT_EIO,
|
|
"the wait times out rather than hanging");
|
|
check_u64(report.stalled_slot, 1u, "and names the core that is short");
|
|
check_u64(report.completed_total, 1u, "with the one that finished counted");
|
|
check_u64(report.stalled_state, FT_TT_QUEUE_STATE_SEM_WAIT, "the stalled core is in SEM_WAIT");
|
|
|
|
config.overall_timeout_ms = 0u;
|
|
check(ft_tt_watchdog_wait(&noc, &partition_groups[0], 1u, &config, &report) == FT_TT_EINVAL,
|
|
"a zero overall timeout is refused: there is no untimed path");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 6. Group-level completion — "the group finished item N" */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* The hole this closes: completion was per CORE, and the only group-level answer available
|
|
* was a SUM of `completed_count` across the group. A sum is satisfiable by the wrong thing
|
|
* — one member doing 2N items and another doing none sums to the same 2N as every member
|
|
* doing N — which is the "RUN_MSG_DONE is 0" family of lie one level up again.
|
|
*
|
|
* These cases are written against the SLOT ARRAY the kernels write, and each one names the
|
|
* REASON the check refuses rather than only that it refused. A check with several
|
|
* independent grounds for failing tests whichever is cheapest, and the one you care about
|
|
* may never run (dev notes §3, and Phase 5's own H1).
|
|
*/
|
|
static void write_group_slot(simulated_core *core, uint32_t address, uint32_t member,
|
|
uint32_t sequence, uint32_t token)
|
|
{
|
|
uint32_t base = address + (FT_TT_QUEUE_GROUP_HEADER_WORDS + member * FT_TT_QUEUE_GROUP_SLOT_WORDS) * 4u;
|
|
|
|
sim_l1_store_u32(core, base, sequence);
|
|
sim_l1_store_u32(core, base + 4u, token);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* The HOT-PATH slot read — the one that has to respect the 16-byte L1 unit */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Last length ft_tt_queue_group_read_slots() asked the accessor for. */
|
|
static size_t sim_last_read_bytes;
|
|
|
|
/**
|
|
* The simulated accessor with the L1 rule the REAL one carries: 16 bytes in address AND
|
|
* length. Without this the off-hardware suite cannot see the defect at all — a plain
|
|
* memcpy model accepts any length, and the refusal only appears on silicon, where it cost
|
|
* a residency three retire/re-arm cycles and a core.
|
|
*/
|
|
static int l1_strict_read(void *user, uint16_t x, uint16_t y, uint64_t address, void *dst,
|
|
size_t bytes)
|
|
{
|
|
sim_last_read_bytes = bytes;
|
|
if ((address % 16u) != 0u || (bytes % 16u) != 0u)
|
|
return FT_TT_EINVAL;
|
|
return simulated_read(user, x, y, address, dst, bytes);
|
|
}
|
|
|
|
static void test_group_slots_read_fits_the_l1_unit(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_noc_access strict = simulated_access();
|
|
const uint32_t address = FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET;
|
|
ft_tt_queue_group_slot wide[FT_TT_QUEUE_GROUP_MAX_MEMBERS];
|
|
ft_tt_queue_group_slot narrow[FT_TT_QUEUE_GROUP_MAX_MEMBERS];
|
|
simulated_core *leader;
|
|
unsigned members;
|
|
bool every_odd_count_fits = true;
|
|
bool wide_agrees_with_the_header_read = true;
|
|
|
|
printf("\ngroup: the hot-path slot read is WIDE, so its length must be a multiple of 16\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
leader = sim_find(sim_xs[0], sim_ys[0]);
|
|
strict.read = l1_strict_read;
|
|
|
|
check(ft_tt_queue_group_init(&noc, sim_xs[0], sim_ys[0], address, 8u) == FT_TT_OK,
|
|
"a block for eight members writes");
|
|
for (members = 0; members < 8u; members++)
|
|
write_group_slot(leader, address, members, members + 1u, 0x1000u + members);
|
|
|
|
/* EVERY count 1..8, not just the even ones. A slot is 8 bytes, so an ODD count is 8
|
|
* short of the unit and is exactly what the divisibility item made reachable: N=3, 5
|
|
* and 7 were unusable before and are the first callers this ever had. */
|
|
for (members = 1u; members <= 8u; members++) {
|
|
if (ft_tt_queue_group_read_slots(&strict, sim_xs[0], sim_ys[0], address, members,
|
|
narrow) != FT_TT_OK ||
|
|
(sim_last_read_bytes % 16u) != 0u)
|
|
every_odd_count_fits = false;
|
|
if (ft_tt_queue_group_read(&noc, sim_xs[0], sim_ys[0], address, members, wide) !=
|
|
FT_TT_OK ||
|
|
narrow[members - 1u].sequence != wide[members - 1u].sequence ||
|
|
narrow[members - 1u].token != wide[members - 1u].token)
|
|
wide_agrees_with_the_header_read = false;
|
|
}
|
|
check(every_odd_count_fits,
|
|
"every member count 1..8 reads through an accessor that ENFORCES the 16-byte unit");
|
|
check(wide_agrees_with_the_header_read,
|
|
"and the slots-only read says the same as the read that checks the magic");
|
|
|
|
/* The rounding must not be a licence to read past the block. */
|
|
check_u64((uint64_t)(((size_t)FT_TT_QUEUE_GROUP_MAX_MEMBERS *
|
|
FT_TT_QUEUE_GROUP_SLOT_WORDS * 4u + 15u) & ~(size_t)15u) +
|
|
FT_TT_QUEUE_GROUP_HEADER_WORDS * 4u,
|
|
(uint64_t)(FT_TT_QUEUE_GROUP_HEADER_WORDS * 4u +
|
|
FT_TT_QUEUE_GROUP_MAX_MEMBERS * FT_TT_QUEUE_GROUP_SLOT_WORDS * 4u),
|
|
"at the widest group the rounding adds nothing, so nothing runs off the block");
|
|
|
|
/* And the magic is what turns a wrong ADDRESS into a refusal rather than a number. */
|
|
check(ft_tt_queue_group_read(&noc, sim_xs[0], sim_ys[0], address + 0x40u, 2u, wide) ==
|
|
FT_TT_ERROR,
|
|
"a block read at the wrong address is REFUSED on the magic, not believed");
|
|
}
|
|
|
|
/**
|
|
* The COMPLETION LINE: one transaction, three assertions, and the fourth PROVABLY not
|
|
* checked.
|
|
*
|
|
* Three separate things have to hold and each has its own red arm:
|
|
* 1. the read is ONE 16-byte-aligned, 16-byte-long access — proved through the accessor
|
|
* that REFUSES anything else, the same instrument the group-slot read uses;
|
|
* 2. it agrees with the wide read on the three fields it carries;
|
|
* 3. it does NOT carry `error`, and the check beside it does not pretend to — a queue
|
|
* whose kernel set an error word AFTER publishing completion passes the line check and
|
|
* fails the wide one. That asymmetry is the entire cost of the transaction saved, and
|
|
* it is measured here rather than described in a comment.
|
|
*/
|
|
static void test_completion_line_is_one_transaction(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_noc_access strict = simulated_access();
|
|
ft_tt_queue queue;
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
uint32_t line[FT_TT_QUEUE_FIELD_COUNT];
|
|
uint32_t wide[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
simulated_core *core;
|
|
uint32_t reads_before;
|
|
|
|
printf("\nqueue: the completion LINE is one transaction and drops exactly one assertion\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(sim_xs[0], sim_ys[0]);
|
|
strict.read = l1_strict_read;
|
|
(void)ft_tt_queue_init(&noc, sim_xs[0], sim_ys[0], FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
0x7331u, &queue);
|
|
|
|
fill_nops(descriptors, 4u);
|
|
(void)ft_tt_queue_enqueue(&noc, &queue, descriptors, 4u);
|
|
{
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 64u);
|
|
}
|
|
|
|
/* (1) ONE access, through the accessor that enforces the L1 unit. */
|
|
reads_before = sim_read_calls;
|
|
check(ft_tt_queue_read_completion_line(&strict, &queue, line) == FT_TT_OK,
|
|
"the line reads through an accessor that REFUSES a non-16-byte access");
|
|
check_u64(sim_read_calls - reads_before, 1u, "and it is ONE accessor call, not two");
|
|
check_u64((uint64_t)sim_last_read_bytes, 16u, "asking for exactly 16 bytes");
|
|
|
|
check(ft_tt_queue_read_completion_wide(&strict, &queue, wide) == FT_TT_OK,
|
|
"the wide read is the same answer for twice the transactions");
|
|
check_u64((uint64_t)sim_last_read_bytes, 32u, "it asks for 32 bytes — two of them");
|
|
|
|
/* (2) They agree on the three fields the line carries. */
|
|
check(line[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] == wide[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] &&
|
|
line[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ==
|
|
wide[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] &&
|
|
line[FT_TT_QUEUE_FIELD_STATE] == wide[FT_TT_QUEUE_FIELD_STATE],
|
|
"count, token and state read the same out of one transaction as out of two");
|
|
check(ft_tt_queue_check_completion_line(&queue, line, 4u, &reason) == FT_TT_OK,
|
|
"a finished run passes the line check");
|
|
|
|
/* RED: one more descriptor than the kernel ran. */
|
|
check(ft_tt_queue_check_completion_line(&queue, line, 5u, &reason) == FT_TT_ERROR &&
|
|
reason != NULL && strstr(reason, "completed_count") != NULL,
|
|
"RED: a count short of `expected` is REFUSED by the line check");
|
|
|
|
/* RED: the host poison. */
|
|
{
|
|
uint32_t poisoned[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
memcpy(poisoned, line, sizeof(poisoned));
|
|
poisoned[FT_TT_QUEUE_FIELD_STATE] = (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON;
|
|
check(ft_tt_queue_check_completion_line(&queue, poisoned, 4u, &reason) == FT_TT_ERROR &&
|
|
reason != NULL && strstr(reason, "never ran") != NULL,
|
|
"RED: the host poison is REFUSED by the line check");
|
|
}
|
|
|
|
/* RED: a token that is not this run's last. */
|
|
{
|
|
uint32_t stale[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
memcpy(stale, line, sizeof(stale));
|
|
stale[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ^= 1u;
|
|
check(ft_tt_queue_check_completion_line(&queue, stale, 4u, &reason) == FT_TT_ERROR &&
|
|
reason != NULL && strstr(reason, "token") != NULL,
|
|
"RED: a stale token is REFUSED by the line check");
|
|
}
|
|
|
|
/* (3) THE ASYMMETRY. An error word set AFTER completion was published — the group
|
|
* post's own failure mode — is invisible to the line and visible to the wide read.
|
|
* A queue that arms the post must therefore keep the wide read, and this is the
|
|
* check that says so in a form that can fail. */
|
|
sim_l1_store_u32(core, queue.control_address + FT_TT_QUEUE_FIELD_ERROR * 4u,
|
|
(uint32_t)FT_TT_QUEUE_KERR_BAD_PARAMETERS);
|
|
check(ft_tt_queue_read_completion_line(&strict, &queue, line) == FT_TT_OK &&
|
|
ft_tt_queue_check_completion_line(&queue, line, 4u, &reason) == FT_TT_OK,
|
|
"an error word set after completion is INVISIBLE to the line check");
|
|
check(ft_tt_queue_read_completion_wide(&strict, &queue, wide) == FT_TT_OK &&
|
|
ft_tt_queue_check_completion(&queue, wide, 4u, &reason) == FT_TT_ERROR,
|
|
"and VISIBLE to the wide one — the saved transaction IS that assertion");
|
|
check_u64(line[FT_TT_QUEUE_FIELD_ERROR], (uint64_t)FT_TT_QUEUE_KERR_NONE,
|
|
"the line buffer's ERROR word is ZEROED, never read");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* The RESIDENT path's per-descriptor transaction shape */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** PCIe accesses the WORD32 accessor has issued, and the BLOCK16 one. Counted as
|
|
* `bytes / unit` where the units are issued, which is what this link is priced in — a CALL
|
|
* count cannot tell a 4-byte read from a 64-byte one, and a BYTE count cannot tell sixteen
|
|
* words from four blocks. */
|
|
static uint64_t sim_word32_reads, sim_word32_writes, sim_block16_reads, sim_block16_writes;
|
|
|
|
static int word32_read(void *user, uint16_t x, uint16_t y, uint64_t address, void *dst,
|
|
size_t bytes)
|
|
{
|
|
if ((address % 4u) != 0u || (bytes % 4u) != 0u)
|
|
return FT_TT_EINVAL;
|
|
sim_word32_reads += bytes / 4u;
|
|
return simulated_read(user, x, y, address, dst, bytes);
|
|
}
|
|
|
|
static int word32_write(void *user, uint16_t x, uint16_t y, uint64_t address, const void *src,
|
|
size_t bytes)
|
|
{
|
|
if ((address % 4u) != 0u || (bytes % 4u) != 0u)
|
|
return FT_TT_EINVAL;
|
|
sim_word32_writes += bytes / 4u;
|
|
return simulated_write(user, x, y, address, src, bytes);
|
|
}
|
|
|
|
static int block16_read(void *user, uint16_t x, uint16_t y, uint64_t address, void *dst,
|
|
size_t bytes)
|
|
{
|
|
if ((address % 16u) != 0u || (bytes % 16u) != 0u)
|
|
return FT_TT_EINVAL;
|
|
sim_block16_reads += bytes / 16u;
|
|
return simulated_read(user, x, y, address, dst, bytes);
|
|
}
|
|
|
|
static int block16_write(void *user, uint16_t x, uint16_t y, uint64_t address, const void *src,
|
|
size_t bytes)
|
|
{
|
|
/* THE SAME REFUSAL THE REAL ACCESSOR MAKES, and it is the whole reason the head bump
|
|
* cannot come through here: `ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, ...)` demands
|
|
* 16 bytes of alignment in ADDRESS and in LENGTH, and a head bump is four bytes at an
|
|
* arbitrary word. So `wide` issue is structurally two accessors, never one. */
|
|
if ((address % 16u) != 0u || (bytes % 16u) != 0u)
|
|
return FT_TT_EINVAL;
|
|
sim_block16_writes += bytes / 16u;
|
|
return simulated_write(user, x, y, address, src, bytes);
|
|
}
|
|
|
|
/**
|
|
* WHAT ONE RESIDENT DESCRIPTOR COSTS THE HOST, IN PCIe ACCESSES, ON THREE SHAPES.
|
|
*
|
|
* `pn_tt_residency_enqueue()` and `pn_tt_residency_await()` drive exactly this sequence
|
|
* through the UC / WORD32 accessor, which moves FOUR BYTES PER ACCESS. That is why the
|
|
* eltwise split's per-member await measured flat at ~58 µs whatever the width: it is
|
|
* SEVENTEEN non-posted reads and it never touched the card's work. The recorded matmul
|
|
* figures — 2.08 µs for a 64-byte parameter block, 4.84 µs for a descriptor plus a head
|
|
* bump — are the ISSUE side, which is posted stores, and were never a measurement of this.
|
|
*
|
|
* A `read_field(COUNT)` + `read_control()` 1 + 16 = 17 what ships
|
|
* B `read_field(COUNT)` + `read_completion()` 1 + 5 = 6 the SAME four agreements
|
|
* C `read_completion_line()` on BLOCK16 1 three of the four
|
|
*
|
|
* B gives up nothing: the completion WINDOW is words 5..9 and all four of rule 1's
|
|
* agreements are inside it. This test is what makes that a fact about the code rather than
|
|
* a claim in a header.
|
|
*/
|
|
static void test_resident_await_transaction_shape(void)
|
|
{
|
|
ft_tt_noc_access word32 = simulated_access();
|
|
ft_tt_noc_access block16 = simulated_access();
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
uint32_t parameters[FT_TT_QUEUE_PARAM_WORDS];
|
|
uint32_t control[FT_TT_QUEUE_FIELD_COUNT];
|
|
uint32_t window[FT_TT_QUEUE_FIELD_COUNT];
|
|
uint32_t line[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
ft_tt_queue queue;
|
|
simulated_core *core;
|
|
uint32_t address = 0u;
|
|
uint32_t value = 0u;
|
|
unsigned index;
|
|
|
|
printf("\nresident: one descriptor is 25 posted writes and SEVENTEEN non-posted reads\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(sim_xs[0], sim_ys[0]);
|
|
word32.read = word32_read;
|
|
word32.write = word32_write;
|
|
block16.read = block16_read;
|
|
(void)ft_tt_queue_init(&word32, sim_xs[0], sim_ys[0], FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
0x51D3u, &queue);
|
|
|
|
/* THE ISSUE SIDE, in the order and the calls pn_tt_residency_enqueue() makes. */
|
|
for (index = 0; index < FT_TT_QUEUE_PARAM_WORDS; index++)
|
|
parameters[index] = 0xA5000000u + index;
|
|
sim_word32_writes = 0u;
|
|
check(ft_tt_queue_write_op_parameters(&word32, &queue, 0u, parameters, &address) == FT_TT_OK,
|
|
"the 64-byte parameter block writes");
|
|
check_u64(sim_word32_writes, 16u, "and it is SIXTEEN word accesses, not one");
|
|
sim_word32_writes = 0u;
|
|
fill_nops(descriptors, 1u);
|
|
descriptors[0].a0 = address;
|
|
check(ft_tt_queue_enqueue(&word32, &queue, descriptors, 1u) == FT_TT_OK,
|
|
"the 32-byte descriptor and the 4-byte head bump write");
|
|
check_u64(sim_word32_writes, 9u, "eight words of ring and one of head");
|
|
|
|
{
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
model_kernel_run(core, 64u);
|
|
}
|
|
|
|
/* SHAPE A — what ships. The poll, then the whole 64-byte control block. */
|
|
sim_word32_reads = 0u;
|
|
check(ft_tt_queue_read_field(&word32, &queue, FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value) ==
|
|
FT_TT_OK &&
|
|
value >= 1u,
|
|
"the poll sees the descriptor complete");
|
|
check(ft_tt_queue_read_control(&word32, &queue, control) == FT_TT_OK,
|
|
"and the control block reads");
|
|
check_u64(sim_word32_reads, 17u,
|
|
"SHAPE A: the shipped await is SEVENTEEN non-posted reads a descriptor");
|
|
check(ft_tt_queue_check_completion(&queue, control, 1u, &reason) == FT_TT_OK,
|
|
"and all four agreements hold");
|
|
|
|
/* SHAPE B — the completion WINDOW. Same accessor, same verdict, six accesses. */
|
|
sim_word32_reads = 0u;
|
|
check(ft_tt_queue_read_field(&word32, &queue, FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value) ==
|
|
FT_TT_OK,
|
|
"the same poll");
|
|
check(ft_tt_queue_read_completion(&word32, &queue, window) == FT_TT_OK,
|
|
"and the completion window reads");
|
|
check_u64(sim_word32_reads, 6u, "SHAPE B: the window is SIX — eleven accesses removed");
|
|
check(ft_tt_queue_check_completion(&queue, window, 1u, &reason) == FT_TT_OK,
|
|
"the FOUR-agreement check still runs, because ERROR is word 9 and inside it");
|
|
check(window[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] ==
|
|
control[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] &&
|
|
window[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ==
|
|
control[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] &&
|
|
window[FT_TT_QUEUE_FIELD_STATE] == control[FT_TT_QUEUE_FIELD_STATE] &&
|
|
window[FT_TT_QUEUE_FIELD_ERROR] == control[FT_TT_QUEUE_FIELD_ERROR],
|
|
"and it is the SAME four words the control block carried");
|
|
|
|
/* SHAPE C — the line, on the accessor that enforces the 16-byte L1 unit. */
|
|
sim_block16_reads = 0u;
|
|
check(ft_tt_queue_completion_line_is_addressable(&queue),
|
|
"the queue's line is addressable through a BLOCK16 accessor");
|
|
check(ft_tt_queue_read_completion_line(&block16, &queue, line) == FT_TT_OK,
|
|
"the line reads");
|
|
check_u64(sim_block16_reads, 1u, "SHAPE C: ONE access — the poll IS the check");
|
|
check(ft_tt_queue_check_completion_line(&queue, line, 1u, &reason) == FT_TT_OK,
|
|
"and the three agreements it carries hold");
|
|
|
|
/* RED, on every shape: a stale token must be refused, or none of the above is a check. */
|
|
{
|
|
uint32_t stale[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
memcpy(stale, control, sizeof(stale));
|
|
stale[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ^= 1u;
|
|
check(ft_tt_queue_check_completion(&queue, stale, 1u, &reason) == FT_TT_ERROR,
|
|
"RED: shape A refuses a stale token");
|
|
memcpy(stale, window, sizeof(stale));
|
|
stale[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ^= 1u;
|
|
check(ft_tt_queue_check_completion(&queue, stale, 1u, &reason) == FT_TT_ERROR,
|
|
"RED: shape B refuses a stale token");
|
|
memcpy(stale, line, sizeof(stale));
|
|
stale[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] ^= 1u;
|
|
check(ft_tt_queue_check_completion_line(&queue, stale, 1u, &reason) == FT_TT_ERROR,
|
|
"RED: shape C refuses a stale token");
|
|
}
|
|
|
|
/* RED: shape B is not vacuous about ERROR. An error word set AFTER completion — the one
|
|
* state the line cannot see — must still be refused by the window, because keeping that
|
|
* fourth agreement is the whole argument for making B unconditional. */
|
|
sim_l1_store_u32(core, queue.control_address + FT_TT_QUEUE_FIELD_ERROR * 4u,
|
|
(uint32_t)FT_TT_QUEUE_KERR_BAD_PARAMETERS);
|
|
check(ft_tt_queue_read_completion(&word32, &queue, window) == FT_TT_OK &&
|
|
ft_tt_queue_check_completion(&queue, window, 1u, &reason) == FT_TT_ERROR,
|
|
"RED: the completion WINDOW still sees an error word the line cannot");
|
|
sim_l1_store_u32(core, queue.control_address + FT_TT_QUEUE_FIELD_ERROR * 4u,
|
|
(uint32_t)FT_TT_QUEUE_KERR_NONE);
|
|
|
|
/* RED: a queue the fast path CANNOT reach has to say so before it is used. A word-aligned
|
|
* control address is legal to `ft_tt_queue_init()` and fatal to a BLOCK16 read, and on
|
|
* the resident path that failure arrives as a completion that never completes. */
|
|
{
|
|
ft_tt_queue misplaced = queue;
|
|
|
|
misplaced.control_address = FT_TT_OP_QUEUE_BASE + 4u;
|
|
check(!ft_tt_queue_completion_line_is_addressable(&misplaced),
|
|
"RED: a word-aligned control block is REFUSED as line-addressable");
|
|
check(ft_tt_queue_read_completion_line(&block16, &misplaced, line) != FT_TT_OK,
|
|
"RED: and the BLOCK16 accessor really does refuse it");
|
|
misplaced.control_address = FT_TT_OP_QUEUE_BASE + 16u;
|
|
check(ft_tt_queue_completion_line_is_addressable(&misplaced),
|
|
"while a 16-byte-aligned one is accepted — the guard is not a constant false");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WHAT ONE RESIDENT DESCRIPTOR COSTS THE HOST TO *ISSUE*, IN PCIe ACCESSES, ON TWO SHAPES —
|
|
* AND WHETHER THE ORDERING THE SECOND SHAPE PUTS AT RISK IS CHECKED.
|
|
*
|
|
* The await ladder above took the READ half from seventeen accesses to one. The write half is
|
|
* the same arithmetic on the same link: `pn_tt_residency_enqueue()` writes a 64-byte parameter
|
|
* block, a 32-byte descriptor and a 4-byte head bump, and through the UC / WORD32 accessor
|
|
* that is **25 posted accesses**. The parameter block and the ring slot are both 16-byte
|
|
* aligned in address AND length, so both fit the BLOCK16 L1 accessor; the head bump is four
|
|
* bytes at an arbitrary word and does not. So:
|
|
*
|
|
* word 16 + 8 + 1 = 25 accesses, ONE accessor
|
|
* wide 4 + 2 = 6 BLOCK16 accesses, PLUS 1 WORD32 head bump = **7**
|
|
*
|
|
* **AND THE SPLIT IS THE WHOLE RISK.** `wide` puts the descriptor on the write-combined
|
|
* window and the head bump on the uncached one, and "descriptors before head" is one of the
|
|
* two orderings this protocol rests on (see ft_tt_queue.h). If the head lands first the kernel
|
|
* executes whatever the ring slot held — a *previous* descriptor, which is well-formed and
|
|
* carries a STALE TOKEN. That is not a hang and not a refusal on the card; it is a wrong
|
|
* answer, and the only thing standing between it and the model is the four-agreement check.
|
|
*
|
|
* So the last part of this test does not check arithmetic at all. It publishes a head for a
|
|
* descriptor that was never written — the exact inversion — and requires the check to REFUSE
|
|
* and to NAME the token. A `wide` arm that runs clean on silicon is evidence only because
|
|
* this is what happens when it does not.
|
|
*/
|
|
static void test_resident_issue_transaction_shape(void)
|
|
{
|
|
ft_tt_noc_access word32 = simulated_access();
|
|
ft_tt_noc_access block16 = simulated_access();
|
|
ft_tt_queue_descriptor descriptors[TEST_CAPACITY];
|
|
uint32_t parameters[FT_TT_QUEUE_PARAM_WORDS];
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
ft_tt_queue queue;
|
|
simulated_core *core;
|
|
uint32_t address = 0u;
|
|
unsigned index;
|
|
|
|
printf("\nresident: the ISSUE side is 25 posted accesses, and SEVEN on the wide shape\n");
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(sim_xs[0], sim_ys[0]);
|
|
word32.read = word32_read;
|
|
word32.write = word32_write;
|
|
block16.read = block16_read;
|
|
block16.write = block16_write;
|
|
(void)ft_tt_queue_init(&word32, sim_xs[0], sim_ys[0], FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
0x1550u, &queue);
|
|
for (index = 0; index < FT_TT_QUEUE_PARAM_WORDS; index++)
|
|
parameters[index] = 0x5E000000u + index;
|
|
{
|
|
uint32_t args[FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
|
|
(void)ft_tt_queue_kernel_args(&queue, sim_binding(TEST_IDLE_LIMIT), args);
|
|
model_kernel_start(core, args);
|
|
}
|
|
|
|
/* SHAPE `word` — every write through the UC accessor, which is what ships. */
|
|
sim_word32_writes = 0u;
|
|
sim_block16_writes = 0u;
|
|
check(ft_tt_queue_write_op_parameters(&word32, &queue, 0u, parameters, &address) == FT_TT_OK,
|
|
"word: the 64-byte parameter block writes");
|
|
fill_nops(descriptors, 1u);
|
|
descriptors[0].a0 = address;
|
|
check(ft_tt_queue_stage(&word32, &queue, descriptors, 1u) == FT_TT_OK,
|
|
"word: the 32-byte descriptor stages");
|
|
check(ft_tt_queue_publish(&word32, &queue) == FT_TT_OK, "word: the head bump publishes it");
|
|
check_u64(sim_word32_writes, 25u, "word: 16 params + 8 ring + 1 head = TWENTY-FIVE accesses");
|
|
check_u64(sim_block16_writes, 0u, "word: and nothing went wide");
|
|
model_kernel_run(core, 64u);
|
|
check(ft_tt_queue_read_completion(&word32, &queue, fields) == FT_TT_OK &&
|
|
ft_tt_queue_check_completion(&queue, fields, 1u, &reason) == FT_TT_OK,
|
|
"word: the descriptor completes and all four agreements hold");
|
|
|
|
/* SHAPE `wide` — the two 16-byte-aligned writes through BLOCK16, the head bump through
|
|
* WORD32 because nothing else can carry it. Same bytes, same order, same L1. */
|
|
sim_word32_writes = 0u;
|
|
sim_block16_writes = 0u;
|
|
check(ft_tt_queue_write_op_parameters(&block16, &queue, 0u, parameters, &address) == FT_TT_OK,
|
|
"wide: the parameter block writes through the BLOCK16 accessor");
|
|
check_u64(sim_block16_writes, 4u, "wide: 64 bytes is FOUR accesses, not sixteen");
|
|
fill_nops(descriptors, 1u);
|
|
descriptors[0].a0 = address;
|
|
check(ft_tt_queue_stage(&block16, &queue, descriptors, 1u) == FT_TT_OK,
|
|
"wide: the descriptor stages through the same accessor");
|
|
check_u64(sim_block16_writes, 6u, "wide: 32 bytes is TWO more");
|
|
check(ft_tt_queue_publish(&word32, &queue) == FT_TT_OK,
|
|
"wide: the head bump still goes through WORD32");
|
|
check_u64(sim_word32_writes, 1u, "wide: ONE word access, and it is the head");
|
|
check_u64(sim_block16_writes + sim_word32_writes, 7u, "wide: SEVEN accesses against 25");
|
|
model_kernel_run(core, 64u);
|
|
check(ft_tt_queue_read_completion(&word32, &queue, fields) == FT_TT_OK &&
|
|
ft_tt_queue_check_completion(&queue, fields, 2u, &reason) == FT_TT_OK,
|
|
"wide: the SAME four agreements hold — the bytes that landed are the same bytes");
|
|
|
|
/* RED: the head bump CANNOT go wide. A four-byte write at a word address is refused by
|
|
* the 16-byte L1 rule, so the two-accessor split is structural and not a convention —
|
|
* a caller that tried to put the whole issue on one wide accessor gets an error, not a
|
|
* silently different transaction. */
|
|
check(ft_tt_queue_write_head(&block16, &queue, queue.head) != FT_TT_OK,
|
|
"RED: the BLOCK16 accessor REFUSES a 4-byte head bump");
|
|
/* AND THE STAND-IN IS TIED TO THE SHIPPED RULE. The refusal above comes from this file's
|
|
* simulated accessor; this asks the function the real one calls, so the two cannot drift
|
|
* and the structural claim is not a property of the test. */
|
|
check(ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false,
|
|
queue.control_address + FT_TT_QUEUE_FIELD_HEAD * 4u,
|
|
4u) != FT_TT_OK,
|
|
"RED: and ft_tt_noc_check_transfer() refuses it for the same reason on the card");
|
|
check(ft_tt_queue_issue_is_wide_addressable(&queue),
|
|
"while the parameter blocks and the ring slot BOTH fit it — the guard is not a "
|
|
"constant false");
|
|
{
|
|
/* RED: a queue the wide shape cannot reach has to SAY SO before it is used. A
|
|
* word-aligned control block is legal to ft_tt_queue_init() and is refused by every
|
|
* 16-byte write, so a caller that armed `wide` on one would fail on its first
|
|
* descriptor instead of falling back. */
|
|
ft_tt_queue misplaced = queue;
|
|
|
|
misplaced.control_address = FT_TT_OP_QUEUE_BASE + 4u;
|
|
misplaced.ring_address = misplaced.control_address + FT_TT_QUEUE_RING_OFFSET;
|
|
check(!ft_tt_queue_issue_is_wide_addressable(&misplaced),
|
|
"RED: a word-aligned control block is REFUSED as wide-issue addressable");
|
|
}
|
|
|
|
/* RED — THE ORDERING, WHICH IS THE ONLY THING `wide` PUTS AT RISK.
|
|
*
|
|
* The realistic inversion is not "the slot is empty" — it is "the slot still holds the
|
|
* descriptor that occupied it one lap of the ring ago", which is WELL FORMED and executes
|
|
* cleanly. So run the ring all the way round first, and only then publish a head for a
|
|
* descriptor that was never written. The card then completes a legal descriptor, the
|
|
* COUNT is exactly what the host expected, and the ONLY thing that separates that from a
|
|
* correct token is the per-index nonce. If this reads green, a clean `wide` arm on silicon
|
|
* means nothing. */
|
|
while (queue.issued < TEST_CAPACITY) {
|
|
fill_nops(descriptors, 1u);
|
|
descriptors[0].a0 = address;
|
|
check(ft_tt_queue_enqueue(&word32, &queue, descriptors, 1u) == FT_TT_OK,
|
|
"the ring runs one full lap, so slot 0 holds a well-formed old descriptor");
|
|
model_kernel_run(core, 64u);
|
|
}
|
|
{
|
|
const uint32_t expected = queue.issued + 1u;
|
|
|
|
check(ft_tt_queue_write_head(&word32, &queue, queue.head + 1u) == FT_TT_OK,
|
|
"RED: a head is published for a descriptor that was never written");
|
|
model_kernel_run(core, 64u);
|
|
check(ft_tt_queue_read_completion(&word32, &queue, fields) == FT_TT_OK,
|
|
"RED: and the card completed something");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], expected,
|
|
"RED: the COUNT is exactly what the host expected — it catches nothing");
|
|
check_u64(fields[FT_TT_QUEUE_FIELD_ERROR], (uint64_t)FT_TT_QUEUE_KERR_NONE,
|
|
"RED: and the kernel reports NO error — a stale slot is a legal descriptor");
|
|
check(ft_tt_queue_check_completion(&queue, fields, expected, &reason) == FT_TT_ERROR &&
|
|
reason != NULL && strstr(reason, "token") != NULL,
|
|
"RED: the four-agreement check REFUSES it and the TOKEN is what caught it");
|
|
}
|
|
}
|
|
|
|
static void test_group_completion_needs_every_member(void)
|
|
{
|
|
const ft_tt_grid_info grid = p150a_grid();
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_group_member_completion members[2];
|
|
const uint32_t address = FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET;
|
|
const uint32_t token = 0xA1B2C3D4u;
|
|
const char *reason = NULL;
|
|
uint32_t short_slot = 0xFFFFFFFFu;
|
|
simulated_core *leader;
|
|
unsigned slot;
|
|
|
|
printf("\ngroup: completion is per GROUP now, and one member behind refuses it\n");
|
|
|
|
(void)ft_tt_group_partition(&grid, 0u, 2u, 1u, partition_groups, partition_placements,
|
|
partition_queues, PARTITION_STORAGE);
|
|
{
|
|
uint16_t xs[2];
|
|
uint16_t ys[2];
|
|
|
|
for (slot = 0; slot < 2u; slot++) {
|
|
const ft_tt_launch_core_placement *placement =
|
|
ft_tt_launch_core_set_at(&partition_groups[0].cores, slot);
|
|
xs[slot] = placement->x;
|
|
ys[slot] = placement->y;
|
|
}
|
|
sim_reset(xs, ys, 2u);
|
|
leader = sim_find(xs[0], ys[0]);
|
|
}
|
|
|
|
check(ft_tt_group_completion_init(&noc, &partition_groups[0], address) == FT_TT_OK,
|
|
"the completion block writes on the group's FIRST core");
|
|
check_u64(partition_groups[0].completion_address, address,
|
|
"and the group remembers where it put it");
|
|
check_u64(sim_l1_u32(leader, address + FT_TT_QUEUE_GROUP_MAGIC * 4u),
|
|
FT_TT_QUEUE_GROUP_MAGIC_VALUE, "the block carries its magic");
|
|
check_u64(sim_l1_u32(leader, address + FT_TT_QUEUE_GROUP_MEMBERS * 4u), 2u,
|
|
"and the live member count, not a constant");
|
|
|
|
/* Nothing posted yet. */
|
|
check(ft_tt_group_completion_read(&noc, &partition_groups[0], members, 2u) == FT_TT_OK,
|
|
"the block reads back in one access");
|
|
check(ft_tt_group_check_completion(&partition_groups[0], members, 2u, 1u, token, &reason,
|
|
&short_slot) == FT_TT_ERROR,
|
|
"with nothing posted, sequence 1 is REFUSED");
|
|
check(reason != NULL && strstr(reason, "has not reached") != NULL,
|
|
"and the reason is that a member has not reached it");
|
|
|
|
/* ONE member posts. This is the case a SUM would pass and a slot array must not. */
|
|
write_group_slot(leader, address, 0u, 1u, token);
|
|
(void)ft_tt_group_completion_read(&noc, &partition_groups[0], members, 2u);
|
|
check_u64(members[0].sequence, 1u, "member 0's slot carries the sequence it posted");
|
|
check_u64(members[1].sequence, 0u, "and member 1's is untouched");
|
|
check(ft_tt_group_check_completion(&partition_groups[0], members, 2u, 1u, token, &reason,
|
|
&short_slot) == FT_TT_ERROR,
|
|
"ONE member at sequence 1 is still REFUSED — a sum would have passed here");
|
|
check_u64(short_slot, 1u, "and it names WHICH member is behind");
|
|
|
|
/* Both post. */
|
|
write_group_slot(leader, address, 1u, 1u, token);
|
|
(void)ft_tt_group_completion_read(&noc, &partition_groups[0], members, 2u);
|
|
check(ft_tt_group_check_completion(&partition_groups[0], members, 2u, 1u, token, &reason,
|
|
&short_slot) == FT_TT_OK,
|
|
"with BOTH members at sequence 1, it passes");
|
|
|
|
/* A stale block from a previous run: a large enough sequence, the wrong token. This is
|
|
* the case the sequence alone cannot refuse. */
|
|
write_group_slot(leader, address, 0u, 99u, 0xDEADBEEFu);
|
|
write_group_slot(leader, address, 1u, 99u, 0xDEADBEEFu);
|
|
(void)ft_tt_group_completion_read(&noc, &partition_groups[0], members, 2u);
|
|
check(ft_tt_group_check_completion(&partition_groups[0], members, 2u, 1u, token, &reason,
|
|
&short_slot) == FT_TT_ERROR,
|
|
"a residue with a HIGHER sequence and the wrong token is refused");
|
|
check(reason != NULL && strstr(reason, "token") != NULL,
|
|
"and it refuses BECAUSE of the token, not the sequence");
|
|
|
|
/* A block at the wrong address has no magic, and that is a refusal rather than a
|
|
* plausible number. Zeroed storage answers the same as absent storage (dev notes §3),
|
|
* so the magic is what makes "you read the wrong place" visible. */
|
|
sim_l1_store_u32(leader, address + FT_TT_QUEUE_GROUP_MAGIC * 4u, 0u);
|
|
check(ft_tt_group_completion_read(&noc, &partition_groups[0], members, 2u) == FT_TT_ERROR,
|
|
"a block without the magic is REFUSED, not read as zeros");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 7. Per-processor status, and the retirement it makes honest */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* With five processors on one queue, the control block's `state` word has ONE writer, so a
|
|
* reader that has exited while the compute thread is still spinning out a bound reads EXIT
|
|
* — and a shutdown that believed it would relaunch a second kernel onto a core that still
|
|
* has one running. The status array is what makes "retired" mean all five.
|
|
*/
|
|
static void test_status_says_which_processor(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
const uint32_t triad = (1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) |
|
|
(1u << FT_TT_PROC_MATH0) | (1u << FT_TT_PROC_MATH1) |
|
|
(1u << FT_TT_PROC_MATH2);
|
|
uint32_t status[FT_TT_QUEUE_STATUS_WORDS];
|
|
uint32_t pending = 0xFFFFFFFFu;
|
|
simulated_core *core;
|
|
unsigned processor;
|
|
|
|
printf("\nstatus: 'the core exited' is not the same as 'all five exited'\n");
|
|
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x7777u, &queue);
|
|
|
|
/* The control block says EXIT — which is exactly the lie a triad can tell. */
|
|
sim_set_control(core, FT_TT_QUEUE_FIELD_STATE, FT_TT_QUEUE_STATE_EXIT);
|
|
check(ft_tt_queue_all_processors_exited(&noc, &queue, 0u, &pending) == FT_TT_OK,
|
|
"with NO mask, the control block is the whole answer (the BRISC-only kernel)");
|
|
check(ft_tt_queue_all_processors_exited(&noc, &queue, triad, &pending) == FT_TT_EIO,
|
|
"with the TRIAD mask, that same EXIT is NOT enough");
|
|
check_u64(pending, triad, "and every processor is still pending");
|
|
|
|
/* Retire four of the five. */
|
|
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
|
|
if (processor == FT_TT_PROC_MATH0)
|
|
continue;
|
|
sim_l1_store_u32(core,
|
|
FT_TT_QUEUE_STATUS_ADDRESS(queue.control_address, processor) +
|
|
FT_TT_QUEUE_STATUS_STATE * 4u,
|
|
FT_TT_QUEUE_STATE_EXIT);
|
|
}
|
|
check(ft_tt_queue_all_processors_exited(&noc, &queue, triad, &pending) == FT_TT_EIO,
|
|
"four of five is still not retired");
|
|
check_u64(pending, 1u << FT_TT_PROC_MATH0, "and the pending mask names the ONE that is not");
|
|
|
|
sim_l1_store_u32(core,
|
|
FT_TT_QUEUE_STATUS_ADDRESS(queue.control_address, FT_TT_PROC_MATH0) +
|
|
FT_TT_QUEUE_STATUS_STATE * 4u,
|
|
FT_TT_QUEUE_STATE_EXIT);
|
|
check(ft_tt_queue_all_processors_exited(&noc, &queue, triad, &pending) == FT_TT_OK,
|
|
"all five at EXIT is retired");
|
|
|
|
/* The status slot is readable as a block, and it is per PROCESSOR — two processors must
|
|
* not share one, or a heartbeat from one would look like liveness from the other. */
|
|
sim_l1_store_u32(core,
|
|
FT_TT_QUEUE_STATUS_ADDRESS(queue.control_address, FT_TT_PROC_MATH2) +
|
|
FT_TT_QUEUE_STATUS_CURSOR * 4u,
|
|
42u);
|
|
(void)ft_tt_queue_read_status(&noc, &queue, FT_TT_PROC_MATH2, status);
|
|
check_u64(status[FT_TT_QUEUE_STATUS_CURSOR], 42u, "a status slot reads back per processor");
|
|
(void)ft_tt_queue_read_status(&noc, &queue, FT_TT_PROC_MATH1, status);
|
|
check_u64(status[FT_TT_QUEUE_STATUS_CURSOR], 0u, "and its neighbour is unaffected");
|
|
check(ft_tt_queue_read_status(&noc, &queue, FT_TT_PROC_COUNT, status) == FT_TT_EINVAL,
|
|
"a processor index off the end is refused");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 8. The matmul parameter block */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void test_matmul_parameter_blocks(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
uint32_t words[FT_TT_QUEUE_MATMUL_WORDS];
|
|
uint32_t first = 0u;
|
|
uint32_t second = 0u;
|
|
simulated_core *core;
|
|
unsigned index;
|
|
|
|
printf("\nmatmul params: a shape that does not fit six descriptor words gets a pointer\n");
|
|
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(1u, 2u);
|
|
(void)ft_tt_queue_init(&noc, 1u, 2u, FT_TT_OP_QUEUE_BASE, TEST_CAPACITY, 0x8888u, &queue);
|
|
|
|
for (index = 0; index < FT_TT_QUEUE_MATMUL_WORDS; index++)
|
|
words[index] = 0x1000u + index;
|
|
|
|
check(ft_tt_queue_write_matmul_parameters(&noc, &queue, 0u, words, &first) == FT_TT_OK,
|
|
"a parameter block writes");
|
|
check(ft_tt_queue_write_matmul_parameters(&noc, &queue, 1u, words, &second) == FT_TT_OK,
|
|
"and a second one writes");
|
|
check_u64(second - first, FT_TT_QUEUE_MATMUL_PARAM_BYTES,
|
|
"consecutive slots are one block apart, so a batch can be pipelined");
|
|
check(first >= queue.control_address + FT_TT_QUEUE_MATMUL_PARAM_OFFSET,
|
|
"and they land in the parameter region, clear of the ring");
|
|
check_u64(sim_l1_u32(core, first + FT_TT_QUEUE_MATMUL_TILE_INNER * 4u), 0x1001u,
|
|
"the words arrive in the order the kernel indexes them");
|
|
check(ft_tt_queue_write_matmul_parameters(&noc, &queue, FT_TT_QUEUE_MATMUL_PARAM_SLOTS, words,
|
|
&first) == FT_TT_EINVAL,
|
|
"a slot past the end is refused rather than written over the group block");
|
|
}
|
|
|
|
/**
|
|
* The parameter CACHE: a second region whose blocks are pointed at, not rewritten.
|
|
*
|
|
* What has to hold is geometric and it is the whole safety argument — the cache must not
|
|
* overlap the rotating slots (which ARE rewritten, every few descriptors) and must not
|
|
* overlap the group block. Both ends are checked against a real write here as well as by
|
|
* the static asserts, because an assert on a macro cannot catch a function that computes
|
|
* the address a second way.
|
|
*/
|
|
static void test_parameter_cache_is_a_disjoint_region(void)
|
|
{
|
|
ft_tt_noc_access noc = simulated_access();
|
|
ft_tt_queue queue;
|
|
uint32_t words[FT_TT_QUEUE_PARAM_WORDS];
|
|
uint32_t rotating = 0u;
|
|
uint32_t cached = 0u;
|
|
uint32_t last = 0u;
|
|
simulated_core *core;
|
|
unsigned index;
|
|
|
|
printf("\nmatmul params: the CACHE region is disjoint from the slots and the group block\n");
|
|
|
|
sim_reset(sim_xs, sim_ys, 1u);
|
|
core = sim_find(sim_xs[0], sim_ys[0]);
|
|
(void)ft_tt_queue_init(&noc, sim_xs[0], sim_ys[0], FT_TT_OP_QUEUE_BASE, TEST_CAPACITY,
|
|
0x9999u, &queue);
|
|
|
|
for (index = 0; index < FT_TT_QUEUE_PARAM_WORDS; index++)
|
|
words[index] = 0x2000u + index;
|
|
|
|
check(ft_tt_queue_write_matmul_parameters(
|
|
&noc, &queue, FT_TT_QUEUE_MATMUL_PARAM_SLOTS - 1u, words, &rotating) == FT_TT_OK,
|
|
"the LAST rotating slot writes");
|
|
check(ft_tt_queue_write_param_cache(&noc, &queue, 0u, words, &cached) == FT_TT_OK,
|
|
"and cache slot 0 writes");
|
|
check(cached >= rotating + FT_TT_QUEUE_MATMUL_PARAM_BYTES,
|
|
"the cache begins AFTER the last rotating slot ends — a cached block cannot be "
|
|
"rewritten by the slot rotation");
|
|
|
|
check(ft_tt_queue_write_param_cache(&noc, &queue, FT_TT_QUEUE_PARAM_CACHE_SLOTS - 1u, words,
|
|
&last) == FT_TT_OK,
|
|
"the LAST cache slot writes");
|
|
check(last + FT_TT_QUEUE_MATMUL_PARAM_BYTES <=
|
|
queue.control_address + FT_TT_QUEUE_GROUP_OFFSET,
|
|
"and it ends at or before the group completion block");
|
|
check(ft_tt_queue_write_param_cache(&noc, &queue, FT_TT_QUEUE_PARAM_CACHE_SLOTS, words,
|
|
&last) == FT_TT_EINVAL,
|
|
"a cache slot past the end is REFUSED, not written over the group block");
|
|
|
|
check_u64(sim_l1_u32(core, cached + FT_TT_QUEUE_MATMUL_TILE_INNER * 4u), 0x2001u,
|
|
"the cached words arrive in the order the kernel indexes them");
|
|
/* The magic of the group block is what a run-off would destroy, so prove it survives
|
|
* the widest cache write this region admits. */
|
|
check(ft_tt_queue_group_init(&noc, sim_xs[0], sim_ys[0],
|
|
FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET, 8u) == FT_TT_OK,
|
|
"a group block writes above the cache");
|
|
(void)ft_tt_queue_write_param_cache(&noc, &queue, FT_TT_QUEUE_PARAM_CACHE_SLOTS - 1u, words,
|
|
&last);
|
|
check_u64(sim_l1_u32(core, FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET),
|
|
FT_TT_QUEUE_GROUP_MAGIC_VALUE,
|
|
"and the last cache write leaves the group magic intact");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("Phase 5 work queue and core groups — off-device\n");
|
|
|
|
test_queue_init_writes_a_recognisable_block();
|
|
test_enqueue_is_one_write_plus_a_head_bump();
|
|
test_enqueue_wraps_the_ring();
|
|
test_completion_cannot_report_work_that_never_ran();
|
|
test_completion_rejects_a_stale_token();
|
|
test_two_runs_cannot_share_a_token();
|
|
test_stop_request_retires_a_blocked_kernel();
|
|
test_stop_sentinel_is_ordered_with_the_work();
|
|
test_bounded_residency();
|
|
test_partition_is_derived_from_the_live_grid();
|
|
test_partition_on_a_synthetic_twelve_column_part();
|
|
test_partition_refusals_and_disjointness();
|
|
test_watchdog_reports_alive_but_blocked();
|
|
test_group_completion_needs_every_member();
|
|
test_group_slots_read_fits_the_l1_unit();
|
|
test_completion_line_is_one_transaction();
|
|
test_resident_await_transaction_shape();
|
|
test_resident_issue_transaction_shape();
|
|
test_status_says_which_processor();
|
|
test_matmul_parameter_blocks();
|
|
test_parameter_cache_is_a_disjoint_region();
|
|
|
|
printf("\n%d/%d checks failed\n", checks_failed, checks_run);
|
|
return checks_failed == 0 ? 0 : 1;
|
|
}
|