1164 lines
46 KiB
C
1164 lines
46 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_weight_convert.c
|
|
* @brief Implementation of the streaming GGUF -> Blackhole weight conversion.
|
|
*
|
|
* The whole file is one loop shape:
|
|
*
|
|
* for each tensor
|
|
* for each strip of 32 rows
|
|
* dequantize the strip to fp32 (ft_gguf_dequant)
|
|
* lay it out as tiles (ft_tt_tilize / swizzle)
|
|
* encode to the target format (ft_bfp / ft_float_convert)
|
|
* write it
|
|
* optionally decode it back and fold the error into a PCC
|
|
*
|
|
* so the peak memory is the strip buffers, which are sized once for the widest
|
|
* tensor in the file and then reused. Nothing here allocates per strip.
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "ft_tt_weight_convert.h"
|
|
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_bfp.h"
|
|
#include "ft_float_convert.h"
|
|
#include "ft_gguf_dequant.h"
|
|
#include "ft_gguf_map.h"
|
|
#include "ft_tt_precision_ledger.h"
|
|
|
|
#include "../ft_tt_tilize.h"
|
|
|
|
/** Running Pearson accumulator over the fp32 source and its round trip. */
|
|
typedef struct {
|
|
double sum_x;
|
|
double sum_y;
|
|
double sum_xx;
|
|
double sum_yy;
|
|
double sum_xy;
|
|
double max_abs_error;
|
|
double sum_sq_error;
|
|
uint64_t count;
|
|
} pearson_accumulator;
|
|
|
|
/** Buffers reused across every strip of every tensor. */
|
|
typedef struct {
|
|
float *source_strip; /**< 32 x cols fp32, dequantized */
|
|
float *tile_strip; /**< tile_cols tiles, row-major within each tile */
|
|
unsigned char *packed; /**< the encoded strip, ready to write */
|
|
float *decoded; /**< round trip, only when PCC is being measured */
|
|
float *reference_strip; /**< the same rows of a reference checkpoint, or NULL */
|
|
uint32_t max_cols;
|
|
uint32_t max_tile_cols;
|
|
} strip_buffers;
|
|
|
|
static void pearson_reset(pearson_accumulator *acc)
|
|
{
|
|
memset(acc, 0, sizeof(*acc));
|
|
}
|
|
|
|
static void pearson_add(pearson_accumulator *acc, double x, double y)
|
|
{
|
|
double error = y - x;
|
|
double magnitude = error < 0 ? -error : error;
|
|
|
|
acc->sum_x += x;
|
|
acc->sum_y += y;
|
|
acc->sum_xx += x * x;
|
|
acc->sum_yy += y * y;
|
|
acc->sum_xy += x * y;
|
|
acc->sum_sq_error += error * error;
|
|
if (magnitude > acc->max_abs_error)
|
|
acc->max_abs_error = magnitude;
|
|
acc->count++;
|
|
}
|
|
|
|
/**
|
|
* Pearson correlation of the two accumulated series.
|
|
*
|
|
* A constant series has zero variance and no defined correlation. That is not
|
|
* a corner case here — a zero-initialized tensor is exactly that — so it
|
|
* reports 1.0 when the two series are identical and 0.0 when they are not,
|
|
* rather than a NaN that would silently poison a table of results.
|
|
*/
|
|
static double pearson_value(const pearson_accumulator *acc)
|
|
{
|
|
double n = (double)acc->count;
|
|
double cov, var_x, var_y;
|
|
|
|
if (acc->count == 0)
|
|
return 1.0;
|
|
|
|
cov = n * acc->sum_xy - acc->sum_x * acc->sum_y;
|
|
var_x = n * acc->sum_xx - acc->sum_x * acc->sum_x;
|
|
var_y = n * acc->sum_yy - acc->sum_y * acc->sum_y;
|
|
|
|
if (var_x <= 0.0 || var_y <= 0.0)
|
|
return acc->sum_sq_error == 0.0 ? 1.0 : 0.0;
|
|
|
|
return cov / sqrt(var_x * var_y);
|
|
}
|
|
|
|
static double pearson_rms(const pearson_accumulator *acc)
|
|
{
|
|
if (acc->count == 0)
|
|
return 0.0;
|
|
return sqrt(acc->sum_sq_error / (double)acc->count);
|
|
}
|
|
|
|
/**
|
|
* Error RMS as a fraction of the source's own RMS.
|
|
*
|
|
* `sum_xx` is already the sum of the squared source values, so this costs
|
|
* nothing beyond a divide — and it is the only error figure that can be
|
|
* compared between two tensors of different scale.
|
|
*/
|
|
static double pearson_relative_rms(const pearson_accumulator *acc)
|
|
{
|
|
if (acc->count == 0 || acc->sum_xx <= 0.0)
|
|
return 0.0;
|
|
return sqrt(acc->sum_sq_error / acc->sum_xx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Geometry */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* A GGUF tensor's logical shape as a matrix. dims[0] is the fastest axis, so
|
|
* it is the column count; everything above it collapses into rows, which is
|
|
* correct for the 2-D weight matrices this converter handles and keeps a
|
|
* 3-D tensor from silently being tiled along the wrong axis.
|
|
*/
|
|
static void tensor_shape(const ft_gguf_tensor *tensor, uint64_t *rows, uint64_t *cols)
|
|
{
|
|
*cols = tensor->dims[0];
|
|
*rows = tensor->dims[1] * tensor->dims[2] * tensor->dims[3];
|
|
}
|
|
|
|
static size_t format_element_bytes(ft_tt_tensor_format format)
|
|
{
|
|
return format == FT_TT_FORMAT_FP32 ? 4u : 2u;
|
|
}
|
|
|
|
static ft_bfp_format bfp_format_for(ft_tt_tensor_format format)
|
|
{
|
|
return format == FT_TT_FORMAT_BFP4_B ? FT_BFP_BFP4_B : FT_BFP_BFP8_B;
|
|
}
|
|
|
|
static int format_is_block_float(ft_tt_tensor_format format)
|
|
{
|
|
return format == FT_TT_FORMAT_BFP8_B || format == FT_TT_FORMAT_BFP4_B;
|
|
}
|
|
|
|
int ft_tt_convert_layout_for_checked(ft_tt_tensor_role role, uint32_t n_dims, ft_tt_tensor_layout *out)
|
|
{
|
|
if (!out)
|
|
return FT_TT_CONVERT_EINVAL;
|
|
if (n_dims < 2) {
|
|
*out = FT_TT_LAYOUT_SOURCE;
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
switch (role) {
|
|
case FT_TT_ROLE_ATTENTION:
|
|
case FT_TT_ROLE_FFN_UP:
|
|
case FT_TT_ROLE_FFN_DOWN:
|
|
case FT_TT_ROLE_LM_HEAD:
|
|
case FT_TT_ROLE_SSM_IN:
|
|
case FT_TT_ROLE_SSM_OUT:
|
|
case FT_TT_ROLE_SSM_GATE:
|
|
case FT_TT_ROLE_PROJECTION:
|
|
case FT_TT_ROLE_ROUTER:
|
|
*out = FT_TT_LAYOUT_MATMUL_NMAJOR;
|
|
return FT_TT_CONVERT_OK;
|
|
case FT_TT_ROLE_EMBEDDING:
|
|
/* Indexed BY ROW by a token id. Transposing it turns a lookup into a
|
|
* gather across the whole tensor. */
|
|
*out = FT_TT_LAYOUT_SOURCE;
|
|
return FT_TT_CONVERT_OK;
|
|
case FT_TT_ROLE_SSM_CONV:
|
|
/* A depthwise convolution over `taps` positions, not a matmul: there is
|
|
* no K axis to put first. */
|
|
*out = FT_TT_LAYOUT_SOURCE;
|
|
return FT_TT_CONVERT_OK;
|
|
case FT_TT_ROLE_NORM:
|
|
case FT_TT_ROLE_BIAS:
|
|
case FT_TT_ROLE_SSM_DECAY:
|
|
/* Reached only by a 2-D tensor carrying one of these names, which is
|
|
* not a shape we have seen; keeping the source order is the answer that
|
|
* matches the 1-D case these always are. */
|
|
*out = FT_TT_LAYOUT_SOURCE;
|
|
return FT_TT_CONVERT_OK;
|
|
default:
|
|
/* ROLE_UNKNOWN. See the header: both defaults are a guess about a
|
|
* kernel, so neither is taken. */
|
|
return FT_TT_CONVERT_EUNCLASSIFIED;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Rotary row permutation */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* The head geometry one tensor's rows are permuted under.
|
|
*
|
|
* `head_rows` is the whole head; `rot_rows` is the rotated prefix of it. Rows at
|
|
* or past `rot_rows` inside a head are not rotated by either form of RoPE and so
|
|
* are not moved.
|
|
*/
|
|
typedef struct {
|
|
uint64_t head_rows;
|
|
uint64_t rot_rows;
|
|
/** Reflect the rotary PAIR INDEX: slot p carries the model's pair half-1-p. */
|
|
int reverse_pairs;
|
|
/** Keep GGUF's INTERLEAVED grouping — a pair's partner stays adjacent — instead of
|
|
* undoing it to split-half. Only meaningful with `reverse_pairs`, because interleaved
|
|
* without a reflection is just the source order and needs no permutation at all. */
|
|
int keep_interleaved;
|
|
} rope_row_permutation;
|
|
|
|
/**
|
|
* Which SOURCE row supplies target row `row`, undoing llama.cpp's HF->GGUF
|
|
* permutation.
|
|
*
|
|
* llama.cpp maps split-half index `(a, b)` — `a` in {0,1}, `b` in [0, rot/2) —
|
|
* to interleaved index `2b + a`. Inverting it: target `n` below `rot/2` is
|
|
* `(a=0, b=n)` and comes from source `2n`; target `n` at or above `rot/2` is
|
|
* `(a=1, b=n-rot/2)` and comes from source `2(n - rot/2) + 1`.
|
|
*
|
|
* With `reverse_pairs` the pair index is reflected FIRST — target slot `(a, b)`
|
|
* takes what split-half would have put at `(a, half-1-b)` — and the split-half
|
|
* inverse then runs unchanged. Composing in that order is what preserves the
|
|
* property everything downstream rests on: slots `(0, b)` and `(1, b)` reflect to
|
|
* the same `half-1-b`, so a partner is still exactly `half` rows away.
|
|
*/
|
|
static uint64_t rope_permuted_source_row(const rope_row_permutation *permutation, uint64_t row)
|
|
{
|
|
uint64_t head = row / permutation->head_rows;
|
|
uint64_t within = row % permutation->head_rows;
|
|
uint64_t half = permutation->rot_rows / 2u;
|
|
|
|
if (within >= permutation->rot_rows)
|
|
return row;
|
|
if (permutation->keep_interleaved) {
|
|
/* The partner stays ADJACENT, so a slot is (pair, component) = (within/2, within%2)
|
|
* and the model's pair `half-1-p` sits at GGUF row `2*(half-1-p) + component`. No
|
|
* split-half undo runs at all: this permutation stays inside GGUF's own order. */
|
|
uint64_t pair = within / 2u;
|
|
uint64_t component = within % 2u;
|
|
|
|
return head * permutation->head_rows + (half - 1u - pair) * 2u + component;
|
|
}
|
|
if (permutation->reverse_pairs) {
|
|
uint64_t upper = within / half; /* 0 = first component of the pair, 1 = second */
|
|
uint64_t pair = within % half; /* the slot's pair index */
|
|
|
|
within = upper * half + (half - 1u - pair);
|
|
}
|
|
within = within < half ? within * 2u : (within - half) * 2u + 1u;
|
|
return head * permutation->head_rows + within;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Tile transpose */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Where element (row, column) of a 32x32 tile sits, in elements from the tile base.
|
|
*
|
|
* Two layouts, because the converter produces both. A block-float strip is SWIZZLED —
|
|
* row-major inside the tile — because tt-metal's packer does the face reordering itself,
|
|
* while fp32 and bf16 strips are already FACE-ORDERED, four 16x16 faces in the order
|
|
* (0,0) (0,1) (1,0) (1,1). Transposing a face-ordered tile is therefore not a transpose
|
|
* of a 32x32 array: it transposes each face AND swaps the two off-diagonal faces, and
|
|
* doing only the first half is the kind of mistake PCC would not see.
|
|
*/
|
|
static size_t tile_element_index(uint32_t row, uint32_t column, int face_ordered)
|
|
{
|
|
size_t face;
|
|
|
|
if (!face_ordered)
|
|
return (size_t)row * FT_TT_TILE_WIDTH + column;
|
|
face = (size_t)(row / FT_TT_FACE_HEIGHT) * 2u + (column / FT_TT_FACE_WIDTH);
|
|
return face * FT_TT_FACE_VALUES + (size_t)(row % FT_TT_FACE_HEIGHT) * FT_TT_FACE_WIDTH +
|
|
(column % FT_TT_FACE_WIDTH);
|
|
}
|
|
|
|
/** Transpose one 32x32 tile in place. `element_bytes` is 2 (bf16) or 4 (fp32 or float). */
|
|
static void transpose_tile(void *tile, size_t element_bytes, int face_ordered)
|
|
{
|
|
unsigned char scratch[FT_TT_TILE_VALUES * 4];
|
|
unsigned char *bytes = tile;
|
|
uint32_t row;
|
|
|
|
memcpy(scratch, bytes, (size_t)FT_TT_TILE_VALUES * element_bytes);
|
|
for (row = 0; row < FT_TT_TILE_HEIGHT; row++) {
|
|
uint32_t column;
|
|
|
|
for (column = 0; column < FT_TT_TILE_WIDTH; column++)
|
|
memcpy(bytes + tile_element_index(row, column, face_ordered) * element_bytes,
|
|
scratch + tile_element_index(column, row, face_ordered) * element_bytes,
|
|
element_bytes);
|
|
}
|
|
}
|
|
|
|
/** Transpose every tile of one strip in place. */
|
|
static void transpose_strip_tiles(void *tiles, uint32_t tile_count, size_t element_bytes,
|
|
int face_ordered)
|
|
{
|
|
unsigned char *bytes = tiles;
|
|
uint32_t index;
|
|
|
|
for (index = 0; index < tile_count; index++)
|
|
transpose_tile(bytes + (size_t)index * FT_TT_TILE_VALUES * element_bytes, element_bytes,
|
|
face_ordered);
|
|
}
|
|
|
|
/** Bytes one strip of `tile_cols` tiles occupies in `format`. */
|
|
static size_t strip_bytes_for(ft_tt_tensor_format format, uint32_t tile_cols)
|
|
{
|
|
if (format_is_block_float(format))
|
|
return (size_t)tile_cols * ft_bfp_tile_bytes(bfp_format_for(format));
|
|
return (size_t)tile_cols * FT_TT_TILE_VALUES * format_element_bytes(format);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Buffers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void strip_buffers_free(strip_buffers *buffers)
|
|
{
|
|
free(buffers->source_strip);
|
|
free(buffers->tile_strip);
|
|
free(buffers->packed);
|
|
free(buffers->decoded);
|
|
free(buffers->reference_strip);
|
|
memset(buffers, 0, sizeof(*buffers));
|
|
}
|
|
|
|
/** Add the reference strip, for a measurement scored against a second file. */
|
|
static int strip_buffers_add_reference(strip_buffers *buffers)
|
|
{
|
|
buffers->reference_strip = calloc((size_t)FT_TT_TILE_HEIGHT * buffers->max_cols, sizeof(float));
|
|
return buffers->reference_strip ? FT_TT_CONVERT_OK : FT_TT_CONVERT_ENOMEM;
|
|
}
|
|
|
|
/**
|
|
* Size the buffers once, for the widest tensor in the file. `measure_pcc`
|
|
* adds the decode buffer; without it that allocation is skipped entirely.
|
|
*/
|
|
static int strip_buffers_alloc(strip_buffers *buffers, const ft_gguf_map *map, int measure_pcc)
|
|
{
|
|
uint32_t max_cols = 0;
|
|
size_t strip_values;
|
|
|
|
memset(buffers, 0, sizeof(*buffers));
|
|
|
|
for (uint32_t i = 0; i < ft_gguf_map_tensor_count(map); i++) {
|
|
const ft_gguf_tensor *tensor = ft_gguf_map_tensor(map, i);
|
|
uint64_t rows, cols;
|
|
|
|
tensor_shape(tensor, &rows, &cols);
|
|
if (cols > UINT32_MAX)
|
|
return FT_TT_CONVERT_EINVAL;
|
|
if ((uint32_t)cols > max_cols)
|
|
max_cols = (uint32_t)cols;
|
|
}
|
|
if (max_cols == 0)
|
|
return FT_TT_CONVERT_EINVAL;
|
|
|
|
buffers->max_cols = max_cols;
|
|
buffers->max_tile_cols = ft_tt_tile_count_for(max_cols);
|
|
strip_values = (size_t)buffers->max_tile_cols * FT_TT_TILE_VALUES;
|
|
|
|
buffers->source_strip = calloc((size_t)FT_TT_TILE_HEIGHT * max_cols, sizeof(float));
|
|
buffers->tile_strip = calloc(strip_values, sizeof(float));
|
|
/* fp32 tiles are the largest encoding, so sizing on them covers every
|
|
* format without a per-format branch here. */
|
|
buffers->packed = calloc(strip_values, 4u);
|
|
if (measure_pcc)
|
|
buffers->decoded = calloc(strip_values, sizeof(float));
|
|
|
|
if (!buffers->source_strip || !buffers->tile_strip || !buffers->packed || (measure_pcc && !buffers->decoded)) {
|
|
strip_buffers_free(buffers);
|
|
return FT_TT_CONVERT_ENOMEM;
|
|
}
|
|
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* One strip */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Dequantize `valid_rows` source rows starting at `first_row` into the strip
|
|
* buffer, one row at a time so the mapping is touched strictly sequentially.
|
|
*
|
|
* `permutation` is NULL for every tensor but a permuted `attn_q`/`attn_k`, and
|
|
* that is the only case where the mapping is NOT walked in order: the split-half
|
|
* partner of a row sits `rot/2` rows away, which for MiniCPM5-1B is two strips
|
|
* off, so the gather cannot be made strip-local. It stays inside ONE HEAD —
|
|
* 128 rows, ~192 KiB of a Q8_0 tensor — so it is a walk over a few pages, not a
|
|
* walk over the tensor.
|
|
*/
|
|
static int load_source_strip(const ft_gguf_tensor *tensor, uint64_t first_row, uint32_t valid_rows, uint64_t cols,
|
|
const rope_row_permutation *permutation, float *strip)
|
|
{
|
|
size_t row_bytes = ft_gguf_type_bytes(tensor->ggml_type, cols);
|
|
const unsigned char *base = (const unsigned char *)tensor->data;
|
|
|
|
if (row_bytes == 0)
|
|
return FT_TT_CONVERT_EUNSUPPORTED;
|
|
|
|
for (uint32_t row = 0; row < valid_rows; row++) {
|
|
uint64_t target = first_row + row;
|
|
uint64_t source = permutation ? rope_permuted_source_row(permutation, target) : target;
|
|
const unsigned char *src = base + (size_t)source * row_bytes;
|
|
|
|
if (ft_gguf_dequant_to_float(tensor->ggml_type, src, cols, strip + (size_t)row * cols) != 0)
|
|
return FT_TT_CONVERT_EUNSUPPORTED;
|
|
}
|
|
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/**
|
|
* Encode one strip into `buffers->packed` and report its size.
|
|
*
|
|
* Block-float takes the SWIZZLED layout (row-major within each tile) because
|
|
* tt-metal's packer does the face reordering itself; fp32 and bf16 take the
|
|
* face-ordered layout because nothing downstream will reorder them.
|
|
*
|
|
* `transposed` is FT_TT_LAYOUT_MATMUL_NMAJOR's half of the work that is not
|
|
* free: each 32x32 tile is transposed before it is encoded, so the strip's
|
|
* tiles come out as target column `nt`'s Kt tiles, in kt order — which IS
|
|
* N-major order for that column, so the write stays sequential. The transpose
|
|
* has to happen before ft_bfp packs: a packed tile's exponent is shared along
|
|
* a row, so transposing afterwards would mean unpack and repack.
|
|
*/
|
|
static int encode_strip(strip_buffers *buffers, ft_tt_tensor_format format, uint32_t valid_rows, uint64_t cols,
|
|
uint32_t tile_cols, int transposed, size_t *bytes_out)
|
|
{
|
|
size_t src_stride = (size_t)cols * sizeof(float);
|
|
int rc;
|
|
|
|
if (format_is_block_float(format)) {
|
|
rc = ft_tt_tile_swizzle_strip(buffers->source_strip, sizeof(float), valid_rows, (uint32_t)cols, src_stride,
|
|
buffers->tile_strip);
|
|
if (rc != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->tile_strip, tile_cols, sizeof(float), 0);
|
|
if (ft_bfp_pack_tiles(bfp_format_for(format), buffers->tile_strip, tile_cols, buffers->packed) != FT_BFP_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
*bytes_out = strip_bytes_for(format, tile_cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
if (format == FT_TT_FORMAT_FP32) {
|
|
rc = ft_tt_tilize_strip(buffers->source_strip, sizeof(float), valid_rows, (uint32_t)cols, src_stride,
|
|
buffers->packed);
|
|
if (rc != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->packed, tile_cols, sizeof(float), 1);
|
|
*bytes_out = strip_bytes_for(format, tile_cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/* bf16: narrow first, into the tile-strip buffer reused as bf16 scratch,
|
|
* then tilize at 2 bytes per element. */
|
|
ft_bf16_from_float_array(buffers->source_strip, (uint16_t *)buffers->tile_strip,
|
|
(size_t)valid_rows * (size_t)cols);
|
|
rc = ft_tt_tilize_strip(buffers->tile_strip, sizeof(uint16_t), valid_rows, (uint32_t)cols,
|
|
(size_t)cols * sizeof(uint16_t), buffers->packed);
|
|
if (rc != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->packed, tile_cols, sizeof(uint16_t), 1);
|
|
*bytes_out = strip_bytes_for(format, tile_cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/**
|
|
* Decode the strip that was just encoded, back into `buffers->decoded`.
|
|
*
|
|
* The PCC this feeds is against the SOURCE strip, so a transposed strip has to
|
|
* be transposed back first — otherwise the round trip would score a transpose
|
|
* against an untransposed original and report a number near zero for a
|
|
* conversion that is exactly right. This runs after the strip has been
|
|
* written, so mutating `packed` in place is free.
|
|
*/
|
|
static int decode_strip(strip_buffers *buffers, ft_tt_tensor_format format, uint32_t valid_rows, uint64_t cols,
|
|
uint32_t tile_cols, int transposed)
|
|
{
|
|
size_t dst_stride = (size_t)cols * sizeof(float);
|
|
|
|
if (format_is_block_float(format)) {
|
|
if (ft_bfp_unpack_tiles(bfp_format_for(format), buffers->packed, tile_cols, buffers->tile_strip) != FT_BFP_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->tile_strip, tile_cols, sizeof(float), 0);
|
|
if (ft_tt_tile_unswizzle_strip(buffers->tile_strip, sizeof(float), valid_rows, (uint32_t)cols, dst_stride,
|
|
buffers->decoded) != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
if (format == FT_TT_FORMAT_FP32) {
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->packed, tile_cols, sizeof(float), 1);
|
|
if (ft_tt_untilize_strip(buffers->packed, sizeof(float), valid_rows, (uint32_t)cols, dst_stride,
|
|
buffers->decoded) != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
if (transposed)
|
|
transpose_strip_tiles(buffers->packed, tile_cols, sizeof(uint16_t), 1);
|
|
if (ft_tt_untilize_strip(buffers->packed, sizeof(uint16_t), valid_rows, (uint32_t)cols,
|
|
(size_t)cols * sizeof(uint16_t), buffers->tile_strip) != FT_TT_TILIZE_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
ft_float_from_bf16_array((const uint16_t *)buffers->tile_strip, buffers->decoded,
|
|
(size_t)valid_rows * (size_t)cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/** Fold one strip into an accumulator. `truth` is the x series, `estimate` the y. */
|
|
static void accumulate_strip_error(pearson_accumulator *acc, const float *truth, const float *estimate,
|
|
uint32_t valid_rows, uint64_t cols)
|
|
{
|
|
for (uint32_t row = 0; row < valid_rows; row++) {
|
|
const float *x = truth + (size_t)row * cols;
|
|
const float *y = estimate + (size_t)row * cols;
|
|
|
|
for (uint64_t col = 0; col < cols; col++)
|
|
pearson_add(acc, (double)x[col], (double)y[col]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Score one strip, against the reference when there is one.
|
|
*
|
|
* With a reference the truth is the reference, NOT the source — which is the
|
|
* whole point of supplying one — and `source_acc` picks up how far the source
|
|
* itself already was from it.
|
|
*/
|
|
static int score_strip(strip_buffers *buffers, const ft_gguf_tensor *reference, uint64_t first, uint32_t valid,
|
|
uint64_t cols, pearson_accumulator *acc, pearson_accumulator *source_acc)
|
|
{
|
|
if (!reference) {
|
|
accumulate_strip_error(acc, buffers->source_strip, buffers->decoded, valid, cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
{
|
|
int rc = load_source_strip(reference, first, valid, cols, NULL, buffers->reference_strip);
|
|
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
}
|
|
accumulate_strip_error(acc, buffers->reference_strip, buffers->decoded, valid, cols);
|
|
accumulate_strip_error(source_acc, buffers->reference_strip, buffers->source_strip, valid, cols);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* One tensor */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* How one tensor is written: what it is packed as, how its tiles are arranged,
|
|
* and whether its output rows are permuted on the way in.
|
|
*
|
|
* Three fields that always travel together and are always decided together, so
|
|
* they are one argument rather than three that could be passed inconsistently.
|
|
*/
|
|
typedef struct {
|
|
ft_tt_tensor_format format;
|
|
ft_tt_tensor_layout layout;
|
|
/** NULL for every tensor except a permuted `attn_q`/`attn_k`. */
|
|
const rope_row_permutation *rope;
|
|
} tensor_encoding;
|
|
|
|
/** A 1-D tensor is written row-major and untiled; see the ledger header. */
|
|
static int write_untiled(FILE *out, const ft_gguf_tensor *tensor, uint64_t cols, strip_buffers *buffers,
|
|
uint64_t *bytes_written)
|
|
{
|
|
int rc = load_source_strip(tensor, 0, 1, cols, NULL, buffers->source_strip);
|
|
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
if (out && fwrite(buffers->source_strip, sizeof(float), (size_t)cols, out) != cols)
|
|
return FT_TT_CONVERT_EIO;
|
|
|
|
*bytes_written = cols * sizeof(float);
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/**
|
|
* Convert one tensor, writing to `out` when it is non-NULL. With `out` NULL
|
|
* this is the measure-only path — same arithmetic, no artifact.
|
|
*/
|
|
static int convert_tensor(FILE *out, const ft_gguf_tensor *tensor, const ft_gguf_tensor *reference,
|
|
const tensor_encoding *encoding, strip_buffers *buffers, int measure_pcc,
|
|
ft_tt_convert_tensor_report *report)
|
|
{
|
|
ft_tt_tensor_format format = encoding->format;
|
|
int transposed = encoding->layout == FT_TT_LAYOUT_MATMUL_NMAJOR;
|
|
uint64_t rows, cols;
|
|
uint32_t tile_cols;
|
|
pearson_accumulator acc;
|
|
pearson_accumulator source_acc;
|
|
uint64_t written = 0;
|
|
int rc;
|
|
|
|
tensor_shape(tensor, &rows, &cols);
|
|
pearson_reset(&acc);
|
|
pearson_reset(&source_acc);
|
|
|
|
memset(report, 0, sizeof(*report));
|
|
snprintf(report->name, sizeof(report->name), "%s", tensor->name);
|
|
report->role = ft_tt_ledger_role(tensor->name);
|
|
report->format = format;
|
|
report->rows = rows;
|
|
report->cols = cols;
|
|
report->source_bytes = tensor->byte_count;
|
|
report->pcc = 1.0;
|
|
|
|
if (!ft_gguf_type_supported(tensor->ggml_type))
|
|
return FT_TT_CONVERT_EUNSUPPORTED;
|
|
|
|
if (tensor->n_dims < 2) {
|
|
rc = write_untiled(out, tensor, cols, buffers, &written);
|
|
report->converted_bytes = written;
|
|
return rc;
|
|
}
|
|
|
|
tile_cols = ft_tt_tile_count_for((uint32_t)cols);
|
|
|
|
for (uint64_t first = 0; first < rows; first += FT_TT_TILE_HEIGHT) {
|
|
uint32_t valid = rows - first < FT_TT_TILE_HEIGHT ? (uint32_t)(rows - first) : FT_TT_TILE_HEIGHT;
|
|
size_t bytes = 0;
|
|
|
|
rc = load_source_strip(tensor, first, valid, cols, encoding->rope, buffers->source_strip);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
|
|
rc = encode_strip(buffers, format, valid, cols, tile_cols, transposed, &bytes);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
|
|
if (out && fwrite(buffers->packed, 1, bytes, out) != bytes)
|
|
return FT_TT_CONVERT_EIO;
|
|
written += bytes;
|
|
|
|
if (measure_pcc) {
|
|
rc = decode_strip(buffers, format, valid, cols, tile_cols, transposed);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
rc = score_strip(buffers, reference, first, valid, cols, &acc, &source_acc);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
report->converted_bytes = written;
|
|
if (measure_pcc) {
|
|
report->pcc = pearson_value(&acc);
|
|
report->max_abs_error = acc.max_abs_error;
|
|
report->rms_error = pearson_rms(&acc);
|
|
report->relative_rms_error = pearson_relative_rms(&acc);
|
|
report->source_relative_rms_error = pearson_relative_rms(&source_acc);
|
|
}
|
|
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Artifact */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static int write_padding(FILE *out, uint64_t bytes)
|
|
{
|
|
static const unsigned char zeros[FT_TT_ARTIFACT_ALIGNMENT] = {0};
|
|
|
|
while (bytes > 0) {
|
|
size_t chunk = bytes < sizeof(zeros) ? (size_t)bytes : sizeof(zeros);
|
|
|
|
if (fwrite(zeros, 1, chunk, out) != chunk)
|
|
return FT_TT_CONVERT_EIO;
|
|
bytes -= chunk;
|
|
}
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
static uint64_t align_up(uint64_t value, uint64_t alignment)
|
|
{
|
|
return (value + alignment - 1) / alignment * alignment;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* The rotary permutation plan */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* The head geometry the whole model is permuted under, read once from metadata.
|
|
*
|
|
* `head_rows` is 0 when the metadata does not pin it, which is a refusal and not
|
|
* a default: deriving it from a tensor's own row count would silently accept
|
|
* `attn_q` tensors that carry more than one projection per head — Qwen3.8's
|
|
* do — and permute rows that are not rotary features at all.
|
|
*/
|
|
typedef struct {
|
|
int enabled;
|
|
int reverse_pairs;
|
|
int keep_interleaved;
|
|
uint64_t n_head;
|
|
uint64_t n_head_kv;
|
|
uint64_t head_rows;
|
|
uint64_t rot_rows;
|
|
} rope_plan;
|
|
|
|
static int metadata_u64_for_arch(const ft_gguf_map *map, const char *architecture, const char *suffix,
|
|
uint64_t *out)
|
|
{
|
|
char key[128];
|
|
|
|
if (snprintf(key, sizeof(key), "%s.%s", architecture, suffix) >= (int)sizeof(key))
|
|
return FT_TT_CONVERT_EINVAL;
|
|
return ft_gguf_map_metadata_u64(map, key, out) == FT_GGUF_OK ? FT_TT_CONVERT_OK
|
|
: FT_TT_CONVERT_EROPEGEOMETRY;
|
|
}
|
|
|
|
/**
|
|
* Build the permutation plan, or refuse and say why.
|
|
*
|
|
* Every refusal here is a case where permuting would produce an artifact that is
|
|
* wrong in a way nothing downstream can detect, so none of them fall back.
|
|
*/
|
|
static int rope_plan_build(const ft_gguf_map *map, const char *architecture, ft_tt_model_family family,
|
|
ft_tt_rope_pairing pairing, rope_plan *plan)
|
|
{
|
|
memset(plan, 0, sizeof(*plan));
|
|
if (pairing == FT_TT_ROPE_PAIRING_SOURCE)
|
|
return FT_TT_CONVERT_OK;
|
|
|
|
if (family == FT_TT_FAMILY_QWEN) {
|
|
fprintf(stderr, "ft_tt_convert: --rope-pairing split-half refused: architecture '%s' already "
|
|
"stores q/k in split-half (NeoX) order, so permuting would BREAK it\n",
|
|
architecture);
|
|
return FT_TT_CONVERT_EROPEGEOMETRY;
|
|
}
|
|
if (metadata_u64_for_arch(map, architecture, "attention.head_count", &plan->n_head) != FT_TT_CONVERT_OK ||
|
|
metadata_u64_for_arch(map, architecture, "attention.head_count_kv", &plan->n_head_kv) !=
|
|
FT_TT_CONVERT_OK ||
|
|
metadata_u64_for_arch(map, architecture, "attention.key_length", &plan->head_rows) !=
|
|
FT_TT_CONVERT_OK ||
|
|
metadata_u64_for_arch(map, architecture, "rope.dimension_count", &plan->rot_rows) !=
|
|
FT_TT_CONVERT_OK) {
|
|
fprintf(stderr, "ft_tt_convert: --rope-pairing split-half refused: '%s' does not declare all of "
|
|
"attention.head_count, attention.head_count_kv, attention.key_length and "
|
|
"rope.dimension_count\n",
|
|
architecture);
|
|
return FT_TT_CONVERT_EROPEGEOMETRY;
|
|
}
|
|
if (plan->n_head == 0u || plan->n_head_kv == 0u || plan->head_rows == 0u || plan->rot_rows == 0u ||
|
|
plan->rot_rows > plan->head_rows || (plan->rot_rows % 2u) != 0u) {
|
|
fprintf(stderr, "ft_tt_convert: --rope-pairing split-half refused: head geometry heads=%llu kv=%llu "
|
|
"head_dim=%llu rope_dim=%llu is not a pairing this permutation expresses\n",
|
|
(unsigned long long)plan->n_head, (unsigned long long)plan->n_head_kv,
|
|
(unsigned long long)plan->head_rows, (unsigned long long)plan->rot_rows);
|
|
return FT_TT_CONVERT_EROPEGEOMETRY;
|
|
}
|
|
|
|
/* The reversal reflects `p -> half-1-p`, so it needs `half` to exist as an integer.
|
|
* `rot_rows % 2 == 0` is already checked above, which is the whole requirement. */
|
|
plan->enabled = 1;
|
|
plan->reverse_pairs = (pairing == FT_TT_ROPE_PAIRING_SPLIT_HALF_REVERSED ||
|
|
pairing == FT_TT_ROPE_PAIRING_INTERLEAVED_REVERSED);
|
|
plan->keep_interleaved = (pairing == FT_TT_ROPE_PAIRING_INTERLEAVED_REVERSED);
|
|
if (plan->reverse_pairs)
|
|
fprintf(stderr, "ft_tt_convert: --rope-pairing %s is an INSTRUMENT, not a checkpoint "
|
|
"format: slot p carries the model's rotary pair %llu-1-p, so a consumer "
|
|
"that does not read FT_TT_ARTIFACT_FLAG_ROPE_PAIRS_REVERSED will apply "
|
|
"every rotary frequency to the wrong feature\n",
|
|
plan->keep_interleaved ? "interleaved-reversed" : "split-half-reversed",
|
|
(unsigned long long)(plan->rot_rows / 2u));
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
/** Whether `name` is one of the two tensors whose output rows carry rotary features. */
|
|
static int name_is_rotary_projection(const char *name)
|
|
{
|
|
/* `attn_q_norm`/`attn_k_norm` are ROLE_NORM and never reach here; `attn_qkv`
|
|
* is a GDN in-projection and is ROLE_SSM_IN. So matching the stem is enough
|
|
* once the caller has already established the role is ATTENTION. */
|
|
return strstr(name, "attn_q") != NULL || strstr(name, "attn_k") != NULL;
|
|
}
|
|
|
|
/**
|
|
* The permutation for one tensor, or 0 when this tensor is not permuted.
|
|
*
|
|
* @return 1 and fills `out` when the tensor is permuted; 0 when it is not;
|
|
* FT_TT_CONVERT_EROPEGEOMETRY when it should be but its shape disagrees
|
|
* with the declared head geometry
|
|
*/
|
|
static int rope_plan_for_tensor(const rope_plan *plan, const char *name, ft_tt_tensor_role role, uint64_t rows,
|
|
rope_row_permutation *out)
|
|
{
|
|
uint64_t heads;
|
|
|
|
if (!plan->enabled || role != FT_TT_ROLE_ATTENTION || !name_is_rotary_projection(name))
|
|
return 0;
|
|
|
|
heads = strstr(name, "attn_q") != NULL ? plan->n_head : plan->n_head_kv;
|
|
if (rows != heads * plan->head_rows) {
|
|
fprintf(stderr, "ft_tt_convert: '%s' has %llu output rows but the declared geometry says %llu heads "
|
|
"x %llu — refusing to permute rows whose meaning is not established\n",
|
|
name, (unsigned long long)rows, (unsigned long long)heads,
|
|
(unsigned long long)plan->head_rows);
|
|
return FT_TT_CONVERT_EROPEGEOMETRY;
|
|
}
|
|
out->head_rows = plan->head_rows;
|
|
out->rot_rows = plan->rot_rows;
|
|
out->reverse_pairs = plan->reverse_pairs;
|
|
out->keep_interleaved = plan->keep_interleaved;
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Decide format, layout and rotary permutation for one tensor.
|
|
*
|
|
* The refusals are the point of the function: an unclassified 2-D tensor and a
|
|
* shape that contradicts the declared head geometry both stop the conversion
|
|
* rather than producing an artifact whose bytes mean something other than what
|
|
* the directory says.
|
|
*/
|
|
static int tensor_encoding_for(const ft_gguf_tensor *tensor, const ft_tt_convert_options *opts,
|
|
ft_tt_model_family family, const rope_plan *plan, uint64_t rows,
|
|
ft_tt_tensor_role *role_out, rope_row_permutation *rope_out,
|
|
tensor_encoding *encoding)
|
|
{
|
|
ft_tt_tensor_role role = ft_tt_ledger_role(tensor->name);
|
|
int rc;
|
|
|
|
*role_out = role;
|
|
encoding->format = ft_tt_ledger_format_for(opts->mode, family, role, tensor->n_dims);
|
|
/* The experiment override, applied AFTER the ledger so the ledger remains the one
|
|
* description of what production is. 1-D tensors keep the fp32 every rung gives them:
|
|
* overriding those would measure a norm, not a matmul. */
|
|
if (opts->format_override_name_contains && tensor->n_dims >= 2 && role != FT_TT_ROLE_NORM &&
|
|
role != FT_TT_ROLE_BIAS && strstr(tensor->name, opts->format_override_name_contains)) {
|
|
fprintf(stderr, "ft_tt_convert: OVERRIDE '%s' %s -> %s (matched '%s')\n", tensor->name,
|
|
ft_tt_ledger_format_name(encoding->format),
|
|
ft_tt_ledger_format_name(opts->format_override),
|
|
opts->format_override_name_contains);
|
|
encoding->format = opts->format_override;
|
|
}
|
|
encoding->rope = NULL;
|
|
|
|
rc = ft_tt_convert_layout_for_checked(role, tensor->n_dims, &encoding->layout);
|
|
if (rc != FT_TT_CONVERT_OK) {
|
|
fprintf(stderr, "ft_tt_convert: '%s' is %u-D and matches no role rule, so neither layout can be "
|
|
"chosen for it — both would be a guess about a kernel, and a wrong one is "
|
|
"silent. Add a rule to ft_tt_ledger_role().\n",
|
|
tensor->name, tensor->n_dims);
|
|
return rc;
|
|
}
|
|
|
|
rc = rope_plan_for_tensor(plan, tensor->name, role, rows, rope_out);
|
|
if (rc < 0)
|
|
return rc;
|
|
if (rc == 1)
|
|
encoding->rope = rope_out;
|
|
return FT_TT_CONVERT_OK;
|
|
}
|
|
|
|
int ft_tt_convert_gguf(const char *gguf_path, const char *output_path, const ft_tt_convert_options *options)
|
|
{
|
|
static const ft_tt_convert_options defaults = {FT_TT_LEDGER_PRODUCTION, 0, NULL, NULL,
|
|
FT_TT_ROPE_PAIRING_SOURCE, NULL,
|
|
FT_TT_FORMAT_FP32};
|
|
const ft_tt_convert_options *opts = options ? options : &defaults;
|
|
ft_gguf_map *map = NULL;
|
|
FILE *out = NULL;
|
|
strip_buffers buffers;
|
|
ft_tt_artifact_entry *directory = NULL;
|
|
ft_tt_artifact_header header;
|
|
ft_tt_model_family family;
|
|
rope_plan plan;
|
|
char architecture[64] = "";
|
|
uint64_t cursor;
|
|
uint32_t count;
|
|
int rc;
|
|
|
|
memset(&buffers, 0, sizeof(buffers));
|
|
|
|
if (!gguf_path || !output_path)
|
|
return FT_TT_CONVERT_EINVAL;
|
|
|
|
rc = ft_gguf_map_open(gguf_path, &map);
|
|
if (rc != FT_GGUF_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
|
|
count = ft_gguf_map_tensor_count(map);
|
|
(void)ft_gguf_map_metadata_string(map, "general.architecture", architecture, sizeof(architecture));
|
|
family = ft_tt_ledger_family_from_architecture(architecture);
|
|
|
|
/* Before the output file is created, so a refused geometry leaves no
|
|
* half-written artifact behind to be mistaken for a real one. */
|
|
rc = rope_plan_build(map, architecture, family, opts->rope_pairing, &plan);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
|
|
rc = strip_buffers_alloc(&buffers, map, opts->measure_pcc);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
|
|
directory = calloc(count, sizeof(*directory));
|
|
if (!directory) {
|
|
rc = FT_TT_CONVERT_ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
|
|
out = fopen(output_path, "wb");
|
|
if (!out) {
|
|
fprintf(stderr, "ft_tt_convert: fopen('%s'): %s\n", output_path, strerror(errno));
|
|
rc = FT_TT_CONVERT_EIO;
|
|
goto cleanup;
|
|
}
|
|
|
|
memset(&header, 0, sizeof(header));
|
|
header.magic = FT_TT_ARTIFACT_MAGIC;
|
|
header.version = FT_TT_ARTIFACT_VERSION;
|
|
header.tensor_count = count;
|
|
/* The two bits are independent: SPLIT_HALF says WHERE a partner is, PAIRS_REVERSED says
|
|
* WHICH pair a slot holds. A reader that knows only the first must refuse the second
|
|
* rather than serve it as plain split-half, which is what FT_TT_ARTIFACT_FLAGS_KNOWN
|
|
* and ft_tt_fttw_open()'s refusal enforce. */
|
|
header.flags = (plan.enabled && !plan.keep_interleaved) ? FT_TT_ARTIFACT_FLAG_ROPE_SPLIT_HALF : 0u;
|
|
if (plan.reverse_pairs)
|
|
header.flags |= FT_TT_ARTIFACT_FLAG_ROPE_PAIRS_REVERSED;
|
|
header.data_offset = align_up(sizeof(header), FT_TT_ARTIFACT_ALIGNMENT);
|
|
|
|
if (fwrite(&header, sizeof(header), 1, out) != 1) {
|
|
rc = FT_TT_CONVERT_EIO;
|
|
goto cleanup;
|
|
}
|
|
rc = write_padding(out, header.data_offset - sizeof(header));
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
|
|
cursor = header.data_offset;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
const ft_gguf_tensor *tensor = ft_gguf_map_tensor(map, i);
|
|
ft_tt_artifact_entry *entry = &directory[i];
|
|
ft_tt_convert_tensor_report report;
|
|
ft_tt_tensor_role role;
|
|
rope_row_permutation permutation;
|
|
tensor_encoding encoding;
|
|
uint64_t padded;
|
|
uint64_t rows, cols;
|
|
|
|
tensor_shape(tensor, &rows, &cols);
|
|
rc = tensor_encoding_for(tensor, opts, family, &plan, rows, &role, &permutation, &encoding);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
|
|
rc = convert_tensor(out, tensor, NULL, &encoding, &buffers, opts->measure_pcc, &report);
|
|
if (rc != FT_TT_CONVERT_OK) {
|
|
fprintf(stderr, "ft_tt_convert: tensor '%s' (%s) failed: %d\n", tensor->name,
|
|
ft_gguf_type_name(tensor->ggml_type), rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
snprintf(entry->name, sizeof(entry->name), "%s", tensor->name);
|
|
entry->format = (uint32_t)encoding.format;
|
|
entry->role = (uint32_t)role;
|
|
entry->n_dims = tensor->n_dims;
|
|
entry->layout = (uint32_t)encoding.layout;
|
|
/* The STORED grid. A transposed weight is Kt x Nt = ceil(cols/32) x ceil(rows/32),
|
|
* i.e. the swap of the source grid — the count of tiles is the same either way,
|
|
* which is why the artifact's size does not move. */
|
|
if (tensor->n_dims < 2) {
|
|
entry->tile_rows = 0;
|
|
entry->tile_cols = 0;
|
|
} else if (encoding.layout == FT_TT_LAYOUT_MATMUL_NMAJOR) {
|
|
entry->tile_rows = ft_tt_tile_count_for((uint32_t)cols);
|
|
entry->tile_cols = ft_tt_tile_count_for((uint32_t)rows);
|
|
} else {
|
|
entry->tile_rows = ft_tt_tile_count_for((uint32_t)rows);
|
|
entry->tile_cols = ft_tt_tile_count_for((uint32_t)cols);
|
|
}
|
|
for (int d = 0; d < 4; d++)
|
|
entry->dims[d] = tensor->dims[d];
|
|
entry->data_offset = cursor;
|
|
entry->byte_count = report.converted_bytes;
|
|
|
|
cursor += report.converted_bytes;
|
|
padded = align_up(cursor, FT_TT_ARTIFACT_ALIGNMENT);
|
|
rc = write_padding(out, padded - cursor);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
cursor = padded;
|
|
|
|
ft_gguf_map_release(map, tensor);
|
|
|
|
if (opts->progress && opts->progress(&report, opts->progress_user) != 0) {
|
|
rc = FT_TT_CONVERT_ERROR;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
header.directory_offset = cursor;
|
|
if (fwrite(directory, sizeof(*directory), count, out) != count) {
|
|
rc = FT_TT_CONVERT_EIO;
|
|
goto cleanup;
|
|
}
|
|
header.total_bytes = cursor + (uint64_t)count * sizeof(*directory);
|
|
|
|
/* Rewrite the header now that the directory is placed. This is the only
|
|
* seek in the whole path. */
|
|
if (fseek(out, 0, SEEK_SET) != 0 || fwrite(&header, sizeof(header), 1, out) != 1) {
|
|
rc = FT_TT_CONVERT_EIO;
|
|
goto cleanup;
|
|
}
|
|
|
|
rc = FT_TT_CONVERT_OK;
|
|
|
|
cleanup:
|
|
if (out && fclose(out) != 0 && rc == FT_TT_CONVERT_OK)
|
|
rc = FT_TT_CONVERT_EIO;
|
|
free(directory);
|
|
strip_buffers_free(&buffers);
|
|
ft_gguf_map_close(map);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* The reference tensor matching `tensor`, checked for shape agreement.
|
|
*
|
|
* A missing or differently-shaped tensor is refused rather than skipped: a
|
|
* measurement that silently covered 90% of a model is how a regression hides.
|
|
*/
|
|
static const ft_gguf_tensor *matching_reference(const ft_gguf_map *reference, const ft_gguf_tensor *tensor)
|
|
{
|
|
const ft_gguf_tensor *found = ft_gguf_map_find(reference, tensor->name);
|
|
|
|
if (!found) {
|
|
fprintf(stderr, "ft_tt_convert_measure: the reference has no tensor '%s'\n", tensor->name);
|
|
return NULL;
|
|
}
|
|
if (found->n_dims != tensor->n_dims || memcmp(found->dims, tensor->dims, sizeof(found->dims)) != 0) {
|
|
fprintf(stderr, "ft_tt_convert_measure: '%s' is %llux%llu here and %llux%llu in the reference\n", tensor->name,
|
|
(unsigned long long)tensor->dims[1], (unsigned long long)tensor->dims[0],
|
|
(unsigned long long)found->dims[1], (unsigned long long)found->dims[0]);
|
|
return NULL;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
int ft_tt_convert_measure_against(const char *gguf_path, const char *reference_gguf_path,
|
|
ft_tt_tensor_format format, ft_tt_convert_progress_fn progress, void *user)
|
|
{
|
|
ft_gguf_map *map = NULL;
|
|
ft_gguf_map *reference = NULL;
|
|
strip_buffers buffers;
|
|
int rc;
|
|
|
|
memset(&buffers, 0, sizeof(buffers));
|
|
|
|
if (!gguf_path || !progress)
|
|
return FT_TT_CONVERT_EINVAL;
|
|
|
|
rc = ft_gguf_map_open(gguf_path, &map);
|
|
if (rc != FT_GGUF_OK)
|
|
return FT_TT_CONVERT_ERROR;
|
|
if (reference_gguf_path && ft_gguf_map_open(reference_gguf_path, &reference) != FT_GGUF_OK) {
|
|
ft_gguf_map_close(map);
|
|
return FT_TT_CONVERT_ERROR;
|
|
}
|
|
|
|
rc = strip_buffers_alloc(&buffers, map, 1);
|
|
if (rc == FT_TT_CONVERT_OK && reference)
|
|
rc = strip_buffers_add_reference(&buffers);
|
|
if (rc != FT_TT_CONVERT_OK)
|
|
goto cleanup;
|
|
|
|
for (uint32_t i = 0; i < ft_gguf_map_tensor_count(map); i++) {
|
|
const ft_gguf_tensor *tensor = ft_gguf_map_tensor(map, i);
|
|
const ft_gguf_tensor *truth = NULL;
|
|
ft_tt_convert_tensor_report report;
|
|
|
|
/* Score every tensor the caller could actually place in this format.
|
|
* 1-D tensors are fp32 on every rung, so scoring them at BFP would
|
|
* report a number no configuration can produce. */
|
|
tensor_encoding encoding;
|
|
|
|
encoding.format = tensor->n_dims < 2 ? FT_TT_FORMAT_FP32 : format;
|
|
encoding.rope = NULL;
|
|
/* The gate scores the format AS SHIPPED, so it must use the shipped layout: the
|
|
* transpose changes which 16 elements share a block-float exponent, and a gate
|
|
* that measured the other grouping would be scoring a tensor nobody stores.
|
|
* An unclassified tensor has no shipped layout, so it is refused here too — a
|
|
* gate that quietly scored the wrong grouping is worse than one that stops. */
|
|
rc = ft_tt_convert_layout_for_checked(ft_tt_ledger_role(tensor->name), tensor->n_dims,
|
|
&encoding.layout);
|
|
if (rc != FT_TT_CONVERT_OK) {
|
|
fprintf(stderr, "ft_tt_convert_measure: '%s' matches no role rule, so it has no shipped "
|
|
"layout to score\n",
|
|
tensor->name);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (reference) {
|
|
truth = matching_reference(reference, tensor);
|
|
if (!truth) {
|
|
rc = FT_TT_CONVERT_EINVAL;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
rc = convert_tensor(NULL, tensor, truth, &encoding, &buffers, 1, &report);
|
|
if (rc != FT_TT_CONVERT_OK) {
|
|
fprintf(stderr, "ft_tt_convert_measure: tensor '%s' failed: %d\n", tensor->name, rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
ft_gguf_map_release(map, tensor);
|
|
if (truth)
|
|
ft_gguf_map_release(reference, truth);
|
|
|
|
if (progress(&report, user) != 0) {
|
|
rc = FT_TT_CONVERT_ERROR;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
rc = FT_TT_CONVERT_OK;
|
|
|
|
cleanup:
|
|
strip_buffers_free(&buffers);
|
|
ft_gguf_map_close(reference);
|
|
ft_gguf_map_close(map);
|
|
return rc;
|
|
}
|
|
|
|
int ft_tt_convert_measure(const char *gguf_path, ft_tt_tensor_format format, ft_tt_convert_progress_fn progress,
|
|
void *user)
|
|
{
|
|
return ft_tt_convert_measure_against(gguf_path, NULL, format, progress, user);
|
|
}
|