2416bc9079
* feat: standardize runtime output in English * test: cover English CLI output
393 lines
18 KiB
C
393 lines
18 KiB
C
/* Motore di inferenza OLMoE in C puro, con EXPERT-STREAMING dal disco.
|
|
* Porting del motore Python (engine.py). Obiettivo Stadio A: produrre gli STESSI
|
|
* token id del riferimento (ref.json) -> valida il core prima di scalare a GLM-5.2.
|
|
*
|
|
* Densa (embed, attn, router, norme, lm_head) residente in RAM (float32).
|
|
* Expert letti dal disco on-demand via pread+fadvise(DONTNEED), cache LRU per-layer.
|
|
* Matmul multi-thread con OpenMP (niente BLAS).
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
#if defined(__APPLE__) || defined(__linux__)
|
|
#include <sys/resource.h>
|
|
#endif
|
|
#include "st.h"
|
|
|
|
/* ---------- config ---------- */
|
|
typedef struct {
|
|
int hidden, n_layers, n_heads, n_kv_heads, head_dim;
|
|
int n_experts, topk, inter, vocab;
|
|
float theta, eps; int norm_topk;
|
|
} Cfg;
|
|
|
|
/* ---------- pesi densi per-layer ---------- */
|
|
typedef struct {
|
|
float *in_ln, *post_ln, *q, *k, *v, *o, *qn, *kn, *gate;
|
|
} Layer;
|
|
|
|
/* ---------- cache LRU degli expert (pesi QUANTIZZATI) ----------
|
|
* Ogni weight [out,in] tenuto come int8 (per-riga) + scala float per riga.
|
|
* Cosi' la RAM-cache scende da 4 byte/param (f32) a 1 byte/param: e' il
|
|
* meccanismo che fa stare GLM-5.2 nei 15 GB. dequant-on-use nel matmul. */
|
|
typedef struct { int eid; int8_t *g, *u, *d; float *gs, *us, *ds; uint64_t used; } Slot;
|
|
typedef struct { Slot *slots; int n, cap; } LCache;
|
|
|
|
typedef struct {
|
|
Cfg c;
|
|
shards S;
|
|
int quant_bits; /* bit di quantizzazione degli expert (2..8); 16 = f32 */
|
|
float *embed, *lm_head, *final_norm;
|
|
Layer *L;
|
|
LCache *cache; /* [n_layers] */
|
|
uint64_t clock, hits, miss;
|
|
/* kv-cache per-layer: K,V come [H * maxT * head_dim] */
|
|
float **K, **V; int kv_len, max_t;
|
|
double dense_load_s;
|
|
} Model;
|
|
|
|
/* ---------- utility ---------- */
|
|
static double now_s(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; }
|
|
static double rss_gb(void) { struct rusage r; getrusage(RUSAGE_SELF, &r); return r.ru_maxrss / (1024.0*1024.0); }
|
|
static float *falloc(int64_t n) { float *p = malloc(n*sizeof(float)); if(!p){fprintf(stderr,"OOM %ld\n",(long)n);exit(1);} return p; }
|
|
|
|
/* y[S,O] = x[S,I] @ W^T, W e' [O,I] row-major */
|
|
static void matmul(float *y, const float *x, const float *W, int S, int I, int O) {
|
|
#pragma omp parallel for schedule(static)
|
|
for (int o = 0; o < O; o++) {
|
|
const float *w = W + (int64_t)o * I;
|
|
for (int s = 0; s < S; s++) {
|
|
const float *xs = x + (int64_t)s * I;
|
|
float acc = 0.f;
|
|
for (int i = 0; i < I; i++) acc += xs[i] * w[i];
|
|
y[(int64_t)s * O + o] = acc;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* y[1,O] = x[1,I] @ W^T con W quantizzato: q[O,I] int8 + scala per riga.
|
|
* W[o,i] ~= q[o,i]*scale[o] -> y[o] = scale[o] * sum_i x[i]*q[o,i]. */
|
|
static void matmul_q(float *y, const float *x, const int8_t *q, const float *scale, int I, int O) {
|
|
#pragma omp parallel for schedule(static)
|
|
for (int o = 0; o < O; o++) {
|
|
const int8_t *w = q + (int64_t)o * I;
|
|
float acc = 0.f;
|
|
for (int i = 0; i < I; i++) acc += x[i] * (float)w[i];
|
|
y[o] = acc * scale[o];
|
|
}
|
|
}
|
|
|
|
/* quantizza un weight f32 [O,I] -> int8 q[O,I] + scala[O], simmetrica per riga.
|
|
* Replica quant_dequant() del Python: scale = amax(|w|, riga)/qmax, q = round(w/scale). */
|
|
static void quantize_rows(const float *w, int8_t *q, float *scale, int O, int I, int bits) {
|
|
int qmax = (1 << (bits - 1)) - 1; /* 8->127, 4->7, 2->1 */
|
|
#pragma omp parallel for schedule(static)
|
|
for (int o = 0; o < O; o++) {
|
|
const float *wr = w + (int64_t)o * I;
|
|
float amax = 0.f; for (int i = 0; i < I; i++) { float a = fabsf(wr[i]); if (a > amax) amax = a; }
|
|
float s = amax / qmax; if (s < 1e-8f) s = 1e-8f;
|
|
scale[o] = s;
|
|
int8_t *qr = q + (int64_t)o * I;
|
|
for (int i = 0; i < I; i++) {
|
|
int v = (int)lrintf(wr[i] / s);
|
|
if (v > qmax) v = qmax;
|
|
if (v < -qmax-1) v = -qmax-1;
|
|
qr[i] = (int8_t)v;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* rmsnorm su una riga di lunghezza D, in-place su out (out puo' essere == x) */
|
|
static void rmsnorm_row(float *out, const float *x, const float *w, int D, float eps) {
|
|
double ms = 0; for (int i = 0; i < D; i++) ms += (double)x[i]*x[i];
|
|
float r = 1.f / sqrtf((float)(ms / D) + eps);
|
|
for (int i = 0; i < D; i++) out[i] = x[i] * r * w[i];
|
|
}
|
|
|
|
static void softmax_row(float *x, int n) {
|
|
float m = -1e30f; for (int i = 0; i < n; i++) if (x[i] > m) m = x[i];
|
|
float s = 0; for (int i = 0; i < n; i++) { x[i] = expf(x[i]-m); s += x[i]; }
|
|
for (int i = 0; i < n; i++) x[i] /= s;
|
|
}
|
|
|
|
/* ---------- caricamento ---------- */
|
|
static void load_cfg(Cfg *c, const char *snap) {
|
|
char path[2048]; snprintf(path, sizeof(path), "%s/config.json", snap);
|
|
FILE *f = fopen(path, "rb"); if(!f){perror(path);exit(1);}
|
|
fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
|
|
char *buf = malloc(n+1); if(fread(buf,1,n,f)!=(size_t)n){} buf[n]=0; fclose(f);
|
|
char *arena=NULL; jval *r = json_parse(buf, &arena);
|
|
c->hidden = (int)json_get(r,"hidden_size")->num;
|
|
c->n_layers = (int)json_get(r,"num_hidden_layers")->num;
|
|
c->n_heads = (int)json_get(r,"num_attention_heads")->num;
|
|
c->n_kv_heads= (int)json_get(r,"num_key_value_heads")->num;
|
|
c->n_experts = (int)json_get(r,"num_experts")->num;
|
|
c->topk = (int)json_get(r,"num_experts_per_tok")->num;
|
|
c->inter = (int)json_get(r,"intermediate_size")->num;
|
|
c->vocab = (int)json_get(r,"vocab_size")->num;
|
|
c->head_dim = c->hidden / c->n_heads;
|
|
jval *th = json_get(r,"rope_theta"); c->theta = th ? (float)th->num : 10000.f;
|
|
jval *ep = json_get(r,"rms_norm_eps"); c->eps = ep ? (float)ep->num : 1e-5f;
|
|
jval *nt = json_get(r,"norm_topk_prob"); c->norm_topk = (nt && nt->t==J_BOOL) ? nt->boolean : 0;
|
|
free(buf); free(arena);
|
|
}
|
|
|
|
static float *load_t(Model *m, const char *name) {
|
|
int64_t n = st_numel(&m->S, name);
|
|
if (n < 0) { fprintf(stderr, "missing %s\n", name); exit(1); }
|
|
float *p = falloc(n);
|
|
st_read_f32(&m->S, name, p, 0); /* densa: niente DONTNEED, resta residente */
|
|
return p;
|
|
}
|
|
|
|
static void model_init(Model *m, const char *snap, int cap, int bits) {
|
|
memset(m, 0, sizeof(*m));
|
|
m->quant_bits = bits;
|
|
load_cfg(&m->c, snap);
|
|
st_init(&m->S, snap);
|
|
Cfg *c = &m->c;
|
|
double t0 = now_s();
|
|
m->embed = load_t(m, "model.embed_tokens.weight");
|
|
m->lm_head = load_t(m, "lm_head.weight");
|
|
m->final_norm = load_t(m, "model.norm.weight");
|
|
m->L = calloc(c->n_layers, sizeof(Layer));
|
|
char nm[256];
|
|
for (int i = 0; i < c->n_layers; i++) {
|
|
Layer *l = &m->L[i];
|
|
#define LD(field, suffix) snprintf(nm,sizeof(nm),"model.layers.%d." suffix,i); l->field = load_t(m,nm)
|
|
LD(in_ln, "input_layernorm.weight");
|
|
LD(post_ln,"post_attention_layernorm.weight");
|
|
LD(q, "self_attn.q_proj.weight"); LD(k, "self_attn.k_proj.weight");
|
|
LD(v, "self_attn.v_proj.weight"); LD(o, "self_attn.o_proj.weight");
|
|
LD(qn,"self_attn.q_norm.weight"); LD(kn,"self_attn.k_norm.weight");
|
|
LD(gate, "mlp.gate.weight");
|
|
#undef LD
|
|
}
|
|
m->cache = calloc(c->n_layers, sizeof(LCache));
|
|
for (int i = 0; i < c->n_layers; i++) { m->cache[i].cap = cap; m->cache[i].slots = calloc(cap, sizeof(Slot)); }
|
|
m->dense_load_s = now_s() - t0;
|
|
}
|
|
|
|
/* legge un weight dal disco (streaming) e lo quantizza in q[O,I]+scale[O] */
|
|
static void load_expert_w(Model *m, const char *name, int8_t *q, float *scale, int O, int I, float *tmp) {
|
|
st_read_f32(&m->S, name, tmp, 1); /* pread + fadvise DONTNEED */
|
|
quantize_rows(tmp, q, scale, O, I, m->quant_bits);
|
|
}
|
|
|
|
/* ---------- cache expert: ritorna i pesi quantizzati (q+scale) da cache o disco ---------- */
|
|
static void expert_get(Model *m, int layer, int eid, Slot **out) {
|
|
LCache *lc = &m->cache[layer];
|
|
for (int i = 0; i < lc->n; i++) if (lc->slots[i].eid == eid) {
|
|
m->hits++; lc->slots[i].used = ++m->clock; *out = &lc->slots[i]; return;
|
|
}
|
|
m->miss++;
|
|
Cfg *c = &m->c;
|
|
int64_t ng = (int64_t)c->inter * c->hidden, nd = (int64_t)c->hidden * c->inter;
|
|
Slot *s;
|
|
if (lc->n < lc->cap) {
|
|
s = &lc->slots[lc->n++];
|
|
s->g = malloc(ng); s->u = malloc(ng); s->d = malloc(nd);
|
|
s->gs = falloc(c->inter); s->us = falloc(c->inter); s->ds = falloc(c->hidden);
|
|
} else { int lru = 0; for (int i = 1; i < lc->n; i++) if (lc->slots[i].used < lc->slots[lru].used) lru = i; s = &lc->slots[lru]; }
|
|
float *tmp = falloc(ng > nd ? ng : nd);
|
|
char nm[256];
|
|
snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.gate_proj.weight",layer,eid); load_expert_w(m,nm,s->g,s->gs,c->inter,c->hidden,tmp);
|
|
snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.up_proj.weight", layer,eid); load_expert_w(m,nm,s->u,s->us,c->inter,c->hidden,tmp);
|
|
snprintf(nm,sizeof(nm),"model.layers.%d.mlp.experts.%d.down_proj.weight",layer,eid); load_expert_w(m,nm,s->d,s->ds,c->hidden,c->inter,tmp);
|
|
free(tmp);
|
|
s->eid = eid; s->used = ++m->clock;
|
|
*out = s;
|
|
}
|
|
|
|
/* ---------- RoPE su un vettore di una testa (head_dim) a posizione assoluta pos ---------- */
|
|
static void rope_head(float *x, int pos, const Cfg *c) {
|
|
int h = c->head_dim / 2;
|
|
for (int j = 0; j < h; j++) {
|
|
float inv = powf(c->theta, -2.0f * j / c->head_dim);
|
|
float ang = pos * inv, cs = cosf(ang), sn = sinf(ang);
|
|
float a = x[j], b = x[j+h];
|
|
x[j] = a*cs - b*sn;
|
|
x[j+h] = b*cs + a*sn;
|
|
}
|
|
}
|
|
|
|
/* attenzione sui token nuovi x[S,hidden]; pos_base = posizione assoluta del primo token nuovo */
|
|
static void attention(Model *m, Layer *l, int layer, float *x, int S, int pos_base, float *out) {
|
|
Cfg *c = &m->c; int H = c->n_heads, hd = c->head_dim, D = c->hidden;
|
|
float *q = falloc((int64_t)S*D), *k = falloc((int64_t)S*D), *vv = falloc((int64_t)S*D);
|
|
matmul(q, x, l->q, S, D, D);
|
|
matmul(k, x, l->k, S, D, D);
|
|
matmul(vv, x, l->v, S, D, D);
|
|
/* qk-norm sull'intero vettore hidden, poi RoPE per testa */
|
|
for (int s = 0; s < S; s++) {
|
|
rmsnorm_row(q + (int64_t)s*D, q + (int64_t)s*D, l->qn, D, c->eps);
|
|
rmsnorm_row(k + (int64_t)s*D, k + (int64_t)s*D, l->kn, D, c->eps);
|
|
int pos = pos_base + s;
|
|
for (int hh = 0; hh < H; hh++) { rope_head(q + (int64_t)s*D + hh*hd, pos, c); rope_head(k + (int64_t)s*D + hh*hd, pos, c); }
|
|
}
|
|
/* scrive k,v nella kv-cache alle posizioni pos_base..pos_base+S-1 */
|
|
for (int s = 0; s < S; s++) for (int hh = 0; hh < H; hh++) {
|
|
int t = pos_base + s;
|
|
memcpy(m->K[layer] + ((int64_t)hh*m->max_t + t)*hd, k + (int64_t)s*D + hh*hd, hd*sizeof(float));
|
|
memcpy(m->V[layer] + ((int64_t)hh*m->max_t + t)*hd, vv + (int64_t)s*D + hh*hd, hd*sizeof(float));
|
|
}
|
|
int Tk = pos_base + S; /* numero di key totali disponibili */
|
|
float scale = 1.f / sqrtf((float)hd);
|
|
float *ctx = falloc((int64_t)S*D);
|
|
#pragma omp parallel for collapse(2) schedule(static)
|
|
for (int hh = 0; hh < H; hh++) {
|
|
for (int s = 0; s < S; s++) {
|
|
int qpos = pos_base + s;
|
|
const float *qv = q + (int64_t)s*D + hh*hd;
|
|
float sc[4096];
|
|
for (int t = 0; t <= qpos; t++) { /* causale: t <= qpos */
|
|
const float *kv = m->K[layer] + ((int64_t)hh*m->max_t + t)*hd;
|
|
float acc = 0; for (int dd = 0; dd < hd; dd++) acc += qv[dd]*kv[dd];
|
|
sc[t] = acc * scale;
|
|
}
|
|
softmax_row(sc, qpos+1);
|
|
float *cx = ctx + (int64_t)s*D + hh*hd;
|
|
for (int dd = 0; dd < hd; dd++) cx[dd] = 0;
|
|
for (int t = 0; t <= qpos; t++) {
|
|
const float *vrow = m->V[layer] + ((int64_t)hh*m->max_t + t)*hd;
|
|
float a = sc[t];
|
|
for (int dd = 0; dd < hd; dd++) cx[dd] += a * vrow[dd];
|
|
}
|
|
}
|
|
}
|
|
(void)Tk;
|
|
matmul(out, ctx, l->o, S, D, D);
|
|
free(q); free(k); free(vv); free(ctx);
|
|
}
|
|
|
|
/* MoE sui token x[S,hidden] -> out[S,hidden] */
|
|
static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out) {
|
|
Cfg *c = &m->c; int D = c->hidden, E = c->n_experts, K = c->topk, I = c->inter;
|
|
float *logits = falloc((int64_t)S*E);
|
|
matmul(logits, x, l->gate, S, D, E);
|
|
memset(out, 0, (int64_t)S*D*sizeof(float));
|
|
float *g = falloc(I), *u = falloc(I), *hh = falloc(D);
|
|
for (int s = 0; s < S; s++) {
|
|
float *pr = logits + (int64_t)s*E;
|
|
softmax_row(pr, E);
|
|
/* top-K indici (selezione parziale) */
|
|
int idx[64]; float val[64];
|
|
for (int kk = 0; kk < K; kk++) {
|
|
int best = -1; float bv = -1e30f;
|
|
for (int e = 0; e < E; e++) {
|
|
int taken = 0; for (int j = 0; j < kk; j++) if (idx[j]==e){taken=1;break;}
|
|
if (!taken && pr[e] > bv) { bv = pr[e]; best = e; }
|
|
}
|
|
idx[kk] = best; val[kk] = bv;
|
|
}
|
|
if (c->norm_topk) { float sm=0; for(int kk=0;kk<K;kk++) sm+=val[kk]; for(int kk=0;kk<K;kk++) val[kk]/=sm; }
|
|
const float *xs = x + (int64_t)s*D;
|
|
for (int kk = 0; kk < K; kk++) {
|
|
Slot *e; expert_get(m, layer, idx[kk], &e);
|
|
matmul_q(g, xs, e->g, e->gs, D, I); /* gate_proj [I,D] */
|
|
matmul_q(u, xs, e->u, e->us, D, I); /* up_proj [I,D] */
|
|
for (int i = 0; i < I; i++) { float gv = g[i]; g[i] = (gv / (1.f + expf(-gv))) * u[i]; }
|
|
matmul_q(hh, g, e->d, e->ds, I, D); /* down_proj [D,I] */
|
|
float w = val[kk];
|
|
float *os = out + (int64_t)s*D;
|
|
for (int d = 0; d < D; d++) os[d] += w * hh[d];
|
|
}
|
|
}
|
|
free(logits); free(g); free(u); free(hh);
|
|
}
|
|
|
|
/* un passo: token nuovi ids[S] a posizione pos_base. Ritorna logits dell'ultimo token (malloc'd). */
|
|
static float *step(Model *m, const int *ids, int S, int pos_base) {
|
|
Cfg *c = &m->c; int D = c->hidden;
|
|
float *x = falloc((int64_t)S*D);
|
|
for (int s = 0; s < S; s++) memcpy(x + (int64_t)s*D, m->embed + (int64_t)ids[s]*D, D*sizeof(float));
|
|
float *nrm = falloc((int64_t)S*D), *tmp = falloc((int64_t)S*D);
|
|
for (int i = 0; i < c->n_layers; i++) {
|
|
Layer *l = &m->L[i];
|
|
for (int s = 0; s < S; s++) rmsnorm_row(nrm + (int64_t)s*D, x + (int64_t)s*D, l->in_ln, D, c->eps);
|
|
attention(m, l, i, nrm, S, pos_base, tmp);
|
|
for (int64_t j = 0; j < (int64_t)S*D; j++) x[j] += tmp[j];
|
|
for (int s = 0; s < S; s++) rmsnorm_row(nrm + (int64_t)s*D, x + (int64_t)s*D, l->post_ln, D, c->eps);
|
|
moe(m, l, i, nrm, S, tmp);
|
|
for (int64_t j = 0; j < (int64_t)S*D; j++) x[j] += tmp[j];
|
|
}
|
|
m->kv_len = pos_base + S;
|
|
/* solo l'ultimo token -> logits */
|
|
float *last = falloc(D);
|
|
rmsnorm_row(last, x + (int64_t)(S-1)*D, m->final_norm, D, c->eps);
|
|
float *logit = falloc(c->vocab);
|
|
matmul(logit, last, m->lm_head, 1, D, c->vocab);
|
|
free(x); free(nrm); free(tmp); free(last);
|
|
return logit;
|
|
}
|
|
|
|
/* generazione greedy. prompt[np] -> riempie out[np+n_new] */
|
|
static void generate(Model *m, const int *prompt, int np, int n_new, int *out) {
|
|
Cfg *c = &m->c;
|
|
m->max_t = np + n_new;
|
|
m->K = calloc(c->n_layers, sizeof(float*)); m->V = calloc(c->n_layers, sizeof(float*));
|
|
for (int i = 0; i < c->n_layers; i++) {
|
|
m->K[i] = falloc((int64_t)c->n_heads * m->max_t * c->head_dim);
|
|
m->V[i] = falloc((int64_t)c->n_heads * m->max_t * c->head_dim);
|
|
}
|
|
for (int i = 0; i < np; i++) out[i] = prompt[i];
|
|
float *logit = step(m, prompt, np, 0); /* PREFILL */
|
|
int len = np;
|
|
for (int s = 0; s < n_new; s++) {
|
|
int best = 0; float bv = logit[0];
|
|
for (int i = 1; i < c->vocab; i++) if (logit[i] > bv) { bv = logit[i]; best = i; }
|
|
free(logit);
|
|
out[len++] = best;
|
|
if (s == n_new - 1) break;
|
|
int one = best;
|
|
logit = step(m, &one, 1, len - 1); /* DECODE */
|
|
}
|
|
}
|
|
|
|
/* ---------- lettura ref.json ---------- */
|
|
static int *read_int_array(jval *o, const char *key, int *n_out) {
|
|
jval *a = json_get(o, key);
|
|
int *r = malloc(a->len * sizeof(int));
|
|
for (int i = 0; i < a->len; i++) r[i] = (int)a->kids[i]->num;
|
|
*n_out = a->len; return r;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *snap = getenv("SNAP");
|
|
if (!snap) { fprintf(stderr, "set SNAP=<snapshot directory>\n"); return 1; }
|
|
int cap = argc > 1 ? atoi(argv[1]) : 16;
|
|
int bits = argc > 2 ? atoi(argv[2]) : 8;
|
|
const char *refpath = argc > 3 ? argv[3] : "ref.json";
|
|
|
|
FILE *f = fopen(refpath, "rb"); if(!f){perror(refpath);return 1;}
|
|
fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
|
|
char *buf=malloc(n+1); if(fread(buf,1,n,f)!=(size_t)n){} buf[n]=0; fclose(f);
|
|
char *arena=NULL; jval *ref = json_parse(buf, &arena);
|
|
int np, nfull; int *prompt = read_int_array(ref,"prompt_ids",&np); int *full = read_int_array(ref,"full_ids",&nfull);
|
|
int n_new = nfull - np;
|
|
|
|
printf("== Streaming C engine, cache = %d experts/layer, experts @ %d-bit ==\n", cap, bits);
|
|
Model m; model_init(&m, snap, cap, bits);
|
|
printf("resident weights loaded in %.1fs | RSS after load: %.2f GB\n", m.dense_load_s, rss_gb());
|
|
|
|
int *out = malloc((np + n_new) * sizeof(int));
|
|
double t = now_s();
|
|
generate(&m, prompt, np, n_new, out);
|
|
double dt = now_s() - t;
|
|
|
|
int match = 0;
|
|
printf("\nReference: "); for (int i=np;i<nfull;i++) printf("%d ", full[i]);
|
|
printf("\nC engine : "); for (int i=np;i<nfull;i++) { printf("%d ", out[i]); if (out[i]==full[i]) match++; }
|
|
printf("\nMatching tokens: %d/%d\n", match, n_new);
|
|
double tot = m.hits + m.miss;
|
|
printf("\nPEAK RSS: %.2f GB\n", rss_gb());
|
|
printf("Expert cache hit rate: %.1f%% (hit=%llu miss=%llu)\n", tot?100.0*m.hits/tot:0.0,
|
|
(unsigned long long)m.hits, (unsigned long long)m.miss);
|
|
printf("Speed: %.2f tok/s (%.1fs for %d tokens)\n", n_new/dt, dt, n_new);
|
|
free(buf); free(arena);
|
|
return 0;
|
|
}
|