392 lines
12 KiB
C
392 lines
12 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
#include "export.h"
|
|
#include "quantization.h"
|
|
#include "../safetensors/safetensors.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
/* ============================================================= */
|
|
/* Internal state */
|
|
/* ============================================================= */
|
|
|
|
struct fq_export_state {
|
|
fs_file* input;
|
|
fs_writer* writer;
|
|
fq_export_config config;
|
|
|
|
const char** tensor_names;
|
|
int num_tensors;
|
|
int current;
|
|
|
|
int quantized_count;
|
|
int passthrough_count;
|
|
};
|
|
|
|
/* ============================================================= */
|
|
/* BF16/F16 -> F32 conversion (self-contained) */
|
|
/* ============================================================= */
|
|
|
|
static void bf16_to_f32(const uint16_t* src, float* dst, int64_t n) {
|
|
for (int64_t i = 0; i < n; i++) {
|
|
uint32_t bits = (uint32_t)src[i] << 16;
|
|
memcpy(&dst[i], &bits, 4);
|
|
}
|
|
}
|
|
|
|
static void f16_to_f32(const uint16_t* src, float* dst, int64_t n) {
|
|
for (int64_t i = 0; i < n; i++) {
|
|
uint16_t h = src[i];
|
|
uint32_t sign = (uint32_t)(h >> 15) << 31;
|
|
uint32_t exp = (h >> 10) & 0x1F;
|
|
uint32_t mant = h & 0x3FF;
|
|
uint32_t bits;
|
|
if (exp == 0) {
|
|
if (mant == 0) {
|
|
bits = sign;
|
|
} else {
|
|
exp = 1;
|
|
while (!(mant & 0x400)) { mant <<= 1; exp--; }
|
|
mant &= 0x3FF;
|
|
bits = sign | ((uint32_t)(exp + 127 - 15) << 23) | ((uint32_t)mant << 13);
|
|
}
|
|
} else if (exp == 31) {
|
|
bits = sign | 0x7F800000 | ((uint32_t)mant << 13);
|
|
} else {
|
|
bits = sign | ((uint32_t)(exp + 127 - 15) << 23) | ((uint32_t)mant << 13);
|
|
}
|
|
memcpy(&dst[i], &bits, 4);
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Tensor skip logic */
|
|
/* ============================================================= */
|
|
|
|
static bool str_contains(const char* haystack, const char* needle) {
|
|
return strstr(haystack, needle) != NULL;
|
|
}
|
|
|
|
static bool str_ends_with(const char* s, const char* suffix) {
|
|
size_t slen = strlen(s);
|
|
size_t sufflen = strlen(suffix);
|
|
if (sufflen > slen) return false;
|
|
return strcmp(s + slen - sufflen, suffix) == 0;
|
|
}
|
|
|
|
static bool should_quantize(const char* name, fs_tensor_info* info, fq_export_config* cfg) {
|
|
/* Only quantize float types */
|
|
if (info->dtype != FS_DTYPE_F32 && info->dtype != FS_DTYPE_F16 &&
|
|
info->dtype != FS_DTYPE_BF16) {
|
|
return false;
|
|
}
|
|
|
|
/* Compute numel */
|
|
int64_t numel = 1;
|
|
for (int i = 0; i < info->ndim; i++) numel *= info->shape[i];
|
|
|
|
/* Skip small tensors */
|
|
if (numel < cfg->min_elements) return false;
|
|
|
|
/* Skip 1D tensors */
|
|
if (cfg->skip_1d && info->ndim <= 1) return false;
|
|
|
|
/* Skip embeddings */
|
|
if (cfg->skip_embeddings) {
|
|
if (str_contains(name, "embed") || str_contains(name, "wte") ||
|
|
str_contains(name, "wpe")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Skip norm layers */
|
|
if (cfg->skip_norms) {
|
|
if (str_contains(name, "norm") || str_contains(name, "ln_") ||
|
|
str_contains(name, "layernorm") || str_contains(name, "rmsnorm")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Skip head */
|
|
if (cfg->skip_head) {
|
|
if (str_contains(name, "lm_head") || str_contains(name, "head.weight") ||
|
|
str_contains(name, "output.weight")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Skip biases */
|
|
if (cfg->skip_biases) {
|
|
if (str_ends_with(name, ".bias")) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Defaults */
|
|
/* ============================================================= */
|
|
|
|
fq_export_config fq_export_config_defaults(void) {
|
|
fq_export_config cfg = {0};
|
|
cfg.target_dtype = FQ_INT8;
|
|
cfg.granularity = FQ_PER_TENSOR;
|
|
cfg.group_size = 128;
|
|
cfg.ternary_threshold = 0.0f; /* auto */
|
|
cfg.skip_embeddings = true;
|
|
cfg.skip_norms = true;
|
|
cfg.skip_head = false;
|
|
cfg.skip_biases = true;
|
|
cfg.skip_1d = true;
|
|
cfg.min_elements = 1024;
|
|
cfg.progress_cb = NULL;
|
|
cfg.userdata = NULL;
|
|
return cfg;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Internal: process one tensor */
|
|
/* ============================================================= */
|
|
|
|
static bool process_tensor(fq_export_state* s, int idx) {
|
|
const char* name = s->tensor_names[idx];
|
|
fs_tensor_info* info = fs_get_info(s->input, name);
|
|
if (!info) return false;
|
|
|
|
if (!should_quantize(name, info, &s->config)) {
|
|
/* Passthrough: copy raw bytes with original dtype */
|
|
const void* raw = fs_get_data(s->input, name);
|
|
if (raw) {
|
|
fs_writer_add_tensor(s->writer, name, info->dtype,
|
|
info->shape, info->ndim, raw);
|
|
}
|
|
s->passthrough_count++;
|
|
fs_free_info(info);
|
|
return true;
|
|
}
|
|
|
|
/* Compute numel */
|
|
int64_t numel = 1;
|
|
for (int i = 0; i < info->ndim; i++) numel *= info->shape[i];
|
|
|
|
/* Get data as F32 */
|
|
float* f32_data = NULL;
|
|
bool owns_f32 = false;
|
|
|
|
if (info->dtype == FS_DTYPE_F32) {
|
|
f32_data = (float*)fs_get_data(s->input, name);
|
|
} else if (info->dtype == FS_DTYPE_BF16) {
|
|
f32_data = malloc(numel * sizeof(float));
|
|
if (!f32_data) { fs_free_info(info); return false; }
|
|
bf16_to_f32((const uint16_t*)fs_get_data(s->input, name), f32_data, numel);
|
|
owns_f32 = true;
|
|
} else if (info->dtype == FS_DTYPE_F16) {
|
|
f32_data = malloc(numel * sizeof(float));
|
|
if (!f32_data) { fs_free_info(info); return false; }
|
|
f16_to_f32((const uint16_t*)fs_get_data(s->input, name), f32_data, numel);
|
|
owns_f32 = true;
|
|
}
|
|
|
|
if (!f32_data) {
|
|
fs_free_info(info);
|
|
return false;
|
|
}
|
|
|
|
/* Quantize */
|
|
fq_tensor* qt = NULL;
|
|
|
|
if (s->config.target_dtype == FQ_TERNARY) {
|
|
if (s->config.ternary_threshold > 0.0f) {
|
|
qt = fq_quantize_ternary_threshold(f32_data, info->shape, info->ndim,
|
|
s->config.ternary_threshold);
|
|
} else if (s->config.granularity == FQ_PER_CHANNEL) {
|
|
qt = fq_quantize_ternary_per_channel(f32_data, info->shape, info->ndim, 0);
|
|
} else {
|
|
qt = fq_quantize_ternary(f32_data, info->shape, info->ndim);
|
|
}
|
|
} else {
|
|
qt = fq_quantize(f32_data, info->shape, info->ndim,
|
|
s->config.target_dtype, FQ_SYMMETRIC,
|
|
s->config.granularity, 0, s->config.group_size);
|
|
}
|
|
|
|
if (owns_f32) free(f32_data);
|
|
|
|
if (!qt) {
|
|
fs_free_info(info);
|
|
return false;
|
|
}
|
|
|
|
/* Write quantized data tensor */
|
|
fs_dtype out_dtype;
|
|
size_t data_bytes;
|
|
|
|
if (qt->dtype == FQ_INT8) {
|
|
out_dtype = FS_DTYPE_I8;
|
|
data_bytes = numel;
|
|
} else if (qt->dtype == FQ_INT4 || qt->dtype == FQ_UINT4) {
|
|
out_dtype = FS_DTYPE_U8;
|
|
data_bytes = (numel + 1) / 2;
|
|
} else { /* FQ_TERNARY */
|
|
out_dtype = FS_DTYPE_U8;
|
|
data_bytes = (numel + 3) / 4;
|
|
}
|
|
|
|
/* Data tensor: shape reflects the packed byte count for INT4/ternary */
|
|
int64_t packed_shape[] = {(int64_t)data_bytes};
|
|
if (qt->dtype == FQ_INT8) {
|
|
fs_writer_add_tensor(s->writer, name, out_dtype,
|
|
info->shape, info->ndim, qt->data);
|
|
} else {
|
|
fs_writer_add_tensor(s->writer, name, out_dtype,
|
|
packed_shape, 1, qt->data);
|
|
}
|
|
|
|
/* Scale tensor */
|
|
char scale_name[512];
|
|
snprintf(scale_name, sizeof(scale_name), "%s.__scale", name);
|
|
int64_t scale_shape[] = {qt->num_scales};
|
|
fs_writer_add_tensor(s->writer, scale_name, FS_DTYPE_F32,
|
|
scale_shape, 1, qt->scales);
|
|
|
|
/* Shape tensor (original dimensions, needed for packed formats) */
|
|
char shape_name[512];
|
|
snprintf(shape_name, sizeof(shape_name), "%s.__shape", name);
|
|
fs_writer_add_tensor(s->writer, shape_name, FS_DTYPE_I64,
|
|
(int64_t[]){info->ndim}, 1, info->shape);
|
|
|
|
/* Zero point tensor (only for asymmetric) */
|
|
if (qt->mode == FQ_ASYMMETRIC && qt->zero_points) {
|
|
char zp_name[512];
|
|
snprintf(zp_name, sizeof(zp_name), "%s.__zp", name);
|
|
int64_t zp_shape[] = {qt->num_scales};
|
|
fs_writer_add_tensor(s->writer, zp_name, FS_DTYPE_I32,
|
|
zp_shape, 1, qt->zero_points);
|
|
}
|
|
|
|
s->quantized_count++;
|
|
fq_free(qt);
|
|
fs_free_info(info);
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Step-by-step API */
|
|
/* ============================================================= */
|
|
|
|
fq_export_state* fq_export_begin(const char* input, fq_export_config* cfg) {
|
|
if (!input || !cfg) return NULL;
|
|
|
|
fs_file* f = fs_open(input);
|
|
if (!f) return NULL;
|
|
|
|
fq_export_state* s = calloc(1, sizeof(fq_export_state));
|
|
if (!s) { fs_close(f); return NULL; }
|
|
|
|
s->input = f;
|
|
s->config = *cfg;
|
|
|
|
s->tensor_names = fs_tensor_names(f, &s->num_tensors);
|
|
if (!s->tensor_names) {
|
|
fs_close(f);
|
|
free(s);
|
|
return NULL;
|
|
}
|
|
|
|
s->writer = fs_writer_new();
|
|
if (!s->writer) {
|
|
free((void*)s->tensor_names);
|
|
fs_close(f);
|
|
free(s);
|
|
return NULL;
|
|
}
|
|
|
|
/* Add quantization metadata */
|
|
const char* dtype_str = fq_dtype_name(cfg->target_dtype);
|
|
fs_writer_add_metadata(s->writer, "quantization.dtype", dtype_str);
|
|
|
|
const char* gran_str = "per_tensor";
|
|
if (cfg->granularity == FQ_PER_CHANNEL) gran_str = "per_channel";
|
|
else if (cfg->granularity == FQ_PER_GROUP) gran_str = "per_group";
|
|
fs_writer_add_metadata(s->writer, "quantization.granularity", gran_str);
|
|
|
|
char gs_buf[32];
|
|
snprintf(gs_buf, sizeof(gs_buf), "%d", cfg->group_size);
|
|
fs_writer_add_metadata(s->writer, "quantization.group_size", gs_buf);
|
|
|
|
fs_writer_add_metadata(s->writer, "quantization.format_version", "1");
|
|
|
|
s->current = 0;
|
|
return s;
|
|
}
|
|
|
|
int fq_export_tensor_count(fq_export_state* s) {
|
|
return s ? s->num_tensors : 0;
|
|
}
|
|
|
|
bool fq_export_step(fq_export_state* s) {
|
|
if (!s || s->current >= s->num_tensors) return false;
|
|
|
|
process_tensor(s, s->current);
|
|
|
|
if (s->config.progress_cb) {
|
|
s->config.progress_cb(s->current + 1, s->num_tensors,
|
|
s->tensor_names[s->current], s->config.userdata);
|
|
}
|
|
|
|
s->current++;
|
|
return s->current < s->num_tensors;
|
|
}
|
|
|
|
float fq_export_progress(fq_export_state* s) {
|
|
if (!s || s->num_tensors == 0) return 0.0f;
|
|
return (float)s->current / s->num_tensors;
|
|
}
|
|
|
|
const char* fq_export_current_tensor(fq_export_state* s) {
|
|
if (!s || s->current >= s->num_tensors) return NULL;
|
|
return s->tensor_names[s->current];
|
|
}
|
|
|
|
bool fq_export_finish(fq_export_state* s, const char* output) {
|
|
if (!s || !output) return false;
|
|
|
|
bool ok = fs_writer_save(s->writer, output);
|
|
|
|
fs_writer_free(s->writer);
|
|
free((void*)s->tensor_names);
|
|
fs_close(s->input);
|
|
free(s);
|
|
|
|
return ok;
|
|
}
|
|
|
|
void fq_export_abort(fq_export_state* s) {
|
|
if (!s) return;
|
|
|
|
fs_writer_free(s->writer);
|
|
free((void*)s->tensor_names);
|
|
fs_close(s->input);
|
|
free(s);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* One-shot API */
|
|
/* ============================================================= */
|
|
|
|
bool fq_export_quantized(const char* input, const char* output, fq_export_config* cfg) {
|
|
if (!input || !output || !cfg) return false;
|
|
|
|
fq_export_state* s = fq_export_begin(input, cfg);
|
|
if (!s) return false;
|
|
|
|
while (fq_export_step(s)) {
|
|
/* process all tensors */
|
|
}
|
|
|
|
return fq_export_finish(s, output);
|
|
}
|