1909 lines
69 KiB
C
1909 lines
69 KiB
C
/*
|
|
* DeepSeek V4 Flash/Pro HF -> GGUF quantizer.
|
|
*
|
|
* This is a plain C, model-specific version of the DS4 quantization pipeline.
|
|
* It deliberately keeps only the pieces needed by the DeepSeek V4 Flash and
|
|
* Pro GGUF recipes used by this repository:
|
|
*
|
|
* - safetensors index/header loading;
|
|
* - FP8 E4M3 + E8M0 dequantization for dense tensors;
|
|
* - packed FP4 + E8M0 dequantization for routed experts;
|
|
* - local Q8_0, Q4_K, Q2_K, and IQ2_XXS quantization;
|
|
* - GGUF metadata/tensor-order reuse from an existing template GGUF.
|
|
*
|
|
* The optional imatrix is the legacy llama.cpp binary .dat format emitted by
|
|
* ds4's collector. DS4 stores one packed vector per routed tensor, laid out as
|
|
* n_experts consecutive per-expert importance vectors. When no external
|
|
* imatrix is supplied and IQ2_XXS requires one, this tool falls back to the
|
|
* same synthetic weight-energy heuristic used by the old generator:
|
|
* each column importance is sum(row[column]^2) over the dequantized weight.
|
|
*/
|
|
|
|
#define _DARWIN_C_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "quants.h"
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <pthread.h>
|
|
|
|
#if defined(_WIN32)
|
|
#error "deepseek4-quantize.c currently targets POSIX systems"
|
|
#endif
|
|
|
|
#define DS4_KV_QUANTIZE_IMATRIX_FILE "quantize.imatrix.file"
|
|
#define DS4_KV_QUANTIZE_IMATRIX_DATASET "quantize.imatrix.dataset"
|
|
#define DS4_KV_QUANTIZE_IMATRIX_N_ENTRIES "quantize.imatrix.entries_count"
|
|
#define DS4_KV_QUANTIZE_IMATRIX_N_CHUNKS "quantize.imatrix.chunks_count"
|
|
#define DS4_GGUF_DEFAULT_ALIGNMENT 32
|
|
|
|
typedef enum {
|
|
GGUF_TYPE_UINT8 = 0,
|
|
GGUF_TYPE_INT8 = 1,
|
|
GGUF_TYPE_UINT16 = 2,
|
|
GGUF_TYPE_INT16 = 3,
|
|
GGUF_TYPE_UINT32 = 4,
|
|
GGUF_TYPE_INT32 = 5,
|
|
GGUF_TYPE_FLOAT32 = 6,
|
|
GGUF_TYPE_BOOL = 7,
|
|
GGUF_TYPE_STRING = 8,
|
|
GGUF_TYPE_ARRAY = 9,
|
|
GGUF_TYPE_UINT64 = 10,
|
|
GGUF_TYPE_INT64 = 11,
|
|
GGUF_TYPE_FLOAT64 = 12,
|
|
} gguf_value_type;
|
|
|
|
static void die(const char *msg) {
|
|
fprintf(stderr, "error: %s\n", msg);
|
|
exit(1);
|
|
}
|
|
|
|
static void die_errno(const char *what, const char *path) {
|
|
fprintf(stderr, "error: %s %s: %s\n", what, path ? path : "", strerror(errno));
|
|
exit(1);
|
|
}
|
|
|
|
static void *xmalloc(size_t n) {
|
|
void *p = malloc(n ? n : 1);
|
|
if (!p) die("out of memory");
|
|
return p;
|
|
}
|
|
|
|
static void *xcalloc(size_t n, size_t sz) {
|
|
void *p = calloc(n ? n : 1, sz ? sz : 1);
|
|
if (!p) die("out of memory");
|
|
return p;
|
|
}
|
|
|
|
static void *xrealloc(void *p, size_t n) {
|
|
void *q = realloc(p, n ? n : 1);
|
|
if (!q) die("out of memory");
|
|
return q;
|
|
}
|
|
|
|
static char *xstrdup(const char *s) {
|
|
size_t n = strlen(s);
|
|
char *p = xmalloc(n + 1);
|
|
memcpy(p, s, n + 1);
|
|
return p;
|
|
}
|
|
|
|
static char *xstrndup(const char *s, size_t n) {
|
|
char *p = xmalloc(n + 1);
|
|
memcpy(p, s, n);
|
|
p[n] = '\0';
|
|
return p;
|
|
}
|
|
|
|
static char *path_join(const char *a, const char *b) {
|
|
const size_t na = strlen(a);
|
|
const size_t nb = strlen(b);
|
|
const bool slash = na && a[na - 1] == '/';
|
|
char *out = xmalloc(na + (slash ? 0 : 1) + nb + 1);
|
|
memcpy(out, a, na);
|
|
size_t pos = na;
|
|
if (!slash) out[pos++] = '/';
|
|
memcpy(out + pos, b, nb + 1);
|
|
return out;
|
|
}
|
|
|
|
static bool str_starts(const char *s, const char *prefix) {
|
|
return strncmp(s, prefix, strlen(prefix)) == 0;
|
|
}
|
|
|
|
static bool str_ends(const char *s, const char *suffix) {
|
|
const size_t ns = strlen(s);
|
|
const size_t nf = strlen(suffix);
|
|
return ns >= nf && memcmp(s + ns - nf, suffix, nf) == 0;
|
|
}
|
|
|
|
static char *read_file(const char *path, size_t *len_out) {
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) die_errno("open", path);
|
|
if (fseeko(fp, 0, SEEK_END) != 0) die_errno("seek", path);
|
|
off_t n = ftello(fp);
|
|
if (n < 0) die_errno("tell", path);
|
|
if (fseeko(fp, 0, SEEK_SET) != 0) die_errno("seek", path);
|
|
char *buf = xmalloc((size_t)n + 1);
|
|
if (n && fread(buf, 1, (size_t)n, fp) != (size_t)n) die_errno("read", path);
|
|
buf[n] = '\0';
|
|
fclose(fp);
|
|
if (len_out) *len_out = (size_t)n;
|
|
return buf;
|
|
}
|
|
|
|
static uint64_t read_u64_le_fp(FILE *fp, const char *what) {
|
|
uint8_t b[8];
|
|
if (fread(b, 1, sizeof(b), fp) != sizeof(b)) {
|
|
fprintf(stderr, "error: short read while reading %s\n", what);
|
|
exit(1);
|
|
}
|
|
uint64_t v = 0;
|
|
for (int i = 0; i < 8; i++) v |= (uint64_t)b[i] << (8 * i);
|
|
return v;
|
|
}
|
|
|
|
static uint32_t read_u32_le_fp(FILE *fp, const char *what) {
|
|
uint32_t v;
|
|
if (fread(&v, 1, sizeof(v), fp) != sizeof(v)) {
|
|
fprintf(stderr, "error: short read while reading %s\n", what);
|
|
exit(1);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static int32_t read_i32_fp(FILE *fp, const char *what) {
|
|
int32_t v;
|
|
if (fread(&v, 1, sizeof(v), fp) != sizeof(v)) {
|
|
fprintf(stderr, "error: short read while reading %s\n", what);
|
|
exit(1);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static uint16_t load_u16_le(const uint8_t *p) {
|
|
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
|
}
|
|
|
|
static int64_t load_i64_le(const uint8_t *p) {
|
|
uint64_t v = 0;
|
|
for (int i = 0; i < 8; i++) v |= (uint64_t)p[i] << (8 * i);
|
|
return (int64_t)v;
|
|
}
|
|
|
|
/* =====
|
|
* Minimal JSON tokenizer
|
|
*
|
|
* Safetensors uses ordinary JSON for the model index and per-shard headers.
|
|
* We only need objects, arrays, strings, and primitive numbers; escaped tensor
|
|
* names do not occur in the files produced by Hugging Face, so strings are
|
|
* copied as raw UTF-8 slices after locating the closing quote.
|
|
*/
|
|
|
|
typedef enum {
|
|
JT_OBJECT,
|
|
JT_ARRAY,
|
|
JT_STRING,
|
|
JT_PRIMITIVE,
|
|
} json_type;
|
|
|
|
typedef struct {
|
|
json_type type;
|
|
int start;
|
|
int end;
|
|
int parent;
|
|
int size;
|
|
} json_tok;
|
|
|
|
typedef struct {
|
|
json_tok *v;
|
|
int len;
|
|
int cap;
|
|
const char *js;
|
|
int js_len;
|
|
} json_doc;
|
|
|
|
static int json_add(json_doc *d, json_type type, int start, int end, int parent) {
|
|
if (d->len == d->cap) {
|
|
d->cap = d->cap ? d->cap * 2 : 4096;
|
|
d->v = xrealloc(d->v, (size_t)d->cap * sizeof(d->v[0]));
|
|
}
|
|
int id = d->len++;
|
|
d->v[id] = (json_tok){ .type = type, .start = start, .end = end, .parent = parent, .size = 0 };
|
|
if (parent >= 0) d->v[parent].size++;
|
|
return id;
|
|
}
|
|
|
|
static json_doc json_parse_text(const char *js, size_t len) {
|
|
json_doc d = { .js = js, .js_len = (int)len };
|
|
int parent = -1;
|
|
for (int i = 0; i < (int)len; i++) {
|
|
unsigned char c = (unsigned char)js[i];
|
|
if (isspace(c) || c == ':' || c == ',') continue;
|
|
if (c == '{' || c == '[') {
|
|
parent = json_add(&d, c == '{' ? JT_OBJECT : JT_ARRAY, i, -1, parent);
|
|
continue;
|
|
}
|
|
if (c == '}' || c == ']') {
|
|
if (parent < 0) die("bad JSON: unmatched close");
|
|
d.v[parent].end = i + 1;
|
|
parent = d.v[parent].parent;
|
|
continue;
|
|
}
|
|
if (c == '"') {
|
|
int start = i + 1;
|
|
i++;
|
|
bool esc = false;
|
|
for (; i < (int)len; i++) {
|
|
if (esc) {
|
|
esc = false;
|
|
} else if (js[i] == '\\') {
|
|
esc = true;
|
|
} else if (js[i] == '"') {
|
|
break;
|
|
}
|
|
}
|
|
if (i >= (int)len) die("bad JSON: unterminated string");
|
|
json_add(&d, JT_STRING, start, i, parent);
|
|
continue;
|
|
}
|
|
int start = i;
|
|
while (i < (int)len && !isspace((unsigned char)js[i]) &&
|
|
js[i] != ',' && js[i] != ']' && js[i] != '}') {
|
|
i++;
|
|
}
|
|
json_add(&d, JT_PRIMITIVE, start, i, parent);
|
|
i--;
|
|
}
|
|
if (parent != -1) die("bad JSON: unterminated object/array");
|
|
return d;
|
|
}
|
|
|
|
static void json_free(json_doc *d) {
|
|
free(d->v);
|
|
memset(d, 0, sizeof(*d));
|
|
}
|
|
|
|
static bool json_tok_eq(const json_doc *d, int tok, const char *s) {
|
|
const json_tok *t = &d->v[tok];
|
|
const int n = t->end - t->start;
|
|
return t->type == JT_STRING && (int)strlen(s) == n && memcmp(d->js + t->start, s, (size_t)n) == 0;
|
|
}
|
|
|
|
static char *json_strdup_tok(const json_doc *d, int tok) {
|
|
const json_tok *t = &d->v[tok];
|
|
return xstrndup(d->js + t->start, (size_t)(t->end - t->start));
|
|
}
|
|
|
|
static bool json_is_descendant(const json_doc *d, int tok, int parent) {
|
|
for (int p = d->v[tok].parent; p >= 0; p = d->v[p].parent) {
|
|
if (p == parent) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int json_skip(const json_doc *d, int tok) {
|
|
int i = tok + 1;
|
|
while (i < d->len && json_is_descendant(d, i, tok)) i++;
|
|
return i;
|
|
}
|
|
|
|
static int json_obj_get(const json_doc *d, int obj, const char *key) {
|
|
if (obj < 0 || d->v[obj].type != JT_OBJECT) return -1;
|
|
for (int i = obj + 1; i < d->len && d->v[i].parent == obj;) {
|
|
int k = i;
|
|
int v = i + 1;
|
|
if (v >= d->len || d->v[v].parent != obj) return -1;
|
|
if (json_tok_eq(d, k, key)) return v;
|
|
i = json_skip(d, v);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int64_t json_i64(const json_doc *d, int tok) {
|
|
char tmp[64];
|
|
const int n = d->v[tok].end - d->v[tok].start;
|
|
if (n <= 0 || n >= (int)sizeof(tmp)) die("bad JSON integer");
|
|
memcpy(tmp, d->js + d->v[tok].start, (size_t)n);
|
|
tmp[n] = '\0';
|
|
return strtoll(tmp, NULL, 10);
|
|
}
|
|
|
|
/* =====
|
|
* Small string hash map
|
|
*/
|
|
|
|
typedef struct {
|
|
char *key;
|
|
int value;
|
|
} hslot;
|
|
|
|
typedef struct {
|
|
hslot *slots;
|
|
int cap;
|
|
} hmap;
|
|
|
|
static uint64_t fnv1a_str(const char *s) {
|
|
uint64_t h = 1469598103934665603ull;
|
|
while (*s) {
|
|
h ^= (uint8_t)*s++;
|
|
h *= 1099511628211ull;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static void hmap_build(hmap *m, char **keys, int n) {
|
|
int cap = 1;
|
|
while (cap < n * 3) cap <<= 1;
|
|
m->cap = cap ? cap : 2;
|
|
m->slots = xcalloc((size_t)m->cap, sizeof(m->slots[0]));
|
|
for (int i = 0; i < n; i++) {
|
|
uint64_t h = fnv1a_str(keys[i]);
|
|
int p = (int)(h & (uint64_t)(m->cap - 1));
|
|
while (m->slots[p].key) p = (p + 1) & (m->cap - 1);
|
|
m->slots[p].key = keys[i];
|
|
m->slots[p].value = i;
|
|
}
|
|
}
|
|
|
|
static int hmap_get(const hmap *m, const char *key) {
|
|
if (!m->slots) return -1;
|
|
uint64_t h = fnv1a_str(key);
|
|
int p = (int)(h & (uint64_t)(m->cap - 1));
|
|
while (m->slots[p].key) {
|
|
if (strcmp(m->slots[p].key, key) == 0) return m->slots[p].value;
|
|
p = (p + 1) & (m->cap - 1);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void hmap_free(hmap *m) {
|
|
free(m->slots);
|
|
memset(m, 0, sizeof(*m));
|
|
}
|
|
|
|
/* =====
|
|
* safetensors database
|
|
*/
|
|
|
|
#define MAX_DIMS 8
|
|
|
|
typedef struct {
|
|
char *dtype;
|
|
int n_dims;
|
|
int64_t shape[MAX_DIMS];
|
|
uint64_t begin;
|
|
uint64_t end;
|
|
} st_info;
|
|
|
|
typedef struct {
|
|
char *name;
|
|
char *file;
|
|
} weight_map_entry;
|
|
|
|
typedef struct {
|
|
char *name;
|
|
st_info info;
|
|
} tensor_entry;
|
|
|
|
typedef struct {
|
|
char *file;
|
|
char *path;
|
|
uint64_t data_base;
|
|
tensor_entry *tensors;
|
|
int n_tensors;
|
|
int cap_tensors;
|
|
hmap tensor_map;
|
|
FILE *fp;
|
|
pthread_mutex_t lock;
|
|
bool loaded;
|
|
} shard;
|
|
|
|
typedef struct {
|
|
char *hf_dir;
|
|
weight_map_entry *weights;
|
|
int n_weights;
|
|
hmap weight_map;
|
|
shard *shards;
|
|
int n_shards;
|
|
int cap_shards;
|
|
pthread_mutex_t lock;
|
|
} st_db;
|
|
|
|
typedef struct {
|
|
char *dtype;
|
|
int n_dims;
|
|
int64_t shape[MAX_DIMS];
|
|
uint8_t *data;
|
|
size_t nbytes;
|
|
} st_value;
|
|
|
|
static void st_value_free(st_value *v) {
|
|
free(v->dtype);
|
|
free(v->data);
|
|
memset(v, 0, sizeof(*v));
|
|
}
|
|
|
|
static void parse_shape(const json_doc *d, int arr_tok, st_info *info, const char *name) {
|
|
if (d->v[arr_tok].type != JT_ARRAY) {
|
|
fprintf(stderr, "error: bad shape for %s\n", name);
|
|
exit(1);
|
|
}
|
|
int nd = 0;
|
|
for (int i = arr_tok + 1; i < d->len && d->v[i].parent == arr_tok; i = json_skip(d, i)) {
|
|
if (nd >= MAX_DIMS) die("too many safetensors dimensions");
|
|
info->shape[nd++] = json_i64(d, i);
|
|
}
|
|
info->n_dims = nd;
|
|
}
|
|
|
|
static int db_find_shard(st_db *db, const char *file) {
|
|
for (int i = 0; i < db->n_shards; i++) {
|
|
if (strcmp(db->shards[i].file, file) == 0) return i;
|
|
}
|
|
if (db->n_shards == db->cap_shards) {
|
|
db->cap_shards = db->cap_shards ? db->cap_shards * 2 : 32;
|
|
db->shards = xrealloc(db->shards, (size_t)db->cap_shards * sizeof(db->shards[0]));
|
|
}
|
|
shard *s = &db->shards[db->n_shards];
|
|
memset(s, 0, sizeof(*s));
|
|
s->file = xstrdup(file);
|
|
s->path = path_join(db->hf_dir, file);
|
|
pthread_mutex_init(&s->lock, NULL);
|
|
return db->n_shards++;
|
|
}
|
|
|
|
static void shard_add_tensor(shard *s, char *name, st_info info) {
|
|
if (s->n_tensors == s->cap_tensors) {
|
|
s->cap_tensors = s->cap_tensors ? s->cap_tensors * 2 : 256;
|
|
s->tensors = xrealloc(s->tensors, (size_t)s->cap_tensors * sizeof(s->tensors[0]));
|
|
}
|
|
s->tensors[s->n_tensors++] = (tensor_entry){ .name = name, .info = info };
|
|
}
|
|
|
|
static void shard_load(shard *s) {
|
|
if (s->loaded) return;
|
|
FILE *fp = fopen(s->path, "rb");
|
|
if (!fp) die_errno("open", s->path);
|
|
uint64_t header_len = read_u64_le_fp(fp, "safetensors header length");
|
|
char *header = xmalloc((size_t)header_len + 1);
|
|
if (fread(header, 1, (size_t)header_len, fp) != (size_t)header_len) die_errno("read header", s->path);
|
|
header[header_len] = '\0';
|
|
s->data_base = 8 + header_len;
|
|
|
|
json_doc d = json_parse_text(header, (size_t)header_len);
|
|
if (d.len < 1 || d.v[0].type != JT_OBJECT) die("bad safetensors header");
|
|
for (int i = 1; i < d.len && d.v[i].parent == 0;) {
|
|
int k = i;
|
|
int v = i + 1;
|
|
if (v >= d.len || d.v[v].parent != 0) die("bad safetensors header object");
|
|
if (!json_tok_eq(&d, k, "__metadata__")) {
|
|
char *name = json_strdup_tok(&d, k);
|
|
st_info info = {0};
|
|
int dtype = json_obj_get(&d, v, "dtype");
|
|
int shape = json_obj_get(&d, v, "shape");
|
|
int offsets = json_obj_get(&d, v, "data_offsets");
|
|
if (dtype < 0 || shape < 0 || offsets < 0) die("bad safetensors tensor entry");
|
|
info.dtype = json_strdup_tok(&d, dtype);
|
|
parse_shape(&d, shape, &info, name);
|
|
int n_off = 0;
|
|
for (int j = offsets + 1; j < d.len && d.v[j].parent == offsets; j = json_skip(&d, j)) {
|
|
int64_t x = json_i64(&d, j);
|
|
if (n_off == 0) info.begin = (uint64_t)x;
|
|
else if (n_off == 1) info.end = (uint64_t)x;
|
|
n_off++;
|
|
}
|
|
if (n_off != 2) die("bad safetensors data_offsets");
|
|
shard_add_tensor(s, name, info);
|
|
}
|
|
i = json_skip(&d, v);
|
|
}
|
|
char **keys = xmalloc((size_t)s->n_tensors * sizeof(keys[0]));
|
|
for (int i = 0; i < s->n_tensors; i++) keys[i] = s->tensors[i].name;
|
|
hmap_build(&s->tensor_map, keys, s->n_tensors);
|
|
free(keys);
|
|
json_free(&d);
|
|
free(header);
|
|
s->fp = fp;
|
|
s->loaded = true;
|
|
}
|
|
|
|
static void db_open(st_db *db, const char *hf_dir) {
|
|
memset(db, 0, sizeof(*db));
|
|
pthread_mutex_init(&db->lock, NULL);
|
|
db->hf_dir = xstrdup(hf_dir);
|
|
char *index_path = path_join(hf_dir, "model.safetensors.index.json");
|
|
size_t len = 0;
|
|
char *text = read_file(index_path, &len);
|
|
json_doc d = json_parse_text(text, len);
|
|
int weight_map = json_obj_get(&d, 0, "weight_map");
|
|
if (weight_map < 0 || d.v[weight_map].type != JT_OBJECT) die("safetensors index has no weight_map");
|
|
|
|
int cap = 4096;
|
|
db->weights = xmalloc((size_t)cap * sizeof(db->weights[0]));
|
|
for (int i = weight_map + 1; i < d.len && d.v[i].parent == weight_map;) {
|
|
int k = i;
|
|
int v = i + 1;
|
|
if (db->n_weights == cap) {
|
|
cap *= 2;
|
|
db->weights = xrealloc(db->weights, (size_t)cap * sizeof(db->weights[0]));
|
|
}
|
|
db->weights[db->n_weights].name = json_strdup_tok(&d, k);
|
|
db->weights[db->n_weights].file = json_strdup_tok(&d, v);
|
|
db->n_weights++;
|
|
i = json_skip(&d, v);
|
|
}
|
|
char **keys = xmalloc((size_t)db->n_weights * sizeof(keys[0]));
|
|
for (int i = 0; i < db->n_weights; i++) {
|
|
keys[i] = db->weights[i].name;
|
|
db_find_shard(db, db->weights[i].file);
|
|
}
|
|
hmap_build(&db->weight_map, keys, db->n_weights);
|
|
free(keys);
|
|
json_free(&d);
|
|
free(text);
|
|
free(index_path);
|
|
}
|
|
|
|
static void db_close(st_db *db) {
|
|
for (int i = 0; i < db->n_weights; i++) {
|
|
free(db->weights[i].name);
|
|
free(db->weights[i].file);
|
|
}
|
|
for (int i = 0; i < db->n_shards; i++) {
|
|
shard *s = &db->shards[i];
|
|
if (s->fp) fclose(s->fp);
|
|
for (int j = 0; j < s->n_tensors; j++) {
|
|
free(s->tensors[j].name);
|
|
free(s->tensors[j].info.dtype);
|
|
}
|
|
free(s->tensors);
|
|
hmap_free(&s->tensor_map);
|
|
pthread_mutex_destroy(&s->lock);
|
|
free(s->file);
|
|
free(s->path);
|
|
}
|
|
hmap_free(&db->weight_map);
|
|
pthread_mutex_destroy(&db->lock);
|
|
free(db->weights);
|
|
free(db->shards);
|
|
free(db->hf_dir);
|
|
memset(db, 0, sizeof(*db));
|
|
}
|
|
|
|
static bool db_has(const st_db *db, const char *name) {
|
|
return hmap_get(&db->weight_map, name) >= 0;
|
|
}
|
|
|
|
static tensor_entry *db_tensor(st_db *db, const char *name, shard **shard_out) {
|
|
pthread_mutex_lock(&db->lock);
|
|
int wi = hmap_get(&db->weight_map, name);
|
|
if (wi < 0) {
|
|
fprintf(stderr, "error: HF tensor not found: %s\n", name);
|
|
exit(1);
|
|
}
|
|
const char *file = db->weights[wi].file;
|
|
int si = db_find_shard(db, file);
|
|
shard *s = &db->shards[si];
|
|
shard_load(s);
|
|
int ti = hmap_get(&s->tensor_map, name);
|
|
if (ti < 0) {
|
|
fprintf(stderr, "error: HF tensor %s missing from shard %s\n", name, file);
|
|
exit(1);
|
|
}
|
|
if (shard_out) *shard_out = s;
|
|
tensor_entry *te = &s->tensors[ti];
|
|
pthread_mutex_unlock(&db->lock);
|
|
return te;
|
|
}
|
|
|
|
static st_value db_read(st_db *db, const char *name) {
|
|
shard *s = NULL;
|
|
tensor_entry *te = db_tensor(db, name, &s);
|
|
const size_t nbytes = (size_t)(te->info.end - te->info.begin);
|
|
st_value v = {0};
|
|
v.dtype = xstrdup(te->info.dtype);
|
|
v.n_dims = te->info.n_dims;
|
|
memcpy(v.shape, te->info.shape, sizeof(v.shape));
|
|
v.nbytes = nbytes;
|
|
v.data = xmalloc(nbytes);
|
|
pthread_mutex_lock(&s->lock);
|
|
if (fseeko(s->fp, (off_t)(s->data_base + te->info.begin), SEEK_SET) != 0) die_errno("seek", s->path);
|
|
if (nbytes && fread(v.data, 1, nbytes, s->fp) != nbytes) die_errno("read tensor", s->path);
|
|
pthread_mutex_unlock(&s->lock);
|
|
return v;
|
|
}
|
|
|
|
/* =====
|
|
* DeepSeek V4 data conversion
|
|
*/
|
|
|
|
static float e8m0_to_f32(uint8_t e) {
|
|
const uint32_t bits = e == 0 ? 0x00400000u : ((uint32_t)e << 23);
|
|
float result;
|
|
memcpy(&result, &bits, sizeof(result));
|
|
return result;
|
|
}
|
|
|
|
static float e4m3fn_to_f32(uint8_t x) {
|
|
const uint8_t abs = x & 0x7f;
|
|
const bool sign = (x & 0x80) != 0;
|
|
if (abs == 0) return sign ? -0.0f : 0.0f;
|
|
if (abs == 0x7f) return 0.0f;
|
|
const int exp = (x >> 3) & 0x0f;
|
|
const int man = x & 0x07;
|
|
float value = exp == 0 ? ldexpf((float)man, -9)
|
|
: ldexpf(1.0f + (float)man / 8.0f, exp - 7);
|
|
return sign ? -value : value;
|
|
}
|
|
|
|
static float bf16_to_f32_bits(uint16_t bits) {
|
|
return ds4q_bf16_to_f32(bits);
|
|
}
|
|
|
|
static int64_t value_nelements(const st_value *v) {
|
|
int64_t n = 1;
|
|
for (int i = 0; i < v->n_dims; i++) n *= v->shape[i];
|
|
return n;
|
|
}
|
|
|
|
static float *tensor_to_f32(const st_value *t, int64_t *n_out) {
|
|
const int64_t n = value_nelements(t);
|
|
float *out = xmalloc((size_t)n * sizeof(float));
|
|
if (strcmp(t->dtype, "F32") == 0) {
|
|
if (t->nbytes != (size_t)n * sizeof(float)) die("bad F32 byte size");
|
|
memcpy(out, t->data, t->nbytes);
|
|
} else if (strcmp(t->dtype, "BF16") == 0) {
|
|
if (t->nbytes != (size_t)n * sizeof(uint16_t)) die("bad BF16 byte size");
|
|
for (int64_t i = 0; i < n; i++) out[i] = bf16_to_f32_bits(load_u16_le(t->data + (size_t)i * 2));
|
|
} else if (strcmp(t->dtype, "F16") == 0) {
|
|
if (t->nbytes != (size_t)n * sizeof(uint16_t)) die("bad F16 byte size");
|
|
for (int64_t i = 0; i < n; i++) out[i] = ds4q_f16_to_f32(load_u16_le(t->data + (size_t)i * 2));
|
|
} else if (strcmp(t->dtype, "F8_E4M3") == 0) {
|
|
if (t->nbytes != (size_t)n) die("bad F8_E4M3 byte size");
|
|
for (int64_t i = 0; i < n; i++) out[i] = e4m3fn_to_f32(t->data[i]);
|
|
} else {
|
|
fprintf(stderr, "error: cannot convert HF dtype directly: %s\n", t->dtype);
|
|
exit(1);
|
|
}
|
|
if (n_out) *n_out = n;
|
|
return out;
|
|
}
|
|
|
|
static float *dequant_fp8_weight(const st_value *w, const st_value *scale, int64_t *n_out) {
|
|
if (strcmp(w->dtype, "F8_E4M3") != 0 || strcmp(scale->dtype, "F8_E8M0") != 0) die("bad FP8 weight/scale dtype");
|
|
if (w->n_dims != 2 || scale->n_dims != 2) die("FP8 tensor must be 2D");
|
|
const int64_t out_dim = w->shape[0];
|
|
const int64_t in_dim = w->shape[1];
|
|
const int64_t block_out = 128;
|
|
const int64_t block_in = 128;
|
|
if (out_dim % block_out || in_dim % block_in) die("FP8 dims are not divisible by 128");
|
|
const int64_t scale_rows = out_dim / block_out;
|
|
const int64_t scale_cols = in_dim / block_in;
|
|
if (scale->shape[0] != scale_rows || scale->shape[1] != scale_cols) die("FP8 scale shape mismatch");
|
|
float *out = xmalloc((size_t)out_dim * (size_t)in_dim * sizeof(float));
|
|
for (int64_t ob = 0; ob < scale_rows; ob++) {
|
|
for (int64_t ib = 0; ib < scale_cols; ib++) {
|
|
const float s = e8m0_to_f32(scale->data[(size_t)ob * (size_t)scale_cols + (size_t)ib]);
|
|
for (int64_t r = 0; r < block_out; r++) {
|
|
const int64_t row = ob * block_out + r;
|
|
const size_t base = (size_t)row * (size_t)in_dim + (size_t)ib * (size_t)block_in;
|
|
for (int64_t c = 0; c < block_in; c++) {
|
|
out[base + (size_t)c] = e4m3fn_to_f32(w->data[base + (size_t)c]) * s;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (n_out) *n_out = out_dim * in_dim;
|
|
return out;
|
|
}
|
|
|
|
static float *dequant_fp4_weight(const st_value *w, const st_value *scale, int64_t *n_out) {
|
|
static const float fp4_table[16] = {
|
|
0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f,
|
|
0.0f, -0.5f, -1.0f, -1.5f, -2.0f, -3.0f, -4.0f, -6.0f,
|
|
};
|
|
if (strcmp(w->dtype, "I8") != 0 || strcmp(scale->dtype, "F8_E8M0") != 0) die("bad FP4 weight/scale dtype");
|
|
if (w->n_dims != 2 || scale->n_dims != 2) die("FP4 tensor must be 2D");
|
|
const int64_t out_dim = w->shape[0];
|
|
const int64_t packed_in = w->shape[1];
|
|
const int64_t in_dim = packed_in * 2;
|
|
if (in_dim % 32) die("FP4 in_dim is not divisible by 32");
|
|
const int64_t n_blocks = in_dim / 32;
|
|
if (scale->shape[0] != out_dim || scale->shape[1] != n_blocks) die("FP4 scale shape mismatch");
|
|
float *out = xmalloc((size_t)out_dim * (size_t)in_dim * sizeof(float));
|
|
for (int64_t r = 0; r < out_dim; r++) {
|
|
for (int64_t b = 0; b < n_blocks; b++) {
|
|
const float s = e8m0_to_f32(scale->data[(size_t)r * (size_t)n_blocks + (size_t)b]);
|
|
const size_t wbase = ((size_t)r * (size_t)n_blocks + (size_t)b) * 16;
|
|
const size_t obase = (size_t)r * (size_t)in_dim + (size_t)b * 32;
|
|
for (int64_t j = 0; j < 16; j++) {
|
|
const uint8_t q = w->data[wbase + (size_t)j];
|
|
out[obase + (size_t)(2*j + 0)] = fp4_table[q & 0x0f] * s;
|
|
out[obase + (size_t)(2*j + 1)] = fp4_table[(q >> 4) & 0x0f] * s;
|
|
}
|
|
}
|
|
}
|
|
if (n_out) *n_out = out_dim * in_dim;
|
|
return out;
|
|
}
|
|
|
|
/* =====
|
|
* Imatrix
|
|
*/
|
|
|
|
typedef struct {
|
|
char *name;
|
|
float *values;
|
|
int n_values;
|
|
} imatrix_entry;
|
|
|
|
typedef struct {
|
|
char *file;
|
|
char *dataset;
|
|
imatrix_entry *entries;
|
|
int n_entries;
|
|
hmap map;
|
|
int chunks;
|
|
bool strict;
|
|
} imatrix_store;
|
|
|
|
static void imatrix_load(imatrix_store *im, const char *path, bool strict) {
|
|
memset(im, 0, sizeof(*im));
|
|
im->file = xstrdup(path);
|
|
im->strict = strict;
|
|
im->chunks = -1;
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) die_errno("open imatrix", path);
|
|
int32_t n_entries = read_i32_fp(fp, "imatrix entry count");
|
|
if (n_entries < 1) die("imatrix has no entries");
|
|
im->entries = xcalloc((size_t)n_entries, sizeof(im->entries[0]));
|
|
im->n_entries = n_entries;
|
|
for (int i = 0; i < n_entries; i++) {
|
|
int32_t len = read_i32_fp(fp, "imatrix name length");
|
|
if (len <= 0 || len > 4096) die("bad imatrix name length");
|
|
char *name = xmalloc((size_t)len + 1);
|
|
if (fread(name, 1, (size_t)len, fp) != (size_t)len) die("short imatrix name read");
|
|
name[len] = '\0';
|
|
int32_t ncall = read_i32_fp(fp, "imatrix calls");
|
|
int32_t nval = read_i32_fp(fp, "imatrix values");
|
|
if (nval < 1) die("bad imatrix value count");
|
|
float *values = xmalloc((size_t)nval * sizeof(float));
|
|
if (fread(values, sizeof(float), (size_t)nval, fp) != (size_t)nval) die("short imatrix value read");
|
|
if (ncall > 0) {
|
|
for (int j = 0; j < nval; j++) values[j] /= (float)ncall;
|
|
}
|
|
for (int j = 0; j < nval; j++) {
|
|
if (!isfinite(values[j])) die("non-finite imatrix value");
|
|
}
|
|
im->entries[i] = (imatrix_entry){ .name = name, .values = values, .n_values = nval };
|
|
}
|
|
if (fgetc(fp) != EOF) {
|
|
if (fseeko(fp, -1, SEEK_CUR) == 0) {
|
|
im->chunks = read_i32_fp(fp, "imatrix chunks");
|
|
int32_t dlen = read_i32_fp(fp, "imatrix dataset length");
|
|
if (dlen > 0 && dlen < (1 << 20)) {
|
|
im->dataset = xmalloc((size_t)dlen + 1);
|
|
if (fread(im->dataset, 1, (size_t)dlen, fp) == (size_t)dlen) {
|
|
im->dataset[dlen] = '\0';
|
|
} else {
|
|
free(im->dataset);
|
|
im->dataset = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fclose(fp);
|
|
char **keys = xmalloc((size_t)n_entries * sizeof(keys[0]));
|
|
for (int i = 0; i < n_entries; i++) keys[i] = im->entries[i].name;
|
|
hmap_build(&im->map, keys, n_entries);
|
|
free(keys);
|
|
fprintf(stderr, "loaded imatrix %s: %d entries%s%s\n",
|
|
path, n_entries, im->dataset ? ", dataset=" : "", im->dataset ? im->dataset : "");
|
|
}
|
|
|
|
static bool imatrix_enabled(const imatrix_store *im) {
|
|
return im && im->n_entries > 0;
|
|
}
|
|
|
|
static const float *imatrix_find(
|
|
const imatrix_store *im,
|
|
const char **names,
|
|
int n_names,
|
|
int64_t ncols,
|
|
int expert_id,
|
|
int n_experts) {
|
|
if (!imatrix_enabled(im)) return NULL;
|
|
char tmp[4096];
|
|
for (int pass = 0; pass < 3; pass++) {
|
|
for (int i = 0; i < n_names; i++) {
|
|
if (!names[i]) continue;
|
|
const char *candidate = names[i];
|
|
if (expert_id >= 0 && pass < 2) {
|
|
snprintf(tmp, sizeof(tmp), "%s.expert%s%d", names[i], pass == 0 ? "." : "_", expert_id);
|
|
candidate = tmp;
|
|
} else if (pass < 2) {
|
|
continue;
|
|
}
|
|
int idx = hmap_get(&im->map, candidate);
|
|
if (idx < 0) continue;
|
|
const imatrix_entry *e = &im->entries[idx];
|
|
if ((int64_t)e->n_values == ncols) return e->values;
|
|
if (expert_id >= 0 && n_experts > 0 && (int64_t)e->n_values == ncols * (int64_t)n_experts) {
|
|
return e->values + (size_t)expert_id * (size_t)ncols;
|
|
}
|
|
fprintf(stderr, "error: imatrix size mismatch for %s: got %d expected %" PRId64 "\n",
|
|
candidate, e->n_values, ncols);
|
|
exit(1);
|
|
}
|
|
}
|
|
if (im->strict) {
|
|
fprintf(stderr, "error: missing imatrix entry for %s\n", names[0] ? names[0] : "(unnamed)");
|
|
exit(1);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void imatrix_free(imatrix_store *im) {
|
|
for (int i = 0; i < im->n_entries; i++) {
|
|
free(im->entries[i].name);
|
|
free(im->entries[i].values);
|
|
}
|
|
free(im->entries);
|
|
free(im->file);
|
|
free(im->dataset);
|
|
hmap_free(&im->map);
|
|
memset(im, 0, sizeof(*im));
|
|
}
|
|
|
|
/* =====
|
|
* GGUF tensor mapping and quantization policy
|
|
*/
|
|
|
|
typedef enum { EXP_NONE, EXP_W1, EXP_W2, EXP_W3 } expert_part;
|
|
|
|
typedef struct {
|
|
bool is_expert;
|
|
int layer;
|
|
expert_part part;
|
|
} expert_tensor;
|
|
|
|
static expert_tensor parse_expert_tensor(const char *name) {
|
|
expert_tensor e = {0};
|
|
int layer = -1;
|
|
char kind[16];
|
|
int rest = 0;
|
|
if (sscanf(name, "blk.%d.ffn_%15[^_]_exps.weight%n", &layer, kind, &rest) == 2
|
|
&& rest == (int)strlen(name))
|
|
{
|
|
if (strcmp(kind, "gate") == 0 || strcmp(kind, "down") == 0 || strcmp(kind, "up") == 0) {
|
|
e.is_expert = true;
|
|
e.layer = layer;
|
|
e.part = strcmp(kind, "gate") == 0 ? EXP_W1 : strcmp(kind, "down") == 0 ? EXP_W2 : EXP_W3;
|
|
}
|
|
}
|
|
return e;
|
|
}
|
|
|
|
static const char *expert_part_name(expert_part p) {
|
|
switch (p) {
|
|
case EXP_W1: return "w1";
|
|
case EXP_W2: return "w2";
|
|
case EXP_W3: return "w3";
|
|
default: die("bad expert part");
|
|
}
|
|
return "";
|
|
}
|
|
|
|
typedef struct {
|
|
const char *gguf;
|
|
const char *hf;
|
|
} name_map;
|
|
|
|
static const name_map top_map[] = {
|
|
{ "token_embd.weight", "embed.weight" },
|
|
{ "output_norm.weight", "norm.weight" },
|
|
{ "output.weight", "head.weight" },
|
|
{ "output_hc_base.weight", "hc_head_base" },
|
|
{ "output_hc_fn.weight", "hc_head_fn" },
|
|
{ "output_hc_scale.weight", "hc_head_scale" },
|
|
};
|
|
|
|
static const name_map layer_map[] = {
|
|
{ "hc_attn_base.weight", "hc_attn_base" },
|
|
{ "hc_attn_fn.weight", "hc_attn_fn" },
|
|
{ "hc_attn_scale.weight", "hc_attn_scale" },
|
|
{ "hc_ffn_base.weight", "hc_ffn_base" },
|
|
{ "hc_ffn_fn.weight", "hc_ffn_fn" },
|
|
{ "hc_ffn_scale.weight", "hc_ffn_scale" },
|
|
{ "attn_sinks.weight", "attn.attn_sink" },
|
|
{ "attn_q_a.weight", "attn.wq_a.weight" },
|
|
{ "attn_q_b.weight", "attn.wq_b.weight" },
|
|
{ "attn_q_a_norm.weight", "attn.q_norm.weight" },
|
|
{ "attn_kv.weight", "attn.wkv.weight" },
|
|
{ "attn_kv_a_norm.weight", "attn.kv_norm.weight" },
|
|
{ "attn_output_a.weight", "attn.wo_a.weight" },
|
|
{ "attn_output_b.weight", "attn.wo_b.weight" },
|
|
{ "attn_compressor_ape.weight", "attn.compressor.ape" },
|
|
{ "attn_compressor_kv.weight", "attn.compressor.wkv.weight" },
|
|
{ "attn_compressor_gate.weight", "attn.compressor.wgate.weight" },
|
|
{ "attn_compressor_norm.weight", "attn.compressor.norm.weight" },
|
|
{ "indexer.attn_q_b.weight", "attn.indexer.wq_b.weight" },
|
|
{ "indexer.proj.weight", "attn.indexer.weights_proj.weight" },
|
|
{ "indexer_compressor_ape.weight", "attn.indexer.compressor.ape" },
|
|
{ "indexer_compressor_kv.weight", "attn.indexer.compressor.wkv.weight" },
|
|
{ "indexer_compressor_gate.weight", "attn.indexer.compressor.wgate.weight" },
|
|
{ "indexer_compressor_norm.weight", "attn.indexer.compressor.norm.weight" },
|
|
{ "attn_norm.weight", "attn_norm.weight" },
|
|
{ "ffn_norm.weight", "ffn_norm.weight" },
|
|
{ "ffn_gate_shexp.weight", "ffn.shared_experts.w1.weight" },
|
|
{ "ffn_up_shexp.weight", "ffn.shared_experts.w3.weight" },
|
|
{ "ffn_down_shexp.weight", "ffn.shared_experts.w2.weight" },
|
|
{ "ffn_gate_inp.weight", "ffn.gate.weight" },
|
|
{ "exp_probs_b.bias", "ffn.gate.bias" },
|
|
{ "ffn_gate_tid2eid.weight", "ffn.gate.tid2eid" },
|
|
};
|
|
|
|
static char *hf_name_for_regular(const char *gguf_name) {
|
|
for (size_t i = 0; i < sizeof(top_map) / sizeof(top_map[0]); i++) {
|
|
if (strcmp(gguf_name, top_map[i].gguf) == 0) return xstrdup(top_map[i].hf);
|
|
}
|
|
int layer = -1;
|
|
const char *p = gguf_name;
|
|
if (sscanf(p, "blk.%d.", &layer) != 1) {
|
|
fprintf(stderr, "error: cannot map GGUF tensor to HF tensor: %s\n", gguf_name);
|
|
exit(1);
|
|
}
|
|
const char *rest = strchr(p + 4, '.');
|
|
if (!rest) die("bad layer tensor name");
|
|
rest++;
|
|
for (size_t i = 0; i < sizeof(layer_map) / sizeof(layer_map[0]); i++) {
|
|
if (strcmp(rest, layer_map[i].gguf) == 0) {
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "layers.%d.%s", layer, layer_map[i].hf);
|
|
return xstrdup(buf);
|
|
}
|
|
}
|
|
fprintf(stderr, "error: cannot map GGUF tensor to HF tensor: %s\n", gguf_name);
|
|
exit(1);
|
|
}
|
|
|
|
typedef struct {
|
|
char *prefix;
|
|
ds4q_type type;
|
|
} type_override;
|
|
|
|
typedef struct {
|
|
ds4q_type routed_w1, routed_w2, routed_w3;
|
|
ds4q_type attention_proj, attention, shared, embedding, output, dense;
|
|
type_override *overrides;
|
|
int n_overrides;
|
|
} quant_policy;
|
|
|
|
static bool is_attention_projection(const char *name) {
|
|
return strstr(name, ".attn_kv.weight") || strstr(name, ".attn_q_a.weight") ||
|
|
strstr(name, ".attn_q_b.weight") || strstr(name, ".attn_output_a.weight") ||
|
|
strstr(name, ".attn_output_b.weight");
|
|
}
|
|
|
|
static bool is_attention_tensor(const char *name) {
|
|
return strstr(name, ".attn") || strstr(name, "attn_") || strstr(name, ".indexer") || strstr(name, "indexer_");
|
|
}
|
|
|
|
static bool is_shared_expert(const char *name) {
|
|
return strstr(name, "_shexp.") != NULL;
|
|
}
|
|
|
|
static bool is_output_tensor(const char *name) {
|
|
return str_starts(name, "output.");
|
|
}
|
|
|
|
typedef struct {
|
|
char *name;
|
|
int n_dims;
|
|
int64_t ne[DS4Q_MAX_DIMS];
|
|
ds4q_type type;
|
|
uint64_t old_offset;
|
|
uint64_t new_offset;
|
|
size_t size;
|
|
} tensor_meta;
|
|
|
|
static int tensor_n_dims(const tensor_meta *t) {
|
|
int n = t->n_dims;
|
|
while (n > 1 && t->ne[n - 1] == 1) n--;
|
|
return n;
|
|
}
|
|
|
|
static ds4q_type policy_type(const quant_policy *p, const char *name, const tensor_meta *tmpl) {
|
|
for (int i = 0; i < p->n_overrides; i++) {
|
|
if (strcmp(name, p->overrides[i].prefix) == 0 || str_starts(name, p->overrides[i].prefix)) {
|
|
return p->overrides[i].type;
|
|
}
|
|
}
|
|
expert_tensor e = parse_expert_tensor(name);
|
|
if (e.is_expert) {
|
|
if (e.part == EXP_W1 && p->routed_w1 != DS4Q_TYPE_COUNT) return p->routed_w1;
|
|
if (e.part == EXP_W2 && p->routed_w2 != DS4Q_TYPE_COUNT) return p->routed_w2;
|
|
if (e.part == EXP_W3 && p->routed_w3 != DS4Q_TYPE_COUNT) return p->routed_w3;
|
|
return tmpl->type;
|
|
}
|
|
if (tmpl->type != DS4Q_TYPE_F32 && tmpl->type != DS4Q_TYPE_F16 &&
|
|
tmpl->type != DS4Q_TYPE_BF16 && !ds4q_can_quantize(tmpl->type)) {
|
|
return tmpl->type;
|
|
}
|
|
if (tensor_n_dims(tmpl) <= 1) return tmpl->type;
|
|
if (strcmp(name, "token_embd.weight") == 0 && p->embedding != DS4Q_TYPE_COUNT) return p->embedding;
|
|
if (is_output_tensor(name) && p->output != DS4Q_TYPE_COUNT) return p->output;
|
|
if (is_shared_expert(name) && p->shared != DS4Q_TYPE_COUNT) return p->shared;
|
|
if (is_attention_projection(name) && p->attention_proj != DS4Q_TYPE_COUNT) return p->attention_proj;
|
|
if (is_attention_tensor(name) && p->attention != DS4Q_TYPE_COUNT) return p->attention;
|
|
if (p->dense != DS4Q_TYPE_COUNT) return p->dense;
|
|
return tmpl->type;
|
|
}
|
|
|
|
static ds4q_type parse_type(const char *raw) {
|
|
char wanted[64];
|
|
size_t n = 0;
|
|
for (const char *p = raw; *p && n + 1 < sizeof(wanted); p++) {
|
|
if (*p != '-' && *p != '_') wanted[n++] = (char)tolower((unsigned char)*p);
|
|
}
|
|
wanted[n] = '\0';
|
|
if (strcmp(wanted, "copy") == 0 || strcmp(wanted, "template") == 0) return DS4Q_TYPE_COUNT;
|
|
for (int i = 0; i < DS4Q_TYPE_COUNT; i++) {
|
|
char name[64];
|
|
size_t m = 0;
|
|
const char *tn = ds4q_type_name((ds4q_type)i);
|
|
if (!tn) continue;
|
|
for (const char *p = tn; *p && m + 1 < sizeof(name); p++) {
|
|
if (*p != '-' && *p != '_') name[m++] = (char)tolower((unsigned char)*p);
|
|
}
|
|
name[m] = '\0';
|
|
if (strcmp(name, wanted) == 0) return (ds4q_type)i;
|
|
}
|
|
fprintf(stderr, "error: unknown quant type: %s\n", raw);
|
|
exit(1);
|
|
}
|
|
|
|
static bool is_quantizable_target(ds4q_type type) {
|
|
return type == DS4Q_TYPE_F32 || type == DS4Q_TYPE_F16 || type == DS4Q_TYPE_BF16 || ds4q_can_quantize(type);
|
|
}
|
|
|
|
/* =====
|
|
* Tensor generation
|
|
*/
|
|
|
|
typedef struct {
|
|
uint8_t *data;
|
|
size_t size;
|
|
} byte_buf;
|
|
|
|
static byte_buf f32_to_type(const float *src, int64_t n, ds4q_type type, int64_t ncols, const float *imat) {
|
|
if (ncols <= 0 || n % ncols != 0) die("bad ncols for tensor conversion");
|
|
byte_buf out = {0};
|
|
if (type == DS4Q_TYPE_F32) {
|
|
out.size = (size_t)n * sizeof(float);
|
|
out.data = xmalloc(out.size);
|
|
memcpy(out.data, src, out.size);
|
|
return out;
|
|
}
|
|
if (type == DS4Q_TYPE_F16) {
|
|
out.size = (size_t)n * sizeof(uint16_t);
|
|
out.data = xmalloc(out.size);
|
|
ds4q_f32_to_f16_row(src, (uint16_t *)out.data, n);
|
|
return out;
|
|
}
|
|
if (type == DS4Q_TYPE_BF16) {
|
|
out.size = (size_t)n * sizeof(uint16_t);
|
|
out.data = xmalloc(out.size);
|
|
ds4q_f32_to_bf16_row(src, (uint16_t *)out.data, n);
|
|
return out;
|
|
}
|
|
if (!ds4q_can_quantize(type)) die("unsupported quant target type");
|
|
if (ncols % ds4q_block_size(type) != 0) die("ncols is not divisible by quant block size");
|
|
const int64_t nrows = n / ncols;
|
|
out.size = (size_t)nrows * ds4q_row_size(type, ncols);
|
|
out.data = xmalloc(out.size);
|
|
|
|
float *synthetic = NULL;
|
|
const float *im_ptr = imat;
|
|
if (!im_ptr && ds4q_requires_imatrix(type)) {
|
|
synthetic = xcalloc((size_t)ncols, sizeof(float));
|
|
for (int64_t r = 0; r < nrows; r++) {
|
|
const float *row = src + (size_t)r * (size_t)ncols;
|
|
for (int64_t c = 0; c < ncols; c++) synthetic[c] += row[c] * row[c];
|
|
}
|
|
im_ptr = synthetic;
|
|
}
|
|
size_t written = ds4q_quantize_chunk(type, src, out.data, 0, nrows, ncols, im_ptr);
|
|
free(synthetic);
|
|
if (written != out.size) die("ds4q_quantize_chunk wrote unexpected byte count");
|
|
return out;
|
|
}
|
|
|
|
static byte_buf i64_to_i32(const st_value *src) {
|
|
if (strcmp(src->dtype, "I64") != 0) die("expected I64 source for I32 tensor");
|
|
const int64_t n = value_nelements(src);
|
|
if (src->nbytes != (size_t)n * sizeof(int64_t)) die("bad I64 byte size");
|
|
byte_buf out = { .size = (size_t)n * sizeof(int32_t), .data = xmalloc((size_t)n * sizeof(int32_t)) };
|
|
int32_t *dst = (int32_t *)out.data;
|
|
for (int64_t i = 0; i < n; i++) {
|
|
int64_t v = load_i64_le(src->data + (size_t)i * 8);
|
|
if (v < INT32_MIN || v > INT32_MAX) die("I64 value out of I32 range");
|
|
dst[i] = (int32_t)v;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static size_t tensor_nbytes(ds4q_type type, const int64_t *ne, int n_dims) {
|
|
size_t nbytes = ds4q_row_size(type, ne[0]);
|
|
for (int i = 1; i < n_dims; i++) nbytes *= (size_t)ne[i];
|
|
return nbytes;
|
|
}
|
|
|
|
static void check_reversed_shape(const char *gguf_name, const st_info *info, const tensor_meta *tmpl) {
|
|
int nd = tensor_n_dims(tmpl);
|
|
if (info->n_dims != nd) {
|
|
fprintf(stderr, "error: rank mismatch for %s\n", gguf_name);
|
|
exit(1);
|
|
}
|
|
for (int i = 0; i < nd; i++) {
|
|
if (tmpl->ne[i] != info->shape[nd - 1 - i]) {
|
|
fprintf(stderr, "error: shape mismatch for %s\n", gguf_name);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static byte_buf generate_regular(st_db *db, const char *gguf_name, const tensor_meta *tmpl,
|
|
ds4q_type target, const imatrix_store *imatrix) {
|
|
char *hf_name = hf_name_for_regular(gguf_name);
|
|
tensor_entry *te = db_tensor(db, hf_name, NULL);
|
|
check_reversed_shape(gguf_name, &te->info, tmpl);
|
|
if (target == DS4Q_TYPE_I32) {
|
|
st_value sv = db_read(db, hf_name);
|
|
byte_buf b = i64_to_i32(&sv);
|
|
st_value_free(&sv);
|
|
free(hf_name);
|
|
return b;
|
|
}
|
|
if (!is_quantizable_target(target)) die("unsupported regular target type");
|
|
int64_t n = 0;
|
|
float *f32 = NULL;
|
|
if (strcmp(te->info.dtype, "F8_E4M3") == 0) {
|
|
if (!str_ends(hf_name, ".weight")) die("FP8 tensor without .weight suffix");
|
|
char *scale_name = xstrdup(hf_name);
|
|
strcpy(scale_name + strlen(scale_name) - strlen(".weight"), ".scale");
|
|
if (!db_has(db, scale_name)) die("missing FP8 scale tensor");
|
|
st_value w = db_read(db, hf_name);
|
|
st_value s = db_read(db, scale_name);
|
|
f32 = dequant_fp8_weight(&w, &s, &n);
|
|
st_value_free(&w);
|
|
st_value_free(&s);
|
|
free(scale_name);
|
|
} else {
|
|
st_value w = db_read(db, hf_name);
|
|
f32 = tensor_to_f32(&w, &n);
|
|
st_value_free(&w);
|
|
}
|
|
const char *names[2] = { gguf_name, hf_name };
|
|
const float *imat = imatrix_find(imatrix, names, 2, tmpl->ne[0], -1, 0);
|
|
byte_buf b = f32_to_type(f32, n, target, tmpl->ne[0], imat);
|
|
free(f32);
|
|
free(hf_name);
|
|
return b;
|
|
}
|
|
|
|
typedef struct {
|
|
st_db *db;
|
|
const char *gguf_name;
|
|
const tensor_meta *tmpl;
|
|
ds4q_type target;
|
|
int n_experts;
|
|
const imatrix_store *imatrix;
|
|
expert_tensor expert;
|
|
const char *wid;
|
|
int64_t ncols;
|
|
int64_t nrows;
|
|
size_t per_expert;
|
|
byte_buf *out;
|
|
int next;
|
|
int done;
|
|
pthread_mutex_t lock;
|
|
} expert_job;
|
|
|
|
static void generate_one_expert(expert_job *j, int xid) {
|
|
char prefix[256];
|
|
snprintf(prefix, sizeof(prefix), "layers.%d.ffn.experts.%d.%s", j->expert.layer, xid, j->wid);
|
|
char weight_name[320];
|
|
char scale_name[320];
|
|
snprintf(weight_name, sizeof(weight_name), "%s.weight", prefix);
|
|
snprintf(scale_name, sizeof(scale_name), "%s.scale", prefix);
|
|
st_value w = db_read(j->db, weight_name);
|
|
st_value s = db_read(j->db, scale_name);
|
|
if (w.n_dims != 2 || w.shape[0] != j->nrows || w.shape[1] * 2 != j->ncols) die("expert shape mismatch");
|
|
int64_t n = 0;
|
|
float *f32 = dequant_fp4_weight(&w, &s, &n);
|
|
const char *names[3] = { j->gguf_name, weight_name, NULL };
|
|
const float *imat = imatrix_find(j->imatrix, names, 2, j->ncols, xid, j->n_experts);
|
|
byte_buf q = f32_to_type(f32, n, j->target, j->ncols, imat);
|
|
if (q.size != j->per_expert) die("expert quantized size mismatch");
|
|
memcpy(j->out->data + (size_t)xid * j->per_expert, q.data, q.size);
|
|
free(q.data);
|
|
free(f32);
|
|
st_value_free(&w);
|
|
st_value_free(&s);
|
|
}
|
|
|
|
static void *expert_worker(void *arg) {
|
|
expert_job *j = arg;
|
|
for (;;) {
|
|
pthread_mutex_lock(&j->lock);
|
|
int xid = j->next++;
|
|
pthread_mutex_unlock(&j->lock);
|
|
if (xid >= j->n_experts) break;
|
|
generate_one_expert(j, xid);
|
|
pthread_mutex_lock(&j->lock);
|
|
int done = ++j->done;
|
|
if (done % 32 == 0 || done == j->n_experts) {
|
|
fprintf(stderr, "generate_expert_tensor: layer %d %s %d/%d experts\n",
|
|
j->expert.layer, j->wid, done, j->n_experts);
|
|
}
|
|
pthread_mutex_unlock(&j->lock);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static byte_buf generate_expert(st_db *db, const char *gguf_name, const tensor_meta *tmpl,
|
|
ds4q_type target, int n_experts, int n_threads,
|
|
const imatrix_store *imatrix) {
|
|
expert_tensor e = parse_expert_tensor(gguf_name);
|
|
if (!e.is_expert) die("not an expert tensor");
|
|
if (!is_quantizable_target(target)) die("unsupported expert target type");
|
|
const char *wid = expert_part_name(e.part);
|
|
const int64_t ncols = tmpl->ne[0];
|
|
const int64_t nrows = tmpl->ne[1];
|
|
const size_t per_expert = (size_t)nrows * ds4q_row_size(target, ncols);
|
|
byte_buf out = { .size = per_expert * (size_t)n_experts, .data = xmalloc(per_expert * (size_t)n_experts) };
|
|
ds4q_quantize_init(target);
|
|
int worker_count = n_threads > 0 ? n_threads : 8;
|
|
if (worker_count < 1) worker_count = 1;
|
|
if (worker_count > n_experts) worker_count = n_experts;
|
|
fprintf(stderr, "generate_expert_tensor: layer %d %s using %d worker%s\n",
|
|
e.layer, wid, worker_count, worker_count == 1 ? "" : "s");
|
|
expert_job job = {
|
|
.db = db, .gguf_name = gguf_name, .tmpl = tmpl, .target = target,
|
|
.n_experts = n_experts, .imatrix = imatrix, .expert = e, .wid = wid,
|
|
.ncols = ncols, .nrows = nrows, .per_expert = per_expert, .out = &out,
|
|
};
|
|
pthread_mutex_init(&job.lock, NULL);
|
|
pthread_t *threads = xcalloc((size_t)worker_count, sizeof(threads[0]));
|
|
for (int i = 1; i < worker_count; i++) pthread_create(&threads[i], NULL, expert_worker, &job);
|
|
expert_worker(&job);
|
|
for (int i = 1; i < worker_count; i++) pthread_join(threads[i], NULL);
|
|
pthread_mutex_destroy(&job.lock);
|
|
free(threads);
|
|
return out;
|
|
}
|
|
|
|
static byte_buf generate_tensor(st_db *db, const char *name, const tensor_meta *tmpl,
|
|
ds4q_type target, int n_experts, int n_threads,
|
|
const imatrix_store *imatrix) {
|
|
if (parse_expert_tensor(name).is_expert) {
|
|
return generate_expert(db, name, tmpl, target, n_experts, n_threads, imatrix);
|
|
}
|
|
return generate_regular(db, name, tmpl, target, imatrix);
|
|
}
|
|
|
|
/* =====
|
|
* Minimal GGUF reader/writer
|
|
*
|
|
* GGUF metadata is copied as raw KV records from the template. Tensor infos
|
|
* are rewritten with the new target types and offsets. This keeps the tool C
|
|
* only and independent from general-purpose GGUF libraries.
|
|
*/
|
|
|
|
typedef struct {
|
|
size_t start;
|
|
size_t end;
|
|
} byte_span;
|
|
|
|
typedef struct {
|
|
char *path;
|
|
uint32_t version;
|
|
uint64_t n_kv;
|
|
uint64_t n_tensors;
|
|
uint8_t *kv_raw;
|
|
size_t kv_raw_len;
|
|
size_t alignment;
|
|
int n_experts;
|
|
size_t data_offset;
|
|
tensor_meta *tensors;
|
|
hmap tensor_map;
|
|
} gguf_file;
|
|
|
|
typedef struct {
|
|
tensor_meta *tensors;
|
|
uint64_t n_tensors;
|
|
uint64_t n_kv_extra;
|
|
size_t meta_size;
|
|
size_t data_offset;
|
|
size_t tensor_bytes;
|
|
size_t alignment;
|
|
} output_context;
|
|
|
|
static size_t gguf_scalar_size(uint32_t type) {
|
|
switch (type) {
|
|
case GGUF_TYPE_UINT8:
|
|
case GGUF_TYPE_INT8:
|
|
case GGUF_TYPE_BOOL: return 1;
|
|
case GGUF_TYPE_UINT16:
|
|
case GGUF_TYPE_INT16: return 2;
|
|
case GGUF_TYPE_UINT32:
|
|
case GGUF_TYPE_INT32:
|
|
case GGUF_TYPE_FLOAT32: return 4;
|
|
case GGUF_TYPE_UINT64:
|
|
case GGUF_TYPE_INT64:
|
|
case GGUF_TYPE_FLOAT64: return 8;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static char *read_gguf_string_fp(FILE *fp) {
|
|
uint64_t n = read_u64_le_fp(fp, "GGUF string length");
|
|
char *s = xmalloc((size_t)n + 1);
|
|
if (n && fread(s, 1, (size_t)n, fp) != (size_t)n) die("short GGUF string read");
|
|
s[n] = '\0';
|
|
return s;
|
|
}
|
|
|
|
static void skip_bytes_fp(FILE *fp, uint64_t n) {
|
|
if (fseeko(fp, (off_t)n, SEEK_CUR) != 0) die("GGUF seek failed");
|
|
}
|
|
|
|
static void skip_gguf_value_fp(FILE *fp, uint32_t type) {
|
|
if (type == GGUF_TYPE_STRING) {
|
|
uint64_t n = read_u64_le_fp(fp, "GGUF string length");
|
|
skip_bytes_fp(fp, n);
|
|
return;
|
|
}
|
|
if (type == GGUF_TYPE_ARRAY) {
|
|
uint32_t elem_type = read_u32_le_fp(fp, "GGUF array type");
|
|
uint64_t n = read_u64_le_fp(fp, "GGUF array count");
|
|
if (elem_type == GGUF_TYPE_STRING) {
|
|
for (uint64_t i = 0; i < n; i++) {
|
|
uint64_t len = read_u64_le_fp(fp, "GGUF array string length");
|
|
skip_bytes_fp(fp, len);
|
|
}
|
|
} else {
|
|
size_t sz = gguf_scalar_size(elem_type);
|
|
if (!sz) die("unsupported GGUF array type");
|
|
skip_bytes_fp(fp, n * sz);
|
|
}
|
|
return;
|
|
}
|
|
size_t sz = gguf_scalar_size(type);
|
|
if (!sz) die("unsupported GGUF value type");
|
|
skip_bytes_fp(fp, sz);
|
|
}
|
|
|
|
static size_t gguf_string_size(const char *s) {
|
|
return sizeof(uint64_t) + strlen(s);
|
|
}
|
|
|
|
static void write_u32(FILE *fp, uint32_t v) {
|
|
if (fwrite(&v, sizeof(v), 1, fp) != 1) die("write u32 failed");
|
|
}
|
|
|
|
static void write_u64(FILE *fp, uint64_t v) {
|
|
if (fwrite(&v, sizeof(v), 1, fp) != 1) die("write u64 failed");
|
|
}
|
|
|
|
static void write_gguf_string(FILE *fp, const char *s) {
|
|
uint64_t n = strlen(s);
|
|
write_u64(fp, n);
|
|
if (n && fwrite(s, 1, (size_t)n, fp) != (size_t)n) die("write string failed");
|
|
}
|
|
|
|
static bool is_imatrix_kv_key(const char *key) {
|
|
return str_starts(key, "quantize.imatrix.");
|
|
}
|
|
|
|
static size_t extra_imatrix_kv_size(const imatrix_store *im) {
|
|
if (!imatrix_enabled(im)) return 0;
|
|
size_t n = 0;
|
|
n += gguf_string_size(DS4_KV_QUANTIZE_IMATRIX_FILE) + 4 + gguf_string_size(im->file);
|
|
n += gguf_string_size(DS4_KV_QUANTIZE_IMATRIX_N_ENTRIES) + 4 + 8;
|
|
if (im->dataset) n += gguf_string_size(DS4_KV_QUANTIZE_IMATRIX_DATASET) + 4 + gguf_string_size(im->dataset);
|
|
if (im->chunks > 0) n += gguf_string_size(DS4_KV_QUANTIZE_IMATRIX_N_CHUNKS) + 4 + 8;
|
|
return n;
|
|
}
|
|
|
|
static uint64_t extra_imatrix_kv_count(const imatrix_store *im) {
|
|
if (!imatrix_enabled(im)) return 0;
|
|
return 2 + (im->dataset ? 1 : 0) + (im->chunks > 0 ? 1 : 0);
|
|
}
|
|
|
|
static void write_imatrix_kvs(FILE *fp, const imatrix_store *im) {
|
|
if (!imatrix_enabled(im)) return;
|
|
write_gguf_string(fp, DS4_KV_QUANTIZE_IMATRIX_FILE);
|
|
write_u32(fp, GGUF_TYPE_STRING);
|
|
write_gguf_string(fp, im->file);
|
|
|
|
write_gguf_string(fp, DS4_KV_QUANTIZE_IMATRIX_N_ENTRIES);
|
|
write_u32(fp, GGUF_TYPE_UINT64);
|
|
write_u64(fp, (uint64_t)im->n_entries);
|
|
|
|
if (im->dataset) {
|
|
write_gguf_string(fp, DS4_KV_QUANTIZE_IMATRIX_DATASET);
|
|
write_u32(fp, GGUF_TYPE_STRING);
|
|
write_gguf_string(fp, im->dataset);
|
|
}
|
|
if (im->chunks > 0) {
|
|
write_gguf_string(fp, DS4_KV_QUANTIZE_IMATRIX_N_CHUNKS);
|
|
write_u32(fp, GGUF_TYPE_UINT64);
|
|
write_u64(fp, (uint64_t)im->chunks);
|
|
}
|
|
}
|
|
|
|
static gguf_file load_gguf_metadata(const char *path) {
|
|
gguf_file g = {0};
|
|
g.path = xstrdup(path);
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) die_errno("open GGUF", path);
|
|
char magic[4];
|
|
if (fread(magic, 1, sizeof(magic), fp) != sizeof(magic) || memcmp(magic, "GGUF", 4) != 0) {
|
|
die("bad GGUF template");
|
|
}
|
|
g.version = read_u32_le_fp(fp, "GGUF version");
|
|
g.n_tensors = read_u64_le_fp(fp, "GGUF tensor count");
|
|
g.n_kv = read_u64_le_fp(fp, "GGUF KV count");
|
|
g.alignment = DS4_GGUF_DEFAULT_ALIGNMENT;
|
|
byte_span *kv_keep = xcalloc((size_t)g.n_kv, sizeof(kv_keep[0]));
|
|
uint64_t n_kv_keep = 0;
|
|
|
|
off_t kv_start = ftello(fp);
|
|
if (kv_start < 0) die("GGUF ftell failed");
|
|
for (uint64_t i = 0; i < g.n_kv; i++) {
|
|
off_t rec_start = ftello(fp);
|
|
if (rec_start < 0 || rec_start < kv_start) die("GGUF ftell failed");
|
|
char *key = read_gguf_string_fp(fp);
|
|
uint32_t type = read_u32_le_fp(fp, "GGUF KV type");
|
|
if (strcmp(key, "general.alignment") == 0 && type == GGUF_TYPE_UINT32) {
|
|
uint32_t a = read_u32_le_fp(fp, "GGUF alignment");
|
|
if (a) g.alignment = a;
|
|
} else if (strcmp(key, "deepseek4.expert_count") == 0 && type == GGUF_TYPE_UINT32) {
|
|
uint32_t n = read_u32_le_fp(fp, "GGUF expert count");
|
|
if (n <= (uint32_t)INT_MAX) g.n_experts = (int)n;
|
|
} else if (strcmp(key, "deepseek4.expert_count") == 0 && type == GGUF_TYPE_UINT64) {
|
|
uint64_t n = read_u64_le_fp(fp, "GGUF expert count");
|
|
if (n <= (uint64_t)INT_MAX) g.n_experts = (int)n;
|
|
} else {
|
|
skip_gguf_value_fp(fp, type);
|
|
}
|
|
off_t rec_end = ftello(fp);
|
|
if (rec_end < 0 || rec_end < rec_start) die("GGUF ftell failed");
|
|
|
|
/*
|
|
* Template GGUFs may already carry imatrix provenance from a previous
|
|
* quantization. Drop those keys and write the current run's keys later,
|
|
* otherwise the output can contain duplicate GGUF metadata with stale
|
|
* and new values.
|
|
*/
|
|
if (!is_imatrix_kv_key(key)) {
|
|
kv_keep[n_kv_keep++] = (byte_span){
|
|
.start = (size_t)(rec_start - kv_start),
|
|
.end = (size_t)(rec_end - kv_start),
|
|
};
|
|
}
|
|
free(key);
|
|
}
|
|
off_t tensor_start = ftello(fp);
|
|
if (tensor_start < 0 || tensor_start < kv_start) die("GGUF ftell failed");
|
|
size_t kv_full_len = (size_t)(tensor_start - kv_start);
|
|
uint8_t *kv_full = xmalloc(kv_full_len);
|
|
if (fseeko(fp, kv_start, SEEK_SET) != 0) die("GGUF seek failed");
|
|
if (kv_full_len && fread(kv_full, 1, kv_full_len, fp) != kv_full_len) die("GGUF KV read failed");
|
|
|
|
for (uint64_t i = 0; i < n_kv_keep; i++) g.kv_raw_len += kv_keep[i].end - kv_keep[i].start;
|
|
g.kv_raw = xmalloc(g.kv_raw_len);
|
|
size_t kv_pos = 0;
|
|
for (uint64_t i = 0; i < n_kv_keep; i++) {
|
|
size_t n = kv_keep[i].end - kv_keep[i].start;
|
|
memcpy(g.kv_raw + kv_pos, kv_full + kv_keep[i].start, n);
|
|
kv_pos += n;
|
|
}
|
|
g.n_kv = n_kv_keep;
|
|
free(kv_full);
|
|
free(kv_keep);
|
|
if (fseeko(fp, tensor_start, SEEK_SET) != 0) die("GGUF seek failed");
|
|
|
|
g.tensors = xcalloc((size_t)g.n_tensors, sizeof(g.tensors[0]));
|
|
for (uint64_t i = 0; i < g.n_tensors; i++) {
|
|
tensor_meta *t = &g.tensors[i];
|
|
t->name = read_gguf_string_fp(fp);
|
|
t->n_dims = (int)read_u32_le_fp(fp, "GGUF tensor rank");
|
|
if (t->n_dims < 1 || t->n_dims > DS4Q_MAX_DIMS) die("bad GGUF tensor rank");
|
|
for (int j = 0; j < t->n_dims; j++) t->ne[j] = (int64_t)read_u64_le_fp(fp, "GGUF tensor dim");
|
|
t->type = (ds4q_type)read_u32_le_fp(fp, "GGUF tensor type");
|
|
t->old_offset = read_u64_le_fp(fp, "GGUF tensor offset");
|
|
t->size = tensor_nbytes(t->type, t->ne, t->n_dims);
|
|
}
|
|
off_t meta_end = ftello(fp);
|
|
if (meta_end < 0) die("GGUF ftell failed");
|
|
g.data_offset = ds4q_pad((size_t)meta_end, g.alignment);
|
|
char **keys = xmalloc((size_t)g.n_tensors * sizeof(keys[0]));
|
|
for (uint64_t i = 0; i < g.n_tensors; i++) keys[i] = g.tensors[i].name;
|
|
hmap_build(&g.tensor_map, keys, (int)g.n_tensors);
|
|
free(keys);
|
|
fclose(fp);
|
|
return g;
|
|
}
|
|
|
|
static byte_buf read_gguf_tensor_data(const gguf_file *g, const char *path, const char *name) {
|
|
int idx = hmap_get(&g->tensor_map, name);
|
|
if (idx < 0) {
|
|
fprintf(stderr, "error: tensor not found in GGUF: %s\n", name);
|
|
exit(1);
|
|
}
|
|
const tensor_meta *t = &g->tensors[idx];
|
|
byte_buf b = { .size = t->size, .data = xmalloc(t->size) };
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) die_errno("open GGUF", path);
|
|
if (fseeko(fp, (off_t)(g->data_offset + t->old_offset), SEEK_SET) != 0) die_errno("seek GGUF", path);
|
|
if (b.size && fread(b.data, 1, b.size, fp) != b.size) die_errno("read GGUF tensor", path);
|
|
fclose(fp);
|
|
return b;
|
|
}
|
|
|
|
static uint64_t fnv1a64_bytes(const uint8_t *data, size_t n) {
|
|
uint64_t h = 1469598103934665603ull;
|
|
for (size_t i = 0; i < n; i++) {
|
|
h ^= data[i];
|
|
h *= 1099511628211ull;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static output_context build_output_context(const gguf_file *tmpl, const quant_policy *policy, const imatrix_store *im) {
|
|
output_context out = {0};
|
|
out.n_tensors = tmpl->n_tensors;
|
|
out.n_kv_extra = extra_imatrix_kv_count(im);
|
|
out.alignment = tmpl->alignment;
|
|
out.tensors = xcalloc((size_t)out.n_tensors, sizeof(out.tensors[0]));
|
|
size_t tensor_info = 0;
|
|
size_t off = 0;
|
|
for (uint64_t i = 0; i < out.n_tensors; i++) {
|
|
const tensor_meta *src = &tmpl->tensors[i];
|
|
tensor_meta *dst = &out.tensors[i];
|
|
*dst = *src;
|
|
dst->name = src->name;
|
|
ds4q_type type = policy_type(policy, src->name, src);
|
|
if (type == DS4Q_TYPE_COUNT) type = src->type;
|
|
if (type != DS4Q_TYPE_I32 && !is_quantizable_target(type)) die("unsupported planned tensor type");
|
|
if (ds4q_can_quantize(type) && src->ne[0] % ds4q_block_size(type) != 0) die("ne[0] not divisible by block size");
|
|
dst->type = type;
|
|
dst->size = tensor_nbytes(type, src->ne, src->n_dims);
|
|
dst->new_offset = off;
|
|
off += ds4q_pad(dst->size, tmpl->alignment);
|
|
tensor_info += gguf_string_size(dst->name) + 4 + (size_t)dst->n_dims * 8 + 4 + 8;
|
|
}
|
|
out.tensor_bytes = off;
|
|
out.meta_size = 4 + 4 + 8 + 8 + tmpl->kv_raw_len + extra_imatrix_kv_size(im) + tensor_info;
|
|
out.data_offset = ds4q_pad(out.meta_size, tmpl->alignment);
|
|
return out;
|
|
}
|
|
|
|
static void write_padding(FILE *fp, size_t n) {
|
|
static const uint8_t zeros[4096] = {0};
|
|
while (n) {
|
|
size_t chunk = n < sizeof(zeros) ? n : sizeof(zeros);
|
|
if (fwrite(zeros, 1, chunk, fp) != chunk) die("write padding failed");
|
|
n -= chunk;
|
|
}
|
|
}
|
|
|
|
static void write_full_gguf(st_db *db, const gguf_file *tmpl, const output_context *out_ctx,
|
|
const char *out_path, int n_experts, int n_threads,
|
|
const imatrix_store *imatrix) {
|
|
FILE *fp = fopen(out_path, "wb");
|
|
if (!fp) die_errno("open output", out_path);
|
|
if (fwrite("GGUF", 1, 4, fp) != 4) die("write GGUF magic failed");
|
|
write_u32(fp, tmpl->version);
|
|
write_u64(fp, tmpl->n_tensors);
|
|
write_u64(fp, tmpl->n_kv + out_ctx->n_kv_extra);
|
|
if (fwrite(tmpl->kv_raw, 1, tmpl->kv_raw_len, fp) != tmpl->kv_raw_len) die("write GGUF KV failed");
|
|
write_imatrix_kvs(fp, imatrix);
|
|
for (uint64_t i = 0; i < out_ctx->n_tensors; i++) {
|
|
const tensor_meta *t = &out_ctx->tensors[i];
|
|
write_gguf_string(fp, t->name);
|
|
write_u32(fp, (uint32_t)t->n_dims);
|
|
for (int j = 0; j < t->n_dims; j++) write_u64(fp, (uint64_t)t->ne[j]);
|
|
write_u32(fp, (uint32_t)t->type);
|
|
write_u64(fp, t->new_offset);
|
|
}
|
|
long pos = ftell(fp);
|
|
if (pos < 0) die("ftell failed");
|
|
if ((size_t)pos > out_ctx->data_offset) die("GGUF metadata larger than planned");
|
|
write_padding(fp, out_ctx->data_offset - (size_t)pos);
|
|
|
|
for (uint64_t i = 0; i < out_ctx->n_tensors; i++) {
|
|
const tensor_meta *src = &tmpl->tensors[i];
|
|
const tensor_meta *dst = &out_ctx->tensors[i];
|
|
fprintf(stderr, "[%4" PRIu64 "/%4" PRIu64 "] %s -> %s\n", i + 1, out_ctx->n_tensors, dst->name, ds4q_type_name(dst->type));
|
|
byte_buf data = generate_tensor(db, dst->name, src, dst->type, n_experts, n_threads, imatrix);
|
|
size_t expected = dst->size;
|
|
if (data.size != expected) {
|
|
fprintf(stderr, "error: generated size mismatch for %s: got %zu expected %zu\n", dst->name, data.size, expected);
|
|
exit(1);
|
|
}
|
|
if (fwrite(data.data, 1, data.size, fp) != data.size) die_errno("write tensor", out_path);
|
|
size_t padded = ds4q_pad(data.size, out_ctx->alignment);
|
|
write_padding(fp, padded - data.size);
|
|
fprintf(stderr, " generated %.2f MiB\n", (double)data.size / 1048576.0);
|
|
free(data.data);
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
static void print_plan(const gguf_file *tmpl, const output_context *out_ctx) {
|
|
size_t tensor_bytes = 0;
|
|
size_t changed = 0;
|
|
for (uint64_t i = 0; i < out_ctx->n_tensors; i++) {
|
|
tensor_bytes += out_ctx->tensors[i].size;
|
|
const tensor_meta *src = &tmpl->tensors[i];
|
|
const tensor_meta *dst = &out_ctx->tensors[i];
|
|
if (src->type != dst->type) {
|
|
changed++;
|
|
printf("type_change: %s %s -> %s\n", dst->name, ds4q_type_name(src->type), ds4q_type_name(dst->type));
|
|
}
|
|
}
|
|
printf("n_tensors: %" PRIu64 "\n", out_ctx->n_tensors);
|
|
printf("meta_bytes: %zu\n", out_ctx->data_offset);
|
|
printf("tensor_bytes_unpadded: %zu\n", tensor_bytes);
|
|
printf("approx_file_bytes: %zu\n", out_ctx->data_offset + out_ctx->tensor_bytes);
|
|
printf("type_changes: %zu\n", changed);
|
|
}
|
|
|
|
/* =====
|
|
* CLI
|
|
*/
|
|
|
|
typedef struct {
|
|
char *hf_dir;
|
|
char *template_gguf;
|
|
char *out_gguf;
|
|
char *compare_gguf;
|
|
char *compare_tensor;
|
|
char *imatrix_file;
|
|
quant_policy policy;
|
|
int n_experts;
|
|
int n_threads;
|
|
bool dry_run;
|
|
bool overwrite;
|
|
bool imatrix_strict;
|
|
} params;
|
|
|
|
static void usage(const char *argv0) {
|
|
printf("usage: %s --hf DIR --template MODEL.gguf --out OUT.gguf [options]\n", argv0);
|
|
printf("\nDeepSeek V4 Flash/Pro safetensors -> GGUF quantizer in plain C.\n\n");
|
|
printf("options:\n");
|
|
printf(" --hf DIR Hugging Face model directory with model.safetensors.index.json\n");
|
|
printf(" --template FILE existing DS4 GGUF used for metadata, tensor order, shapes\n");
|
|
printf(" --out FILE output GGUF path\n");
|
|
printf(" --compare-gguf FILE reference GGUF for --compare-tensor, default template\n");
|
|
printf(" --compare-tensor NAME regenerate one tensor, byte-compare, and exit\n");
|
|
printf(" --overwrite replace --out if it already exists\n");
|
|
printf(" --dry-run print output plan without reading HF tensor data\n");
|
|
printf(" --imatrix FILE legacy .dat imatrix from ds4 --imatrix-out\n");
|
|
printf(" --imatrix-strict fail if a quantized tensor has no matching imatrix vector\n");
|
|
printf(" --experts TYPE set routed w1/w2/w3 expert tensors to TYPE\n");
|
|
printf(" --routed-w1 TYPE routed gate expert tensor type\n");
|
|
printf(" --routed-w2 TYPE routed down expert tensor type\n");
|
|
printf(" --routed-w3 TYPE routed up expert tensor type\n");
|
|
printf(" --attention-proj TYPE attn_q/kv/output projection type\n");
|
|
printf(" --attention TYPE other 2D attention/indexer/compressor type\n");
|
|
printf(" --shared TYPE shared expert tensor type\n");
|
|
printf(" --embedding TYPE token embedding type\n");
|
|
printf(" --output TYPE output.* tensor type\n");
|
|
printf(" --dense TYPE remaining 2D+ non-routed tensor type\n");
|
|
printf(" --tensor-type PFX=TYPE exact tensor-name or prefix override; may repeat\n");
|
|
printf(" --n-experts N routed expert count, default template metadata\n");
|
|
printf(" --threads N expert worker count, default 8\n");
|
|
printf("\nTYPE examples: f16, f32, bf16, q8_0, q4_k, q2_k, iq2_xxs\n");
|
|
}
|
|
|
|
static char *need_value(int argc, char **argv, int *i, const char *arg) {
|
|
if (++*i >= argc) {
|
|
fprintf(stderr, "error: missing value for %s\n", arg);
|
|
exit(1);
|
|
}
|
|
return argv[*i];
|
|
}
|
|
|
|
static bool file_exists(const char *path) {
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) return false;
|
|
fclose(fp);
|
|
return true;
|
|
}
|
|
|
|
static params parse_args(int argc, char **argv) {
|
|
params p = {0};
|
|
p.policy.routed_w1 = p.policy.routed_w2 = p.policy.routed_w3 = DS4Q_TYPE_COUNT;
|
|
p.policy.attention_proj = p.policy.attention = p.policy.shared = DS4Q_TYPE_COUNT;
|
|
p.policy.embedding = p.policy.output = p.policy.dense = DS4Q_TYPE_COUNT;
|
|
p.n_experts = 0;
|
|
p.n_threads = 8;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
const char *arg = argv[i];
|
|
if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) {
|
|
usage(argv[0]);
|
|
exit(0);
|
|
} else if (strcmp(arg, "--hf") == 0) {
|
|
p.hf_dir = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--template") == 0) {
|
|
p.template_gguf = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--out") == 0) {
|
|
p.out_gguf = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--compare-gguf") == 0) {
|
|
p.compare_gguf = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--compare-tensor") == 0) {
|
|
p.compare_tensor = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--overwrite") == 0) {
|
|
p.overwrite = true;
|
|
} else if (strcmp(arg, "--dry-run") == 0) {
|
|
p.dry_run = true;
|
|
} else if (strcmp(arg, "--imatrix") == 0) {
|
|
p.imatrix_file = need_value(argc, argv, &i, arg);
|
|
} else if (strcmp(arg, "--imatrix-strict") == 0) {
|
|
p.imatrix_strict = true;
|
|
} else if (strcmp(arg, "--experts") == 0 || strcmp(arg, "--routed") == 0) {
|
|
ds4q_type t = parse_type(need_value(argc, argv, &i, arg));
|
|
p.policy.routed_w1 = p.policy.routed_w2 = p.policy.routed_w3 = t;
|
|
} else if (strcmp(arg, "--routed-w1") == 0 || strcmp(arg, "--routed-gate") == 0) {
|
|
p.policy.routed_w1 = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--routed-w2") == 0 || strcmp(arg, "--routed-down") == 0) {
|
|
p.policy.routed_w2 = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--routed-w3") == 0 || strcmp(arg, "--routed-up") == 0) {
|
|
p.policy.routed_w3 = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--attention-proj") == 0 || strcmp(arg, "--attn-proj") == 0) {
|
|
p.policy.attention_proj = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--attention") == 0) {
|
|
p.policy.attention = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--shared") == 0) {
|
|
p.policy.shared = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--embedding") == 0) {
|
|
p.policy.embedding = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--output") == 0) {
|
|
p.policy.output = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--dense") == 0) {
|
|
p.policy.dense = parse_type(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--tensor-type") == 0) {
|
|
char *spec = need_value(argc, argv, &i, arg);
|
|
char *eq = strchr(spec, '=');
|
|
if (!eq || eq == spec || !eq[1]) die("bad --tensor-type, expected NAME=TYPE");
|
|
*eq = '\0';
|
|
p.policy.overrides = xrealloc(p.policy.overrides, (size_t)(p.policy.n_overrides + 1) * sizeof(p.policy.overrides[0]));
|
|
p.policy.overrides[p.policy.n_overrides++] = (type_override){ xstrdup(spec), parse_type(eq + 1) };
|
|
} else if (strcmp(arg, "--n-experts") == 0) {
|
|
p.n_experts = atoi(need_value(argc, argv, &i, arg));
|
|
} else if (strcmp(arg, "--threads") == 0) {
|
|
p.n_threads = atoi(need_value(argc, argv, &i, arg));
|
|
} else {
|
|
fprintf(stderr, "error: unknown argument: %s\n", arg);
|
|
exit(1);
|
|
}
|
|
}
|
|
if (!p.hf_dir) die("--hf is required");
|
|
if (!p.template_gguf) die("--template is required");
|
|
if (!p.dry_run && !p.compare_tensor && !p.out_gguf) die("--out is required unless --dry-run or --compare-tensor is used");
|
|
if (p.compare_tensor && !p.compare_gguf) p.compare_gguf = p.template_gguf;
|
|
if (p.out_gguf && file_exists(p.out_gguf) && !p.overwrite) die("output exists; use --overwrite");
|
|
return p;
|
|
}
|
|
|
|
static void free_gguf_file(gguf_file *g) {
|
|
free(g->path);
|
|
free(g->kv_raw);
|
|
for (uint64_t i = 0; i < g->n_tensors; i++) free(g->tensors[i].name);
|
|
free(g->tensors);
|
|
hmap_free(&g->tensor_map);
|
|
memset(g, 0, sizeof(*g));
|
|
}
|
|
|
|
static void compare_one_tensor(st_db *db, const gguf_file *tmpl, const output_context *out_ctx,
|
|
const params *p, const imatrix_store *imatrix) {
|
|
int idx = hmap_get(&tmpl->tensor_map, p->compare_tensor);
|
|
if (idx < 0) {
|
|
fprintf(stderr, "error: tensor not found in template: %s\n", p->compare_tensor);
|
|
exit(1);
|
|
}
|
|
fprintf(stderr, "regenerating %s as %s\n",
|
|
p->compare_tensor, ds4q_type_name(out_ctx->tensors[idx].type));
|
|
byte_buf generated = generate_tensor(db, p->compare_tensor, &tmpl->tensors[idx],
|
|
out_ctx->tensors[idx].type, p->n_experts, p->n_threads, imatrix);
|
|
gguf_file ref = load_gguf_metadata(p->compare_gguf);
|
|
byte_buf reference = read_gguf_tensor_data(&ref, p->compare_gguf, p->compare_tensor);
|
|
printf("tensor: %s\n", p->compare_tensor);
|
|
printf("type: %s\n", ds4q_type_name(out_ctx->tensors[idx].type));
|
|
printf("generated_bytes: %zu\n", generated.size);
|
|
printf("reference_bytes: %zu\n", reference.size);
|
|
printf("generated_fnv1a64: %016" PRIx64 "\n", fnv1a64_bytes(generated.data, generated.size));
|
|
printf("reference_fnv1a64: %016" PRIx64 "\n", fnv1a64_bytes(reference.data, reference.size));
|
|
size_t mismatches = 0;
|
|
size_t first = SIZE_MAX;
|
|
const size_t n = generated.size < reference.size ? generated.size : reference.size;
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (generated.data[i] != reference.data[i]) {
|
|
if (first == SIZE_MAX) first = i;
|
|
mismatches++;
|
|
}
|
|
}
|
|
if (generated.size != reference.size) {
|
|
if (first == SIZE_MAX) first = n;
|
|
mismatches += generated.size > reference.size ? generated.size - reference.size : reference.size - generated.size;
|
|
}
|
|
if (!mismatches) {
|
|
printf("byte_compare: OK\n");
|
|
} else {
|
|
printf("byte_compare: FAIL mismatches=%zu first=%zu\n", mismatches, first);
|
|
}
|
|
free(generated.data);
|
|
free(reference.data);
|
|
free_gguf_file(&ref);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
params p = parse_args(argc, argv);
|
|
imatrix_store imatrix = {0};
|
|
if (p.imatrix_file) imatrix_load(&imatrix, p.imatrix_file, p.imatrix_strict);
|
|
|
|
gguf_file tmpl = load_gguf_metadata(p.template_gguf);
|
|
if (p.n_experts <= 0) {
|
|
if (tmpl.n_experts > 0) {
|
|
p.n_experts = tmpl.n_experts;
|
|
fprintf(stderr, "using %d routed experts from template metadata\n", p.n_experts);
|
|
} else {
|
|
p.n_experts = 256;
|
|
fprintf(stderr, "warning: template has no deepseek4.expert_count; using Flash default %d routed experts\n", p.n_experts);
|
|
}
|
|
} else {
|
|
fprintf(stderr, "using %d routed experts from --n-experts\n", p.n_experts);
|
|
}
|
|
output_context out_ctx = build_output_context(&tmpl, &p.policy, &imatrix);
|
|
print_plan(&tmpl, &out_ctx);
|
|
if (p.dry_run) return 0;
|
|
|
|
st_db db;
|
|
db_open(&db, p.hf_dir);
|
|
if (p.compare_tensor) {
|
|
compare_one_tensor(&db, &tmpl, &out_ctx, &p, &imatrix);
|
|
db_close(&db);
|
|
imatrix_free(&imatrix);
|
|
free_gguf_file(&tmpl);
|
|
free(out_ctx.tensors);
|
|
return 0;
|
|
}
|
|
write_full_gguf(&db, &tmpl, &out_ctx, p.out_gguf, p.n_experts, p.n_threads, &imatrix);
|
|
fprintf(stderr, "wrote %s\n", p.out_gguf);
|
|
|
|
db_close(&db);
|
|
imatrix_free(&imatrix);
|
|
free_gguf_file(&tmpl);
|
|
free(out_ctx.tensors);
|
|
for (int i = 0; i < p.policy.n_overrides; i++) free(p.policy.overrides[i].prefix);
|
|
free(p.policy.overrides);
|
|
return 0;
|
|
}
|