242 lines
10 KiB
C
242 lines
10 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_tt_column_runs.c — the column-tile permutation that de-interleaves Ornith's joint
|
|
* Q+gate projection, scored with NO CARD and NO USE_TT.
|
|
*
|
|
* WHAT IT PROVES, and why each part is here rather than left to the card:
|
|
*
|
|
* 1. The FIELD LIST is Ornith's. 32 heads x 16 column tiles a head, Q at offset 0 and the
|
|
* gate at offset 8, each 8 wide. Both halves are 256 destination columns and together
|
|
* they are a PARTITION of the 512-column source — checked as a partition, not as two
|
|
* counts that happen to add up.
|
|
*
|
|
* 2. The MAPPING agrees with a brute-force model built the other way round. A permutation
|
|
* that drops one column and repeats another has the right total and the right shape and
|
|
* is a smoothly wrong projection on the card, where nothing can see it.
|
|
*
|
|
* 3. The CONTIGUOUS RUN LENGTH is right, because it is what turns the permutation into 32
|
|
* transfers instead of 256. A `*out_contiguous` that is too LARGE is a read across a
|
|
* run boundary — the source of a whole head's wrong weights — and a value that is too
|
|
* small is only slow, so the two failures are asymmetric and both are checked.
|
|
*
|
|
* 4. The REFUSALS fire: a run past the end, two runs that overlap, a zero-width run, a
|
|
* field wider than its record.
|
|
*
|
|
* The de-interleave is the one item of Phase 5 with no existing shape to inherit, so it is
|
|
* the one whose arithmetic has to be settled before a model is ever loaded.
|
|
*/
|
|
|
|
#include "../src/accel/tt/pn_tt_column_runs.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
/* Ornith-9B: 32 Q heads, head_dim 256 (8 column tiles), a joint Q+gate projection. */
|
|
#define ORNITH_HEADS 32u
|
|
#define ORNITH_HEAD_TILES 8u
|
|
#define ORNITH_JOINT_TILES (2u * ORNITH_HEAD_TILES)
|
|
#define ORNITH_SOURCE_COLS (ORNITH_HEADS * ORNITH_JOINT_TILES)
|
|
|
|
static int g_failed = 0;
|
|
static int g_checked = 0;
|
|
|
|
#define CHECK(cond, msg) do { \
|
|
g_checked++; \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, " FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
|
|
g_failed++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/** The de-interleave, written the OTHER way round: destination column `d` of the field at
|
|
* `offset` is source column `(d / width) * stride + offset + (d % width)`. If this and the
|
|
* run list ever disagree, one of them is the defect and the test says which column. */
|
|
static uint32_t model_source_column(uint32_t destination, uint32_t stride, uint32_t offset,
|
|
uint32_t width)
|
|
{
|
|
return (destination / width) * stride + offset + (destination % width);
|
|
}
|
|
|
|
static void test_ornith_field_lists(void)
|
|
{
|
|
pn_tt_column_run q[PN_TT_COLUMN_RUNS_MAX];
|
|
pn_tt_column_run gate[PN_TT_COLUMN_RUNS_MAX];
|
|
unsigned char seen[ORNITH_SOURCE_COLS];
|
|
uint32_t q_runs, gate_runs, index;
|
|
|
|
printf("Ornith's joint Q+gate projection\n");
|
|
|
|
q_runs = pn_tt_column_runs_field(ORNITH_HEADS, ORNITH_JOINT_TILES, 0u, ORNITH_HEAD_TILES,
|
|
q, PN_TT_COLUMN_RUNS_MAX);
|
|
gate_runs = pn_tt_column_runs_field(ORNITH_HEADS, ORNITH_JOINT_TILES, ORNITH_HEAD_TILES,
|
|
ORNITH_HEAD_TILES, gate, PN_TT_COLUMN_RUNS_MAX);
|
|
CHECK(q_runs == ORNITH_HEADS && gate_runs == ORNITH_HEADS,
|
|
"each half is one run per head — 32 transfers, not 256");
|
|
CHECK(pn_tt_column_runs_total(q, q_runs) == ORNITH_HEADS * ORNITH_HEAD_TILES,
|
|
"the Q half is 256 destination column tiles (8192 features)");
|
|
CHECK(pn_tt_column_runs_total(gate, gate_runs) == ORNITH_HEADS * ORNITH_HEAD_TILES,
|
|
"the gate half is 256 destination column tiles");
|
|
CHECK(pn_tt_column_runs_are_injective(q, q_runs, ORNITH_SOURCE_COLS),
|
|
"the Q half uses no source column twice");
|
|
CHECK(pn_tt_column_runs_are_injective(gate, gate_runs, ORNITH_SOURCE_COLS),
|
|
"the gate half uses no source column twice");
|
|
|
|
/* A PARTITION, not two halves that add up. Two lists can both be injective, both total
|
|
* 256, and both miss the same column. */
|
|
memset(seen, 0, sizeof(seen));
|
|
for (index = 0; index < ORNITH_HEADS * ORNITH_HEAD_TILES; index++) {
|
|
uint32_t source, contiguous;
|
|
|
|
if (pn_tt_column_runs_map(q, q_runs, index, &source, &contiguous) &&
|
|
source < ORNITH_SOURCE_COLS)
|
|
seen[source]++;
|
|
if (pn_tt_column_runs_map(gate, gate_runs, index, &source, &contiguous) &&
|
|
source < ORNITH_SOURCE_COLS)
|
|
seen[source]++;
|
|
}
|
|
{
|
|
bool partition = true;
|
|
|
|
for (index = 0; index < ORNITH_SOURCE_COLS; index++) {
|
|
if (seen[index] != 1u)
|
|
partition = false;
|
|
}
|
|
CHECK(partition, "Q and gate PARTITION the 512-column joint weight — every source "
|
|
"column lands in exactly one half");
|
|
}
|
|
|
|
/* The first head, spelled out, because a permutation that is right on average is the
|
|
* failure this whole module exists to make impossible. */
|
|
CHECK(q[0].first == 0u && q[0].count == 8u && q[1].first == 16u && q[1].count == 8u,
|
|
"Q head 0 is source tiles 0..7 and Q head 1 is 16..23");
|
|
CHECK(gate[0].first == 8u && gate[0].count == 8u && gate[1].first == 24u,
|
|
"gate head 0 is source tiles 8..15 and gate head 1 starts at 24");
|
|
CHECK(q[ORNITH_HEADS - 1u].first == 496u && gate[ORNITH_HEADS - 1u].first == 504u,
|
|
"the last head's Q starts at 496 and its gate at 504");
|
|
}
|
|
|
|
static void test_mapping_against_the_model(void)
|
|
{
|
|
pn_tt_column_run runs[PN_TT_COLUMN_RUNS_MAX];
|
|
uint32_t run_count;
|
|
uint32_t offset;
|
|
bool agrees = true;
|
|
bool contiguity_agrees = true;
|
|
|
|
printf("the mapping, against a brute-force model built the other way round\n");
|
|
|
|
for (offset = 0u; offset <= ORNITH_HEAD_TILES; offset += ORNITH_HEAD_TILES) {
|
|
uint32_t destination;
|
|
|
|
run_count = pn_tt_column_runs_field(ORNITH_HEADS, ORNITH_JOINT_TILES, offset,
|
|
ORNITH_HEAD_TILES, runs, PN_TT_COLUMN_RUNS_MAX);
|
|
for (destination = 0; destination < ORNITH_HEADS * ORNITH_HEAD_TILES; destination++) {
|
|
uint32_t source = 0u, contiguous = 0u;
|
|
uint32_t want_source = model_source_column(destination, ORNITH_JOINT_TILES, offset,
|
|
ORNITH_HEAD_TILES);
|
|
uint32_t want_contiguous =
|
|
ORNITH_HEAD_TILES - (destination % ORNITH_HEAD_TILES);
|
|
|
|
if (!pn_tt_column_runs_map(runs, run_count, destination, &source, &contiguous) ||
|
|
source != want_source)
|
|
agrees = false;
|
|
if (contiguous != want_contiguous)
|
|
contiguity_agrees = false;
|
|
/* The run length must never claim more than the source actually has in a row:
|
|
* an over-long range is a read across a head boundary. */
|
|
if (contiguous == 0u ||
|
|
model_source_column(destination + contiguous - 1u, ORNITH_JOINT_TILES, offset,
|
|
ORNITH_HEAD_TILES) != source + contiguous - 1u)
|
|
contiguity_agrees = false;
|
|
}
|
|
}
|
|
CHECK(agrees, "every one of the 512 destination columns maps where the model says");
|
|
CHECK(contiguity_agrees, "and the contiguous run length is exact — never past a head "
|
|
"boundary, never short of one");
|
|
|
|
{
|
|
uint32_t source, contiguous;
|
|
|
|
run_count = pn_tt_column_runs_field(ORNITH_HEADS, ORNITH_JOINT_TILES, 0u,
|
|
ORNITH_HEAD_TILES, runs, PN_TT_COLUMN_RUNS_MAX);
|
|
CHECK(!pn_tt_column_runs_map(runs, run_count, ORNITH_HEADS * ORNITH_HEAD_TILES,
|
|
&source, &contiguous),
|
|
"one destination column past the end is refused, not wrapped");
|
|
}
|
|
}
|
|
|
|
static void test_refusals(void)
|
|
{
|
|
pn_tt_column_run runs[4];
|
|
uint32_t source, contiguous;
|
|
|
|
printf("the refusals\n");
|
|
|
|
runs[0].first = 0u;
|
|
runs[0].count = 0u;
|
|
CHECK(pn_tt_column_runs_total(runs, 1u) == 0u, "a zero-width run is refused");
|
|
CHECK(!pn_tt_column_runs_map(runs, 1u, 0u, &source, &contiguous),
|
|
"...and nothing maps through it");
|
|
|
|
runs[0].first = 0u;
|
|
runs[0].count = 8u;
|
|
runs[1].first = 504u;
|
|
runs[1].count = 16u;
|
|
CHECK(!pn_tt_column_runs_are_injective(runs, 2u, ORNITH_SOURCE_COLS),
|
|
"a run that ends past the source is refused");
|
|
|
|
runs[1].first = 4u;
|
|
runs[1].count = 8u;
|
|
CHECK(!pn_tt_column_runs_are_injective(runs, 2u, ORNITH_SOURCE_COLS),
|
|
"two runs that OVERLAP are refused — a duplicated column is a smoothly wrong "
|
|
"projection nothing on the card can see");
|
|
|
|
runs[1].first = 8u;
|
|
CHECK(pn_tt_column_runs_are_injective(runs, 2u, ORNITH_SOURCE_COLS),
|
|
"...and two runs that merely abut are fine");
|
|
|
|
CHECK(pn_tt_column_runs_field(ORNITH_HEADS, 8u, 0u, 16u, runs, 4u) == 0u,
|
|
"a field WIDER than its record is refused — it would overlap the next group");
|
|
CHECK(pn_tt_column_runs_field(ORNITH_HEADS, 16u, 12u, 8u, runs, 4u) == 0u,
|
|
"a field that starts too late in its record is refused");
|
|
CHECK(pn_tt_column_runs_field(ORNITH_HEADS, 16u, 0u, 8u, runs, 4u) == 0u,
|
|
"a run array too small for the group count is refused, not truncated");
|
|
CHECK(pn_tt_column_runs_field(0u, 16u, 0u, 8u, runs, 4u) == 0u, "zero groups is refused");
|
|
}
|
|
|
|
/** The identity permutation has to be expressible, because the shared placement path takes
|
|
* a run list for every weight and the ordinary ones must come out byte for byte unchanged. */
|
|
static void test_identity(void)
|
|
{
|
|
pn_tt_column_run identity;
|
|
uint32_t index;
|
|
bool ok = true;
|
|
|
|
printf("the identity permutation\n");
|
|
|
|
identity.first = 0u;
|
|
identity.count = 512u;
|
|
CHECK(pn_tt_column_runs_total(&identity, 1u) == 512u, "identity totals the whole weight");
|
|
CHECK(pn_tt_column_runs_are_injective(&identity, 1u, 512u), "identity is injective");
|
|
for (index = 0; index < 512u; index++) {
|
|
uint32_t source, contiguous;
|
|
|
|
if (!pn_tt_column_runs_map(&identity, 1u, index, &source, &contiguous) ||
|
|
source != index || contiguous != 512u - index)
|
|
ok = false;
|
|
}
|
|
CHECK(ok, "identity maps every column to itself in ONE transfer");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("test_tt_column_runs — the joint-projection de-interleave, off-hardware\n\n");
|
|
test_ornith_field_lists();
|
|
test_mapping_against_the_model();
|
|
test_refusals();
|
|
test_identity();
|
|
printf("\n%d checks, %d failed\n", g_checked, g_failed);
|
|
return g_failed == 0 ? 0 : 1;
|
|
}
|