361 lines
15 KiB
C
361 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_gguf_layer_bytes.c — per-layer weight accounting for the multi-die
|
|
* admission gate. Self-contained: builds a synthetic tensor table in memory, no
|
|
* GGUF file, no model, no GPU.
|
|
*
|
|
* The regression it pins (2026-08-21): gpu_admit_die_share projected a die's
|
|
* resident weights as `mmap_size * n_layers_here / n_layer`. The file also
|
|
* carries token_embd (never uploaded — the embed is a host row copy) and
|
|
* output.weight (one die's, not every die's), so every die was charged for
|
|
* both. On Qwen3.6-35B-A3B Q4_K_M that over-projected each die by ~15% and
|
|
* refused a 2-die load that fits with ~1.3 GiB of headroom per die.
|
|
*
|
|
* Covers:
|
|
* - non-layer tensors are excluded from a layer range
|
|
* - ranges partition: [0,split) + [split,n) == the whole layer stack
|
|
* - mixed quants per layer sum by tensor, not by layer count
|
|
* - the proration bug is measurably worse than the tensor-table sum
|
|
* - blk.<i>. name parsing rejects near-misses
|
|
* - absent / empty / malformed inputs return 0, not garbage
|
|
*/
|
|
|
|
#include "../src/formats/gguf_layer_bytes.h"
|
|
#include "../src/formats/gguf_dequant.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static int g_failed = 0;
|
|
|
|
#define CHECK(cond, msg) do { \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, " FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
|
|
g_failed++; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define MAX_T 256
|
|
|
|
/* A synthetic GGUF whose tensor names own their storage, so name.data is the
|
|
* NUL-terminated char* pn_gguf_find_tensor's strcmp expects. */
|
|
typedef struct {
|
|
pn_gguf_file file;
|
|
pn_gguf_tensor_info t[MAX_T];
|
|
char names[MAX_T][64];
|
|
} fake_gguf;
|
|
|
|
static void fg_init(fake_gguf *f)
|
|
{
|
|
memset(f, 0, sizeof(*f));
|
|
f->file.tensors = f->t;
|
|
f->file.n_tensors = 0;
|
|
}
|
|
|
|
static void fg_add(fake_gguf *f, const char *name, pn_gguf_quant type,
|
|
uint64_t rows, uint64_t cols)
|
|
{
|
|
uint64_t i = f->file.n_tensors;
|
|
if (i >= MAX_T) return;
|
|
snprintf(f->names[i], sizeof(f->names[i]), "%s", name);
|
|
f->t[i].name.data = f->names[i];
|
|
f->t[i].name.len = strlen(f->names[i]);
|
|
f->t[i].type = type;
|
|
f->t[i].n_dims = 2;
|
|
f->t[i].dims[0] = cols;
|
|
f->t[i].dims[1] = rows;
|
|
f->file.n_tensors = i + 1;
|
|
}
|
|
|
|
static uint64_t bytes_of(pn_gguf_quant type, uint64_t rows, uint64_t cols)
|
|
{
|
|
return (uint64_t)pn_gguf_type_size(type, (size_t)(rows * cols));
|
|
}
|
|
|
|
/* 8 layers shaped like the Q3_K_M mini: the merged ffn_down_exps is Q6_K on the
|
|
* back half and Q4_K on the front, which is exactly the asymmetry that makes a
|
|
* layer-COUNT split and a layer-BYTE split different numbers. */
|
|
#define N_LAYER 8
|
|
#define SPLIT 4
|
|
#define ATTN_ROWS 32
|
|
#define ATTN_COLS 256
|
|
#define FFN_ROWS 64
|
|
#define FFN_COLS 256
|
|
|
|
static void build_model(fake_gguf *f)
|
|
{
|
|
fg_init(f);
|
|
fg_add(f, "token_embd.weight", GGUF_QUANT_Q6_K, 1024, 256);
|
|
for (int i = 0; i < N_LAYER; i++) {
|
|
char n[64];
|
|
snprintf(n, sizeof(n), "blk.%d.attn_qkv.weight", i);
|
|
fg_add(f, n, GGUF_QUANT_Q4_K, ATTN_ROWS, ATTN_COLS);
|
|
snprintf(n, sizeof(n), "blk.%d.ffn_down_exps.weight", i);
|
|
fg_add(f, n, (i < SPLIT) ? GGUF_QUANT_Q4_K : GGUF_QUANT_Q6_K,
|
|
FFN_ROWS, FFN_COLS);
|
|
}
|
|
fg_add(f, "output_norm.weight", GGUF_QUANT_F32, 1, 256);
|
|
fg_add(f, "output.weight", GGUF_QUANT_Q6_K, 1024, 256);
|
|
}
|
|
|
|
static void test_excludes_non_layer_tensors(void)
|
|
{
|
|
printf("excludes_non_layer_tensors:\n");
|
|
fake_gguf f;
|
|
build_model(&f);
|
|
|
|
uint64_t front_expect =
|
|
SPLIT * (bytes_of(GGUF_QUANT_Q4_K, ATTN_ROWS, ATTN_COLS) +
|
|
bytes_of(GGUF_QUANT_Q4_K, FFN_ROWS, FFN_COLS));
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, SPLIT) == front_expect,
|
|
"front range is its own tensors only");
|
|
|
|
/* token_embd + output.weight are the two big non-layer tensors; neither may
|
|
* appear in any layer range. */
|
|
uint64_t whole = pn_gguf_layer_range_bytes(&f.file, 0, N_LAYER);
|
|
uint64_t embd = pn_gguf_tensor_file_bytes(&f.file, "token_embd.weight");
|
|
uint64_t head = pn_gguf_tensor_file_bytes(&f.file, "output.weight");
|
|
CHECK(embd > 0 && head > 0, "the non-layer tensors are really there");
|
|
|
|
uint64_t sum_all = 0;
|
|
for (uint64_t i = 0; i < f.file.n_tensors; i++)
|
|
sum_all += bytes_of(f.t[i].type, f.t[i].dims[1], f.t[i].dims[0]);
|
|
uint64_t norm = pn_gguf_tensor_file_bytes(&f.file, "output_norm.weight");
|
|
CHECK(whole + embd + head + norm == sum_all,
|
|
"layers + non-layer tensors == every tensor, no double count");
|
|
}
|
|
|
|
static void test_ranges_partition(void)
|
|
{
|
|
printf("ranges_partition:\n");
|
|
fake_gguf f;
|
|
build_model(&f);
|
|
|
|
uint64_t front = pn_gguf_layer_range_bytes(&f.file, 0, SPLIT);
|
|
uint64_t back = pn_gguf_layer_range_bytes(&f.file, SPLIT, N_LAYER);
|
|
uint64_t whole = pn_gguf_layer_range_bytes(&f.file, 0, N_LAYER);
|
|
CHECK(front + back == whole, "the two die shares partition the stack");
|
|
CHECK(front > 0 && back > 0, "both shares are non-empty");
|
|
|
|
/* Same layer COUNT, different BYTES — the Q6_K back half is heavier. This
|
|
* is why an equal-count split is not automatically an equal-byte split. */
|
|
CHECK(back > front, "Q6_K back half outweighs the Q4_K front half");
|
|
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 3, 3) == 0, "empty range is 0");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 5, 2) == 0, "inverted range is 0");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, N_LAYER, N_LAYER + 4) == 0,
|
|
"range past the last layer is 0");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, -3, SPLIT) == front,
|
|
"negative lo clamps to 0 rather than swallowing non-layer tensors");
|
|
}
|
|
|
|
static void test_proration_over_projects(void)
|
|
{
|
|
printf("proration_over_projects:\n");
|
|
fake_gguf f;
|
|
build_model(&f);
|
|
|
|
/* What the gate used to compute: a layer-count share of the whole file. */
|
|
uint64_t file_bytes = 0;
|
|
for (uint64_t i = 0; i < f.file.n_tensors; i++)
|
|
file_bytes += bytes_of(f.t[i].type, f.t[i].dims[1], f.t[i].dims[0]);
|
|
|
|
uint64_t prorated_front = file_bytes * SPLIT / N_LAYER;
|
|
uint64_t real_front = pn_gguf_layer_range_bytes(&f.file, 0, SPLIT);
|
|
CHECK(prorated_front > real_front,
|
|
"the old proration charges die A for tensors it does not hold");
|
|
|
|
/* And it is not a rounding difference: the excess is the non-layer weight,
|
|
* which on this fixture is a large fraction of a die's real share. */
|
|
uint64_t excess = prorated_front - real_front;
|
|
CHECK(excess * 10 > real_front,
|
|
"the over-projection is >10% of the die's real weight, not noise");
|
|
|
|
/* The fixed accounting: die A = its layers, die B = its layers + the head. */
|
|
uint64_t head = pn_gguf_tensor_file_bytes(&f.file, "output.weight") +
|
|
pn_gguf_tensor_file_bytes(&f.file, "output_norm.weight");
|
|
uint64_t fixed_a = real_front;
|
|
uint64_t fixed_b = pn_gguf_layer_range_bytes(&f.file, SPLIT, N_LAYER) + head;
|
|
uint64_t embd = pn_gguf_tensor_file_bytes(&f.file, "token_embd.weight");
|
|
CHECK(fixed_a + fixed_b + embd == file_bytes,
|
|
"the two dies plus the never-uploaded embedding account for the file");
|
|
CHECK(fixed_a < prorated_front, "die A's projection drops");
|
|
}
|
|
|
|
static void test_name_parsing(void)
|
|
{
|
|
printf("name_parsing:\n");
|
|
fake_gguf f;
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.2.real.weight", GGUF_QUANT_Q4_K, 8, 256);
|
|
fg_add(&f, "blk.2suffix.weight", GGUF_QUANT_Q4_K, 8, 256); /* no dot */
|
|
fg_add(&f, "blkX.2.weight", GGUF_QUANT_Q4_K, 8, 256); /* not blk. */
|
|
fg_add(&f, "blk..weight", GGUF_QUANT_Q4_K, 8, 256); /* no index */
|
|
fg_add(&f, "blk.weight", GGUF_QUANT_Q4_K, 8, 256);
|
|
fg_add(&f, "ablk.2.weight", GGUF_QUANT_Q4_K, 8, 256); /* prefix only */
|
|
|
|
uint64_t one = bytes_of(GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 64) == one,
|
|
"only the real blk.<i>. tensor counts");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 2, 3) == one, "layer 2 exactly");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 2) == 0, "layers 0-1 are empty");
|
|
|
|
/* Multi-digit indices are real: a 40-layer model has blk.12 and blk.7. */
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.7.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
fg_add(&f, "blk.12.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 10) == one, "blk.12 is not in [0,10)");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 10, 20) == one, "blk.12 is in [10,20)");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 20) == 2 * one, "both in [0,20)");
|
|
}
|
|
|
|
static void test_degenerate_inputs(void)
|
|
{
|
|
printf("degenerate_inputs:\n");
|
|
CHECK(pn_gguf_layer_range_bytes(NULL, 0, 8) == 0, "NULL file -> 0");
|
|
CHECK(pn_gguf_tensor_file_bytes(NULL, "output.weight") == 0, "NULL file -> 0");
|
|
|
|
fake_gguf f;
|
|
build_model(&f);
|
|
CHECK(pn_gguf_tensor_file_bytes(&f.file, NULL) == 0, "NULL name -> 0");
|
|
CHECK(pn_gguf_tensor_file_bytes(&f.file, "no.such.tensor") == 0,
|
|
"absent tensor -> 0, and the caller reads 0 as 'unknown'");
|
|
|
|
/* A tensor with no dims, a zero dim, or too many dims is malformed; report
|
|
* 0 rather than a wrapped product. */
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.0.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
f.t[0].n_dims = 0;
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) == 0, "0 dims -> 0");
|
|
f.t[0].n_dims = 2; f.t[0].dims[0] = 0;
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) == 0, "zero dim -> 0");
|
|
|
|
/* dims[] is 4 wide. Fill all four so the only thing that can stop a 5-dim
|
|
* read is the bound itself — otherwise a zero dim short-circuits the loop
|
|
* and the guard is never exercised. */
|
|
f.t[0].dims[0] = 256; f.t[0].dims[1] = 8;
|
|
f.t[0].dims[2] = 1; f.t[0].dims[3] = 1;
|
|
f.t[0].n_dims = 4;
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) ==
|
|
bytes_of(GGUF_QUANT_Q4_K, 8, 256), "n_dims == 4 is accepted");
|
|
f.t[0].n_dims = 5;
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) == 0,
|
|
"n_dims > 4 -> 0, never a read past dims[3]");
|
|
|
|
/* An unsupported quant has no known block size; it contributes 0 rather
|
|
* than a bogus size, and does not poison its neighbours. */
|
|
/* An unrecognised quant must make the WHOLE range unmeasurable, not
|
|
* silently contribute 0. I asserted the opposite here and it was wrong:
|
|
* this sum is a safety projection, so "I cannot size this" must never read
|
|
* downstream as "it is free". BF16 shipped in the enum without a block-size
|
|
* entry and every expert tensor sized as zero, projecting 10 MiB per die
|
|
* for a model needing ~8,300. 0 makes the caller fall back to its prorated
|
|
* estimate -- wrong, but never optimistic. */
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.0.w", (pn_gguf_quant)9999, 8, 256);
|
|
fg_add(&f, "blk.1.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) == 0,
|
|
"an unsizeable quant refuses the whole range rather than costing 0");
|
|
|
|
/* BF16 specifically, because that is the one that got shipped half-added. */
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.0.w", GGUF_QUANT_BF16, 8, 256);
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, 8) == 8 * 256 * 2,
|
|
"BF16 sizes as 2 bytes per element");
|
|
}
|
|
|
|
static void test_layer_count(void)
|
|
{
|
|
printf("layer_count:\n");
|
|
fake_gguf f;
|
|
build_model(&f);
|
|
CHECK(pn_gguf_layer_count(&f.file) == N_LAYER,
|
|
"layer count comes off the tensor table, no arch prefix needed");
|
|
CHECK(pn_gguf_layer_count(NULL) == 0, "NULL file -> 0");
|
|
|
|
/* Non-layer tensors must not raise the count, and a gap must not lower it:
|
|
* the count is max index + 1, which is what a [0,n) range needs. */
|
|
fg_init(&f);
|
|
fg_add(&f, "token_embd.weight", GGUF_QUANT_Q6_K, 8, 256);
|
|
fg_add(&f, "output.weight", GGUF_QUANT_Q6_K, 8, 256);
|
|
CHECK(pn_gguf_layer_count(&f.file) == 0, "no blk.* tensors -> 0 layers");
|
|
fg_add(&f, "blk.0.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
fg_add(&f, "blk.39.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_count(&f.file) == 40, "highest index + 1");
|
|
|
|
/* A trailing MTP / NextN block is NOT a layer. Qwen3.6-35B-A3B ships
|
|
* block_count=41 with a speculative-draft head the forward never runs and
|
|
* never uploads; counting it puts weight on a die that will not hold it.
|
|
* Same rule the loader applies (qwen35moe.c). */
|
|
fg_init(&f);
|
|
for (int i = 0; i < 41; i++) {
|
|
char n[64];
|
|
snprintf(n, sizeof(n), "blk.%d.w", i);
|
|
fg_add(&f, n, GGUF_QUANT_Q4_K, 8, 256);
|
|
}
|
|
CHECK(pn_gguf_layer_count(&f.file) == 41, "41 plain blocks are 41 layers");
|
|
fg_add(&f, "blk.40.nextn.eh_proj.weight", GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_count(&f.file) == 40,
|
|
"the trailing nextn block is dropped");
|
|
CHECK(pn_gguf_layer_range_bytes(&f.file, 0, pn_gguf_layer_count(&f.file)) ==
|
|
40 * bytes_of(GGUF_QUANT_Q4_K, 8, 256),
|
|
"and a [0,count) range stops before it");
|
|
|
|
/* Only the LAST block, and only for nextn: an eh_proj in the middle is a
|
|
* real layer's tensor, not a head to drop. */
|
|
fg_init(&f);
|
|
for (int i = 0; i < 41; i++) {
|
|
char n[64];
|
|
snprintf(n, sizeof(n), "blk.%d.w", i);
|
|
fg_add(&f, n, GGUF_QUANT_Q4_K, 8, 256);
|
|
}
|
|
fg_add(&f, "blk.7.nextn.eh_proj.weight", GGUF_QUANT_Q4_K, 8, 256);
|
|
CHECK(pn_gguf_layer_count(&f.file) == 41,
|
|
"a nextn tensor on a middle block drops nothing");
|
|
}
|
|
|
|
static void test_tied_head_sizing(void)
|
|
{
|
|
printf("tied_head_sizing:\n");
|
|
/* A TIED model carries no output.weight — the LM head is token_embd
|
|
* materialized to F32 at load. Sizing it from the packed tensor would
|
|
* under-project the die that holds it by ~4x, so the numel is the number a
|
|
* caller needs. */
|
|
fake_gguf f;
|
|
fg_init(&f);
|
|
fg_add(&f, "blk.0.w", GGUF_QUANT_Q4_K, 8, 256);
|
|
fg_add(&f, "token_embd.weight", GGUF_QUANT_Q6_K, 1024, 256);
|
|
|
|
CHECK(pn_gguf_tensor_file_bytes(&f.file, "output.weight") == 0,
|
|
"a tied model really has no output.weight");
|
|
uint64_t numel = pn_gguf_tensor_numel(&f.file, "token_embd.weight");
|
|
CHECK(numel == 1024 * 256, "numel is the element count, not bytes");
|
|
|
|
uint64_t packed = pn_gguf_tensor_file_bytes(&f.file, "token_embd.weight");
|
|
uint64_t f32 = numel * sizeof(float);
|
|
CHECK(f32 > packed * 4, "the F32 head is >4x the packed embedding");
|
|
|
|
CHECK(pn_gguf_tensor_numel(&f.file, "no.such.tensor") == 0, "absent -> 0");
|
|
CHECK(pn_gguf_tensor_numel(NULL, "token_embd.weight") == 0, "NULL -> 0");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("test_gguf_layer_bytes\n");
|
|
test_excludes_non_layer_tensors();
|
|
test_ranges_partition();
|
|
test_proration_over_projects();
|
|
test_name_parsing();
|
|
test_degenerate_inputs();
|
|
test_layer_count();
|
|
test_tied_head_sizing();
|
|
|
|
if (g_failed) {
|
|
printf("FAILED: %d check(s)\n", g_failed);
|
|
return 1;
|
|
}
|
|
printf("ALL PASS\n");
|
|
return 0;
|
|
}
|