654 lines
31 KiB
C
654 lines
31 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_gdn_layout.c
|
|
* @brief OFF-HARDWARE acceptance for the GATED-DELTA-NET host STAGER's address arithmetic —
|
|
* the four conversions in `src/accel/tt/pn_tt_gdn_layout.c`.
|
|
*
|
|
* Opens no device. Build with `make USE_TT=1 tests/test_gdn_layout` and run it anywhere,
|
|
* including on the Mac where the code is written.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT THIS IS FOR, AND WHY IT SIMULATES THE READER
|
|
* ---------------------------------------------------------------------------
|
|
* The stager and the two dataflow kernels are the two ends of a contract NOTHING checks at
|
|
* run time: a stager that disagrees about where a channel or a head sits produces plausible
|
|
* numbers from the first token instead of an error. `foundry/tt/tests/test_gdn_ops.c` pins
|
|
* the ACCESSORS the kernels compute their offsets with; this pins what the HOST writes into
|
|
* the addresses those accessors name.
|
|
*
|
|
* So every check below reads the staged bytes back the way the READER would — through
|
|
* `ftg_delta_head_tile()`, `ftg_conv_plane_tile()`, `ftg_y_tile()` and the face index — and
|
|
* asserts it recovers the value the CPU ORACLE (`pn_qwen35moe_ssm_recurrence()`) says belongs
|
|
* there. Nothing here is compared against the function that produced it.
|
|
*
|
|
* THE ONE THAT MATTERS MOST is the conv weight. The oracle stores it row-major
|
|
* `[conv_dim, taps]` and the card wants `taps` TAP-MAJOR PLANES, so the stager TRANSPOSES.
|
|
* A reshape is the same bytes, the same length and a different answer, and the arm below
|
|
* asserts that the two are not merely different in principle but disagree at a named channel.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* q | k | v AND y ARE HEAD-MAJOR ROW-0 STRIPS, AND THERE IS NO STAGER LEFT FOR THEM
|
|
* ---------------------------------------------------------------------------
|
|
* A head's `state` values are `state/32` CONSECUTIVE tiles, all of the data in ROW 0 of each,
|
|
* head `h` starting at `pn_tt_gdn_head_tile(h, state)` = `h * state/32`. That is the geometry
|
|
* `pn_tt_gdn_row0_offset()` already addresses and the geometry PHASE CONV's output already
|
|
* has, so there is nothing to tilize: `pn_tt_gdn_stage_stream()` and its inverse are GONE and
|
|
* the traffic is `pn_tt_fp32_row0_move()`, two 64 B chunks a tile.
|
|
*
|
|
* They used to be tilized `[heads, state]` matrices with ONE ROW PER HEAD — 32x denser, and it
|
|
* forced the host to de-tilize conv_out, L2-normalise on the CPU and re-tilize between the two
|
|
* phases, 49,408 B a layer over PCIe. The arms below therefore test what a MOVE plus the
|
|
* offsets place, not what a tilizer produces: `strip_write()` here is `pn_tt_fp32_row0_move()`
|
|
* with the NoC swapped for a memcpy, which is the only part of it a machine without a card can
|
|
* run. The one-row-per-head arithmetic is asserted to DISAGREE at a named head, the same way
|
|
* the reshape is for the conv weight.
|
|
*/
|
|
|
|
#include "../src/accel/tt/pn_tt_gdn_layout.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "tt/ft_tt_op_gdn.h"
|
|
#include "tt/ft_tt_tilize.h"
|
|
|
|
/* The device's L1 pointer attribute is the SFPI GCC fork's, not the contract's; the header
|
|
* guards it so a host compiler can read the file. Same escape test_gdn_ops.c uses. */
|
|
#define FTQ_L1_PTR
|
|
#include "tt/kernels/resident_gdn_protocol.h"
|
|
|
|
static int g_failed = 0;
|
|
static int g_checks = 0;
|
|
|
|
#define CHECK(cond, msg) \
|
|
do { \
|
|
g_checks++; \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, " FAIL %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
|
|
g_failed++; \
|
|
} else { \
|
|
fprintf(stderr, " ok %s\n", (msg)); \
|
|
} \
|
|
} while (0)
|
|
|
|
/** Ornith's shape, read off `ft_tt_op_gdn.h`'s own constants rather than retyped. */
|
|
static ftg_shape ornith_shape(uint32_t phase)
|
|
{
|
|
ftg_shape shape;
|
|
|
|
shape.phase = phase;
|
|
shape.state_tiles = FT_TT_OP_GDN_QWEN35_STATE / 32u;
|
|
shape.v_heads = FT_TT_OP_GDN_QWEN35_V_HEADS;
|
|
shape.k_heads = FT_TT_OP_GDN_QWEN35_K_HEADS;
|
|
shape.conv_tile_cols = FT_TT_OP_GDN_QWEN35_CONV_DIM / 32u;
|
|
shape.conv_taps = FT_TT_OP_GDN_QWEN35_CONV_TAPS;
|
|
shape.tile_bytes = FT_TT_OP_GDN_FP32_TILE_BYTES;
|
|
shape.tile_count = 0u;
|
|
return shape;
|
|
}
|
|
|
|
/** The float a tilized strip holds at [row][column] of tile `tile`. The reader's own
|
|
* arithmetic, spelled from `ft_tt_tilize.h` rather than from the stager. */
|
|
static float tile_element(const unsigned char *tiles, uint32_t tile, uint32_t row,
|
|
uint32_t column)
|
|
{
|
|
const float *base = (const float *)(const void *)tiles;
|
|
|
|
return base[(size_t)tile * 1024u + ft_tt_tile_face_index(row, column)];
|
|
}
|
|
|
|
/* ================================================================
|
|
* Logical row 0 of a strip of fp32 tiles
|
|
* ================================================================ */
|
|
|
|
static void test_row0_offsets(void)
|
|
{
|
|
const uint32_t conv_dim = 256u;
|
|
const uint32_t tiles = conv_dim / 32u;
|
|
float *source = malloc((size_t)conv_dim * sizeof(float));
|
|
unsigned char *staged = calloc((size_t)tiles, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
uint32_t index;
|
|
int recovered = 1;
|
|
|
|
fprintf(stderr, "\nlogical row 0 of an fp32 tile — the two 64 B pieces\n");
|
|
|
|
/* Worked out from ft_tt_tilize.h: face 0 is elements [0,256) and face 1 is [256,512), so
|
|
* in fp32 the two halves of row 0 are 1024 B apart inside a 4096 B tile. */
|
|
CHECK(pn_tt_gdn_row0_offset(0u, 0u) == 0u, "tile 0 chunk 0 is at byte 0");
|
|
CHECK(pn_tt_gdn_row0_offset(0u, 1u) == 1024u, "tile 0 chunk 1 is 1024 B on, not 512");
|
|
CHECK(pn_tt_gdn_row0_offset(1u, 0u) == 4096u, "tile 1 chunk 0 is one whole tile on");
|
|
CHECK(pn_tt_gdn_row0_offset(3u, 1u) == 3u * 4096u + 1024u, "tile 3 chunk 1 is 13312");
|
|
CHECK(PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES == 64u,
|
|
"an fp32 row-0 chunk is exactly the 64 B DRAM read granule — nothing discarded");
|
|
|
|
/* And the semantic version: a strip tilized as ONE row, read back through those offsets,
|
|
* has to reproduce the source. This is what a wrong face stride breaks. */
|
|
for (index = 0; index < conv_dim; index++)
|
|
source[index] = (float)index + 0.5f;
|
|
if (pn_tt_gdn_stage_plane(source, 0u, 1u, conv_dim, source, staged) != 0)
|
|
recovered = 0;
|
|
for (index = 0; index < conv_dim && recovered; index++) {
|
|
const uint32_t tile = index / 32u;
|
|
const uint32_t chunk = (index % 32u) / 16u;
|
|
const float *piece =
|
|
(const float *)(const void *)(staged + pn_tt_gdn_row0_offset(tile, chunk));
|
|
|
|
if (piece[index % 16u] != (float)index + 0.5f)
|
|
recovered = 0;
|
|
}
|
|
CHECK(recovered, "row 0 read through the chunk offsets reproduces the strip, element for "
|
|
"element");
|
|
free(source);
|
|
free(staged);
|
|
}
|
|
|
|
/* ================================================================
|
|
* ANY logical row — the chunk phase-isolation arm's row shuttle
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* The arm that says WHICH chunk kernel owns the measured prefill divergence shuttles row `t`
|
|
* of a chunk's buffer to row 0 and back, so every one of its answers rests on this offset.
|
|
*
|
|
* THE PLAUSIBLE WRONG ANSWER IS A FLAT `row * 64`, which is right for rows 0-15 and wrong for
|
|
* 16-31 because the tile is four 16x16 FACES, not one 32x32 block. That mistake would read
|
|
* half of every chunk out of the wrong face and the attribution would be an artefact — so it
|
|
* is asserted against here, not just avoided.
|
|
*/
|
|
static void test_row_offsets(void)
|
|
{
|
|
const uint32_t rows = 32u;
|
|
const uint32_t cols = 128u;
|
|
const uint32_t tiles = cols / 32u;
|
|
float *source = malloc((size_t)rows * cols * sizeof(float));
|
|
unsigned char *staged = calloc((size_t)tiles, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
uint32_t row;
|
|
uint32_t col;
|
|
int recovered = 1;
|
|
|
|
fprintf(stderr, "\nany logical row of an fp32 tile — the phase-isolation arm's shuttle\n");
|
|
|
|
CHECK(pn_tt_gdn_row_offset(0u, 0u, 0u) == pn_tt_gdn_row0_offset(0u, 0u) &&
|
|
pn_tt_gdn_row_offset(2u, 0u, 1u) == pn_tt_gdn_row0_offset(2u, 1u),
|
|
"row 0 of the general offset IS pn_tt_gdn_row0_offset, tile and chunk alike");
|
|
CHECK(pn_tt_gdn_row_offset(0u, 1u, 0u) == 64u,
|
|
"row 1 is 64 B down inside face 0");
|
|
CHECK(pn_tt_gdn_row_offset(0u, 16u, 0u) == 2048u,
|
|
"row 16 crosses into FACE 2 — the boundary a flat row*64 misses");
|
|
CHECK(pn_tt_gdn_row_offset(0u, 16u, 0u) != 16u * 64u,
|
|
"and it is not 1024: the flat formula is the wrong answer this test exists for");
|
|
CHECK(pn_tt_gdn_row_offset(0u, 31u, 1u) == 3u * 1024u + 15u * 64u,
|
|
"row 31 chunk 1 is the last 64 B of face 3");
|
|
CHECK(pn_tt_gdn_row_offset(2u, 5u, 1u) == 2u * 4096u + 1024u + 5u * 64u,
|
|
"tile 2 row 5 chunk 1 composes tile, face and in-face row");
|
|
|
|
/* The semantic version: a REAL 32-row tilization, read back through these offsets, has to
|
|
* reproduce every element — the row-0 test's argument at 32 rows instead of one. */
|
|
for (row = 0; row < rows; row++)
|
|
for (col = 0; col < cols; col++)
|
|
source[(size_t)row * cols + col] = (float)(row * 1000u + col) + 0.5f;
|
|
if (ft_tt_tilize_strip(source, sizeof(float), rows, cols, (size_t)cols * sizeof(float),
|
|
staged) != FT_TT_TILIZE_OK)
|
|
recovered = 0;
|
|
for (row = 0; row < rows && recovered; row++) {
|
|
for (col = 0; col < cols; col++) {
|
|
const uint32_t tile = col / 32u;
|
|
const uint32_t chunk = (col % 32u) / 16u;
|
|
const float *piece = (const float *)(const void *)(staged +
|
|
pn_tt_gdn_row_offset(tile, row, chunk));
|
|
|
|
if (piece[col % 16u] != (float)(row * 1000u + col) + 0.5f) {
|
|
recovered = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
CHECK(recovered, "every element of a 32x128 tilization is where pn_tt_gdn_row_offset says");
|
|
free(source);
|
|
free(staged);
|
|
}
|
|
|
|
/* ================================================================
|
|
* The conv weight — the TRANSPOSE
|
|
* ================================================================ */
|
|
|
|
static void test_conv_weight_transpose(void)
|
|
{
|
|
const ftg_shape shape = ornith_shape(FT_TT_OP_GDN_PHASE_CONV);
|
|
const uint32_t conv_dim = 256u; /* a small stand-in; the arithmetic is per channel */
|
|
const uint32_t taps = FT_TT_OP_GDN_QWEN35_CONV_TAPS;
|
|
const uint32_t tiles = conv_dim / 32u;
|
|
float *weight = malloc((size_t)conv_dim * taps * sizeof(float));
|
|
float *scratch = malloc((size_t)conv_dim * sizeof(float));
|
|
unsigned char *planes = calloc((size_t)tiles * taps, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
uint32_t channel, tap;
|
|
int transposed = 1;
|
|
int rows_padded = 1;
|
|
|
|
fprintf(stderr, "\nthe conv1d weight — TAP-MAJOR PLANES, which is a TRANSPOSE\n");
|
|
|
|
/* The oracle's order: `w[c*taps + tap]` — qwen35moe_ssm.c's `cw = w + c*k; cw[tap]`. The
|
|
* value encodes both indices so a mis-stage cannot land on a coincidence. */
|
|
for (channel = 0; channel < conv_dim; channel++)
|
|
for (tap = 0; tap < taps; tap++)
|
|
weight[(size_t)channel * taps + tap] = (float)channel * 16.0f + (float)tap;
|
|
|
|
for (tap = 0; tap < taps; tap++) {
|
|
/* THE TRANSPOSE: stride `taps`, starting at `tap`. */
|
|
if (pn_tt_gdn_stage_plane(weight, tap, taps, conv_dim, scratch,
|
|
planes + (size_t)tap * tiles *
|
|
PN_TT_GDN_LAYOUT_TILE_BYTES) != 0)
|
|
transposed = 0;
|
|
}
|
|
for (tap = 0; tap < taps && transposed; tap++) {
|
|
for (channel = 0; channel < conv_dim; channel++) {
|
|
/* Read it back the way the READER does: plane tile, row 0, column c%32. The
|
|
* plane base is `ftg_conv_plane_tile()`, which is the accessor the kernel uses. */
|
|
const uint32_t tile = tap * tiles + channel / 32u;
|
|
const float got = tile_element(planes, tile, 0u, channel % 32u);
|
|
|
|
if (got != (float)channel * 16.0f + (float)tap)
|
|
transposed = 0;
|
|
}
|
|
}
|
|
CHECK(transposed, "plane t, tile c/32, [0][c%32] holds w[c][t] — the oracle's own value");
|
|
|
|
/* The plane index the reader computes for a NON-zero column, against a literal. */
|
|
CHECK(ftg_conv_plane_tile(&shape, 3u, 5u) == 3u * 256u + 5u,
|
|
"the reader reaches tap 3 column 5 at plane tile 773 (tap-major, not channel-major)");
|
|
|
|
/* Rows 1-31 are the tile quantum. They must be ZERO: the compute thread multiplies whole
|
|
* tiles, and a non-zero pad row would come back through SiLU as a non-zero conv_out row,
|
|
* which is the strip invariant the rest of the forward depends on. */
|
|
for (channel = 0; channel < conv_dim && rows_padded; channel++) {
|
|
uint32_t row;
|
|
|
|
for (row = 1u; row < 32u && rows_padded; row++) {
|
|
if (tile_element(planes, channel / 32u, row, channel % 32u) != 0.0f)
|
|
rows_padded = 0;
|
|
}
|
|
}
|
|
CHECK(rows_padded, "rows 1-31 of every plane tile are zero — the tile quantum, staged");
|
|
|
|
/* AND THE RESHAPE IS A DIFFERENT ANSWER. Not "could be": at channel 1 of plane 1 the
|
|
* transpose gives w[1][1] = 17 and the reshape gives w[64][1] = 1025. Asserting they
|
|
* disagree is what makes the distinction a fact rather than a caution. */
|
|
{
|
|
float transposed_value, reshaped_value;
|
|
|
|
(void)pn_tt_gdn_stage_plane(weight, 1u, taps, conv_dim, scratch, planes);
|
|
transposed_value = tile_element(planes, 0u, 0u, 1u);
|
|
(void)pn_tt_gdn_stage_plane(weight, 1u * conv_dim, 1u, conv_dim, scratch, planes);
|
|
reshaped_value = tile_element(planes, 0u, 0u, 1u);
|
|
CHECK(transposed_value == 1.0f * 16.0f + 1.0f && reshaped_value != transposed_value,
|
|
"the RESHAPE (first = t*conv_dim, stride 1) disagrees with the transpose at "
|
|
"channel 1 — the mutation PN_TT_RED=gdn_conv_reshape injects");
|
|
}
|
|
free(weight);
|
|
free(scratch);
|
|
free(planes);
|
|
}
|
|
|
|
/* ================================================================
|
|
* The DELTA input — q | k | v, and y coming back. HEAD-MAJOR ROW-0 STRIPS.
|
|
* ================================================================ */
|
|
|
|
/** Fill `[heads, state]` with a value that encodes both indices. */
|
|
static void fill_matrix(float *matrix, uint32_t heads, uint32_t state, float tag)
|
|
{
|
|
uint32_t head, index;
|
|
|
|
for (head = 0; head < heads; head++)
|
|
for (index = 0; index < state; index++)
|
|
matrix[(size_t)head * state + index] = tag + (float)head * 1024.0f + (float)index;
|
|
}
|
|
|
|
/**
|
|
* `pn_tt_fp32_row0_move()` with the NoC replaced by a memcpy — the ONLY part of that function
|
|
* a machine with no card can run, and the part the layout lives in. Two 64 B chunks a tile, at
|
|
* `pn_tt_gdn_row0_offset(first_tile + t, chunk)`. Everything the real one adds is a transport.
|
|
*/
|
|
static void strip_write(unsigned char *strip, uint32_t first_tile, const float *values,
|
|
uint32_t tiles)
|
|
{
|
|
uint32_t tile, chunk;
|
|
|
|
for (tile = 0; tile < tiles; tile++) {
|
|
for (chunk = 0; chunk < PN_TT_GDN_LAYOUT_ROW0_CHUNKS; chunk++) {
|
|
const float *host = values + (size_t)tile * PN_TT_GDN_LAYOUT_TILE_DIM +
|
|
(size_t)chunk * PN_TT_GDN_LAYOUT_ROW0_CHUNK_ELEMS;
|
|
|
|
memcpy(strip + pn_tt_gdn_row0_offset(first_tile + tile, chunk), host,
|
|
PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The read half of the same primitive. */
|
|
static void strip_read(const unsigned char *strip, uint32_t first_tile, float *values,
|
|
uint32_t tiles)
|
|
{
|
|
uint32_t tile, chunk;
|
|
|
|
for (tile = 0; tile < tiles; tile++) {
|
|
for (chunk = 0; chunk < PN_TT_GDN_LAYOUT_ROW0_CHUNKS; chunk++) {
|
|
float *host = values + (size_t)tile * PN_TT_GDN_LAYOUT_TILE_DIM +
|
|
(size_t)chunk * PN_TT_GDN_LAYOUT_ROW0_CHUNK_ELEMS;
|
|
|
|
memcpy(host, strip + pn_tt_gdn_row0_offset(first_tile + tile, chunk),
|
|
PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `pn_tt_gdn_head_tile()` on its own, and composed with `pn_tt_gdn_row0_offset()` into the
|
|
* BYTE offset of head `h`'s tile `t` — which is what a caller actually passes to the move.
|
|
*/
|
|
static void test_head_tile_offsets(void)
|
|
{
|
|
const uint32_t state = FT_TT_OP_GDN_QWEN35_STATE; /* 128 */
|
|
const uint32_t per_head = state / 32u; /* 4 */
|
|
const ftg_shape shape = ornith_shape(FT_TT_OP_GDN_PHASE_DELTA);
|
|
uint32_t head;
|
|
int agrees = 1;
|
|
int strided = 1;
|
|
|
|
fprintf(stderr, "\nhead-major ROW-0 strips — where head h's four tiles start\n");
|
|
|
|
CHECK(pn_tt_gdn_head_tile(0u, state) == 0u && pn_tt_gdn_head_tile(1u, state) == 4u &&
|
|
pn_tt_gdn_head_tile(31u, state) == 124u,
|
|
"head 0, 1 and 31 start at tiles 0, 4 and 124 — h * state/32");
|
|
CHECK(pn_tt_gdn_head_tile(32u, state) == 128u && pn_tt_gdn_head_tile(63u, state) == 252u,
|
|
"and it keeps going past 32 heads: there is no row block to wrap into");
|
|
CHECK(pn_tt_gdn_head_tile(2u, 256u) == 16u && pn_tt_gdn_head_tile(2u, 64u) == 4u,
|
|
"the stride is state/32, so S=256 puts head 2 at tile 16 and S=64 at tile 4");
|
|
|
|
/* THE BYTE OFFSET, which is the number the move is handed: head 3's tile 2, chunk 1. Head
|
|
* 3 starts at tile 12, so that is tile 14 chunk 1 = 14*4096 + 1024 = 58368. */
|
|
CHECK(pn_tt_gdn_row0_offset(pn_tt_gdn_head_tile(3u, state) + 2u, 1u) == 58368u,
|
|
"head 3's tile 2, chunk 1 is byte 58368 of the strip");
|
|
|
|
/* The HOST's head_tile and the DEVICE's accessors are two spellings of one contract: the
|
|
* y tile the writer computes and the v tile the reader computes must both be the stager's
|
|
* offset, or the two ends disagree with nothing to catch it. */
|
|
for (head = 0; head < shape.v_heads; head++) {
|
|
if (ftg_y_tile(&shape, head, 0u) != pn_tt_gdn_head_tile(head, state))
|
|
agrees = 0;
|
|
if (ftg_delta_head_tile(&shape, kGdnStreamV, head) !=
|
|
ftg_delta_stream_base(&shape, kGdnStreamV) + pn_tt_gdn_head_tile(head, state))
|
|
agrees = 0;
|
|
}
|
|
CHECK(agrees, "pn_tt_gdn_head_tile agrees with ftg_y_tile and ftg_delta_head_tile at every "
|
|
"one of the 32 v-heads — the host and the kernels compute the same tile");
|
|
|
|
/* AND THE ONE-ROW-PER-HEAD ARITHMETIC IS A DIFFERENT ANSWER. It gave every head of a row
|
|
* block the SAME `(h/32)*state_tiles` tiles and told them apart by row `h%32`, so its
|
|
* stride was 0 where this one's is `state/32`. Asserting they disagree at head 1 is what
|
|
* makes the change a fact rather than a note. */
|
|
for (head = 0; head + 1u < 64u; head++) {
|
|
if (pn_tt_gdn_head_tile(head + 1u, state) - pn_tt_gdn_head_tile(head, state) != per_head)
|
|
strided = 0;
|
|
}
|
|
CHECK(strided && pn_tt_gdn_head_tile(1u, state) != (1u / 32u) * per_head,
|
|
"consecutive heads are state/32 apart — the old (h/32)*state_tiles packing put "
|
|
"heads 0 and 1 on the SAME tile and this disagrees with it at head 1");
|
|
}
|
|
|
|
static void test_delta_input_layout(void)
|
|
{
|
|
const ftg_shape shape = ornith_shape(FT_TT_OP_GDN_PHASE_DELTA);
|
|
const uint32_t state = FT_TT_OP_GDN_QWEN35_STATE;
|
|
const uint32_t per_head = state / 32u;
|
|
const uint32_t k_heads = shape.k_heads;
|
|
const uint32_t v_heads = shape.v_heads;
|
|
const uint32_t input_tiles = ftg_delta_input_tiles(&shape);
|
|
float *q = malloc((size_t)k_heads * state * sizeof(float));
|
|
float *k = malloc((size_t)k_heads * state * sizeof(float));
|
|
float *v = malloc((size_t)v_heads * state * sizeof(float));
|
|
unsigned char *staged = calloc(input_tiles, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
uint32_t head, index;
|
|
int placed = 1;
|
|
int repeat_reads = 1;
|
|
int pad_rows_zero = 1;
|
|
|
|
fprintf(stderr, "\nthe DELTA q | k | v — ONE head-major row-0 strip, q and k UN-repeated\n");
|
|
|
|
fill_matrix(q, k_heads, state, 100000.0f);
|
|
fill_matrix(k, k_heads, state, 200000.0f);
|
|
fill_matrix(v, v_heads, state, 300000.0f);
|
|
|
|
/* THE WHOLE OF THE STAGING, now: three moves at three head-major offsets. No tilize
|
|
* buffer, no scratch, nothing to get wrong except the offset. */
|
|
for (head = 0; head < k_heads; head++) {
|
|
strip_write(staged,
|
|
ftg_delta_stream_base(&shape, kGdnStreamQ) +
|
|
pn_tt_gdn_head_tile(head, state),
|
|
q + (size_t)head * state, per_head);
|
|
strip_write(staged,
|
|
ftg_delta_stream_base(&shape, kGdnStreamK) +
|
|
pn_tt_gdn_head_tile(head, state),
|
|
k + (size_t)head * state, per_head);
|
|
}
|
|
for (head = 0; head < v_heads; head++)
|
|
strip_write(staged,
|
|
ftg_delta_stream_base(&shape, kGdnStreamV) +
|
|
pn_tt_gdn_head_tile(head, state),
|
|
v + (size_t)head * state, per_head);
|
|
|
|
/* Read every element back the way `read_one_head()` does: `ftg_delta_head_tile()` for the
|
|
* head's FIRST tile, then tile `index/32` of the head's own consecutive run, ROW 0. */
|
|
for (head = 0; head < v_heads && placed; head++) {
|
|
for (index = 0; index < state; index++) {
|
|
const uint32_t tile =
|
|
ftg_delta_head_tile(&shape, kGdnStreamV, head) + index / 32u;
|
|
|
|
if (tile_element(staged, tile, (uint32_t)kGdnStripRow, index % 32u) !=
|
|
300000.0f + (float)head * 1024.0f + (float)index)
|
|
placed = 0;
|
|
}
|
|
}
|
|
CHECK(placed, "v-head h's element j is at ftg_delta_head_tile(V, h) + j/32, row 0, "
|
|
"column j%32");
|
|
|
|
/* The heads do not overlap: head h owns tiles [h*state/32, (h+1)*state/32) and NOTHING of
|
|
* head h+1's run carries head h's tag. Reading the FIRST tile of every head's successor
|
|
* and finding the successor's own value is what proves the runs are disjoint. */
|
|
for (head = 0; head + 1u < v_heads && placed; head++) {
|
|
const uint32_t next = ftg_delta_head_tile(&shape, kGdnStreamV, head + 1u);
|
|
|
|
if (next - ftg_delta_head_tile(&shape, kGdnStreamV, head) != per_head)
|
|
placed = 0;
|
|
if (tile_element(staged, next, (uint32_t)kGdnStripRow, 0u) !=
|
|
300000.0f + (float)(head + 1u) * 1024.0f)
|
|
placed = 0;
|
|
}
|
|
CHECK(placed, "consecutive v-heads are state/32 tiles apart and head h+1's first tile "
|
|
"holds head h+1's own element 0 — the runs do not overlap");
|
|
|
|
/* Rows 1-31 of a strip tile are never written and never read. A move that touched them
|
|
* would be writing over the neighbouring 64 B granule. */
|
|
for (index = 0; index < input_tiles && pad_rows_zero; index++) {
|
|
uint32_t row;
|
|
|
|
for (row = 1u; row < 32u && pad_rows_zero; row++) {
|
|
if (tile_element(staged, index, row, 0u) != 0.0f ||
|
|
tile_element(staged, index, row, 31u) != 0.0f)
|
|
pad_rows_zero = 0;
|
|
}
|
|
}
|
|
CHECK(pad_rows_zero, "rows 1-31 of every strip tile are untouched — the move writes only "
|
|
"the two 64 B chunks of row 0");
|
|
|
|
/* The GQA repeat is the READER's modulo, so v-head 17 has to find k-head 1's FOUR TILES in
|
|
* the UN-repeated k block — 16 heads' worth, not 32. */
|
|
for (head = 0; head < v_heads && repeat_reads; head++) {
|
|
const uint32_t k_head = ftg_k_head_for(head, k_heads);
|
|
const uint32_t tile = ftg_delta_head_tile(&shape, kGdnStreamK, k_head);
|
|
|
|
if (tile_element(staged, tile, (uint32_t)kGdnStripRow, 7u) !=
|
|
200000.0f + (float)k_head * 1024.0f + 7.0f)
|
|
repeat_reads = 0;
|
|
}
|
|
CHECK(repeat_reads, "v-head vh reads k-head vh%16's four tiles out of a 16-head k block — "
|
|
"the repeat is an address, never materialised");
|
|
|
|
/* And it is NOT the blocked mapping, which is the plausible wrong answer with a recorded
|
|
* gibberish bug behind it. */
|
|
CHECK(ftg_k_head_for(17u, 16u) == 1u && 17u / (32u / 16u) == 8u,
|
|
"v-head 17 pairs with k-head 1, not with k-head 8");
|
|
|
|
/* q, k and v do not overlap, and the whole staging buffer is exactly 256 tiles at Ornith:
|
|
* 16*4 + 16*4 + 32*4. It was 12 as three one-row-per-head matrices. */
|
|
CHECK(ftg_delta_stream_base(&shape, kGdnStreamQ) == 0u &&
|
|
ftg_delta_stream_base(&shape, kGdnStreamK) == 64u &&
|
|
ftg_delta_stream_base(&shape, kGdnStreamV) == 128u && input_tiles == 256u,
|
|
"q | k | v sit at tiles 0, 64, 128 and the staged input is 256 tiles");
|
|
/* The last v-head's run ENDS on the buffer's last tile — no hole, nothing off the end. */
|
|
CHECK(ftg_delta_head_tile(&shape, kGdnStreamV, v_heads - 1u) + per_head == input_tiles,
|
|
"v-head 31's run ends exactly at tile 256 — the three blocks tile the buffer");
|
|
|
|
free(q);
|
|
free(k);
|
|
free(v);
|
|
free(staged);
|
|
}
|
|
|
|
static void test_y_strip(void)
|
|
{
|
|
const ftg_shape shape = ornith_shape(FT_TT_OP_GDN_PHASE_DELTA);
|
|
const uint32_t state = FT_TT_OP_GDN_QWEN35_STATE;
|
|
const uint32_t per_head = state / 32u;
|
|
const uint32_t v_heads = shape.v_heads;
|
|
const uint32_t tiles = ftg_y_tile(&shape, v_heads - 1u, per_head - 1u) + 1u;
|
|
float *y = malloc((size_t)v_heads * state * sizeof(float));
|
|
float *back = calloc((size_t)v_heads * state, sizeof(float));
|
|
unsigned char *staged = calloc(tiles, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
uint32_t head, index;
|
|
int writer_places = 1;
|
|
int injective = 1;
|
|
unsigned char *seen = calloc(tiles, 1u);
|
|
|
|
fprintf(stderr, "\ny — a head-major ROW-0 strip, 128 tiles at Ornith\n");
|
|
|
|
CHECK(tiles == v_heads * per_head && tiles == 128u,
|
|
"y is v_heads * state/32 = 128 tiles, not one shared row block of 4");
|
|
|
|
/* The forward reads y back with the same move it wrote the input with — head h's four
|
|
* tiles, in order. Round-tripping the whole strip head by head is the de-tilize that used
|
|
* to be `pn_tt_gdn_unstage_stream()`. */
|
|
fill_matrix(y, v_heads, state, 400000.0f);
|
|
for (head = 0; head < v_heads; head++)
|
|
strip_write(staged, ftg_y_tile(&shape, head, 0u), y + (size_t)head * state, per_head);
|
|
for (head = 0; head < v_heads; head++)
|
|
strip_read(staged, ftg_y_tile(&shape, head, 0u), back + (size_t)head * state, per_head);
|
|
CHECK(memcmp(y, back, (size_t)v_heads * state * sizeof(float)) == 0,
|
|
"write then read at ftg_y_tile(h, 0) is the identity on [32, 128] — the forward's "
|
|
"own read-back path");
|
|
|
|
/* The WRITER's own placement: head h's tile-row jt goes to `ftg_y_tile(h, jt)`, ROW 0.
|
|
* Reading the strip at exactly those coordinates has to find y. */
|
|
for (head = 0; head < v_heads && writer_places; head++) {
|
|
for (index = 0; index < state; index++) {
|
|
if (tile_element(staged, ftg_y_tile(&shape, head, index / 32u),
|
|
(uint32_t)kGdnStripRow, index % 32u) !=
|
|
400000.0f + (float)head * 1024.0f + (float)index)
|
|
writer_places = 0;
|
|
}
|
|
}
|
|
CHECK(writer_places, "the writer's ftg_y_tile(h, jt) / row 0 finds y[h][32*jt + j]");
|
|
|
|
/* INJECTIVITY, which is the property the old geometry did not have: every head of a row
|
|
* block used to land on the SAME `state_tiles` tiles and was told apart by its row, so
|
|
* (h, jt) -> tile was 32-to-1. On a strip there is no row to hide in, and two heads on one
|
|
* tile would be one head's answer overwriting another's. */
|
|
for (head = 0; head < v_heads && injective; head++) {
|
|
uint32_t row;
|
|
|
|
for (row = 0; row < per_head && injective; row++) {
|
|
const uint32_t tile = ftg_y_tile(&shape, head, row);
|
|
|
|
if (tile >= tiles || seen[tile])
|
|
injective = 0;
|
|
else
|
|
seen[tile] = 1u;
|
|
}
|
|
}
|
|
CHECK(injective, "(head, tile_row) -> y tile is INJECTIVE over all 32 x 4 — the old "
|
|
"one-row-per-head y collapsed all 32 heads onto tiles 0-3");
|
|
|
|
free(seen);
|
|
free(y);
|
|
free(back);
|
|
free(staged);
|
|
}
|
|
|
|
/** 64 v-heads — twice Ornith's. Under the old packing this was the case that needed a SECOND
|
|
* row block; on a strip it is simply twice as long, which is the point. */
|
|
static void test_sixty_four_heads(void)
|
|
{
|
|
const uint32_t state = 128u;
|
|
const uint32_t heads = 64u;
|
|
const uint32_t per_head = state / 32u;
|
|
ftg_shape shape = ornith_shape(FT_TT_OP_GDN_PHASE_DELTA);
|
|
float *source = malloc((size_t)heads * state * sizeof(float));
|
|
float *back = calloc((size_t)heads * state, sizeof(float));
|
|
unsigned char *staged = calloc((size_t)heads * per_head, PN_TT_GDN_LAYOUT_TILE_BYTES);
|
|
int round_trips = 1;
|
|
int index;
|
|
uint32_t head;
|
|
|
|
fprintf(stderr, "\n64 heads — 256 tiles, not two row blocks of 4\n");
|
|
shape.v_heads = heads;
|
|
|
|
fill_matrix(source, heads, state, 500000.0f);
|
|
for (head = 0; head < heads; head++)
|
|
strip_write(staged, pn_tt_gdn_head_tile(head, state), source + (size_t)head * state,
|
|
per_head);
|
|
for (head = 0; head < heads; head++)
|
|
strip_read(staged, pn_tt_gdn_head_tile(head, state), back + (size_t)head * state,
|
|
per_head);
|
|
if (memcmp(source, back, (size_t)heads * state * sizeof(float)) != 0)
|
|
round_trips = 0;
|
|
CHECK(round_trips, "write/read round-trips 64 heads across 256 tiles");
|
|
|
|
/* Head 32 is at tile 128 and holds its OWN values — not row 0 of a second block at tile 4,
|
|
* which is where the old arithmetic sent it. */
|
|
index = 1;
|
|
for (head = 32u; head < heads && index; head++) {
|
|
uint32_t element;
|
|
|
|
for (element = 0; element < state; element++) {
|
|
if (tile_element(staged, ftg_y_tile(&shape, head, element / 32u),
|
|
(uint32_t)kGdnStripRow, element % 32u) !=
|
|
500000.0f + (float)head * 1024.0f + (float)element)
|
|
index = 0;
|
|
}
|
|
}
|
|
CHECK(index && ftg_y_tile(&shape, 32u, 0u) == 128u,
|
|
"head 32 is at tile 128 with its own values, not at tile 4 as row 0 of a second "
|
|
"row block");
|
|
|
|
free(source);
|
|
free(back);
|
|
free(staged);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
fprintf(stderr, "Gated-Delta-Net host stager — off-hardware layout acceptance (no device "
|
|
"is opened)\n");
|
|
test_row0_offsets();
|
|
test_row_offsets();
|
|
test_conv_weight_transpose();
|
|
test_head_tile_offsets();
|
|
test_delta_input_layout();
|
|
test_y_strip();
|
|
test_sixty_four_heads();
|
|
fprintf(stderr, "\n%d checks, %d failed\n", g_checks, g_failed);
|
|
return g_failed == 0 ? 0 : 1;
|
|
}
|