319 lines
15 KiB
C
319 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_tt_eltwise_split.c — the arithmetic of splitting one elementwise descriptor across N
|
|
* Tensix, scored with NO CARD and NO USE_TT.
|
|
*
|
|
* WHAT IT PROVES, and why each part is here rather than left to a card arm:
|
|
*
|
|
* 1. THE SLICES TILE THE DESCRIPTOR EXACTLY AND OVERLAP NOWHERE. Every member of every
|
|
* legal split is walked and the union of their tile ranges is checked to be [0, W) with
|
|
* no gap and no repeat, at every width Ornith carries and at every member count. An
|
|
* overlap here is TWO CORES WRITING ONE DRAM BYTE — the one failure in this whole lever
|
|
* that produces a plausible wrong answer with nothing refusing anywhere.
|
|
*
|
|
* 2. THE BYTE OFFSET IS THE TILE OFFSET TIMES THE PAGE SIZE, and it is asked about the
|
|
* OPERAND's page size rather than the family's, because the render pair proves the two
|
|
* differ (fp32 in, bf16 out) and a split that assumed 2048 would be right by luck.
|
|
*
|
|
* 3. A RAGGED SPLIT IS REFUSED, NOT ROUNDED. The matmul group hands its remainder out one
|
|
* column at a time; this family cannot, because a slice off the `2 * depth` grid walks
|
|
* the CB write pointer up L1 with no error. So a member count that does not divide is
|
|
* refused at the top (`member_limit` never returns it) and refused again at the bottom
|
|
* (`slice` returns false), and BOTH are checked — a single guard is a single edit away
|
|
* from being the only one.
|
|
*
|
|
* 4. THE MEMBER LIMIT IS THE LARGEST DIVISOR AT OR BELOW WHAT WAS ASKED FOR, not the GCD
|
|
* and not the request. Ornith's three widths are 128 / 256 / 384, whose common divisors
|
|
* run 1, 2, 4, 8, 16, 32, 64, 128 — so asking for 3, 6 or 12 must come back 2, 4 and 8
|
|
* rather than 1, and asking for 12 must not come back 12.
|
|
*
|
|
* 5. ONE MEMBER IS THE PRE-SPLIT PATH. `members = 1` yields the whole descriptor at offset
|
|
* zero, which is the claim that makes `PN_TT_ELTWISE_CORES=1` a control rather than a
|
|
* configuration.
|
|
*
|
|
* RED ARMS. Each of these is a one-line mutation of the module that this file must go red
|
|
* on, and each was run:
|
|
* - `first_tile = member * per_member` -> `member * tile_count` .......... 16 failures
|
|
* - the `(tile_count % members) != 0u` refusal deleted ................... 2 failures
|
|
* - `byte_offset = first_tile * tile_bytes` -> `first_tile` ............. 16 failures
|
|
* - `member_limit` walking UP instead of down from the request ........... 8 failures
|
|
*/
|
|
|
|
#include "../src/accel/tt/pn_tt_eltwise_split.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static int g_failed = 0;
|
|
static int g_checked = 0;
|
|
|
|
#define CHECK(cond, msg) do { \
|
|
g_checked++; \
|
|
if (!(cond)) { \
|
|
g_failed++; \
|
|
printf(" FAIL %s\n", (msg)); \
|
|
} \
|
|
} while (0)
|
|
|
|
/** Ornith's three elementwise widths: the residual adds and the GDN gate at 128, the
|
|
* attention gate at 256, the FFN SwiGLU at 384. Every figure in this file is about these. */
|
|
static const uint32_t k_ornith_widths[] = { 128u, 256u, 384u };
|
|
#define ORNITH_WIDTHS 3u
|
|
|
|
/** The widest descriptor there is, so a cover map can be a fixed array. */
|
|
#define MAX_TILES 4096u
|
|
|
|
/**
|
|
* Walk every member of an `members`-way split of `tile_count` and check the ranges TILE the
|
|
* descriptor: every tile covered exactly once, in order, starting at zero.
|
|
*/
|
|
static void check_slices_tile_exactly(uint32_t tile_count, uint32_t members,
|
|
uint32_t tile_bytes)
|
|
{
|
|
unsigned char covered[MAX_TILES];
|
|
uint32_t expected_first = 0u;
|
|
uint32_t member, tile;
|
|
bool all_ok = true;
|
|
|
|
memset(covered, 0, sizeof(covered));
|
|
for (member = 0; member < members; member++) {
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
if (!pn_tt_eltwise_split_slice(tile_count, members, member, tile_bytes, &slice)) {
|
|
all_ok = false;
|
|
break;
|
|
}
|
|
if (slice.first_tile != expected_first ||
|
|
slice.byte_offset != slice.first_tile * tile_bytes ||
|
|
slice.tile_count != tile_count / members)
|
|
all_ok = false;
|
|
for (tile = slice.first_tile; tile < slice.first_tile + slice.tile_count; tile++) {
|
|
if (tile >= MAX_TILES || covered[tile] != 0u)
|
|
all_ok = false;
|
|
else
|
|
covered[tile] = 1u;
|
|
}
|
|
expected_first += slice.tile_count;
|
|
}
|
|
for (tile = 0; tile < tile_count && tile < MAX_TILES; tile++) {
|
|
if (covered[tile] != 1u)
|
|
all_ok = false;
|
|
}
|
|
CHECK(all_ok && expected_first == tile_count,
|
|
"every member's range is disjoint and the set covers the descriptor exactly");
|
|
}
|
|
|
|
/** 1 + 2: the slices tile the descriptor, at every width and every legal member count. */
|
|
static void test_the_slices_tile_the_descriptor(void)
|
|
{
|
|
static const uint32_t members[] = { 1u, 2u, 4u, 8u, 16u };
|
|
unsigned width, index;
|
|
|
|
printf("the slices tile the descriptor and overlap nowhere\n");
|
|
for (width = 0; width < ORNITH_WIDTHS; width++) {
|
|
for (index = 0; index < sizeof(members) / sizeof(members[0]); index++)
|
|
check_slices_tile_exactly(k_ornith_widths[width], members[index], 2048u);
|
|
}
|
|
/* THE PAGE SIZE IS THE OPERAND'S, NOT THE FAMILY'S. The strip bridge runs the same
|
|
* eltwise sources at MIXED page sizes — fp32 in, bf16 out — so a split that assumed 2048
|
|
* would be right only by luck. */
|
|
check_slices_tile_exactly(128u, 4u, 4096u);
|
|
check_slices_tile_exactly(384u, 8u, 4096u);
|
|
{
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
CHECK(pn_tt_eltwise_split_slice(384u, 8u, 3u, 4096u, &slice) &&
|
|
slice.first_tile == 144u && slice.byte_offset == 144u * 4096u,
|
|
"the byte offset is the tile offset times the OPERAND's page size");
|
|
CHECK(pn_tt_eltwise_split_slice(384u, 8u, 3u, 2048u, &slice) &&
|
|
slice.byte_offset == 144u * 2048u,
|
|
"and the same tile offset at a bf16 page is half the bytes");
|
|
}
|
|
}
|
|
|
|
/** W1a: the block-ragged wide split tiles every prefill width exactly, every slice whole
|
|
* blocks, members past the last block empty at the end. */
|
|
static void test_the_block_split_tiles_prefill_widths(void)
|
|
{
|
|
static const uint32_t mts[] = { 1u, 2u, 13u, 24u, 51u, 64u };
|
|
static const uint32_t wides[] = { 128u, 384u };
|
|
uint32_t i, w, members, member;
|
|
|
|
printf("the block-ragged wide split tiles every prefill width\n");
|
|
for (i = 0; i < sizeof(mts) / sizeof(mts[0]); i++)
|
|
for (w = 0; w < 2u; w++)
|
|
for (members = 1u; members <= PN_TT_ELTWISE_SPLIT_MAX_MEMBERS; members++) {
|
|
const uint32_t tiles = mts[i] * wides[w];
|
|
uint32_t next = 0u, most = 0u, least = 0xffffffffu;
|
|
bool ok = true;
|
|
|
|
for (member = 0; member < members; member++) {
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
if (!pn_tt_eltwise_split_slice_blocks(tiles, members, member, 64u, 2048u,
|
|
&slice) ||
|
|
slice.first_tile != next || (slice.tile_count % 64u) != 0u ||
|
|
slice.byte_offset != slice.first_tile * 2048u)
|
|
ok = false;
|
|
next = slice.first_tile + slice.tile_count;
|
|
if (slice.tile_count > most) most = slice.tile_count;
|
|
if (slice.tile_count < least) least = slice.tile_count;
|
|
}
|
|
CHECK(ok && next == tiles && most - least <= 64u,
|
|
"contiguous whole-block slices covering the descriptor, within one block");
|
|
}
|
|
{
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
CHECK(!pn_tt_eltwise_split_slice_blocks(19584u + 32u, 15u, 0u, 64u, 2048u, &slice),
|
|
"a width that is not whole blocks is refused");
|
|
CHECK(pn_tt_eltwise_split_slice_blocks(19584u, 15u, 14u, 64u, 2048u, &slice) &&
|
|
slice.first_tile == 19584u - 1280u && slice.tile_count == 1280u,
|
|
"51 x 384 over 15: 306 blocks, the last member owns 20 of them at the end");
|
|
CHECK(pn_tt_eltwise_split_slice_blocks(128u, 4u, 3u, 64u, 2048u, &slice) &&
|
|
slice.tile_count == 0u && slice.first_tile == 128u,
|
|
"a member past the last block is empty at the end");
|
|
CHECK(!pn_tt_eltwise_split_slice_blocks(128u, 4u, 4u, 64u, 2048u, &slice),
|
|
"a member index equal to the count is refused");
|
|
}
|
|
}
|
|
|
|
/** 5: one member is the whole descriptor at offset zero — the control arm's claim. */
|
|
static void test_one_member_is_the_pre_split_path(void)
|
|
{
|
|
unsigned width;
|
|
|
|
printf("one member is the pre-split path\n");
|
|
for (width = 0; width < ORNITH_WIDTHS; width++) {
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
CHECK(pn_tt_eltwise_split_slice(k_ornith_widths[width], 1u, 0u, 2048u, &slice) &&
|
|
slice.first_tile == 0u && slice.byte_offset == 0u &&
|
|
slice.tile_count == k_ornith_widths[width],
|
|
"members = 1 is the whole descriptor at offset zero");
|
|
}
|
|
}
|
|
|
|
/** 3: a ragged split is refused at the slice, and so is every out-of-range argument. */
|
|
static void test_a_ragged_split_is_refused(void)
|
|
{
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
printf("a ragged split is refused rather than rounded\n");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 3u, 0u, 2048u, &slice),
|
|
"128 tiles over 3 members is refused, not rounded to 42");
|
|
CHECK(!pn_tt_eltwise_split_slice(384u, 5u, 0u, 2048u, &slice),
|
|
"384 tiles over 5 members is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 4u, 4u, 2048u, &slice),
|
|
"a member index equal to the member count is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 4u, 9u, 2048u, &slice),
|
|
"a member index past the member count is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(0u, 4u, 0u, 2048u, &slice),
|
|
"a zero tile count is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 0u, 0u, 2048u, &slice),
|
|
"a zero member count is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, PN_TT_ELTWISE_SPLIT_MAX_MEMBERS + 1u, 0u, 2048u,
|
|
&slice),
|
|
"a member count past the module's bound is refused");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 4u, 0u, 0u, &slice),
|
|
"a zero page size is refused — it would make every offset zero");
|
|
CHECK(!pn_tt_eltwise_split_slice(128u, 4u, 0u, 2048u, NULL), "a NULL output is refused");
|
|
}
|
|
|
|
/** 4: the limit is the largest divisor at or below the request. */
|
|
static void test_the_member_limit_is_the_largest_divisor(void)
|
|
{
|
|
printf("the member limit is the largest divisor at or below the request\n");
|
|
CHECK(pn_tt_eltwise_split_member_limit(8u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 8u,
|
|
"8 divides 128, 256 and 384, so 8 is 8");
|
|
CHECK(pn_tt_eltwise_split_member_limit(16u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 16u,
|
|
"and so does 16");
|
|
/* THE ANSWER IS NOT THE GCD. gcd(128,256,384) is 128, and asking for 3 must come back 2
|
|
* rather than 1 or 128 — the largest DIVISOR AT OR BELOW the request. */
|
|
CHECK(pn_tt_eltwise_split_member_limit(3u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 2u,
|
|
"3 comes back 2, not 1 and not the gcd");
|
|
CHECK(pn_tt_eltwise_split_member_limit(6u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 4u,
|
|
"6 comes back 4");
|
|
CHECK(pn_tt_eltwise_split_member_limit(12u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 8u,
|
|
"12 comes back 8 — 12 does not divide 128");
|
|
CHECK(pn_tt_eltwise_split_member_limit(5u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 4u,
|
|
"5 comes back 4");
|
|
CHECK(pn_tt_eltwise_split_member_limit(7u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 4u,
|
|
"7 comes back 4");
|
|
|
|
/* The clamps. */
|
|
CHECK(pn_tt_eltwise_split_member_limit(64u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 16u,
|
|
"the caller's bound clamps before the widths are consulted");
|
|
CHECK(pn_tt_eltwise_split_member_limit(64u, k_ornith_widths, ORNITH_WIDTHS, 999u) ==
|
|
PN_TT_ELTWISE_SPLIT_MAX_MEMBERS,
|
|
"and the module's own bound clamps the caller's");
|
|
CHECK(pn_tt_eltwise_split_member_limit(1u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 1u,
|
|
"1 is 1");
|
|
CHECK(pn_tt_eltwise_split_member_limit(0u, k_ornith_widths, ORNITH_WIDTHS, 16u) == 1u,
|
|
"0 is 1 — there is no zero-member split");
|
|
|
|
/* A width that shares nothing collapses the split for ALL of them, which is the whole
|
|
* "one depth, one placement, every member" rule. */
|
|
{
|
|
static const uint32_t odd[] = { 128u, 256u, 384u, 129u };
|
|
|
|
CHECK(pn_tt_eltwise_split_member_limit(8u, odd, 4u, 16u) == 1u,
|
|
"one indivisible width collapses the split for every width");
|
|
}
|
|
{
|
|
static const uint32_t zero_width[] = { 128u, 0u };
|
|
|
|
CHECK(pn_tt_eltwise_split_member_limit(8u, zero_width, 2u, 16u) == 1u,
|
|
"a zero width refuses the split rather than being skipped");
|
|
}
|
|
CHECK(pn_tt_eltwise_split_member_limit(8u, NULL, 3u, 16u) == 1u, "a NULL table is 1");
|
|
CHECK(pn_tt_eltwise_split_member_limit(8u, k_ornith_widths, 0u, 16u) == 1u,
|
|
"an empty table is 1");
|
|
}
|
|
|
|
/**
|
|
* 3, the other half: what the limit returns is ALWAYS sliceable.
|
|
*
|
|
* The two guards are deliberately redundant — the limit never returns a ragged count and the
|
|
* slice refuses one anyway — and a redundancy nobody checks is a redundancy one edit removes.
|
|
* This walks every request from 1 to 40 and holds the pair to each other.
|
|
*/
|
|
static void test_the_two_guards_agree(void)
|
|
{
|
|
uint32_t requested;
|
|
bool all_ok = true;
|
|
|
|
printf("what the member limit returns is always sliceable\n");
|
|
for (requested = 1u; requested <= 40u; requested++) {
|
|
uint32_t members = pn_tt_eltwise_split_member_limit(requested, k_ornith_widths,
|
|
ORNITH_WIDTHS, 16u);
|
|
unsigned width;
|
|
|
|
if (members == 0u || members > PN_TT_ELTWISE_SPLIT_MAX_MEMBERS ||
|
|
members > (requested < 16u ? requested : 16u))
|
|
all_ok = false;
|
|
for (width = 0; width < ORNITH_WIDTHS; width++) {
|
|
pn_tt_eltwise_slice slice;
|
|
|
|
if (!pn_tt_eltwise_split_slice(k_ornith_widths[width], members, members - 1u,
|
|
2048u, &slice))
|
|
all_ok = false;
|
|
}
|
|
}
|
|
CHECK(all_ok, "every count the limit returns slices every width, for requests 1..40");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("test_tt_eltwise_split — the elementwise core split's arithmetic, off-hardware\n\n");
|
|
test_the_slices_tile_the_descriptor();
|
|
test_one_member_is_the_pre_split_path();
|
|
test_a_ragged_split_is_refused();
|
|
test_the_member_limit_is_the_largest_divisor();
|
|
test_the_two_guards_agree();
|
|
test_the_block_split_tiles_prefill_widths();
|
|
printf("\n%d checks, %d failed\n", g_checked, g_failed);
|
|
return g_failed == 0 ? 0 : 1;
|
|
}
|