279 lines
11 KiB
C
279 lines
11 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_fttw_list.c
|
|
* @brief CLI: report what a WRITTEN .fttw artifact actually contains — per-tensor
|
|
* format, role, layout and tile grid, plus a per-format and per-layout census.
|
|
*
|
|
* WHY IT PARSES THE BYTES ITSELF. "The converter chose format X" and "the artifact
|
|
* holds format X" are different claims, and only the second one decides which Tensix
|
|
* program directories a run needs — a resident matmul binary is bound to ONE weight
|
|
* tile size. Reading the file back through `ft_tt_fttw_open()` would score our own
|
|
* reader against our own writer, which DEV_NOTES.md §3 calls out as measuring
|
|
* self-consistency rather than correctness. So this walks the documented on-disk
|
|
* layout (a 40-byte header, then fixed 200-byte little-endian directory entries)
|
|
* with nothing but stdio, and the only thing it shares with the converter is the
|
|
* two enum-to-string helpers.
|
|
*
|
|
* ft_tt_fttw_list <artifact.fttw> [--per-tensor]
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_precision_ledger.h"
|
|
#include "ft_tt_weight_convert.h"
|
|
|
|
/** Formats and layouts are small dense enums; these bound the census arrays. */
|
|
#define LIST_FORMAT_SLOTS 8u
|
|
#define LIST_LAYOUT_SLOTS 4u
|
|
|
|
/** One census row: how many tensors, and how many payload bytes they own. */
|
|
typedef struct {
|
|
uint64_t tensors;
|
|
uint64_t bytes;
|
|
} list_census;
|
|
|
|
static uint32_t read_u32(const unsigned char *p)
|
|
{
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
|
}
|
|
|
|
static uint64_t read_u64(const unsigned char *p)
|
|
{
|
|
return (uint64_t)read_u32(p) | ((uint64_t)read_u32(p + 4) << 32);
|
|
}
|
|
|
|
static const char *layout_name(uint32_t layout)
|
|
{
|
|
switch (layout) {
|
|
case FT_TT_LAYOUT_SOURCE:
|
|
return "source";
|
|
case FT_TT_LAYOUT_MATMUL_NMAJOR:
|
|
return "nmajor";
|
|
default:
|
|
return "?";
|
|
}
|
|
}
|
|
|
|
/** Bytes of one 32x32 tile in `format`, or 0 when the format is not tiled here. */
|
|
static uint32_t tile_bytes(uint32_t format)
|
|
{
|
|
switch (format) {
|
|
case FT_TT_FORMAT_FP32:
|
|
return 4096u;
|
|
case FT_TT_FORMAT_BF16:
|
|
return 2048u;
|
|
case FT_TT_FORMAT_BFP8_B:
|
|
return 1088u;
|
|
case FT_TT_FORMAT_BFP4_B:
|
|
return 576u;
|
|
default:
|
|
return 0u;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The provenance trailer, parsed the same way everything else here is: by hand, from the
|
|
* documented offsets, with nothing borrowed from the writer.
|
|
*
|
|
* This is what makes "is the cached artifact the right one" answerable from OUTSIDE the
|
|
* cache. A stale artifact and a fresh one are the same size and the same shape; the
|
|
* trailer is the only thing that tells them apart, and reading it through the cache
|
|
* library would be scoring the cache against itself.
|
|
*
|
|
* An artifact with no trailer is not an error — it was produced by hand, or before the
|
|
* cache existed — so this says so and returns.
|
|
*/
|
|
static void print_provenance(FILE *file, uint64_t file_bytes)
|
|
{
|
|
unsigned char trailer[64];
|
|
|
|
if (file_bytes < sizeof(trailer) ||
|
|
fseek(file, (long)(file_bytes - sizeof(trailer)), SEEK_SET) != 0 ||
|
|
fread(trailer, 1, sizeof(trailer), file) != sizeof(trailer)) {
|
|
printf("\n provenance: none (the file is too short to carry a trailer)\n");
|
|
return;
|
|
}
|
|
if (read_u32(trailer + 60) != 0x50545446u) { /* 'FTTP' */
|
|
printf("\n provenance: none — not a cache-managed artifact\n");
|
|
return;
|
|
}
|
|
|
|
printf("\n provenance trailer (the artifact's own account of where it came from)\n");
|
|
printf(" trailer version %u, %u B; converter behaviour stamp 0x%08x\n",
|
|
read_u32(trailer + 24), read_u32(trailer + 52), read_u32(trailer + 48));
|
|
printf(" source: %llu B, tensor-directory digest 0x%016llx\n",
|
|
(unsigned long long)read_u64(trailer + 0), (unsigned long long)read_u64(trailer + 8));
|
|
printf(" written at rung %s, rope pairing %u, artifact version %u, flags 0x%08x, "
|
|
"%u tensors, %llu B of FTTW\n",
|
|
ft_tt_ledger_mode_name((ft_tt_ledger_mode)read_u32(trailer + 32)),
|
|
read_u32(trailer + 36), read_u32(trailer + 28), read_u32(trailer + 40),
|
|
read_u32(trailer + 44), (unsigned long long)read_u64(trailer + 16));
|
|
|
|
/* The one check this tool can make with no checkpoint present: does the file's own
|
|
* length agree with the trailer's account of it. */
|
|
if (read_u64(trailer + 16) + sizeof(trailer) != file_bytes)
|
|
printf(" MISMATCH: %llu B of FTTW plus a %zu B trailer is %llu B, but the file is "
|
|
"%llu B\n",
|
|
(unsigned long long)read_u64(trailer + 16), sizeof(trailer),
|
|
(unsigned long long)(read_u64(trailer + 16) + sizeof(trailer)),
|
|
(unsigned long long)file_bytes);
|
|
}
|
|
|
|
static void print_census(const char *title, const list_census *census, int with_tile)
|
|
{
|
|
printf("\n census — %s\n", title);
|
|
for (uint32_t f = 0; f < LIST_FORMAT_SLOTS; f++) {
|
|
if (census[f].tensors == 0u)
|
|
continue;
|
|
printf(" %-8s %4llu tensors %12llu B %8.1f MiB", ft_tt_ledger_format_name((ft_tt_tensor_format)f),
|
|
(unsigned long long)census[f].tensors, (unsigned long long)census[f].bytes,
|
|
(double)census[f].bytes / (1024.0 * 1024.0));
|
|
if (with_tile)
|
|
printf(" tile %u B", tile_bytes(f));
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
list_census by_format[LIST_FORMAT_SLOTS];
|
|
list_census nmajor_by_format[LIST_FORMAT_SLOTS];
|
|
uint64_t by_layout[LIST_LAYOUT_SLOTS];
|
|
unsigned char header[FT_TT_ARTIFACT_HEADER_BYTES];
|
|
unsigned char entry[FT_TT_ARTIFACT_ENTRY_BYTES];
|
|
uint64_t directory_offset, total_bytes = 0;
|
|
uint32_t tensor_count, index;
|
|
int per_tensor = 0;
|
|
FILE *file;
|
|
|
|
if (argc < 2) {
|
|
fprintf(stderr, "usage: %s <artifact.fttw> [--per-tensor]\n", argv[0]);
|
|
return 2;
|
|
}
|
|
for (int i = 2; i < argc; i++) {
|
|
if (strcmp(argv[i], "--per-tensor") == 0)
|
|
per_tensor = 1;
|
|
}
|
|
|
|
file = fopen(argv[1], "rb");
|
|
if (!file) {
|
|
fprintf(stderr, "cannot open %s\n", argv[1]);
|
|
return 1;
|
|
}
|
|
if (fread(header, 1, sizeof(header), file) != sizeof(header)) {
|
|
fprintf(stderr, "%s: short header\n", argv[1]);
|
|
fclose(file);
|
|
return 1;
|
|
}
|
|
tensor_count = read_u32(header + 8);
|
|
directory_offset = read_u64(header + 16);
|
|
printf("artifact %s\n magic 0x%08x version %u %u tensors %llu B total\n", argv[1],
|
|
read_u32(header), read_u32(header + 4), tensor_count,
|
|
(unsigned long long)read_u64(header + 32));
|
|
{
|
|
/* The flags change what the BYTES MEAN, so they come before anything about the
|
|
* payload: an artifact with the rotary permutation and one without it are the
|
|
* same size, the same shape and the same tensor list, and this line is the only
|
|
* difference visible from outside the file. */
|
|
uint32_t flags = read_u32(header + 12);
|
|
|
|
printf(" flags 0x%08x rope=%s%s%s\n", flags,
|
|
(flags & FT_TT_ARTIFACT_FLAG_ROPE_SPLIT_HALF) ? "split-half" : "interleaved(source)",
|
|
(flags & FT_TT_ARTIFACT_FLAG_ROPE_PAIRS_REVERSED) ? ", pair index REVERSED" : "",
|
|
(flags & ~FT_TT_ARTIFACT_FLAGS_KNOWN) ? " UNKNOWN BITS SET" : "");
|
|
if (flags & ~FT_TT_ARTIFACT_FLAGS_KNOWN) {
|
|
fprintf(stderr, "%s: header flags 0x%08x carry bits this build does not know; the "
|
|
"payload may not mean what this tool would print\n",
|
|
argv[1], flags);
|
|
fclose(file);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (fseek(file, (long)directory_offset, SEEK_SET) != 0) {
|
|
fprintf(stderr, "%s: directory at %llu is not reachable\n", argv[1],
|
|
(unsigned long long)directory_offset);
|
|
fclose(file);
|
|
return 1;
|
|
}
|
|
|
|
memset(by_format, 0, sizeof(by_format));
|
|
memset(nmajor_by_format, 0, sizeof(nmajor_by_format));
|
|
memset(by_layout, 0, sizeof(by_layout));
|
|
|
|
for (index = 0; index < tensor_count; index++) {
|
|
uint32_t format, role, layout, tile_rows, tile_cols;
|
|
uint64_t byte_count;
|
|
const char *name;
|
|
|
|
if (fread(entry, 1, sizeof(entry), file) != sizeof(entry)) {
|
|
fprintf(stderr, "%s: directory truncated at entry %u of %u\n", argv[1], index,
|
|
tensor_count);
|
|
fclose(file);
|
|
return 1;
|
|
}
|
|
name = (const char *)entry;
|
|
format = read_u32(entry + FT_TT_ARTIFACT_NAME_BYTES);
|
|
role = read_u32(entry + FT_TT_ARTIFACT_NAME_BYTES + 4);
|
|
tile_rows = read_u32(entry + FT_TT_ARTIFACT_NAME_BYTES + 12);
|
|
tile_cols = read_u32(entry + FT_TT_ARTIFACT_NAME_BYTES + 16);
|
|
layout = read_u32(entry + FT_TT_ARTIFACT_NAME_BYTES + 20);
|
|
byte_count = read_u64(entry + FT_TT_ARTIFACT_NAME_BYTES + 24 + 32 + 8);
|
|
|
|
if (format >= LIST_FORMAT_SLOTS || layout >= LIST_LAYOUT_SLOTS) {
|
|
fprintf(stderr, "%s: entry %u (%.*s) has format %u layout %u, which this build "
|
|
"does not model — refusing rather than counting it as something\n",
|
|
argv[1], index, FT_TT_ARTIFACT_NAME_BYTES, name, format, layout);
|
|
fclose(file);
|
|
return 1;
|
|
}
|
|
by_format[format].tensors++;
|
|
by_format[format].bytes += byte_count;
|
|
by_layout[layout]++;
|
|
total_bytes += byte_count;
|
|
if (layout == FT_TT_LAYOUT_MATMUL_NMAJOR) {
|
|
nmajor_by_format[format].tensors++;
|
|
nmajor_by_format[format].bytes += byte_count;
|
|
}
|
|
if (per_tensor)
|
|
printf(" %-44.*s %-8s %-12s %-7s %4u x %-5u tile %4u B %12llu B\n",
|
|
FT_TT_ARTIFACT_NAME_BYTES, name, ft_tt_ledger_format_name((ft_tt_tensor_format)format),
|
|
ft_tt_ledger_role_name((ft_tt_tensor_role)role), layout_name(layout), tile_rows, tile_cols,
|
|
tile_bytes(format), (unsigned long long)byte_count);
|
|
}
|
|
|
|
{
|
|
uint64_t file_bytes = 0;
|
|
|
|
if (fseek(file, 0, SEEK_END) == 0) {
|
|
long end = ftell(file);
|
|
|
|
if (end > 0)
|
|
file_bytes = (uint64_t)end;
|
|
}
|
|
print_provenance(file, file_bytes);
|
|
}
|
|
fclose(file);
|
|
|
|
print_census("ALL tensors", by_format, 0);
|
|
/* The MATMUL_NMAJOR rows are the ones that decide which Tensix program builds a run
|
|
* needs: only these tensors are ever fed to a matmul, and a resident matmul binary
|
|
* is bound to one weight tile size. */
|
|
print_census("MATMUL_NMAJOR only — this is the program-build requirement", nmajor_by_format, 1);
|
|
|
|
printf("\n layouts: source %llu, nmajor %llu\n",
|
|
(unsigned long long)by_layout[FT_TT_LAYOUT_SOURCE],
|
|
(unsigned long long)by_layout[FT_TT_LAYOUT_MATMUL_NMAJOR]);
|
|
printf(" payload total %llu B (%.1f MiB)\n", (unsigned long long)total_bytes,
|
|
(double)total_bytes / (1024.0 * 1024.0));
|
|
return 0;
|
|
}
|