Foundry/tt/convert/reference/generate_bfp_reference.c

450 lines
17 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/**
* @file generate_bfp_reference.c
* @brief Offline generator for ft_bfp_reference_vectors.h: four 32x32 input
* tiles chosen to hit every block-float edge case, packed and unpacked by
* foundry's ft_bfp.c, and written out ONLY after an independent reference in
* this file agrees with ft_bfp.c on every byte and word.
*
* WHY TWO IMPLEMENTATIONS. The header is the oracle test_bfp.c checks ft_bfp.c
* against, so producing it from ft_bfp.c alone would be circular. The reference
* below is written from the format definition (ft_bfp.h; tt-isa-documentation
* FloatBitPatterns.md and Packers/FormatConversion.md) by a different method:
* - the shared exponent comes from frexp() / fpclassify(), not bit fields;
* - alignment is floor() of an exact ldexp() in double, and the datum
* rounding is nearbyint() in the default round-to-nearest-even mode, not
* an integer remainder comparison;
* - the tile walk (faces, blocks, BFP4 nibble order) is re-derived here;
* - a decoded datum in the normal range is checked against ldexpf() of its
* magnitude; out-of-range ones against the wrap rule ft_bfp.h documents.
* Any disagreement prints the first difference and exits 1 without writing.
*
* Build and run with `make bfp-reference` in ../tests.
*
* @status Added 2026-10-08 (clean-room Phase 7): replaces the earlier generator,
* which spliced third-party source. At this regeneration the output was
* word-for-word identical to the vectors it replaced.
* @issues None
* @todo None
*/
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ft_bfp.h"
#define TILE_VALUES 1024u
#define TILE_COUNT 4u
#define TOTAL_VALUES (TILE_VALUES * TILE_COUNT)
#define BLOCKS_PER_TILE 64u
/* ------------------------------------------------------------------------- */
/* Input tiles */
/* ------------------------------------------------------------------------- */
static float float_from_bits(uint32_t bits)
{
float f;
memcpy(&f, &bits, sizeof(f));
return f;
}
static uint32_t bits_from_float(float f)
{
uint32_t bits;
memcpy(&bits, &f, sizeof(bits));
return bits;
}
/** exp is the raw 8-bit exponent field (bias 127); frac the 23-bit fraction. */
static float make_float(uint32_t sign, uint32_t exp, uint32_t frac)
{
return float_from_bits((sign << 31) | ((exp & 0xffu) << 23) | (frac & 0x7fffffu));
}
/** A block is one face row: row `row`, columns half*16 .. half*16+15. */
static void set_block(float *tile, uint32_t row, uint32_t half, const float *values)
{
for (uint32_t j = 0; j < 16; j++)
tile[row * 32u + half * 16u + j] = values[j];
}
/** Tile 0: pseudo-random normals over ~30 binades, both signs. */
static void build_random_tile(float *tile)
{
uint32_t state = 0x5eed1234u;
for (uint32_t i = 0; i < TILE_VALUES; i++) {
uint32_t r;
state = state * 1664525u + 1013904223u;
r = state;
tile[i] = make_float((r >> 31) & 1u, 112u + ((r >> 16) & 0x1fu), r & 0x7fffffu);
}
}
/**
* Tile 1: round-to-nearest-even ties. BFP8 keeps 7 bits of the 24-bit
* significand, so the tie is a remainder of exactly 1<<16 with bit 17 as the
* guard; BFP4 keeps 3, tie 1<<20.
*/
static void build_tie_tile(float *tile)
{
float block[16];
for (uint32_t b = 0; b < BLOCKS_PER_TILE; b++) {
uint32_t exp = 120u + (b % 12u);
for (uint32_t j = 0; j < 16; j++) {
if (b < 16) /* BFP8 ties, no exponent spread, guard bit alternates */
block[j] = make_float(j & 1u, exp, ((((b * 16u + j) & 0x3fu)) << 17) | 0x10000u);
else if (b < 24) /* BFP8 ties after a 1-bit alignment shift */
block[j] = make_float(j & 1u, exp, 0x20000u | (j & 1u));
else if (b < 40) /* BFP4 ties, no spread */
block[j] = make_float(j & 1u, exp, ((((b * 16u + j) & 3u)) << 21) | 0x100000u);
else if (b < 48) /* BFP4 ties after a 1-bit shift */
block[j] = make_float(j & 1u, exp, 0x200000u | ((j & 1u) << 1));
else /* both tie families interleaved */
block[j] = (j & 1u) ? make_float(1, exp, ((j & 3u) << 21) | 0x100000u)
: make_float(0, exp, ((j & 7u) << 17) | 0x10000u);
}
if (b >= 16 && b < 48 && !(b >= 24 && b < 40))
block[0] = make_float(0, exp + 1u, 0); /* one value a binade up sets the block exponent */
set_block(tile, b / 2u, b % 2u, block);
}
}
/** One value of tile 2: zeros, signed zeros, denormals, round-to-zero, saturation. */
static float zero_denormal_saturation_value(uint32_t kind, uint32_t exp, uint32_t j)
{
switch (kind) {
case 0: return 0.0f;
case 1: return (j & 1u) ? -0.0f : 0.0f;
case 2: return make_float(j & 1u, 0, 1u + j * 0x00100000u / 16u);
case 3: return (j == 0) ? make_float(0, exp, 0) : make_float(1, 0, 0x7fffffu >> j);
case 4: return (j == 0) ? make_float(0, exp + 20u, 0x123456u) : make_float(1, exp, 0x400000u);
case 5: return make_float(j & 1u, exp, 0x7fffffu);
case 6: return make_float(j & 1u, exp + (j & 1u), 0x7fffffu);
default: return make_float(j & 1u, exp, 0x2aaaaau);
}
}
static void build_zero_denormal_saturation_tile(float *tile)
{
float block[16];
for (uint32_t b = 0; b < BLOCKS_PER_TILE; b++) {
for (uint32_t j = 0; j < 16; j++)
block[j] = zero_denormal_saturation_value(b % 8u, 100u + (b % 40u), j);
set_block(tile, b / 2u, b % 2u, block);
}
}
/** One value of tile 3: exponent differences past 31, inf/NaN, the 5-bit clamp. */
static float exponent_straddle_value(uint32_t kind, uint32_t j)
{
switch (kind) {
case 0: return make_float(j & 1u, 150u - j, 0x400000u);
case 1: return make_float(j & 1u, 160u - (j % 5u) * ((j % 5u) == 0 ? 0u : 30u) - (j % 5u), 0x555555u);
case 2: return (j == 0) ? make_float(0, 200u, 0x123456u) : make_float(1, 160u, 0x654321u);
case 3: return (j == 0) ? make_float(0, 250u, 0x000001u) : make_float(1, 150u, 0x7ffffeu);
case 4:
if (j == 0) return make_float(0, 255u, 0);
if (j == 1) return make_float(1, 255u, 0);
if (j == 2) return make_float(0, 255u, 0x400000u);
return make_float(j & 1u, 254u - j, 0x321000u);
case 5: return make_float(j & 1u, 144u + j, 0x0f0f0fu);
case 6: return make_float(j & 1u, 100u + j, 0x0f0f0fu);
default: return make_float(j & 1u, (j & 1u) ? 108u : 150u, 0x111111u);
}
}
static void build_exponent_straddle_tile(float *tile)
{
float block[16];
for (uint32_t b = 0; b < BLOCKS_PER_TILE; b++) {
for (uint32_t j = 0; j < 16; j++)
block[j] = exponent_straddle_value(b % 8u, j);
set_block(tile, b / 2u, b % 2u, block);
}
}
/* ------------------------------------------------------------------------- */
/* Independent reference */
/* ------------------------------------------------------------------------- */
typedef struct {
ft_bfp_format format;
const char *array_name;
uint32_t magnitude_bits;
int five_bit_exponent;
} format_case;
static const format_case FORMAT_CASES[] = {
{FT_BFP_BFP8_B, "ft_bfp_ref_bfp8b", 7, 0},
{FT_BFP_BFP4_B, "ft_bfp_ref_bfp4b", 3, 0},
{FT_BFP_BFP8_A, "ft_bfp_ref_bfp8_expa", 7, 1},
{FT_BFP_BFP4_A, "ft_bfp_ref_bfp4_expa", 3, 1},
};
/** Biased (127) exponent of a value, 0 for zero/denormal, 255 for inf/NaN. */
static int reference_biased_exponent(float value)
{
int e2;
switch (fpclassify(value)) {
case FP_ZERO:
case FP_SUBNORMAL: return 0;
case FP_INFINITE:
case FP_NAN: return 255;
default: (void)frexpf(value, &e2); return e2 + 126; /* frexp mantissa is in [0.5, 1) */
}
}
/** The block's shared exponent in the format's own bias. */
static int reference_shared_exponent(const format_case *fc, const float *block)
{
int largest = 0;
for (int j = 0; j < 16; j++) {
int e = reference_biased_exponent(block[j]);
largest = e > largest ? e : largest;
}
if (!fc->five_bit_exponent)
return largest;
return largest <= 112 ? 0 : (largest - 112 > 31 ? 31 : largest - 112);
}
/** One datum, by exact double arithmetic and the C library's ties-to-even. */
static uint8_t reference_datum(const format_case *fc, float value, int shared)
{
uint32_t bits = bits_from_float(value);
uint32_t sign = bits >> 31;
int exponent = reference_biased_exponent(value);
int block_exponent = fc->five_bit_exponent ? shared + 112 : shared;
double max_magnitude = ldexp(1.0, (int)fc->magnitude_bits) - 1.0;
double significand, aligned, magnitude;
if (exponent == 0)
return 0;
if (fc->five_bit_exponent && exponent < 112) {
exponent = 112; /* raised to +-2^-15 */
significand = ldexp(1.0, 23);
} else if (exponent == 255) {
significand = (double)((bits & 0x7fffffu) | 0x800000u);
} else {
int e2;
significand = ldexp(frexp(fabs((double)value), &e2), 24); /* exact 24-bit integer */
}
if (exponent > block_exponent) {
magnitude = max_magnitude;
} else {
aligned = floor(ldexp(significand, -(block_exponent - exponent)));
magnitude = nearbyint(ldexp(aligned, -(int)(24u - fc->magnitude_bits)));
magnitude = magnitude > max_magnitude ? max_magnitude : magnitude;
}
if (magnitude == 0.0)
return 0;
return (uint8_t)((sign << fc->magnitude_bits) | (uint32_t)magnitude);
}
/** Pack one row-major tile: 64 exponent bytes, then datums in face-row order. */
static void reference_pack_tile(const format_case *fc, const float *tile, unsigned char *out)
{
unsigned char *datums = out + BLOCKS_PER_TILE;
for (uint32_t b = 0; b < BLOCKS_PER_TILE; b++) {
uint32_t face = b / 16u, face_row = b % 16u;
const float *row = tile + ((face / 2u) * 16u + face_row) * 32u + (face % 2u) * 16u;
int shared = reference_shared_exponent(fc, row);
out[b] = (unsigned char)shared;
for (uint32_t j = 0; j < 16; j++) {
uint32_t index = b * 16u + j;
uint8_t d = reference_datum(fc, row[j], shared);
if (fc->magnitude_bits == 7)
datums[index] = d;
else if (index % 2u == 0)
datums[index / 2u] = (unsigned char)d;
else
datums[index / 2u] = (unsigned char)(datums[index / 2u] | (d << 4));
}
}
}
/** Decode check: ldexpf for a normal result, the documented wrap rule otherwise. */
static uint32_t reference_decode(const format_case *fc, uint32_t datum, uint32_t shared)
{
uint32_t wide = fc->magnitude_bits == 3 ? (datum & 0x0fu) << 4 : datum;
uint32_t sign = wide >> 7, magnitude = wide & 0x7fu, leading = 0;
int block_exponent = (int)shared + (fc->five_bit_exponent ? 112 : 0);
int out_exponent;
if (magnitude == 0)
return sign << 31;
while (!(magnitude & (0x40u >> leading)))
leading++;
out_exponent = block_exponent - (int)leading;
if (out_exponent >= 1 && out_exponent <= 254) {
float v = ldexpf((float)magnitude, block_exponent - 127 - 6);
return bits_from_float(sign ? -v : v);
}
return (sign << 31) | ((uint32_t)out_exponent << 23) | (((magnitude << leading) & 0x3fu) << 17);
}
/* ------------------------------------------------------------------------- */
/* Output */
/* ------------------------------------------------------------------------- */
static void print_words(FILE *f, const char *name, const uint32_t *values, size_t count, size_t per_line)
{
fprintf(f, "static const uint32_t %s[%zu] = {\n", name, count);
for (size_t i = 0; i < count; i++) {
if (i % per_line == 0)
fprintf(f, " ");
fprintf(f, " 0x%08xu,", values[i]);
if (i % per_line == per_line - 1 || i + 1 == count)
fprintf(f, "\n");
}
fprintf(f, "};\n\n");
}
static void fail(const char *what, size_t at, uint32_t ours, uint32_t reference)
{
fprintf(stderr, "generate_bfp_reference: %s differs at %zu: ft_bfp 0x%08x, reference 0x%08x\n", what, at, ours,
reference);
exit(1);
}
/** Pack with ft_bfp.c and the reference; return ft_bfp's words if they agree. */
static uint32_t *pack_and_check(const format_case *fc, const float *tiles, size_t *words_out)
{
size_t tile_bytes = ft_bfp_tile_bytes(fc->format);
unsigned char *ours = calloc(TILE_COUNT, tile_bytes);
unsigned char *reference = calloc(TILE_COUNT, tile_bytes);
uint32_t *words = calloc(TILE_COUNT * tile_bytes / 4u, sizeof(uint32_t));
if (!ours || !reference || !words || ft_bfp_pack_tiles(fc->format, tiles, TILE_COUNT, ours) != FT_BFP_OK)
fail("pack setup", 0, 0, 0);
for (uint32_t t = 0; t < TILE_COUNT; t++)
reference_pack_tile(fc, tiles + t * TILE_VALUES, reference + t * tile_bytes);
for (size_t i = 0; i < TILE_COUNT * tile_bytes; i++)
if (ours[i] != reference[i])
fail(fc->array_name, i, ours[i], reference[i]);
memcpy(words, ours, TILE_COUNT * tile_bytes);
*words_out = TILE_COUNT * tile_bytes / 4u;
free(ours);
free(reference);
return words;
}
static const uint32_t UNPACK_SHARED_EXP_B[] = {0, 1, 7, 8, 64, 126, 127, 128, 200, 254, 255};
static const uint32_t UNPACK_SHARED_EXP_A[] = {0, 1, 7, 15, 16, 24, 31};
/** Decode every datum for every listed shared exponent, checked, into `out`. */
static size_t unpack_and_check(const format_case *fc, const uint32_t *exps, size_t exp_count, uint32_t *out)
{
uint32_t datums = fc->magnitude_bits == 7 ? 256u : 16u;
size_t n = 0;
for (size_t e = 0; e < exp_count; e++) {
for (uint32_t d = 0; d < datums; d++, n++) {
out[n] = ft_bfp_convert_datum(fc->format, (uint8_t)d, exps[e]);
if (out[n] != reference_decode(fc, d, exps[e]))
fail("unpack", n, out[n], reference_decode(fc, d, exps[e]));
}
}
return n;
}
static void write_header_top(FILE *f, const uint32_t *inputs)
{
fprintf(f, "/* SPDX-License-Identifier: AGPL-3.0-or-later */\n"
"/**\n"
" * @file ft_bfp_reference_vectors.h\n"
" * @brief GENERATED -- do not edit. Block-float reference vectors: four input\n"
" * tiles and their BFP8/BFP4 (exp_b and exp_a) packing and unpacking, produced\n"
" * by ft_bfp.c and written only after the independent reference in\n"
" * generate_bfp_reference.c agreed on every byte and word.\n"
" *\n"
" * Regenerate with `make bfp-reference` in tt/convert/tests.\n"
" *\n"
" * @status None\n"
" * @issues None\n"
" * @todo None\n"
" */\n\n"
"#ifndef FT_BFP_REFERENCE_VECTORS_H\n#define FT_BFP_REFERENCE_VECTORS_H\n\n"
"#include <stdint.h>\n\n"
"#define FT_BFP_REF_TILE_COUNT %u\n#define FT_BFP_REF_TILE_VALUES %u\n\n",
TILE_COUNT, TILE_VALUES);
fprintf(f, "/* Input tiles, row-major 32x32, as raw fp32 bit patterns. */\n");
print_words(f, "ft_bfp_ref_input_bits", inputs, TOTAL_VALUES, 8);
}
static void write_unpack_tables(FILE *f)
{
static uint32_t table[11u * 256u];
const char *names[] = {"ft_bfp_ref_unpack_bfp8b", "ft_bfp_ref_unpack_bfp4b", "ft_bfp_ref_unpack_bfp8_expa",
"ft_bfp_ref_unpack_bfp4_expa"};
size_t exp_b_count = sizeof(UNPACK_SHARED_EXP_B) / sizeof(UNPACK_SHARED_EXP_B[0]);
size_t exp_a_count = sizeof(UNPACK_SHARED_EXP_A) / sizeof(UNPACK_SHARED_EXP_A[0]);
fprintf(f, "#define FT_BFP_REF_UNPACK_EXP_B_COUNT %zu\n#define FT_BFP_REF_UNPACK_EXP_A_COUNT %zu\n\n", exp_b_count,
exp_a_count);
print_words(f, "ft_bfp_ref_unpack_shared_exp_b", UNPACK_SHARED_EXP_B, exp_b_count, 12);
print_words(f, "ft_bfp_ref_unpack_shared_exp_a", UNPACK_SHARED_EXP_A, exp_a_count, 12);
fprintf(f, "/* Every datum 0..255 / 0..15 decoded, per shared exponent. */\n");
for (int c = 0; c < 4; c++) {
const uint32_t *exps = FORMAT_CASES[c].five_bit_exponent ? UNPACK_SHARED_EXP_A : UNPACK_SHARED_EXP_B;
size_t count = FORMAT_CASES[c].five_bit_exponent ? exp_a_count : exp_b_count;
print_words(f, names[c], table, unpack_and_check(&FORMAT_CASES[c], exps, count, table), 8);
}
}
int main(int argc, char **argv)
{
static float tiles[TOTAL_VALUES];
static uint32_t inputs[TOTAL_VALUES];
FILE *f;
if (argc != 2) {
fprintf(stderr, "usage: generate_bfp_reference <out.h>\n");
return 2;
}
build_random_tile(tiles);
build_tie_tile(tiles + TILE_VALUES);
build_zero_denormal_saturation_tile(tiles + 2u * TILE_VALUES);
build_exponent_straddle_tile(tiles + 3u * TILE_VALUES);
for (uint32_t i = 0; i < TOTAL_VALUES; i++)
inputs[i] = bits_from_float(tiles[i]);
f = fopen(argv[1], "w");
if (!f) {
perror(argv[1]);
return 1;
}
write_header_top(f, inputs);
for (int c = 0; c < 4; c++) {
size_t words;
uint32_t *packed = pack_and_check(&FORMAT_CASES[c], tiles, &words);
fprintf(f, "/* Packed output, %s, one tile after another. */\n", FORMAT_CASES[c].array_name);
print_words(f, FORMAT_CASES[c].array_name, packed, words, 8);
free(packed);
}
write_unpack_tables(f);
fprintf(f, "#endif /* FT_BFP_REFERENCE_VECTORS_H */\n");
return fclose(f) == 0 ? 0 : 1;
}