1674 lines
72 KiB
C
1674 lines
72 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_phase6_attention.c
|
|
* @brief ON-CARD acceptance for FLASH-DECODE ATTENTION: PCC and max-abs against a CPU
|
|
* reference, the two invariance tests correlation cannot see, a byte check that the
|
|
* writer scattered where it was told, the residency claim, and the timing that turns all
|
|
* of it into tok/s.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT MAKES THE REFERENCE INDEPENDENT
|
|
* ---------------------------------------------------------------------------
|
|
* The same three things test_ops.c established for Phase 4 (dev notes §3): it is attention
|
|
* from its DEFINITION and knows nothing about blocks or masks; it consumes the values the
|
|
* CARD sees, bf16-rounded, including the query scale applied the way the kernel applies it;
|
|
* and it is compared on UNTILIZED data, so a face-ordering mistake has something to break
|
|
* against instead of cancelling.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* PCC IS THE WEAK GATE HERE AND IT HAS ALREADY FAILED THIS PROJECT TWICE
|
|
* ---------------------------------------------------------------------------
|
|
* Attention is softmax-shaped, so it inherits shift invariance: softmax(x - c) = softmax(x)
|
|
* makes a WRONG RUNNING MAX — the most likely bug in the kernel — structurally invisible to
|
|
* correlation. That exact mutation came back green on seven checks in the reduce suite
|
|
* (docs/tt_bringup_log.md §22.2). So this suite gates on three things PCC is not:
|
|
*
|
|
* - **an outlier planted in a LATE KV block.** A block-local max then puts the early
|
|
* blocks' exponentials outside representable range instead of producing a
|
|
* shifted-but-equal answer.
|
|
* - **garbage poisoned into the cache past the live length.** A mask that does not mask
|
|
* changes the answer rather than being absorbed. Stale KV is not hypothetical — a
|
|
* reused cache is exactly what a server does.
|
|
* - **a byte comparison of the tiles the writer must NOT have touched.** PCC is blind to a
|
|
* consistent permutation, so where an op is a placed copy, the evidence is the bytes
|
|
* (§18.4: nine identical digits is evidence, an identical file is proof).
|
|
*
|
|
* MAX-ABS IS REPORTED BESIDE PCC ALWAYS, because different corruptions are visible to
|
|
* different metrics: a per-element scale error moved PCC 0.99998 -> 0.929 while max-abs
|
|
* moved 35x on the same data.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* DO NOT PARK
|
|
* ---------------------------------------------------------------------------
|
|
* On a five-processor resident program, parking is the hazard — twice, on cores that had
|
|
* retired cleanly (dev notes §1). This test retires by ASKING and leaves the core in its
|
|
* firmware's GO wait, where a healthy idle core sits.
|
|
*
|
|
* Environment:
|
|
* FT_TT_TENSIX_INDEX which enabled Tensix to drive (60-79 are this worker's)
|
|
* FT_TT_FIRMWARE_DIR the program directory whose fw_*.elf to boot
|
|
*
|
|
* Usage:
|
|
* FT_TT_FIRMWARE_DIR=../programs/attention FT_TT_TENSIX_INDEX=70 \
|
|
* ./build-attn-card/test_phase6_attention ../programs/attention
|
|
*
|
|
* @status See the Phase 6 attention report.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <math.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "../convert/ft_float_convert.h"
|
|
#include "../ft_tt_coord.h"
|
|
#include "../ft_tt_op_attention.h"
|
|
#include "../ft_tt_op_dispatch.h"
|
|
#include "../ft_tt_op_l1_layout.h"
|
|
#include "../ft_tt_op_memory.h"
|
|
#include "../ft_tt_queue.h"
|
|
#include "../ft_tt_tilize.h"
|
|
#include "../ft_tt_watchdog.h"
|
|
#include "ft_tt_attention_reference.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_program_directory.h"
|
|
#include "ft_tt_test_report.h"
|
|
|
|
#define TILE_DIM FT_TT_OP_TILE_DIM
|
|
#define TILE_BYTES FT_TT_OP_TILE_BYTES_BF16
|
|
#define TILE_ELEMENTS (TILE_DIM * TILE_DIM)
|
|
#define QUEUE_CAPACITY 64u
|
|
#define RUN_TIMEOUT_MS 60000u
|
|
#define PCC_ACCEPT 0.99
|
|
#define POISON_BYTE 0x5A
|
|
|
|
/** MiniCPM5-1B, read from the GGUF header (docs/tt_phase0_decisions.md §3) and not
|
|
* assumed: 16 Q heads, 2 KV heads, head_dim 128, 24 layers, context 131072.
|
|
*
|
|
* **Overridable at COMPILE time, so one source serves two models.** Ornith is 32 Q heads /
|
|
* 16 KV groups / head_dim 256 over 8 attention layers, i.e. a different `q_rows` AND a
|
|
* different `dim_tiles` — a different RESIDENCY rather than a different descriptor — so it
|
|
* cannot be an environment variable on a running binary the way the block depth can.
|
|
* `Makefile.attention`'s `ornith` target passes them; the defaults leave the MiniCPM
|
|
* acceptance exactly what it was. */
|
|
#ifndef MODEL_Q_HEADS
|
|
#define MODEL_Q_HEADS 16u
|
|
#endif
|
|
#ifndef MODEL_KV_HEADS
|
|
#define MODEL_KV_HEADS 2u
|
|
#endif
|
|
#ifndef MODEL_HEAD_DIM
|
|
#define MODEL_HEAD_DIM 128u
|
|
#endif
|
|
#ifndef MODEL_LAYERS
|
|
#define MODEL_LAYERS 24u
|
|
#endif
|
|
#define MODEL_DIM_TILES (MODEL_HEAD_DIM / TILE_DIM)
|
|
#define GROUP_ROWS (MODEL_Q_HEADS / MODEL_KV_HEADS)
|
|
/** Which KV group this test drives. NOT zero: a Q base that ignored the group offset would
|
|
* read head 0's query and still produce a plausible answer. */
|
|
#define TEST_GROUP 1u
|
|
|
|
/** Position tiles per KV block. A RESIDENCY parameter, overridable from the environment so
|
|
* the depth can be swept without rebuilding — the same reason FT_TT_TENSIX_INDEX exists.
|
|
* It is read once, into g_block_tiles, because the placement and every descriptor have to
|
|
* agree about it or the kernel refuses them. */
|
|
#define BLOCK_TILES_DEFAULT 8u
|
|
#define BLOCK_TILES_ENVIRONMENT "FT_TT_ATTN_BLOCK_TILES"
|
|
|
|
/** The deepest context this suite uploads. 8192 positions is 2 MiB of K and 2 MiB of V.
|
|
* Overridable with the shape, because the phase whose value is at 8k+ has to be measured
|
|
* there and not extrapolated to it. */
|
|
#ifndef MAX_KV
|
|
#define MAX_KV 8192u
|
|
#endif
|
|
#define MAX_KV_TILES (MAX_KV / TILE_DIM)
|
|
|
|
/** Banks 2..5, at a base no other worker's suite uses — the card is shared. */
|
|
#define DRAM_BANK_Q 2u
|
|
#define DRAM_BANK_K 3u
|
|
#define DRAM_BANK_V 4u
|
|
#define DRAM_BANK_OUT 5u
|
|
#define DRAM_BANK_CONST 2u
|
|
#define DRAM_BANK_COUNT 6u
|
|
#define DRAM_BASE 0x02000000u
|
|
#define DRAM_CONST_BASE 0x02400000u
|
|
|
|
#define IDLE_SPIN_LIMIT 400000000u
|
|
#define CB_SPIN_LIMIT 400000000u
|
|
|
|
#define TRIAD_MASK \
|
|
((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))
|
|
|
|
static ft_tt_noc_coord g_dram[DRAM_BANK_COUNT];
|
|
static ft_tt_launch_core_placement g_placement_storage[1];
|
|
static ft_tt_core_group g_group;
|
|
static ft_tt_program_directory g_program;
|
|
static ft_tt_launch_config g_config;
|
|
static ft_tt_queue g_queue;
|
|
static ft_tt_launch_cb_entry g_cb_placed[FT_TT_OP_MAX_CB];
|
|
static unsigned char g_cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4];
|
|
static ft_tt_kv_cache_layout g_cache;
|
|
static uint32_t g_descriptors_done;
|
|
static uint32_t g_block_tiles = BLOCK_TILES_DEFAULT;
|
|
|
|
/* Host-side operands. Static because 8192 positions of K and V is 8 MiB of fp32 and a
|
|
* stack frame is not where that goes. */
|
|
static float g_query[MODEL_Q_HEADS * MODEL_HEAD_DIM];
|
|
static float g_keys[MAX_KV * MODEL_HEAD_DIM];
|
|
static float g_values[MAX_KV * MODEL_HEAD_DIM];
|
|
static float g_expected[GROUP_ROWS * MODEL_HEAD_DIM];
|
|
static float g_actual[GROUP_ROWS * MODEL_HEAD_DIM];
|
|
static uint16_t g_query_tiles[MODEL_Q_HEADS * MODEL_DIM_TILES * TILE_ELEMENTS];
|
|
static uint16_t g_output_tiles[MODEL_Q_HEADS * MODEL_DIM_TILES * TILE_ELEMENTS];
|
|
static uint16_t g_region_tiles[MAX_KV_TILES * MODEL_DIM_TILES * TILE_ELEMENTS];
|
|
static uint16_t g_constants[FT_TT_OP_ATTENTION_CONST_TILES * TILE_ELEMENTS];
|
|
|
|
/* ================================================================
|
|
* Device access
|
|
* ================================================================ */
|
|
|
|
static int device_write(const ft_tt_noc_access *noc, ft_tt_noc_coord where, uint32_t address,
|
|
const void *source, size_t bytes)
|
|
{
|
|
const unsigned char *cursor = source;
|
|
size_t offset = 0;
|
|
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > 0x10000u)
|
|
chunk = 0x10000u;
|
|
rc = noc->write(noc->user, (uint16_t)where.x, (uint16_t)where.y,
|
|
(uint64_t)address + offset, cursor + offset, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int device_read(const ft_tt_noc_access *noc, ft_tt_noc_coord where, uint32_t address,
|
|
void *destination, size_t bytes)
|
|
{
|
|
unsigned char *cursor = destination;
|
|
size_t offset = 0;
|
|
|
|
while (offset < bytes) {
|
|
size_t chunk = bytes - offset;
|
|
int rc;
|
|
|
|
if (chunk > 0x10000u)
|
|
chunk = 0x10000u;
|
|
rc = noc->read(noc->user, (uint16_t)where.x, (uint16_t)where.y,
|
|
(uint64_t)address + offset, cursor + offset, chunk);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
offset += chunk;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static double now_milliseconds(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Layout helpers
|
|
* ================================================================ */
|
|
|
|
/** Row-major [rows x cols] fp32 -> a face-ordered bf16 tile grid `cols/32` wide. */
|
|
static void to_tiles(const float *matrix, uint32_t rows, uint32_t cols, uint16_t *out)
|
|
{
|
|
const uint32_t tile_cols = cols / TILE_DIM;
|
|
uint32_t row;
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
uint32_t column;
|
|
|
|
for (column = 0; column < cols; column++) {
|
|
const size_t tile = (size_t)(row / TILE_DIM) * tile_cols + column / TILE_DIM;
|
|
|
|
out[tile * TILE_ELEMENTS +
|
|
ft_tt_tile_face_index(row % TILE_DIM, column % TILE_DIM)] =
|
|
ft_bf16_from_float(matrix[(size_t)row * cols + column]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** One row of a face-ordered tile grid -> fp32. */
|
|
static void row_from_tiles(const uint16_t *tiles, uint32_t first_tile, uint32_t tile_count,
|
|
uint32_t row, float *out)
|
|
{
|
|
uint32_t tile;
|
|
|
|
for (tile = 0; tile < tile_count; tile++) {
|
|
uint32_t column;
|
|
|
|
for (column = 0; column < TILE_DIM; column++)
|
|
out[tile * TILE_DIM + column] = ft_float_from_bf16(
|
|
tiles[(size_t)(first_tile + tile) * TILE_ELEMENTS +
|
|
ft_tt_tile_face_index(row, column)]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build ONE KV head's region from a row-major [positions x head_dim] array, THROUGH the
|
|
* layout contract's own address function rather than through a second copy of it.
|
|
*
|
|
* `ft_tt_kv_cache_tile_offset()` is ft_tt_op_memory.h PART 1 §1.1 as code. Using it here is
|
|
* what makes this test a check of "attention reads what KV_APPEND writes" instead of a
|
|
* check that two independently-written layouts happen to agree.
|
|
*/
|
|
static void build_kv_region(const float *rows, uint32_t positions, uint16_t *out)
|
|
{
|
|
uint32_t position;
|
|
|
|
memset(out, 0, (size_t)g_cache.capacity_tiles * g_cache.dim_tiles * TILE_ELEMENTS *
|
|
sizeof(out[0]));
|
|
for (position = 0; position < positions; position++) {
|
|
uint32_t element;
|
|
|
|
for (element = 0; element < MODEL_HEAD_DIM; element++) {
|
|
const uint64_t offset =
|
|
ft_tt_kv_cache_tile_offset(&g_cache, 0u, position, element / TILE_DIM);
|
|
|
|
out[offset / sizeof(out[0]) +
|
|
ft_tt_tile_face_index(ft_tt_op_memory_position_row(position),
|
|
element % TILE_DIM)] =
|
|
ft_bf16_from_float(rows[(size_t)position * MODEL_HEAD_DIM + element]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
* Bringing the residency up
|
|
* ================================================================ */
|
|
|
|
static int load_images(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
|
|
{
|
|
unsigned index;
|
|
|
|
for (index = 0; index < g_program.program.image_count; index++) {
|
|
const ft_tt_kernel_image *image = &g_program.program.images[index];
|
|
int rc = ft_tt_launch_load_image(noc, x, y, image->processor, FT_TT_IMAGE_KERNEL,
|
|
image->elf_data, image->elf_size, NULL);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int start_triad(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
|
|
{
|
|
static const unsigned processors[5] = { FT_TT_PROC_DM0, FT_TT_PROC_DM1, FT_TT_PROC_MATH0,
|
|
FT_TT_PROC_MATH1, FT_TT_PROC_MATH2 };
|
|
uint32_t args[5][FT_TT_QUEUE_KERNEL_ARG_COUNT];
|
|
ft_tt_op_args blocks[5];
|
|
unsigned index;
|
|
int rc;
|
|
|
|
for (index = 0; index < 5u; index++) {
|
|
ft_tt_queue_kernel_binding binding;
|
|
|
|
memset(&binding, 0, sizeof(binding));
|
|
binding.idle_spin_limit = IDLE_SPIN_LIMIT;
|
|
binding.cb_wait_spin_limit = CB_SPIN_LIMIT;
|
|
binding.status_address =
|
|
FT_TT_QUEUE_STATUS_ADDRESS(g_queue.control_address, processors[index]);
|
|
rc = ft_tt_queue_kernel_args(&g_queue, &binding, args[index]);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
blocks[index].processor = processors[index];
|
|
blocks[index].values = args[index];
|
|
blocks[index].count = FT_TT_QUEUE_KERNEL_ARG_COUNT;
|
|
}
|
|
|
|
rc = ft_tt_op_stage(noc, x, y, &g_config, g_cb_buffer, sizeof(g_cb_buffer), blocks, 5u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return ft_tt_launch_send_go(noc, x, y);
|
|
}
|
|
|
|
static int start_residency(const ft_tt_hardware_session *session, uint16_t x, uint16_t y)
|
|
{
|
|
ft_tt_op_attention_residency residency;
|
|
uint64_t mask = 0;
|
|
int rc;
|
|
|
|
residency.q_rows = GROUP_ROWS;
|
|
residency.dim_tiles = MODEL_DIM_TILES;
|
|
residency.block_tiles = g_block_tiles;
|
|
residency.tile_bytes = TILE_BYTES;
|
|
|
|
rc = load_images(&session->noc, x, y);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_program_load(&session->noc, x, y, &g_program.program, NULL, &g_config);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_op_attention_place_cbs(&residency, g_cb_placed, g_cb_buffer,
|
|
sizeof(g_cb_buffer), &mask);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
ft_tt_test_check_fmt("the residency's L1 is flat in the context and fits the arena",
|
|
ft_tt_op_attention_l1_bytes(&residency) <
|
|
(size_t)(FT_TT_OP_CB_ARENA_END - FT_TT_OP_CB_ARENA_BASE),
|
|
"%zu bytes of %u", ft_tt_op_attention_l1_bytes(&residency),
|
|
FT_TT_OP_CB_ARENA_END - FT_TT_OP_CB_ARENA_BASE);
|
|
|
|
g_config.local_cb_mask = mask;
|
|
g_config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS;
|
|
g_config.brisc_noc_id = 0u;
|
|
rc = ft_tt_queue_init(&session->noc, x, y, FT_TT_OP_QUEUE_BASE, QUEUE_CAPACITY,
|
|
ft_tt_queue_make_nonce(), &g_queue);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
return start_triad(&session->noc, x, y);
|
|
}
|
|
|
|
/* ================================================================
|
|
* One call
|
|
* ================================================================ */
|
|
|
|
/** Fill the request for one call. Shapes and addresses only. */
|
|
static void fill_request(ft_tt_op_attention_request *request, uint32_t kv_positions,
|
|
uint32_t query_row)
|
|
{
|
|
memset(request, 0, sizeof(*request));
|
|
request->q_rows = GROUP_ROWS;
|
|
request->dim_tiles = MODEL_DIM_TILES;
|
|
request->block_tiles = g_block_tiles;
|
|
request->kv_positions = kv_positions;
|
|
request->capacity_tiles = g_cache.capacity_tiles;
|
|
request->query_row = query_row;
|
|
|
|
request->query.x = (uint16_t)g_dram[DRAM_BANK_Q].x;
|
|
request->query.y = (uint16_t)g_dram[DRAM_BANK_Q].y;
|
|
/* The group's FIRST Q tile, not the buffer's — a base that ignored the group offset
|
|
* would read head 0 and still produce a plausible answer. */
|
|
request->query.address =
|
|
DRAM_BASE + TEST_GROUP * GROUP_ROWS * MODEL_DIM_TILES * TILE_BYTES;
|
|
request->query.tile_bytes = TILE_BYTES;
|
|
|
|
request->keys.x = (uint16_t)g_dram[DRAM_BANK_K].x;
|
|
request->keys.y = (uint16_t)g_dram[DRAM_BANK_K].y;
|
|
request->keys.address = DRAM_BASE;
|
|
request->keys.tile_bytes = TILE_BYTES;
|
|
|
|
request->values.x = (uint16_t)g_dram[DRAM_BANK_V].x;
|
|
request->values.y = (uint16_t)g_dram[DRAM_BANK_V].y;
|
|
request->values.address = DRAM_BASE;
|
|
request->values.tile_bytes = TILE_BYTES;
|
|
|
|
request->constants.x = (uint16_t)g_dram[DRAM_BANK_CONST].x;
|
|
request->constants.y = (uint16_t)g_dram[DRAM_BANK_CONST].y;
|
|
request->constants.address = DRAM_CONST_BASE;
|
|
request->constants.tile_bytes = TILE_BYTES;
|
|
|
|
request->output.x = (uint16_t)g_dram[DRAM_BANK_OUT].x;
|
|
request->output.y = (uint16_t)g_dram[DRAM_BANK_OUT].y;
|
|
request->output.address =
|
|
DRAM_BASE + TEST_GROUP * GROUP_ROWS * MODEL_DIM_TILES * TILE_BYTES;
|
|
request->output.tile_bytes = TILE_BYTES;
|
|
}
|
|
|
|
/** Enqueue one descriptor and wait for it. @return FT_TT_OK, or the first failing step. */
|
|
static int run_one(const ft_tt_hardware_session *session,
|
|
const ft_tt_op_attention_request *request, uint32_t *out_completed)
|
|
{
|
|
uint32_t words[FT_TT_OP_ATTENTION_WORDS];
|
|
ft_tt_queue_descriptor descriptor;
|
|
uint32_t parameter_address = 0u;
|
|
int rc = ft_tt_op_attention_parameters(request, words);
|
|
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_write_op_parameters(&session->noc, &g_queue,
|
|
g_descriptors_done % 32u, words,
|
|
¶meter_address);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ATTENTION;
|
|
descriptor.a0 = parameter_address;
|
|
rc = ft_tt_queue_enqueue(&session->noc, &g_queue, &descriptor, 1u);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
g_descriptors_done++;
|
|
return ft_tt_queue_wait_completed(&session->noc, &g_queue, g_descriptors_done,
|
|
RUN_TIMEOUT_MS, out_completed);
|
|
}
|
|
|
|
/** Poison the whole output buffer, so a kernel that never ran scores as such. */
|
|
static int poison_output(const ft_tt_hardware_session *session)
|
|
{
|
|
memset(g_output_tiles, POISON_BYTE, sizeof(g_output_tiles));
|
|
return device_write(&session->noc, g_dram[DRAM_BANK_OUT], DRAM_BASE, g_output_tiles,
|
|
sizeof(g_output_tiles));
|
|
}
|
|
|
|
/**
|
|
* Upload one problem: the query (whole 16-head activation), the two regions, and a poisoned
|
|
* output. `dead_value` fills the cache PAST the live length — the stale data a real server's
|
|
* reused cache would hold, and what the additive mask has to defeat.
|
|
*/
|
|
static int upload_problem(const ft_tt_hardware_session *session, uint32_t kv_positions,
|
|
uint64_t seed, float dead_value, float outlier)
|
|
{
|
|
const uint32_t padded = ft_tt_op_attention_padded_tiles(kv_positions, g_block_tiles) * TILE_DIM;
|
|
const size_t region_bytes = (size_t)g_cache.capacity_tiles * g_cache.dim_tiles * TILE_BYTES;
|
|
uint32_t index;
|
|
int rc;
|
|
|
|
ft_tt_attention_seed(seed);
|
|
for (index = 0; index < MODEL_Q_HEADS * MODEL_HEAD_DIM; index++)
|
|
g_query[index] = ft_float_from_bf16(ft_bf16_from_float(ft_tt_attention_random()));
|
|
for (index = 0; index < padded * MODEL_HEAD_DIM; index++) {
|
|
const bool dead = (index / MODEL_HEAD_DIM) >= kv_positions;
|
|
|
|
g_keys[index] =
|
|
ft_float_from_bf16(ft_bf16_from_float(dead ? dead_value : ft_tt_attention_random()));
|
|
g_values[index] =
|
|
ft_float_from_bf16(ft_bf16_from_float(dead ? dead_value : ft_tt_attention_random()));
|
|
}
|
|
/* The outlier goes in the LAST live tile — a late block — which is what makes a
|
|
* block-local running max produce non-finite values instead of a shifted answer. */
|
|
if (outlier != 0.0f && kv_positions > TILE_DIM) {
|
|
const uint32_t position = kv_positions - 1u;
|
|
|
|
for (index = 0; index < MODEL_HEAD_DIM; index++)
|
|
g_keys[(size_t)position * MODEL_HEAD_DIM + index] =
|
|
ft_float_from_bf16(ft_bf16_from_float(outlier));
|
|
}
|
|
|
|
/* The query buffer carries ALL 16 heads, and only ONE row of it is this token's.
|
|
* Every other row is different random data, so a reader that packed the wrong row
|
|
* cannot pass — and only the GROUP's 8 heads are attended to, so a Q base that ignored
|
|
* the group offset cannot either. */
|
|
rc = FT_TT_OK;
|
|
{
|
|
/* Query activation: [32 rows x (16 heads * 128)] — the model's real layout. */
|
|
static float activation[TILE_DIM * MODEL_Q_HEADS * MODEL_HEAD_DIM];
|
|
const uint32_t width = MODEL_Q_HEADS * MODEL_HEAD_DIM;
|
|
uint32_t row;
|
|
|
|
for (row = 0; row < TILE_DIM; row++) {
|
|
uint32_t column;
|
|
|
|
for (column = 0; column < width; column++)
|
|
activation[(size_t)row * width + column] =
|
|
ft_float_from_bf16(ft_bf16_from_float(ft_tt_attention_random()));
|
|
}
|
|
/* This token's row carries the query the reference will use. */
|
|
for (index = 0; index < width; index++)
|
|
activation[(size_t)ft_tt_op_memory_position_row(kv_positions - 1u) * width + index] =
|
|
g_query[index % (MODEL_Q_HEADS * MODEL_HEAD_DIM)];
|
|
to_tiles(activation, TILE_DIM, width, g_query_tiles);
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_Q], DRAM_BASE, g_query_tiles,
|
|
sizeof(g_query_tiles));
|
|
}
|
|
|
|
if (rc == FT_TT_OK) {
|
|
build_kv_region(g_keys, padded, g_region_tiles);
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_K], DRAM_BASE, g_region_tiles,
|
|
region_bytes);
|
|
}
|
|
if (rc == FT_TT_OK) {
|
|
build_kv_region(g_values, padded, g_region_tiles);
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_V], DRAM_BASE, g_region_tiles,
|
|
region_bytes);
|
|
}
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(session);
|
|
return rc;
|
|
}
|
|
|
|
/** Score one completed call: read the output back, untilize the group's rows, compare. */
|
|
static void score_call(const ft_tt_hardware_session *session, const char *label,
|
|
uint32_t kv_positions, uint32_t query_row)
|
|
{
|
|
ft_tt_attention_problem problem;
|
|
const uint32_t first_tile = TEST_GROUP * GROUP_ROWS * MODEL_DIM_TILES;
|
|
double pcc;
|
|
double max_abs;
|
|
size_t untouched = 0;
|
|
size_t index;
|
|
uint32_t head;
|
|
char text[192];
|
|
int rc;
|
|
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_OUT], DRAM_BASE, g_output_tiles,
|
|
sizeof(g_output_tiles));
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the output reads back", false, "rc=%d", rc);
|
|
return;
|
|
}
|
|
|
|
/* Row `query_row` of the group's tiles is the answer; every OTHER tile in the buffer
|
|
* belongs to the other group and must still hold the poison. That is a byte check, and
|
|
* it is the only kind that can see a writer scattering to the wrong tile — PCC cannot. */
|
|
for (index = 0; index < MODEL_Q_HEADS * MODEL_DIM_TILES; index++) {
|
|
const bool mine = index >= first_tile && index < first_tile + GROUP_ROWS * MODEL_DIM_TILES;
|
|
const unsigned char *bytes =
|
|
(const unsigned char *)(g_output_tiles + index * TILE_ELEMENTS);
|
|
size_t byte;
|
|
|
|
if (mine)
|
|
continue;
|
|
for (byte = 0; byte < TILE_BYTES; byte++) {
|
|
if (bytes[byte] != POISON_BYTE)
|
|
untouched++;
|
|
}
|
|
}
|
|
snprintf(text, sizeof(text), "%s: the OTHER group's tiles are byte-untouched", label);
|
|
ft_tt_test_check_fmt(text, untouched == 0u, "%zu bytes disturbed", untouched);
|
|
|
|
for (head = 0; head < GROUP_ROWS; head++)
|
|
row_from_tiles(g_output_tiles, first_tile + head * MODEL_DIM_TILES, MODEL_DIM_TILES,
|
|
query_row, g_actual + (size_t)head * MODEL_HEAD_DIM);
|
|
|
|
problem.q_rows = GROUP_ROWS;
|
|
problem.head_dim = MODEL_HEAD_DIM;
|
|
problem.kv_positions = kv_positions;
|
|
problem.query = g_query + (size_t)TEST_GROUP * GROUP_ROWS * MODEL_HEAD_DIM;
|
|
problem.keys = g_keys;
|
|
problem.values = g_values;
|
|
if (!ft_tt_attention_reference(&problem, g_expected)) {
|
|
ft_tt_test_check_fmt("the reference computes", false, "kv=%u", kv_positions);
|
|
return;
|
|
}
|
|
|
|
pcc = ft_tt_attention_pcc(g_expected, g_actual, GROUP_ROWS * MODEL_HEAD_DIM);
|
|
max_abs = ft_tt_attention_max_abs(g_expected, g_actual, GROUP_ROWS * MODEL_HEAD_DIM);
|
|
snprintf(text, sizeof(text), "%s: PCC >= %.2f vs a CPU fp32 reference", label, PCC_ACCEPT);
|
|
ft_tt_test_check_fmt(text, pcc >= PCC_ACCEPT, "pcc %.9f max-abs %.6f", pcc, max_abs);
|
|
snprintf(text, sizeof(text), "%s: max-abs is small (PCC is the coarse instrument)", label);
|
|
ft_tt_test_check_fmt(text, max_abs < 0.05, "max-abs %.6f", max_abs);
|
|
snprintf(text, sizeof(text), "%s: neither side is non-finite", label);
|
|
ft_tt_test_check_fmt(
|
|
text,
|
|
ft_tt_attention_non_finite(g_expected, GROUP_ROWS * MODEL_HEAD_DIM) == 0u &&
|
|
ft_tt_attention_non_finite(g_actual, GROUP_ROWS * MODEL_HEAD_DIM) == 0u,
|
|
"reference %zu card %zu",
|
|
ft_tt_attention_non_finite(g_expected, GROUP_ROWS * MODEL_HEAD_DIM),
|
|
ft_tt_attention_non_finite(g_actual, GROUP_ROWS * MODEL_HEAD_DIM));
|
|
printf(" %-46s pcc %.9f max-abs %.6f\n", label, pcc, max_abs);
|
|
}
|
|
|
|
/** One whole scored call: upload, enqueue, wait, score. */
|
|
static bool run_and_score(const ft_tt_hardware_session *session, const char *label,
|
|
uint32_t kv_positions, uint64_t seed, float dead_value, float outlier)
|
|
{
|
|
ft_tt_op_attention_request request;
|
|
const uint32_t query_row = ft_tt_op_memory_position_row(kv_positions - 1u);
|
|
uint32_t completed = 0u;
|
|
char text[160];
|
|
int rc = upload_problem(session, kv_positions, seed, dead_value, outlier);
|
|
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the problem uploads", false, "rc=%d", rc);
|
|
return false;
|
|
}
|
|
fill_request(&request, kv_positions, query_row);
|
|
rc = run_one(session, &request, &completed);
|
|
snprintf(text, sizeof(text), "%s completes with NO launch, only an enqueue", label);
|
|
ft_tt_test_check_fmt(text, rc == FT_TT_OK, "rc=%d completed=%u of %u", rc, completed,
|
|
g_descriptors_done);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
{
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
const char *reason = NULL;
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queue, fields);
|
|
snprintf(text, sizeof(text), "%s: completion is evidenced by the WRITER", label);
|
|
ft_tt_test_check_fmt(text,
|
|
ft_tt_queue_check_completion(&g_queue, fields, g_descriptors_done,
|
|
&reason) == FT_TT_OK,
|
|
"%s", reason ? reason : "ok");
|
|
}
|
|
score_call(session, label, kv_positions, query_row);
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The stages
|
|
* ================================================================ */
|
|
|
|
static void stage_round_trip(const ft_tt_hardware_session *session)
|
|
{
|
|
uint16_t written[TILE_ELEMENTS];
|
|
uint16_t read_back[TILE_ELEMENTS];
|
|
size_t index;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 0 — host->DRAM->host, so the transfer path is a LINE and not an "
|
|
"assumption");
|
|
|
|
ft_tt_attention_seed(1u);
|
|
for (index = 0; index < TILE_ELEMENTS; index++)
|
|
written[index] = ft_bf16_from_float(ft_tt_attention_random());
|
|
memset(read_back, 0xA5, sizeof(read_back));
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_K], DRAM_BASE, written, sizeof(written));
|
|
if (rc == FT_TT_OK)
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_K], DRAM_BASE, read_back,
|
|
sizeof(read_back));
|
|
ft_tt_test_check_fmt("a tile round-trips through DRAM", rc == FT_TT_OK, "rc=%d", rc);
|
|
ft_tt_test_check("and it comes back byte-exact",
|
|
memcmp(written, read_back, sizeof(written)) == 0, "");
|
|
}
|
|
|
|
static void stage_shapes(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_test_step("stage 1 — the shapes, each scored against a CPU fp32 reference");
|
|
|
|
(void)run_and_score(session, "kv=256, four whole blocks", 256u, 20260812ull, 0.0f, 0.0f);
|
|
(void)run_and_score(session, "kv=1000, a partial last tile", 1000u, 20260813ull, 0.0f, 0.0f);
|
|
(void)run_and_score(session, "kv=33, two live tiles in a padded window", 33u, 20260814ull,
|
|
0.0f, 0.0f);
|
|
(void)run_and_score(session, "kv=1, one live position", 1u, 20260815ull, 0.0f, 0.0f);
|
|
(void)run_and_score(session, "kv=2048, sixty-four blocks", 2048u, 20260816ull, 0.0f, 0.0f);
|
|
}
|
|
|
|
static void stage_invariances(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_test_step("stage 2 — the two things PCC cannot see");
|
|
|
|
/* softmax(x - c) = softmax(x): a stale running max is invisible to correlation on
|
|
* well-scaled data, because the rescale keeps the answer algebraically right whatever
|
|
* reference point it uses. What is NOT invisible is a max so stale that the exponential
|
|
* leaves representable range — so the outlier has to be BIG. +200 is the size the reduce
|
|
* suite settled on for the same reason (docs/tt_bringup_log.md §22.2): a kernel that
|
|
* kept an early block's max computes exp(+200) here, which is inf in bf16 and in fp32
|
|
* alike, while a correct one computes exp(0) = 1.
|
|
*
|
|
* MEASURED, and it is why this number is 200 and not 30: at +30 the mutation that
|
|
* freezes the max after the first block stays GREEN on every check in this file. */
|
|
(void)run_and_score(session, "a +200 outlier planted in a LATE KV block", 2048u,
|
|
20260817ull, 0.0f, 200.0f);
|
|
|
|
/* Stale KV past the live length, which is what a reused cache holds. If the additive
|
|
* mask is absent or applied after the exponential, this dominates the row max and the
|
|
* answer collapses. */
|
|
(void)run_and_score(session, "garbage poisoned past the live length", 1000u, 20260818ull,
|
|
60.0f, 0.0f);
|
|
}
|
|
|
|
/* ================================================================
|
|
* stage 2b — CHUNK MODE: one Q head, C consecutive positions, one descriptor
|
|
* ================================================================ */
|
|
|
|
/** The established split. 103 positions as 32 + 32 + 32 + 7 is the smallest shape that
|
|
* exercises a chunk with no prior context, two chunks with it, and a ragged chunk — and
|
|
* both GDN mixers recorded their boundary bug as INVISIBLE IN CHUNK 0 (spec, 2026-08-21).
|
|
* For attention the analogue is "attend to prior chunks", which chunk 0 cannot see at all,
|
|
* because at chunk 0 there is nothing before the chunk. */
|
|
#define CHUNK_POSITIONS 103u
|
|
#define CHUNK_MAX_ROWS 32u
|
|
#define CHUNK_COUNT 4u
|
|
|
|
/** NOT head 0, and not a head at the start of a KV group: a Q base that dropped the head
|
|
* offset, or one that kept the group offset, would still read a plausible query. */
|
|
#define CHUNK_HEAD 5u
|
|
|
|
/** Max-norm relative, the metric the chunkwise GDN gates settled on: per-element relative
|
|
* is meaningless for an output that crosses zero. The absolute is reported beside it.
|
|
*
|
|
* 1.5e-2 IS CALIBRATED AGAINST THE SHIPPED DECODE DESCRIPTOR, NOT CHOSEN. Measured on this
|
|
* problem, 2026-08-21: the chunk arm reads 4.6e-3 .. 7.7e-3 over the four chunks, and the
|
|
* DECODE descriptor — which chunk_cross_check() shows is bit-identical to the chunk's last
|
|
* row, max-abs 0.000e+00 — reads 3.1e-3 .. 9.5e-3 against the same reference on the same
|
|
* row. So this residue is the op's own bf16/Tf32 residue and not the chunk path's, and a
|
|
* bar below the decode arm's own figure would fail a bit-exact arm — the same mistake the
|
|
* 1e-3 intermediate bar made on the GDN norms. The bar that actually discriminates is
|
|
* `chunk_relative <= decode_relative`, which is checked directly beside this one; this
|
|
* number exists so a gross regression still trips something. */
|
|
#define CHUNK_RELATIVE_GATE 1.5e-2
|
|
/** What a red arm has to clear. Two orders above the gate, so "red" is not "noisy". */
|
|
#define CHUNK_RED_FLOOR 1.0e-2
|
|
/** The chunk's LAST row is the one a decode descriptor would have computed, so the two must
|
|
* agree to about the bf16 rounding of one extra pack. */
|
|
#define CHUNK_CROSS_ABSOLUTE 5.0e-3
|
|
|
|
/** A second constants operand whose 32 causal tiles are ALL LIVE. */
|
|
#define DRAM_CONST_RED_BASE (DRAM_CONST_BASE + 0x40000u)
|
|
|
|
static float g_chunk_query[CHUNK_POSITIONS * MODEL_Q_HEADS * MODEL_HEAD_DIM];
|
|
static float g_chunk_expected[CHUNK_MAX_ROWS * MODEL_HEAD_DIM];
|
|
static float g_chunk_actual[CHUNK_MAX_ROWS * MODEL_HEAD_DIM];
|
|
static float g_chunk_head_query[CHUNK_MAX_ROWS * MODEL_HEAD_DIM];
|
|
static float g_chunk_last_row[MODEL_HEAD_DIM];
|
|
static float g_arm_last_row[MODEL_HEAD_DIM];
|
|
static float g_chunk_arm[CHUNK_MAX_ROWS * MODEL_HEAD_DIM];
|
|
static uint16_t g_constants_red[FT_TT_OP_ATTENTION_CONST_TILES * TILE_ELEMENTS];
|
|
|
|
/** Filled by chunk_score(): the max-norm relative over the whole chunk, and the max-abs of
|
|
* the LAST row alone — which is the row a decode descriptor computes and the row causality
|
|
* cannot move. */
|
|
static double g_chunk_relative;
|
|
static double g_chunk_absolute;
|
|
static double g_chunk_last_relative;
|
|
/** The SHIPPED decode descriptors' own max-norm relative over the WHOLE chunk, filled by
|
|
* chunk_cross_check(). The chunk arm's bar, and the only honest one. */
|
|
static double g_decode_last_relative;
|
|
|
|
/** Positions the reader actually fetches for this stage, whatever the chunk. */
|
|
static uint32_t chunk_padded_positions(void)
|
|
{
|
|
return ft_tt_op_attention_padded_tiles(CHUNK_POSITIONS, g_block_tiles) * TILE_DIM;
|
|
}
|
|
|
|
/**
|
|
* Fill the host operands once.
|
|
*
|
|
* PAST THE 103 POSITIONS THE CACHE HOLDS ORDINARY RANDOM DATA, not a bland constant. For
|
|
* chunk `c` every position after it is a FUTURE position of this same sequence, so a causal
|
|
* mask that leaks reads a perfectly plausible value — which is what makes the causality red
|
|
* arm a real question rather than a check that a 1e30 shows up somewhere.
|
|
*/
|
|
static void chunk_fill_host(void)
|
|
{
|
|
const uint32_t padded = chunk_padded_positions();
|
|
uint32_t index;
|
|
|
|
ft_tt_attention_seed(0xC401BEEFull);
|
|
for (index = 0; index < padded * MODEL_HEAD_DIM; index++) {
|
|
g_keys[index] = ft_float_from_bf16(ft_bf16_from_float(ft_tt_attention_random()));
|
|
g_values[index] = ft_float_from_bf16(ft_bf16_from_float(ft_tt_attention_random()));
|
|
}
|
|
for (index = 0; index < CHUNK_POSITIONS * MODEL_Q_HEADS * MODEL_HEAD_DIM; index++)
|
|
g_chunk_query[index] = ft_float_from_bf16(ft_bf16_from_float(ft_tt_attention_random()));
|
|
}
|
|
|
|
/**
|
|
* Zero the UPLOADED tiles of positions [0, positions).
|
|
*
|
|
* The host arrays are untouched, so the reference still reads the prior context this arm
|
|
* removes. A red arm that mutated `g_keys` would change what BOTH sides compute and score
|
|
* 0.000 — the ninth recorded vacuous-gate class, and the one that cost the delta rule a
|
|
* round (spec, 2026-08-21).
|
|
*/
|
|
static void chunk_wipe_region(uint32_t positions)
|
|
{
|
|
uint32_t position;
|
|
|
|
for (position = 0; position < positions; position += TILE_DIM) {
|
|
uint32_t dim_tile;
|
|
|
|
for (dim_tile = 0; dim_tile < MODEL_DIM_TILES; dim_tile++) {
|
|
const uint64_t offset = ft_tt_kv_cache_tile_offset(&g_cache, 0u, position, dim_tile);
|
|
|
|
memset(g_region_tiles + offset / sizeof(g_region_tiles[0]), 0, TILE_BYTES);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Both regions, optionally with the positions before `wipe_positions` blanked on the card. */
|
|
static int chunk_upload_regions(const ft_tt_hardware_session *session, uint32_t wipe_positions)
|
|
{
|
|
const size_t region_bytes = (size_t)g_cache.capacity_tiles * g_cache.dim_tiles * TILE_BYTES;
|
|
int rc;
|
|
|
|
build_kv_region(g_keys, chunk_padded_positions(), g_region_tiles);
|
|
chunk_wipe_region(wipe_positions);
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_K], DRAM_BASE, g_region_tiles,
|
|
region_bytes);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
build_kv_region(g_values, chunk_padded_positions(), g_region_tiles);
|
|
chunk_wipe_region(wipe_positions);
|
|
return device_write(&session->noc, g_dram[DRAM_BANK_V], DRAM_BASE, g_region_tiles,
|
|
region_bytes);
|
|
}
|
|
|
|
/**
|
|
* The chunk's Q activation: rows 0..rows-1 are positions `first..first+rows-1` for EVERY
|
|
* head, rows `rows`..31 are zero.
|
|
*
|
|
* This is the whole geometry claim, as data: a chunk is the SAME strip a single position is,
|
|
* with rows 1..C-1 carrying positions instead of padding. Nothing about the tile grid moves.
|
|
*/
|
|
static int chunk_upload_query(const ft_tt_hardware_session *session, uint32_t first,
|
|
uint32_t rows)
|
|
{
|
|
static float activation[TILE_DIM * MODEL_Q_HEADS * MODEL_HEAD_DIM];
|
|
const uint32_t width = MODEL_Q_HEADS * MODEL_HEAD_DIM;
|
|
uint32_t row;
|
|
|
|
memset(activation, 0, sizeof(activation));
|
|
for (row = 0; row < rows; row++)
|
|
memcpy(activation + (size_t)row * width,
|
|
g_chunk_query + (size_t)(first + row) * width, (size_t)width * sizeof(float));
|
|
to_tiles(activation, TILE_DIM, width, g_query_tiles);
|
|
return device_write(&session->noc, g_dram[DRAM_BANK_Q], DRAM_BASE, g_query_tiles,
|
|
sizeof(g_query_tiles));
|
|
}
|
|
|
|
/** One chunk descriptor. `head` is a Q head, not a KV group — that is the mode. */
|
|
static void chunk_fill_request(ft_tt_op_attention_request *request, uint32_t first,
|
|
uint32_t rows, uint32_t head, uint32_t constants_base)
|
|
{
|
|
const uint32_t head_offset = head * MODEL_DIM_TILES * TILE_BYTES;
|
|
|
|
memset(request, 0, sizeof(*request));
|
|
request->q_rows = rows;
|
|
request->dim_tiles = MODEL_DIM_TILES;
|
|
request->block_tiles = g_block_tiles;
|
|
request->kv_positions = first + rows;
|
|
request->capacity_tiles = g_cache.capacity_tiles;
|
|
request->query_row = 0u;
|
|
request->flags = FT_TT_OP_ATTENTION_FLAG_CHUNK;
|
|
|
|
request->query.x = (uint16_t)g_dram[DRAM_BANK_Q].x;
|
|
request->query.y = (uint16_t)g_dram[DRAM_BANK_Q].y;
|
|
request->query.address = DRAM_BASE + head_offset;
|
|
request->query.tile_bytes = TILE_BYTES;
|
|
|
|
request->keys.x = (uint16_t)g_dram[DRAM_BANK_K].x;
|
|
request->keys.y = (uint16_t)g_dram[DRAM_BANK_K].y;
|
|
request->keys.address = DRAM_BASE;
|
|
request->keys.tile_bytes = TILE_BYTES;
|
|
|
|
request->values.x = (uint16_t)g_dram[DRAM_BANK_V].x;
|
|
request->values.y = (uint16_t)g_dram[DRAM_BANK_V].y;
|
|
request->values.address = DRAM_BASE;
|
|
request->values.tile_bytes = TILE_BYTES;
|
|
|
|
request->constants.x = (uint16_t)g_dram[DRAM_BANK_CONST].x;
|
|
request->constants.y = (uint16_t)g_dram[DRAM_BANK_CONST].y;
|
|
request->constants.address = constants_base;
|
|
request->constants.tile_bytes = TILE_BYTES;
|
|
|
|
request->output.x = (uint16_t)g_dram[DRAM_BANK_OUT].x;
|
|
request->output.y = (uint16_t)g_dram[DRAM_BANK_OUT].y;
|
|
request->output.address = DRAM_BASE + head_offset;
|
|
request->output.tile_bytes = TILE_BYTES;
|
|
}
|
|
|
|
/**
|
|
* Read the chunk's rows back and score them against the DEFINITIONAL chunk reference.
|
|
*
|
|
* MAX-NORM RELATIVE, with the absolute beside it. Attention's output crosses zero, so a
|
|
* per-element relative is 100% wherever the answer is small and says nothing about the
|
|
* kernel (the same defect the chunkwise convolution's first gate had).
|
|
*
|
|
* @return true if the readback and the reference both succeeded
|
|
*/
|
|
static bool chunk_score(const ft_tt_hardware_session *session, const char *label,
|
|
uint32_t first, uint32_t rows, uint32_t head, bool check_poison)
|
|
{
|
|
ft_tt_attention_problem problem;
|
|
const uint32_t first_tile = head * MODEL_DIM_TILES;
|
|
const uint32_t width = MODEL_Q_HEADS * MODEL_HEAD_DIM;
|
|
double worst_absolute = 0.0;
|
|
double scale = 0.0;
|
|
double last_absolute = 0.0;
|
|
double last_scale = 0.0;
|
|
size_t untouched = 0;
|
|
size_t index;
|
|
uint32_t row;
|
|
char text[224];
|
|
int rc = device_read(&session->noc, g_dram[DRAM_BANK_OUT], DRAM_BASE, g_output_tiles,
|
|
sizeof(g_output_tiles));
|
|
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the chunk output reads back", false, "rc=%d", rc);
|
|
return false;
|
|
}
|
|
|
|
if (check_poison) {
|
|
for (index = 0; index < MODEL_Q_HEADS * MODEL_DIM_TILES; index++) {
|
|
const unsigned char *bytes =
|
|
(const unsigned char *)(g_output_tiles + index * TILE_ELEMENTS);
|
|
size_t byte;
|
|
|
|
if (index >= first_tile && index < first_tile + MODEL_DIM_TILES)
|
|
continue;
|
|
for (byte = 0; byte < TILE_BYTES; byte++) {
|
|
if (bytes[byte] != POISON_BYTE)
|
|
untouched++;
|
|
}
|
|
}
|
|
snprintf(text, sizeof(text), "%s: every OTHER head's tiles are byte-untouched", label);
|
|
ft_tt_test_check_fmt(text, untouched == 0u, "%zu bytes disturbed", untouched);
|
|
}
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
row_from_tiles(g_output_tiles, first_tile, MODEL_DIM_TILES, row,
|
|
g_chunk_actual + (size_t)row * MODEL_HEAD_DIM);
|
|
memcpy(g_chunk_head_query + (size_t)row * MODEL_HEAD_DIM,
|
|
g_chunk_query + (size_t)(first + row) * width + (size_t)head * MODEL_HEAD_DIM,
|
|
MODEL_HEAD_DIM * sizeof(float));
|
|
}
|
|
|
|
problem.q_rows = rows;
|
|
problem.head_dim = MODEL_HEAD_DIM;
|
|
problem.kv_positions = first + rows;
|
|
problem.query = g_chunk_head_query;
|
|
problem.keys = g_keys;
|
|
problem.values = g_values;
|
|
if (!ft_tt_attention_chunk_reference(&problem, first, g_chunk_expected)) {
|
|
ft_tt_test_check_fmt("the chunk reference computes", false, "first=%u rows=%u", first,
|
|
rows);
|
|
return false;
|
|
}
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
uint32_t element;
|
|
|
|
for (element = 0; element < MODEL_HEAD_DIM; element++) {
|
|
const size_t at = (size_t)row * MODEL_HEAD_DIM + element;
|
|
const double want = (double)g_chunk_expected[at];
|
|
const double difference = fabs((double)g_chunk_actual[at] - want);
|
|
|
|
if (difference > worst_absolute)
|
|
worst_absolute = difference;
|
|
if (fabs(want) > scale)
|
|
scale = fabs(want);
|
|
if (row + 1u == rows) {
|
|
if (difference > last_absolute)
|
|
last_absolute = difference;
|
|
if (fabs(want) > last_scale)
|
|
last_scale = fabs(want);
|
|
}
|
|
}
|
|
}
|
|
memcpy(g_chunk_last_row, g_chunk_actual + (size_t)(rows - 1u) * MODEL_HEAD_DIM,
|
|
MODEL_HEAD_DIM * sizeof(float));
|
|
|
|
g_chunk_absolute = worst_absolute;
|
|
g_chunk_relative = worst_absolute / (scale > 1.0e-6 ? scale : 1.0e-6);
|
|
g_chunk_last_relative = last_absolute / (last_scale > 1.0e-6 ? last_scale : 1.0e-6);
|
|
printf(" %-52s max-norm rel %.3e abs %.3e last-row rel %.3e\n", label,
|
|
g_chunk_relative, g_chunk_absolute, g_chunk_last_relative);
|
|
return true;
|
|
}
|
|
|
|
/** Upload the query for a chunk, enqueue its descriptor, and score it. */
|
|
static bool chunk_run(const ft_tt_hardware_session *session, const char *label, uint32_t first,
|
|
uint32_t rows, uint32_t head, uint32_t constants_base, bool check_poison)
|
|
{
|
|
ft_tt_op_attention_request request;
|
|
uint32_t completed = 0u;
|
|
char text[192];
|
|
int rc = chunk_upload_query(session, first, rows);
|
|
|
|
if (rc == FT_TT_OK)
|
|
rc = poison_output(session);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the chunk problem uploads", false, "rc=%d", rc);
|
|
return false;
|
|
}
|
|
chunk_fill_request(&request, first, rows, head, constants_base);
|
|
rc = run_one(session, &request, &completed);
|
|
snprintf(text, sizeof(text), "%s: the chunk descriptor completes", label);
|
|
ft_tt_test_check_fmt(text, rc == FT_TT_OK, "rc=%d completed=%u of %u", rc, completed,
|
|
g_descriptors_done);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
return chunk_score(session, label, first, rows, head, check_poison);
|
|
}
|
|
|
|
/**
|
|
* THE C DECODE DESCRIPTORS THE CHUNK REPLACES, RUN AND COMPARED ROW FOR ROW.
|
|
*
|
|
* THIS IS THE STRONGEST CHECK IN THE STAGE AND IT NEEDS NO REFERENCE AT ALL. Position
|
|
* `first + r` is served by the SHIPPED decode path at query row `r` of the SAME uploaded Q
|
|
* activation, and by the chunk path at row `r` of the same output tiles. The two go through
|
|
* different readers, a different mask and different writers — a face-row pack and scatter
|
|
* against two whole-tile transfers — and they must produce the same numbers.
|
|
*
|
|
* Every decode descriptor writes ROW r of the same tiles, so C of them leave the buffer
|
|
* holding the whole chunk and ONE readback recovers it.
|
|
*
|
|
* It also gives the chunk arm its only honest bar. A definitional fp32 reference measures
|
|
* this op's bf16 KV, its Tf32 unpack and its online rescale — none of which chunk mode
|
|
* touches — so "is the chunk arm accurate" is really "is it as accurate as the descriptor it
|
|
* replaces", and that is a number, not a constant.
|
|
*/
|
|
static bool chunk_cross_check(const ft_tt_hardware_session *session, uint32_t first,
|
|
uint32_t rows)
|
|
{
|
|
const uint32_t group = CHUNK_HEAD / GROUP_ROWS;
|
|
const uint32_t group_offset = group * GROUP_ROWS * MODEL_DIM_TILES * TILE_BYTES;
|
|
double worst_card = 0.0;
|
|
double worst_absolute = 0.0;
|
|
double scale = 0.0;
|
|
uint32_t row;
|
|
int rc;
|
|
|
|
memcpy(g_chunk_arm, g_chunk_actual, (size_t)rows * MODEL_HEAD_DIM * sizeof(float));
|
|
|
|
rc = poison_output(session);
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
ft_tt_op_attention_request request;
|
|
uint32_t completed = 0u;
|
|
|
|
fill_request(&request, first + row + 1u, row);
|
|
/* fill_request points at TEST_GROUP; this call is about CHUNK_HEAD's group. */
|
|
request.query.address = DRAM_BASE + group_offset;
|
|
request.output.address = DRAM_BASE + group_offset;
|
|
if (run_one(session, &request, &completed) != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the decode descriptors for the chunk's positions complete",
|
|
false, "position %u", first + row);
|
|
return false;
|
|
}
|
|
}
|
|
rc = device_read(&session->noc, g_dram[DRAM_BANK_OUT], DRAM_BASE, g_output_tiles,
|
|
sizeof(g_output_tiles));
|
|
if (rc != FT_TT_OK)
|
|
return false;
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
uint32_t element;
|
|
|
|
row_from_tiles(g_output_tiles, CHUNK_HEAD * MODEL_DIM_TILES, MODEL_DIM_TILES, row,
|
|
g_chunk_actual + (size_t)row * MODEL_HEAD_DIM);
|
|
for (element = 0; element < MODEL_HEAD_DIM; element++) {
|
|
const size_t at = (size_t)row * MODEL_HEAD_DIM + element;
|
|
const double want = (double)g_chunk_expected[at];
|
|
const double card = fabs((double)g_chunk_actual[at] - (double)g_chunk_arm[at]);
|
|
const double against = fabs((double)g_chunk_actual[at] - want);
|
|
|
|
if (card > worst_card)
|
|
worst_card = card;
|
|
if (against > worst_absolute)
|
|
worst_absolute = against;
|
|
if (fabs(want) > scale)
|
|
scale = fabs(want);
|
|
}
|
|
}
|
|
g_decode_last_relative = worst_absolute / (scale > 1.0e-6 ? scale : 1.0e-6);
|
|
|
|
ft_tt_test_check_fmt("CARD vs CARD: ONE chunk descriptor IS the decode descriptors it "
|
|
"replaces, row for row",
|
|
worst_card < CHUNK_CROSS_ABSOLUTE, "rows %u max-abs %.3e (bar %.1e)",
|
|
rows, worst_card, CHUNK_CROSS_ABSOLUTE);
|
|
printf(" %-52s max-abs %.3e (decode\'s own rel vs the reference %.3e)\n",
|
|
"chunk vs the C decode descriptors it replaces", worst_card,
|
|
g_decode_last_relative);
|
|
/* Leave the chunk arm's rows where the caller found them. */
|
|
memcpy(g_chunk_actual, g_chunk_arm, (size_t)rows * MODEL_HEAD_DIM * sizeof(float));
|
|
return true;
|
|
}
|
|
|
|
static void stage_chunk(const ft_tt_hardware_session *session)
|
|
{
|
|
static const uint32_t k_first[CHUNK_COUNT] = { 0u, 32u, 64u, 96u };
|
|
static const uint32_t k_rows[CHUNK_COUNT] = { 32u, 32u, 32u, 7u };
|
|
double arm_relative[CHUNK_COUNT];
|
|
double decode_relative[CHUNK_COUNT];
|
|
double causal_relative[CHUNK_COUNT];
|
|
double causal_last_delta[CHUNK_COUNT];
|
|
double prior_relative[CHUNK_COUNT];
|
|
uint32_t chunk;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 2b — CHUNK MODE: %u positions as 32+32+32+7, one Q head a "
|
|
"descriptor", CHUNK_POSITIONS);
|
|
|
|
chunk_fill_host();
|
|
rc = chunk_upload_regions(session, 0u);
|
|
ft_tt_test_check_fmt("the chunk stage's K and V regions upload", rc == FT_TT_OK, "rc=%d",
|
|
rc);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
|
|
/* The RED constants: identical to the shipped ones except that all 32 causal tiles are
|
|
* ALL LIVE. A descriptor pointed at them is causality switched off and NOTHING else —
|
|
* same images, same K, same V, same Q, one address word different. */
|
|
memcpy(g_constants_red, g_constants, sizeof(g_constants_red));
|
|
memset(g_constants_red + (size_t)FT_TT_OP_ATTENTION_CONST_CAUSAL_TILE * TILE_ELEMENTS, 0,
|
|
(size_t)TILE_DIM * TILE_ELEMENTS * sizeof(g_constants_red[0]));
|
|
rc = device_write(&session->noc, g_dram[DRAM_BANK_CONST], DRAM_CONST_RED_BASE,
|
|
g_constants_red, sizeof(g_constants_red));
|
|
ft_tt_test_check_fmt("the all-live causal constants upload beside the real ones",
|
|
rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
|
|
for (chunk = 0; chunk < CHUNK_COUNT; chunk++) {
|
|
char label[96];
|
|
|
|
snprintf(label, sizeof(label), "chunk %u positions %u..%u head %u", chunk,
|
|
k_first[chunk], k_first[chunk] + k_rows[chunk] - 1u, CHUNK_HEAD);
|
|
if (!chunk_run(session, label, k_first[chunk], k_rows[chunk], CHUNK_HEAD,
|
|
DRAM_CONST_BASE, true))
|
|
return;
|
|
arm_relative[chunk] = g_chunk_relative;
|
|
memcpy(g_arm_last_row, g_chunk_last_row, sizeof(g_arm_last_row));
|
|
ft_tt_test_check_fmt(label, g_chunk_relative <= CHUNK_RELATIVE_GATE,
|
|
"max-norm relative %.3e (gate %.1e), abs %.3e", g_chunk_relative,
|
|
CHUNK_RELATIVE_GATE, g_chunk_absolute);
|
|
|
|
g_decode_last_relative = 0.0;
|
|
(void)chunk_cross_check(session, k_first[chunk], k_rows[chunk]);
|
|
decode_relative[chunk] = g_decode_last_relative;
|
|
|
|
/* RED 1 — CAUSALITY OFF. */
|
|
snprintf(label, sizeof(label), "RED causal_off chunk %u", chunk);
|
|
if (!chunk_run(session, label, k_first[chunk], k_rows[chunk], CHUNK_HEAD,
|
|
DRAM_CONST_RED_BASE, false))
|
|
return;
|
|
causal_relative[chunk] = g_chunk_relative;
|
|
{
|
|
double worst = 0.0;
|
|
uint32_t element;
|
|
|
|
for (element = 0; element < MODEL_HEAD_DIM; element++) {
|
|
const double difference =
|
|
fabs((double)g_chunk_last_row[element] - (double)g_arm_last_row[element]);
|
|
|
|
if (difference > worst)
|
|
worst = difference;
|
|
}
|
|
causal_last_delta[chunk] = worst;
|
|
}
|
|
|
|
/* RED 2 — PRIOR CONTEXT WIPED ON THE CARD ONLY. */
|
|
rc = chunk_upload_regions(session, k_first[chunk]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the wiped regions upload", false, "rc=%d", rc);
|
|
return;
|
|
}
|
|
snprintf(label, sizeof(label), "RED prior_wipe chunk %u", chunk);
|
|
if (!chunk_run(session, label, k_first[chunk], k_rows[chunk], CHUNK_HEAD,
|
|
DRAM_CONST_BASE, false))
|
|
return;
|
|
prior_relative[chunk] = g_chunk_relative;
|
|
rc = chunk_upload_regions(session, 0u);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the regions restore", false, "rc=%d", rc);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* RED 3 — the head offset. Chunk 1's descriptor pointed at head 0's query, everything
|
|
* else identical: one address word, and the answer must move. */
|
|
{
|
|
ft_tt_op_attention_request request;
|
|
uint32_t completed = 0u;
|
|
|
|
if (chunk_upload_query(session, k_first[1], k_rows[1]) == FT_TT_OK &&
|
|
poison_output(session) == FT_TT_OK) {
|
|
chunk_fill_request(&request, k_first[1], k_rows[1], CHUNK_HEAD, DRAM_CONST_BASE);
|
|
request.query.address = DRAM_BASE; /* head 0, not CHUNK_HEAD */
|
|
if (run_one(session, &request, &completed) == FT_TT_OK &&
|
|
chunk_score(session, "RED wrong_head chunk 1 reads head 0's Q", k_first[1],
|
|
k_rows[1], CHUNK_HEAD, false))
|
|
ft_tt_test_check_fmt("RED wrong_head: a chunk reading head 0's query goes red",
|
|
g_chunk_relative > CHUNK_RED_FLOOR,
|
|
"relative %.3e (floor %.1e)", g_chunk_relative,
|
|
CHUNK_RED_FLOOR);
|
|
}
|
|
}
|
|
|
|
/* ---- The findings this stage exists for --------------------------------------- */
|
|
|
|
for (chunk = 0; chunk < CHUNK_COUNT; chunk++) {
|
|
char label[192];
|
|
|
|
/* CAUSALITY. Every chunk here is at least 7 rows, so every one of them has a future
|
|
* to leak. */
|
|
snprintf(label, sizeof(label),
|
|
"RED causal_off is RED for chunk %u — a chunk attending to its own future",
|
|
chunk);
|
|
ft_tt_test_check_fmt(label, causal_relative[chunk] > CHUNK_RED_FLOOR,
|
|
"arm %.3e -> red %.3e (floor %.1e)", arm_relative[chunk],
|
|
causal_relative[chunk], CHUNK_RED_FLOOR);
|
|
|
|
/* ...AND ON A FULL CHUNK IT LEAVES THE LAST ROW ALONE. The last position of a full
|
|
* chunk attends to the whole chunk, so causality cannot move its answer — which is
|
|
* exactly why a gate scoring only the last position would call this bug green.
|
|
*
|
|
* ON THE RAGGED CHUNK IT MOVES THE LAST ROW TOO, and that is a second finding rather
|
|
* than an exception: the causal tile also does the TAIL mask's job (`c < C`), so at
|
|
* C = 7 switching it off lets even the last row read 25 positions the sequence does
|
|
* not have. */
|
|
if (k_rows[chunk] == CHUNK_MAX_ROWS) {
|
|
snprintf(label, sizeof(label),
|
|
"RED causal_off leaves FULL chunk %u's LAST row BIT-UNCHANGED — a "
|
|
"last-position gate cannot see it at all", chunk);
|
|
ft_tt_test_check_fmt(label, causal_last_delta[chunk] == 0.0,
|
|
"arm vs red on the last row, max-abs %.3e",
|
|
causal_last_delta[chunk]);
|
|
} else {
|
|
snprintf(label, sizeof(label),
|
|
"RED causal_off moves the RAGGED chunk %u's last row too — the causal "
|
|
"tile is also the tail mask", chunk);
|
|
ft_tt_test_check_fmt(label, causal_last_delta[chunk] > 0.0,
|
|
"arm vs red on the last row, max-abs %.3e",
|
|
causal_last_delta[chunk]);
|
|
}
|
|
|
|
/* THE BAR THAT ACTUALLY DISCRIMINATES. Whatever the reference residue is, the chunk
|
|
* arm may not be worse than the SHIPPED decode descriptor measured on the same row
|
|
* of the same problem — a comparison between two things that ran on this card, with
|
|
* the definitional reference used only to give both of them a common yardstick. */
|
|
snprintf(label, sizeof(label),
|
|
"chunk %u is no worse against the reference than the DECODE descriptor is",
|
|
chunk);
|
|
ft_tt_test_check_fmt(label,
|
|
arm_relative[chunk] <= decode_relative[chunk] * 1.05 + 1.0e-9,
|
|
"chunk %.3e vs decode %.3e", arm_relative[chunk],
|
|
decode_relative[chunk]);
|
|
|
|
/* THE BOUNDARY. The arm itself is the positive evidence — chunks 1..3 score inside
|
|
* the gate against a reference that includes every prior position, which they could
|
|
* not do without reading them. prior_wipe is what proves that check CAN fail, and it
|
|
* is INVISIBLE IN CHUNK 0 for the same structural reason both GDN mixers recorded:
|
|
* at chunk 0 there is nothing before the chunk, so the path is not exercised at all
|
|
* and a chunk-0-only gate is blind to it. */
|
|
if (chunk == 0u) {
|
|
ft_tt_test_check_fmt("RED prior_wipe is INVISIBLE in chunk 0 — nothing precedes "
|
|
"it, so a chunk-0-only gate cannot see this path",
|
|
prior_relative[0] <= CHUNK_RELATIVE_GATE,
|
|
"relative %.3e (gate %.1e)", prior_relative[0],
|
|
CHUNK_RELATIVE_GATE);
|
|
} else {
|
|
snprintf(label, sizeof(label),
|
|
"RED prior_wipe is RED for chunk %u — the chunk DOES attend to what "
|
|
"came before it", chunk);
|
|
ft_tt_test_check_fmt(label, prior_relative[chunk] > CHUNK_RED_FLOOR,
|
|
"arm %.3e -> red %.3e (floor %.1e)", arm_relative[chunk],
|
|
prior_relative[chunk], CHUNK_RED_FLOOR);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** How many contexts the timing stage will sweep, and how it is told which. */
|
|
#define TIMING_CONTEXTS_MAX 16u
|
|
#define CONTEXTS_ENVIRONMENT "FT_TT_ATTN_CONTEXTS"
|
|
#define REPEATS_ENVIRONMENT "FT_TT_ATTN_REPEATS"
|
|
|
|
/**
|
|
* What a chunk descriptor COSTS, against a decode descriptor at the same context.
|
|
*
|
|
* THE RATIO IS THE WHOLE PRICE OF THE RUNG, and it is an arithmetic that has to be measured
|
|
* rather than asserted:
|
|
*
|
|
* decode — `kv_heads` descriptors serve ONE position, so a position costs
|
|
* `kv_heads * t_decode`;
|
|
* chunk — `q_heads` descriptors serve THIRTY-TWO, so a position costs
|
|
* `q_heads * t_chunk / 32`.
|
|
*
|
|
* The claim being tested is that `t_chunk ~= t_decode`: a decode descriptor's score matmul
|
|
* already computes all 32 rows of its tile and throws 32 - q_rows of them away, so filling
|
|
* those rows with positions is free. If that holds, a position costs
|
|
* `32 * kv_heads / q_heads` times less — 8x at Ornith's 16/4 — and the chunk arm ALSO reads
|
|
* the KV cache 8x less often, because 16 walks replace 128.
|
|
*
|
|
* A chunk descriptor reads the cache once per Q HEAD where decode reads it once per KV
|
|
* GROUP, so if this op ever becomes reader bound rather than compute bound the ratio falls
|
|
* towards `32 / q_rows`. That is why it is swept over context and not taken at one point.
|
|
*/
|
|
static void stage_chunk_timing(const ft_tt_hardware_session *session)
|
|
{
|
|
static const uint32_t k_context[] = { 1024u, 8192u };
|
|
const char *repeats_text = getenv(REPEATS_ENVIRONMENT);
|
|
const uint32_t repeats = repeats_text ? (uint32_t)strtoul(repeats_text, NULL, 0) : 8u;
|
|
size_t index;
|
|
|
|
ft_tt_test_step("stage 2c — what a chunk descriptor costs against a decode one");
|
|
if (repeats == 0u)
|
|
return;
|
|
|
|
for (index = 0; index < sizeof(k_context) / sizeof(k_context[0]); index++) {
|
|
const uint32_t context = k_context[index];
|
|
ft_tt_op_attention_request request;
|
|
uint32_t completed = 0u;
|
|
double chunk_ms;
|
|
double decode_ms;
|
|
double started;
|
|
uint32_t repeat;
|
|
double decode_position;
|
|
double chunk_position;
|
|
|
|
if (context > MAX_KV || context < CHUNK_MAX_ROWS)
|
|
continue;
|
|
|
|
chunk_fill_request(&request, context - CHUNK_MAX_ROWS, CHUNK_MAX_ROWS, CHUNK_HEAD,
|
|
DRAM_CONST_BASE);
|
|
started = now_milliseconds();
|
|
for (repeat = 0; repeat < repeats; repeat++) {
|
|
if (run_one(session, &request, &completed) != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the timed chunk descriptor completes", false, "kv=%u",
|
|
context);
|
|
return;
|
|
}
|
|
}
|
|
chunk_ms = (now_milliseconds() - started) / (double)repeats;
|
|
|
|
fill_request(&request, context, CHUNK_MAX_ROWS - 1u);
|
|
started = now_milliseconds();
|
|
for (repeat = 0; repeat < repeats; repeat++) {
|
|
if (run_one(session, &request, &completed) != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the timed decode descriptor completes", false, "kv=%u",
|
|
context);
|
|
return;
|
|
}
|
|
}
|
|
decode_ms = (now_milliseconds() - started) / (double)repeats;
|
|
|
|
decode_position = decode_ms * (double)MODEL_KV_HEADS;
|
|
chunk_position = chunk_ms * (double)MODEL_Q_HEADS / (double)CHUNK_MAX_ROWS;
|
|
printf(" kv=%-6u chunk %.3f ms/desc decode %.3f ms/desc | a POSITION: "
|
|
"%.4f -> %.4f ms (%.2fx)\n",
|
|
context, chunk_ms, decode_ms, decode_position, chunk_position,
|
|
chunk_position > 0.0 ? decode_position / chunk_position : 0.0);
|
|
ft_tt_test_check_fmt("a chunk descriptor costs about what a decode descriptor costs "
|
|
"— the padded rows were free",
|
|
chunk_ms < decode_ms * 2.0, "kv=%u chunk %.3f ms decode %.3f ms",
|
|
context, chunk_ms, decode_ms);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Read the context ladder from the environment, defaulting to what this suite always swept.
|
|
*
|
|
* **The SLOPE is the finding, not any single point.** A phase whose whole value is at 8k+
|
|
* cannot be priced from one descriptor at bench length, and a `block_tiles` big enough to
|
|
* pad a short context does the padded work rather than the real work — so the ladder has to
|
|
* cross the quantum in both directions. Entries above MAX_KV are dropped rather than
|
|
* clamped, because a clamped entry would silently duplicate its neighbour.
|
|
*/
|
|
static uint32_t timing_contexts(uint32_t *out)
|
|
{
|
|
static const uint32_t fallback[] = { 1024u, 2048u, 8192u };
|
|
const char *text = getenv(CONTEXTS_ENVIRONMENT);
|
|
uint32_t count = 0u;
|
|
|
|
if (!text) {
|
|
for (count = 0; count < sizeof(fallback) / sizeof(fallback[0]); count++)
|
|
out[count] = fallback[count];
|
|
return count;
|
|
}
|
|
while (*text && count < TIMING_CONTEXTS_MAX) {
|
|
char *end = NULL;
|
|
unsigned long value = strtoul(text, &end, 0);
|
|
|
|
if (end == text)
|
|
break;
|
|
if (value >= TILE_DIM && value <= MAX_KV)
|
|
out[count++] = (uint32_t)value;
|
|
text = (*end == ',') ? end + 1 : end;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/** ms per descriptor at a given context, and what it projects to. */
|
|
static void stage_timing(const ft_tt_hardware_session *session)
|
|
{
|
|
uint32_t contexts[TIMING_CONTEXTS_MAX];
|
|
const uint32_t context_count = timing_contexts(contexts);
|
|
const char *repeats_text = getenv(REPEATS_ENVIRONMENT);
|
|
const uint32_t repeats = repeats_text ? (uint32_t)strtoul(repeats_text, NULL, 0) : 8u;
|
|
double per_descriptor[TIMING_CONTEXTS_MAX];
|
|
size_t index;
|
|
|
|
ft_tt_test_step("stage 3 — ms per descriptor, per token, and the tok/s it projects to");
|
|
if (context_count == 0u || repeats == 0u) {
|
|
ft_tt_test_check_fmt("the timing ladder parses", false, "%u contexts, %u repeats",
|
|
context_count, repeats);
|
|
return;
|
|
}
|
|
|
|
for (index = 0; index < context_count; index++) {
|
|
ft_tt_op_attention_request request;
|
|
uint32_t completed = 0u;
|
|
double start;
|
|
double elapsed;
|
|
uint32_t repeat;
|
|
int rc = upload_problem(session, contexts[index], 20260819ull, 0.0f, 0.0f);
|
|
|
|
per_descriptor[index] = 0.0;
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the timing problem uploads", false, "rc=%d", rc);
|
|
continue;
|
|
}
|
|
fill_request(&request, contexts[index],
|
|
ft_tt_op_memory_position_row(contexts[index] - 1u));
|
|
|
|
/* One warm call outside the window, so the measurement is of steady-state
|
|
* descriptors and not of the first one's cold circular buffers. */
|
|
rc = run_one(session, &request, &completed);
|
|
start = now_milliseconds();
|
|
for (repeat = 0; repeat < repeats && rc == FT_TT_OK; repeat++)
|
|
rc = run_one(session, &request, &completed);
|
|
elapsed = now_milliseconds() - start;
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the timing run completes", false, "rc=%d kv=%u", rc,
|
|
contexts[index]);
|
|
continue;
|
|
}
|
|
per_descriptor[index] = elapsed / (double)repeats;
|
|
ft_tt_test_check_fmt("the timing run completes", true, "kv=%u %.3f ms per descriptor",
|
|
contexts[index], per_descriptor[index]);
|
|
}
|
|
|
|
printf("\n context padded ms/descriptor us/position ms/token (1 core, %u x "
|
|
"%u)\n",
|
|
MODEL_LAYERS, MODEL_KV_HEADS);
|
|
for (index = 0; index < context_count; index++) {
|
|
const double per_token =
|
|
per_descriptor[index] * (double)MODEL_LAYERS * (double)MODEL_KV_HEADS;
|
|
|
|
if (per_descriptor[index] <= 0.0)
|
|
continue;
|
|
printf(" %7u %6u %13.3f %11.3f %10.1f\n", contexts[index],
|
|
ft_tt_op_attention_padded_tiles(contexts[index], g_block_tiles) * TILE_DIM,
|
|
per_descriptor[index], 1000.0 * per_descriptor[index] / (double)contexts[index],
|
|
per_token);
|
|
}
|
|
|
|
/* THE SLOPE, by least squares over every point that ran. `a + b*context` is the model
|
|
* the whole phase is priced on: `b` is what host attention's 0.53 ms/position is
|
|
* compared against, and `a` is the fixed cost that decides whether a short context is
|
|
* worth moving at all. A per-point us/position figure carries the intercept and is NOT
|
|
* the slope — quoting one as the other is how a padded window reads as fast work. */
|
|
{
|
|
double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0, n = 0.0;
|
|
double slope, intercept;
|
|
|
|
for (index = 0; index < context_count; index++) {
|
|
const double x = (double)contexts[index];
|
|
const double y = per_descriptor[index];
|
|
|
|
if (y <= 0.0)
|
|
continue;
|
|
n += 1.0;
|
|
sx += x;
|
|
sy += y;
|
|
sxx += x * x;
|
|
sxy += x * y;
|
|
}
|
|
if (n >= 2.0 && (n * sxx - sx * sx) != 0.0) {
|
|
slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
|
|
intercept = (sy - slope * sx) / n;
|
|
printf("\n FIT over %.0f points: %.4f ms + %.4f us/position a descriptor "
|
|
"-> %.4f ms + %.4f ms/position a TOKEN on one core (%u x %u "
|
|
"descriptors)\n",
|
|
n, intercept, 1000.0 * slope,
|
|
intercept * (double)MODEL_LAYERS * (double)MODEL_KV_HEADS,
|
|
slope * (double)MODEL_LAYERS * (double)MODEL_KV_HEADS, MODEL_LAYERS,
|
|
MODEL_KV_HEADS);
|
|
}
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
static void stage_unknown_opcode(const ft_tt_hardware_session *session)
|
|
{
|
|
ft_tt_queue_descriptor descriptor;
|
|
uint32_t completed = 0u;
|
|
int rc;
|
|
|
|
ft_tt_test_step("stage 4 — an opcode this binary does not implement is REFUSED, not "
|
|
"ignored");
|
|
|
|
memset(&descriptor, 0, sizeof(descriptor));
|
|
descriptor.opcode = FT_TT_QUEUE_OP_ROPE;
|
|
rc = ft_tt_queue_enqueue(&session->noc, &g_queue, &descriptor, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_queue_wait_completed(&session->noc, &g_queue, g_descriptors_done + 1u, 2000u,
|
|
&completed);
|
|
ft_tt_test_check_fmt("a ROPE descriptor is NOT completed by the attention triad",
|
|
rc != FT_TT_OK && completed == g_descriptors_done,
|
|
"rc=%d completed=%u of %u", rc, completed, g_descriptors_done + 1u);
|
|
{
|
|
uint32_t fields[FT_TT_QUEUE_FIELD_COUNT];
|
|
|
|
(void)ft_tt_queue_read_control(&session->noc, &g_queue, fields);
|
|
ft_tt_test_check_fmt("...and the reason it reports is a BAD OPCODE, not a timeout",
|
|
fields[FT_TT_QUEUE_FIELD_ERROR] == FT_TT_QUEUE_KERR_BAD_OPCODE,
|
|
"error=%u (%s)", fields[FT_TT_QUEUE_FIELD_ERROR],
|
|
ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR]));
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_firmware_set firmware;
|
|
uint16_t x = 0;
|
|
uint16_t y = 0;
|
|
uint32_t tensix_index = 0;
|
|
uint32_t bank;
|
|
uint32_t unstopped = 0u;
|
|
int rc;
|
|
|
|
if (argc < 2) {
|
|
fprintf(stderr, "usage: %s <attention-program-dir>\n", argv[0]);
|
|
return 2;
|
|
}
|
|
|
|
{
|
|
const char *depth = getenv(BLOCK_TILES_ENVIRONMENT);
|
|
|
|
if (depth) {
|
|
const unsigned long value = strtoul(depth, NULL, 0);
|
|
|
|
if (value == 0u || value > 64u) {
|
|
fprintf(stderr, "%s must be 1..64\n", BLOCK_TILES_ENVIRONMENT);
|
|
return 2;
|
|
}
|
|
g_block_tiles = (uint32_t)value;
|
|
}
|
|
}
|
|
|
|
ft_tt_test_step("Phase 6 — FLASH-DECODE ATTENTION, ON-CARD acceptance, block_tiles=%u",
|
|
g_block_tiles);
|
|
|
|
rc = ft_tt_kv_cache_layout_init(1u, MODEL_HEAD_DIM, MAX_KV, TILE_BYTES, &g_cache);
|
|
ft_tt_test_check_fmt("the KV layout contract derives (one head, 8192 positions)",
|
|
rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
|
|
rc = ft_tt_op_attention_build_constants(MODEL_HEAD_DIM, TILE_BYTES, g_constants,
|
|
sizeof(g_constants));
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
|
|
rc = ft_tt_hardware_session_open(&session);
|
|
if (rc != FT_TT_OK)
|
|
return 1;
|
|
rc = ft_tt_firmware_set_load(ft_tt_firmware_directory(), &firmware);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
rc = ft_tt_hardware_session_tensix_from_environment(&session, &x, &y, &tensix_index);
|
|
if (rc != FT_TT_OK)
|
|
goto release;
|
|
|
|
for (bank = 0; bank < DRAM_BANK_COUNT; bank++) {
|
|
rc = ft_tt_coord_dram_bank(&session.grid, bank, FT_TT_COORD_TRANSLATED, &g_dram[bank]);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("DRAM bank coordinates derive", false, "bank %u rc=%d", bank, rc);
|
|
goto release;
|
|
}
|
|
}
|
|
|
|
rc = ft_tt_program_directory_load(argv[1], TRIAD_MASK, &g_program);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the attention program loads", false, "rc=%d", rc);
|
|
goto release;
|
|
}
|
|
|
|
rc = ft_tt_launch_core_set_init(&g_group.cores, g_placement_storage, 1u);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_launch_core_set_add_tensix(&g_group.cores, &session.grid, tensix_index, NULL);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_test_check_fmt("the core set derives and classifies the target", false,
|
|
"index %u rc=%d", tensix_index, rc);
|
|
goto release_program;
|
|
}
|
|
g_group.id = 0u;
|
|
g_group.first_tensix_index = tensix_index;
|
|
g_group.tensix_index_count = 1u;
|
|
g_group.queues = &g_queue;
|
|
g_group.resident_processor_mask = TRIAD_MASK;
|
|
|
|
rc = ft_tt_core_boot_firmware(&session.noc, x, y, &firmware, NULL);
|
|
ft_tt_test_check_fmt("firmware boots on the target core", rc == FT_TT_OK,
|
|
"index %u -> (%u, %u) rc=%d", tensix_index, x, y, rc);
|
|
if (rc != FT_TT_OK)
|
|
goto release_program;
|
|
|
|
stage_round_trip(&session);
|
|
|
|
rc = device_write(&session.noc, g_dram[DRAM_BANK_CONST], DRAM_CONST_BASE, g_constants,
|
|
sizeof(g_constants));
|
|
ft_tt_test_check_fmt("the constants upload (one scaler, one scale, 32 masks)",
|
|
rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
goto release_program;
|
|
|
|
rc = start_residency(&session, x, y);
|
|
ft_tt_test_check_fmt("the attention triad goes resident", rc == FT_TT_OK, "rc=%d", rc);
|
|
if (rc != FT_TT_OK)
|
|
goto release_program;
|
|
|
|
stage_shapes(&session);
|
|
stage_invariances(&session);
|
|
stage_chunk(&session);
|
|
stage_chunk_timing(&session);
|
|
stage_timing(&session);
|
|
stage_unknown_opcode(&session);
|
|
|
|
rc = ft_tt_watchdog_shutdown(&session.noc, &g_group, RUN_TIMEOUT_MS, &unstopped);
|
|
ft_tt_test_check_fmt("every processor retires by ASKING, and none is parked",
|
|
rc == FT_TT_OK && unstopped == 0u, "rc=%d unstopped=0x%X", rc,
|
|
unstopped);
|
|
|
|
/* DO NOT PARK — the core is left in its firmware's GO wait, which is where a healthy
|
|
* idle core sits (dev notes §1). */
|
|
|
|
release_program:
|
|
ft_tt_program_directory_release(&g_program);
|
|
release:
|
|
ft_tt_firmware_set_release(&firmware);
|
|
ft_tt_hardware_session_close(&session);
|
|
return ft_tt_test_finish();
|
|
}
|