333 lines
16 KiB
C
333 lines
16 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* test_resident_multicast_args.c — the RESIDENT cooperative matmul's host half, OFF
|
|
* HARDWARE: the plan, the packed rectangle word the kernel unpacks, the per-member runtime
|
|
* args, and the order the three rendezvous words are staged in.
|
|
*
|
|
* Everything here is arithmetic and three L1 writes, and all of it is protecting against
|
|
* failures that are silent on silicon:
|
|
*
|
|
* - a packed rectangle whose fields land on the wrong bits aims a multicast at a
|
|
* DIFFERENT live endpoint, which on this board is a hung NoC and a walk to the machine;
|
|
* - a receiver handed the leader's geometry is a receiver one bug away from broadcasting;
|
|
* - a destination total that disagrees with the receiver count makes the leader's write
|
|
* barrier either hang or return while a payload is still landing, and neither shows up
|
|
* as a wrong answer;
|
|
* - staging the leader's ready semaphore BEFORE a receiver's arrival word leaves a window
|
|
* in which the leader starts a block into a member that has not cleared its own.
|
|
*
|
|
* The packed word is checked against LITERAL expected values computed by hand from the
|
|
* field widths, not against `ftcoop_pack_rectangle()` run backwards. Two implementations
|
|
* that agree with each other and disagree with the hardware is exactly the failure a round
|
|
* trip cannot see.
|
|
*
|
|
* Build and run:
|
|
* make -f Makefile.broadcastpartition check
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "ft_tt_op_matmul_resident_multicast.h"
|
|
#include "ft_tt_test_report.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
/** Every write the reset made, in order, so the ORDER can be asserted. A test that only
|
|
* reads the words back afterwards is vacuous about the one property that matters: they
|
|
* end up identical whatever order they were written in. */
|
|
#define RECORDER_CAPACITY 64u
|
|
|
|
typedef struct write_recorder {
|
|
struct {
|
|
uint16_t x;
|
|
uint16_t y;
|
|
uint64_t address;
|
|
uint32_t value;
|
|
} entry[RECORDER_CAPACITY];
|
|
unsigned count;
|
|
int fail_after; /**< -1 never; otherwise the write index that returns an error */
|
|
} write_recorder;
|
|
|
|
static int recorder_write(void *user, uint16_t x, uint16_t y, uint64_t address, const void *src,
|
|
size_t bytes)
|
|
{
|
|
write_recorder *recorder = (write_recorder *)user;
|
|
|
|
if (recorder->fail_after >= 0 && (int)recorder->count >= recorder->fail_after)
|
|
return FT_TT_ERROR;
|
|
if (recorder->count >= RECORDER_CAPACITY || bytes != sizeof(uint32_t))
|
|
return FT_TT_ERROR;
|
|
recorder->entry[recorder->count].x = x;
|
|
recorder->entry[recorder->count].y = y;
|
|
recorder->entry[recorder->count].address = address;
|
|
memcpy(&recorder->entry[recorder->count].value, src, sizeof(uint32_t));
|
|
recorder->count++;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static ft_tt_noc_coord coord(uint32_t x, uint32_t y)
|
|
{
|
|
ft_tt_noc_coord result;
|
|
|
|
result.x = x;
|
|
result.y = y;
|
|
return result;
|
|
}
|
|
|
|
static void fill_block(ft_tt_noc_coord *out, uint32_t x, uint32_t y, uint32_t rows,
|
|
uint32_t columns)
|
|
{
|
|
uint32_t row;
|
|
uint32_t column;
|
|
|
|
for (row = 0u; row < rows; row++)
|
|
for (column = 0u; column < columns; column++)
|
|
out[row * columns + column] = coord(x + column, y + row);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void check_the_packed_rectangle_word(void)
|
|
{
|
|
uint32_t packed;
|
|
|
|
ft_tt_test_step("the packed rectangle word, against LITERAL expected bits");
|
|
|
|
/* (13,7)..(16,7) with three destinations:
|
|
* x_low 13 -> bits 0..5 = 0x0000000D
|
|
* y_low 7 -> bits 6..11 = 0x000001C0
|
|
* x_high 16 -> bits 12..17 = 0x00010000
|
|
* y_high 7 -> bits 18..23 = 0x001C0000
|
|
* count 3 -> bits 24..31 = 0x03000000
|
|
* -----------------------------------------
|
|
* 0x031D01CD */
|
|
packed = ftcoop_pack_rectangle(13u, 7u, 16u, 7u, 3u);
|
|
ft_tt_test_check_fmt("(13,7)..(16,7) x3 packs to 0x031D01CD", packed == 0x031D01CDu,
|
|
"0x%08X", packed);
|
|
ft_tt_test_check_fmt("x_low unpacks", ftcoop_rectangle_x_low(packed) == 13u, "%u",
|
|
ftcoop_rectangle_x_low(packed));
|
|
ft_tt_test_check_fmt("y_low unpacks", ftcoop_rectangle_y_low(packed) == 7u, "%u",
|
|
ftcoop_rectangle_y_low(packed));
|
|
ft_tt_test_check_fmt("x_high unpacks", ftcoop_rectangle_x_high(packed) == 16u, "%u",
|
|
ftcoop_rectangle_x_high(packed));
|
|
ft_tt_test_check_fmt("y_high unpacks", ftcoop_rectangle_y_high(packed) == 7u, "%u",
|
|
ftcoop_rectangle_y_high(packed));
|
|
ft_tt_test_check_fmt("the destination count unpacks",
|
|
ftcoop_rectangle_destinations(packed) == 3u, "%u",
|
|
ftcoop_rectangle_destinations(packed));
|
|
|
|
/* An ASYMMETRIC rectangle, because a 1x1 or a square would make a swapped x/y pair
|
|
* invisible — the "an assert on a shape whose two dimensions happen to be equal is
|
|
* vacuous" trap. (3,8)..(6,9): 3 | 8<<6 | 6<<12 | 9<<18 | 4<<24 = 0x04246203. */
|
|
packed = ftcoop_pack_rectangle(3u, 8u, 6u, 9u, 4u);
|
|
ft_tt_test_check_fmt("(3,8)..(6,9) x4 packs to 0x04246203", packed == 0x04246203u,
|
|
"0x%08X", packed);
|
|
|
|
ft_tt_test_check("an inverted rectangle packs to 0",
|
|
ftcoop_pack_rectangle(6u, 7u, 3u, 7u, 3u) == 0u,
|
|
"0 is never a legal piece, so the kernel refuses it");
|
|
ft_tt_test_check("a coordinate above the 6-bit node-id field packs to 0",
|
|
ftcoop_pack_rectangle(3u, 7u, 64u, 7u, 3u) == 0u,
|
|
"it would TRUNCATE onto a different live endpoint");
|
|
ft_tt_test_check("five destinations pack to 0",
|
|
ftcoop_pack_rectangle(3u, 7u, 7u, 7u, 5u) == 0u,
|
|
"the standing hazard limit, restated in the wire format");
|
|
ft_tt_test_check("zero destinations pack to 0",
|
|
ftcoop_pack_rectangle(3u, 7u, 3u, 7u, 0u) == 0u, "");
|
|
|
|
ft_tt_test_check("the counter never takes the INVALID value",
|
|
ftcoop_next_counter(0xFFFFFFFFu) == FT_TT_RESIDENT_COOP_FIRST_COUNTER &&
|
|
FT_TT_RESIDENT_COOP_FIRST_COUNTER != 0u,
|
|
"a receiver clears its arrival word to 0 before announcing");
|
|
}
|
|
|
|
static void check_the_plan_and_the_args(void)
|
|
{
|
|
ft_tt_noc_coord members[8];
|
|
ft_tt_op_matmul_resident_multicast plan;
|
|
uint32_t args[kArgCoopCount];
|
|
uint32_t leader_group;
|
|
int rc;
|
|
|
|
ft_tt_test_step("a 2x4 group at (3,7) — the plan and both kinds of arg block");
|
|
|
|
fill_block(members, 3u, 7u, 2u, 4u);
|
|
memset(&plan, 0, sizeof(plan));
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(members, 8u, 0u, 4096u, &plan);
|
|
ft_tt_test_check_fmt("the plan builds", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
ft_tt_test_check_fmt("seven receivers", plan.receiver_count == 7u, "%u",
|
|
plan.receiver_count);
|
|
ft_tt_test_check_fmt("TWO broadcast packets", plan.partition.rectangle_count == 2u, "%u",
|
|
plan.partition.rectangle_count);
|
|
ft_tt_test_check("the rendezvous words are the cooperative control block's",
|
|
plan.ready_semaphore_address != plan.arrival_semaphore_address &&
|
|
plan.arrival_semaphore_address != plan.counter_word_address &&
|
|
plan.ready_semaphore_address != 0u,
|
|
"three distinct addresses, the same on every member");
|
|
|
|
memset(args, 0xAA, sizeof(args));
|
|
rc = ft_tt_op_matmul_resident_multicast_args(&plan, 0u, args);
|
|
ft_tt_test_check_fmt("the leader's args build", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check_fmt("the leader's role is LEADER",
|
|
args[kArgCoopRole] == FT_TT_RESIDENT_COOP_ROLE_LEADER, "%u",
|
|
args[kArgCoopRole]);
|
|
ft_tt_test_check_fmt("the leader carries two pieces", args[kArgCoopGroupCount] == 2u, "%u",
|
|
args[kArgCoopGroupCount]);
|
|
leader_group = args[kArgCoopGroup0];
|
|
ft_tt_test_check_fmt("piece 0 is the leader's own row minus its own cell",
|
|
ftcoop_rectangle_x_low(leader_group) == 4u &&
|
|
ftcoop_rectangle_x_high(leader_group) == 6u &&
|
|
ftcoop_rectangle_y_low(leader_group) == 7u &&
|
|
ftcoop_rectangle_destinations(leader_group) == 3u,
|
|
"0x%08X", leader_group);
|
|
ft_tt_test_check_fmt("the unused piece words are 0", args[kArgCoopGroup2] == 0u &&
|
|
args[kArgCoopGroup3] == 0u,
|
|
"0x%08X 0x%08X", args[kArgCoopGroup2], args[kArgCoopGroup3]);
|
|
ft_tt_test_check_fmt("the receiver count is carried", args[kArgCoopReceiverCount] == 7u,
|
|
"%u", args[kArgCoopReceiverCount]);
|
|
ft_tt_test_check_fmt("the spin limit is carried and is not 0",
|
|
args[kArgCoopSpinLimit] == 4096u, "%u", args[kArgCoopSpinLimit]);
|
|
|
|
memset(args, 0xAA, sizeof(args));
|
|
rc = ft_tt_op_matmul_resident_multicast_args(&plan, 5u, args);
|
|
ft_tt_test_check_fmt("a receiver's args build", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check_fmt("its role is RECEIVER",
|
|
args[kArgCoopRole] == FT_TT_RESIDENT_COOP_ROLE_RECEIVER, "%u",
|
|
args[kArgCoopRole]);
|
|
/* A RECEIVER GETS NO GEOMETRY AT ALL, and that is the check. A receiver holding the
|
|
* leader's rectangle is one wrong branch away from broadcasting into the group. */
|
|
ft_tt_test_check_fmt("it is given NO rectangle",
|
|
args[kArgCoopGroupCount] == 0u && args[kArgCoopGroup0] == 0u &&
|
|
args[kArgCoopGroup1] == 0u,
|
|
"count=%u", args[kArgCoopGroupCount]);
|
|
ft_tt_test_check_fmt("it is told where the leader is",
|
|
args[kArgCoopLeaderX] == 3u && args[kArgCoopLeaderY] == 7u, "(%u,%u)",
|
|
args[kArgCoopLeaderX], args[kArgCoopLeaderY]);
|
|
ft_tt_test_check("every member is given the same three rendezvous addresses",
|
|
args[kArgCoopReadySemaphore] == plan.ready_semaphore_address &&
|
|
args[kArgCoopArrivalSemaphore] == plan.arrival_semaphore_address &&
|
|
args[kArgCoopCounterWord] == plan.counter_word_address,
|
|
"a multicast writes ONE address into every destination");
|
|
}
|
|
|
|
static void check_the_reset_order(void)
|
|
{
|
|
ft_tt_noc_coord members[4];
|
|
ft_tt_op_matmul_resident_multicast plan;
|
|
ft_tt_noc_access noc;
|
|
write_recorder recorder;
|
|
bool arrivals_first = true;
|
|
unsigned index;
|
|
int rc;
|
|
|
|
ft_tt_test_step("the reset stages the arrival words BEFORE the leader's ready semaphore");
|
|
|
|
fill_block(members, 13u, 7u, 1u, 4u);
|
|
memset(&plan, 0, sizeof(plan));
|
|
if (ft_tt_op_matmul_resident_multicast_plan(members, 4u, 0u, 4096u, &plan) != FT_TT_OK) {
|
|
ft_tt_test_check("the plan builds", false, "");
|
|
return;
|
|
}
|
|
memset(&recorder, 0, sizeof(recorder));
|
|
recorder.fail_after = -1;
|
|
noc.write = recorder_write;
|
|
noc.read = NULL;
|
|
noc.user = &recorder;
|
|
|
|
rc = ft_tt_op_matmul_resident_multicast_reset(&noc, &plan);
|
|
ft_tt_test_check_fmt("the reset succeeds", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check_fmt("six writes: four arrivals, the counter word, the ready semaphore",
|
|
recorder.count == 6u, "%u", recorder.count);
|
|
if (recorder.count != 6u)
|
|
return;
|
|
for (index = 0u; index < 4u; index++)
|
|
arrivals_first = arrivals_first &&
|
|
recorder.entry[index].address == plan.arrival_semaphore_address &&
|
|
recorder.entry[index].x == members[index].x &&
|
|
recorder.entry[index].value == 0u;
|
|
ft_tt_test_check("writes 0..3 clear every member's arrival word", arrivals_first,
|
|
"including the leader's, which receives nothing but must not hold a "
|
|
"previous residency's counter");
|
|
ft_tt_test_check_fmt("write 4 is the leader's counter word",
|
|
recorder.entry[4].address == plan.counter_word_address &&
|
|
recorder.entry[4].x == 13u && recorder.entry[4].value == 0u,
|
|
"addr=0x%X x=%u", (unsigned)recorder.entry[4].address,
|
|
recorder.entry[4].x);
|
|
ft_tt_test_check_fmt("write 5 — LAST — is the leader's ready semaphore",
|
|
recorder.entry[5].address == plan.ready_semaphore_address &&
|
|
recorder.entry[5].value == 0u,
|
|
"addr=0x%X", (unsigned)recorder.entry[5].address);
|
|
|
|
/* A failing write must be REPORTED, not swallowed: a reset that half-ran leaves the
|
|
* group with one stale arrival word, which is the value that makes a desynchronised
|
|
* first block undetectable. */
|
|
memset(&recorder, 0, sizeof(recorder));
|
|
recorder.fail_after = 2;
|
|
rc = ft_tt_op_matmul_resident_multicast_reset(&noc, &plan);
|
|
ft_tt_test_check_fmt("a failing write is reported", rc != FT_TT_OK, "rc=%d", rc);
|
|
}
|
|
|
|
static void check_the_refusals(void)
|
|
{
|
|
ft_tt_noc_coord members[8];
|
|
ft_tt_op_matmul_resident_multicast plan;
|
|
uint32_t args[kArgCoopCount];
|
|
int rc;
|
|
|
|
ft_tt_test_step("the refusals");
|
|
|
|
fill_block(members, 3u, 7u, 2u, 4u);
|
|
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(NULL, 8u, 0u, 4096u, &plan);
|
|
ft_tt_test_check_fmt("NULL members refused", rc == FT_TT_EINVAL, "rc=%d", rc);
|
|
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(members, 1u, 0u, 4096u, &plan);
|
|
ft_tt_test_check_fmt("one member refused", rc == FT_TT_EINVAL, "rc=%d", rc);
|
|
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(members, 8u, 8u, 4096u, &plan);
|
|
ft_tt_test_check_fmt("a leader index outside the group refused", rc == FT_TT_EINVAL,
|
|
"rc=%d", rc);
|
|
|
|
/* AN UNBOUNDED RENDEZVOUS WAIT is the one thing a resident kernel may not be
|
|
* configured into: `assert_reset` is then the only way to end it, and asserting reset
|
|
* on a blocked triad is how cores were wedged. */
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(members, 8u, 0u, 0u, &plan);
|
|
ft_tt_test_check_fmt("a zero spin limit refused", rc == FT_TT_EINVAL, "rc=%d", rc);
|
|
|
|
/* Consecutive live indices wrapping a row is how a real caller produces a set that is
|
|
* not a rectangle, and it is exactly the shape the default member cores have. */
|
|
fill_block(members, 3u, 7u, 1u, 4u);
|
|
members[3] = coord(3u, 8u);
|
|
rc = ft_tt_op_matmul_resident_multicast_plan(members, 4u, 0u, 4096u, &plan);
|
|
ft_tt_test_check_fmt("a member set that is not a rectangle refused", rc != FT_TT_OK,
|
|
"rc=%d", rc);
|
|
|
|
fill_block(members, 3u, 7u, 2u, 4u);
|
|
if (ft_tt_op_matmul_resident_multicast_plan(members, 8u, 0u, 4096u, &plan) == FT_TT_OK) {
|
|
rc = ft_tt_op_matmul_resident_multicast_args(&plan, 8u, args);
|
|
ft_tt_test_check_fmt("args for a member outside the plan refused", rc == FT_TT_EINVAL,
|
|
"rc=%d", rc);
|
|
}
|
|
rc = ft_tt_op_matmul_resident_multicast_reset(NULL, &plan);
|
|
ft_tt_test_check_fmt("a reset with no accessor refused", rc == FT_TT_EINVAL, "rc=%d", rc);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
int main(void)
|
|
{
|
|
printf("ft_tt resident cooperative matmul — host half, off hardware\n");
|
|
printf("===========================================================\n");
|
|
|
|
check_the_packed_rectangle_word();
|
|
check_the_plan_and_the_args();
|
|
check_the_reset_order();
|
|
check_the_refusals();
|
|
|
|
return ft_tt_test_finish();
|
|
}
|