@clutor/cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/c/Makefile CHANGED
@@ -10,7 +10,7 @@ endif
10
10
  LDLIBS = -lm -lpthread
11
11
  # BUILD can point somewhere writable when the source sits in a read-only installed package: make BUILD=$HOME/.clutor/bin
12
12
  BUILD ?= build
13
- SRC = src/gguf.c src/cache.c src/route.c src/kernel.c src/stream.c
13
+ SRC = src/gguf.c src/cache.c src/route.c src/kernel.c src/quant.c src/meta.c src/xs.c src/forward.c src/tokenizer.c src/stream.c
14
14
  BIN = $(BUILD)/clutor-engine
15
15
 
16
16
  all: $(BIN)
@@ -40,6 +40,35 @@ test-avx2: build
40
40
  $(CC) -arch x86_64 $(PATH_FLAGS) -mavx2 $(TEST_SRC) -o build/test-avx2 $(LDLIBS)
41
41
  arch -x86_64 ./build/test-avx2 $(FIXTURE)
42
42
 
43
+ # Golden test: a tiny generated OLMoE with random K-quant weights, checked against an independent pure-Python forward pass (needs only python3).
44
+ # Run on the default build and, to cover every kernel path, on the portable-C and NEON-without-dotprod builds.
45
+ GOLDEN_GGUF = build/tiny_olmoe.gguf
46
+ GOLDEN_Q3 = build/tiny_qwen3moe.gguf
47
+ golden-files: build
48
+ python3 test/tiny_olmoe.py write $(GOLDEN_GGUF)
49
+ python3 test/tiny_olmoe.py write $(GOLDEN_Q3) --arch qwen3moe
50
+ golden: $(BIN) golden-files
51
+ python3 test/tiny_olmoe.py check $(BIN) $(GOLDEN_GGUF)
52
+ python3 test/tiny_olmoe.py check $(BIN) $(GOLDEN_Q3) --arch qwen3moe
53
+ golden-paths: $(BIN) golden-files
54
+ python3 test/tiny_olmoe.py check $(BIN) $(GOLDEN_GGUF)
55
+ python3 test/tiny_olmoe.py check $(BIN) $(GOLDEN_Q3) --arch qwen3moe
56
+ $(CC) $(PATH_FLAGS) -DCL_FORCE_SCALAR $(SRC) src/main.c -o build/engine-scalar $(LDLIBS)
57
+ python3 test/tiny_olmoe.py check build/engine-scalar $(GOLDEN_GGUF)
58
+ python3 test/tiny_olmoe.py check build/engine-scalar $(GOLDEN_Q3) --arch qwen3moe
59
+ ifeq ($(shell uname -m),arm64)
60
+ $(CC) $(PATH_FLAGS) -mcpu=native -DCL_NO_DOTPROD $(SRC) src/main.c -o build/engine-neon $(LDLIBS)
61
+ python3 test/tiny_olmoe.py check build/engine-neon $(GOLDEN_GGUF)
62
+ python3 test/tiny_olmoe.py check build/engine-neon $(GOLDEN_Q3) --arch qwen3moe
63
+ endif
64
+
65
+ # Mutation fuzzing of the file readers under ASan + UBSan: corrupt a valid model many ways, expect clean errors and never a crash or hang.
66
+ fuzz: build
67
+ $(CC) -O1 -g -std=c11 -fsanitize=address,undefined -fno-omit-frame-pointer $(SRC) src/main.c -o build/engine-asan $(LDLIBS)
68
+ python3 test/tiny_olmoe.py write $(GOLDEN_GGUF)
69
+ python3 test/fuzz_mutate.py build/engine-asan $(GOLDEN_GGUF) --iterations $(or $(ITER),600)
70
+ python3 test/fuzz_mutate.py build/engine-asan $(FIXTURE) --iterations $(or $(ITER),600) --seed 7
71
+
43
72
  # Address + undefined-behaviour sanitizers on the unit tests and a threaded streaming run, then the thread sanitizer on the same run.
44
73
  SAN_RUN = stream $(SMALL) --tokens 60 --cache-mb 1 --stage 1 --io-threads 3 --smart 1,8,1 --policy adaptive
45
74
  sanitize: build
@@ -48,6 +77,9 @@ sanitize: build
48
77
  $(CC) -O1 -g -std=c11 -fsanitize=address,undefined -fno-omit-frame-pointer $(SRC) src/main.c -o build/engine-asan $(LDLIBS)
49
78
  $(CC) -O1 -g -std=c11 -fsanitize=thread $(SRC) src/main.c -o build/engine-tsan $(LDLIBS)
50
79
  @if [ -n "$(SMALL)" ]; then ./build/engine-asan $(SAN_RUN) >/dev/null && ./build/engine-tsan $(SAN_RUN) >/dev/null && echo "sanitizers clean on $(SMALL)"; else echo "(set SMALL=<moe.gguf> to also run the streaming engine under the sanitizers)"; fi
80
+ # Compares tokenization, logits and routing with llama.cpp (needs llama-server and llama-tokenize on PATH): make verify-llama MODEL=path/to/olmoe.gguf
81
+ verify-llama: $(BIN)
82
+ python3 test/verify_llama.py $(MODEL) --engine $(BIN)
51
83
  clean:
52
84
  rm -rf build
53
- .PHONY: all test test-paths test-avx2 sanitize clean
85
+ .PHONY: fuzz golden golden-files golden-paths verify-llama all test test-paths test-avx2 sanitize clean
@@ -0,0 +1,354 @@
1
+ #define _GNU_SOURCE
2
+ #include "forward.h"
3
+ #include "quant.h"
4
+ #include <fcntl.h>
5
+ #include <math.h>
6
+ #include <stdio.h>
7
+ #include <stdlib.h>
8
+ #include <string.h>
9
+ #include <sys/mman.h>
10
+ #include <sys/stat.h>
11
+ #include <unistd.h>
12
+
13
+ static const cl_tensor *find(const cl_model *m, const char *name) {
14
+ for (uint64_t i = 0; i < m->n_tensors; i++) if (!strcmp(m->tensors[i].name, name)) return &m->tensors[i];
15
+ return NULL;
16
+ }
17
+
18
+ static int bind(cl_fwd *f, cl_w *w, const char *name, int blk, int want_dims, char *err, size_t errlen) {
19
+ char nm[200];
20
+ if (blk >= 0) snprintf(nm, sizeof nm, "blk.%d.%s", blk, name); else snprintf(nm, sizeof nm, "%s", name);
21
+ const cl_tensor *t = find(&f->model, nm);
22
+ if (!t) { snprintf(err, errlen, "the model has no tensor \"%s\"", nm); return 1; }
23
+ if ((int)t->ndims != want_dims) { snprintf(err, errlen, "tensor \"%s\" has %u dimensions, expected %d", nm, t->ndims, want_dims); return 1; }
24
+ if (!t->nbytes || !cl_type_supported(t->type)) { snprintf(err, errlen, "tensor \"%s\" is stored as %s, which this engine cannot compute with yet", nm, cl_type_name(t->type)); return 1; }
25
+ if (t->dims[0] % cl_type_block(t->type)) { snprintf(err, errlen, "tensor \"%s\" row length %llu is not a multiple of its block", nm, (unsigned long long)t->dims[0]); return 1; }
26
+ w->p = f->map + t->offset; w->type = t->type; w->cols = (int)t->dims[0];
27
+ w->rows = want_dims >= 2 ? (int)t->dims[1] : 1;
28
+ return 0;
29
+ }
30
+
31
+ static float *to_float(const cl_w *w, size_t n) {
32
+ float *o = malloc(n * sizeof(float));
33
+ if (o) cl_dequant_row(w->type, w->p, o, (int)n);
34
+ return o;
35
+ }
36
+
37
+ /* A metadata key of this model's architecture, for example "olmoe.block_count". */
38
+ #define AKEY(buf, suffix) snprintf(buf, sizeof buf, "%s.%s", f->arch, suffix)
39
+ #define REQ_U(suffix, dst) do { char k_[96]; uint64_t v_; AKEY(k_, suffix); if (!cl_meta_u64(&f->meta, k_, &v_)) { snprintf(err, errlen, "the model does not state %s", k_); goto fail; } dst = (int)v_; } while (0)
40
+
41
+ int cl_fwd_open(cl_fwd *f, const char *path, int n_ctx, const cl_fwd_opts *opts, char *err, size_t errlen) {
42
+ memset(f, 0, sizeof *f);
43
+ if (cl_model_open(&f->model, path, err, errlen)) return 1;
44
+ if (strcmp(f->model.arch, "olmoe") && strcmp(f->model.arch, "qwen3moe")) { snprintf(err, errlen, "the forward pass supports the \"olmoe\" and \"qwen3moe\" architectures, not \"%s\"", f->model.arch); goto fail; }
45
+ snprintf(f->arch, sizeof f->arch, "%s", f->model.arch);
46
+ f->qk_per_head = f->norm_topk = !strcmp(f->arch, "qwen3moe");
47
+ int fd = open(path, O_RDONLY);
48
+ struct stat st;
49
+ if (fd < 0 || fstat(fd, &st)) { snprintf(err, errlen, "cannot open %s", path); if (fd >= 0) close(fd); goto fail; }
50
+ f->map_size = (size_t)st.st_size;
51
+ f->map = mmap(NULL, f->map_size, PROT_READ, MAP_PRIVATE, fd, 0);
52
+ close(fd);
53
+ if (f->map == MAP_FAILED) { f->map = NULL; snprintf(err, errlen, "cannot map %s", path); goto fail; }
54
+ if (cl_meta_parse(&f->meta, f->map, f->map_size, err, errlen)) goto fail;
55
+ REQ_U("embedding_length", f->n_embd);
56
+ REQ_U("attention.head_count", f->n_head);
57
+ REQ_U("attention.head_count_kv", f->n_head_kv);
58
+ { char k1[96], k2[96]; uint64_t v_; AKEY(k1, "expert_feed_forward_length"); AKEY(k2, "feed_forward_length");
59
+ if (cl_meta_u64(&f->meta, k1, &v_) || (!f->qk_per_head && cl_meta_u64(&f->meta, k2, &v_))) f->n_ff_exp = (int)v_; else { snprintf(err, errlen, "the model does not state its expert feed-forward length"); goto fail; } } /* olmoe files store the per-expert width under the plain key */
60
+ REQ_U("context_length", f->n_ctx_max);
61
+ f->n_layer = (int)f->model.n_layers; f->n_expert = (int)f->model.n_experts; f->n_used = (int)f->model.n_used;
62
+ double d = 0;
63
+ { char k[96];
64
+ AKEY(k, "attention.layer_norm_rms_epsilon"); f->rms_eps = cl_meta_f64(&f->meta, k, &d) ? (float)d : 1e-5f;
65
+ AKEY(k, "rope.freq_base"); f->rope_base = cl_meta_f64(&f->meta, k, &d) ? (float)d : 10000.0f; }
66
+ if (f->n_embd < 256 || f->n_head < 1 || f->n_head > f->n_embd || f->n_embd % f->n_head || f->n_layer < 1 || f->n_expert < 1 || f->n_used < 1 || f->n_used > f->n_expert || f->n_ff_exp < 1) {
67
+ snprintf(err, errlen, "implausible hyper-parameters (embedding %d, heads %d, layers %d, experts %d, used %d, expert width %d)", f->n_embd, f->n_head, f->n_layer, f->n_expert, f->n_used, f->n_ff_exp); goto fail;
68
+ }
69
+ f->head_dim = f->n_embd / f->n_head;
70
+ { char k[96]; uint64_t v_; AKEY(k, "attention.key_length"); if (cl_meta_u64(&f->meta, k, &v_) && v_ >= 2 && v_ <= 1024 && v_ % 2 == 0) f->head_dim = (int)v_; } /* qwen3moe: 128, not n_embd / n_head */
71
+ if (f->n_head_kv < 1 || f->n_head_kv > f->n_head || f->n_head % f->n_head_kv) { snprintf(err, errlen, "implausible head counts (%d query heads, %d key/value heads)", f->n_head, f->n_head_kv); goto fail; }
72
+ if (f->head_dim * f->n_head != f->n_embd && !f->qk_per_head) { snprintf(err, errlen, "head_dim * heads must equal the embedding width for this architecture"); goto fail; }
73
+ f->n_q = f->n_head * f->head_dim; f->n_kvw = f->n_head_kv * f->head_dim;
74
+ if (f->n_embd % 256 || f->n_ff_exp % 256 || f->n_q % 256) { snprintf(err, errlen, "embedding width %d / expert width %d / query width %d must be multiples of 256 for the K-quant kernels", f->n_embd, f->n_ff_exp, f->n_q); goto fail; }
75
+ if (f->n_used > 64 || f->n_expert > 4096) { snprintf(err, errlen, "unsupported expert counts"); goto fail; }
76
+ if (n_ctx > f->n_ctx_max) n_ctx = f->n_ctx_max;
77
+ if (n_ctx < 1) n_ctx = 512;
78
+ f->n_ctx_max = n_ctx;
79
+
80
+ if (bind(f, &f->tok_embd, "token_embd.weight", -1, 2, err, errlen)) goto fail;
81
+ f->n_vocab = f->tok_embd.rows;
82
+ if (f->tok_embd.cols != f->n_embd || f->n_vocab < 1) { snprintf(err, errlen, "token_embd.weight is %d x %d, expected a vocabulary of rows by %d columns", f->tok_embd.rows, f->tok_embd.cols, f->n_embd); goto fail; }
83
+ if (bind(f, &f->output, "output.weight", -1, 2, err, errlen)) goto fail;
84
+ cl_w on;
85
+ if (bind(f, &on, "output_norm.weight", -1, 1, err, errlen)) goto fail;
86
+ if (on.cols != f->n_embd || f->output.cols != f->n_embd || f->output.rows != f->n_vocab) { snprintf(err, errlen, "output tensors do not match the embedding width and vocabulary"); goto fail; }
87
+ f->out_norm_f = to_float(&on, (size_t)f->n_embd);
88
+ f->L = calloc((size_t)f->n_layer, sizeof *f->L);
89
+ if (!f->L || !f->out_norm_f) { snprintf(err, errlen, "out of memory"); goto fail; }
90
+ for (int l = 0; l < f->n_layer; l++) {
91
+ cl_fwd_layer *y = &f->L[l];
92
+ if (bind(f, &y->attn_norm, "attn_norm.weight", l, 1, err, errlen) || bind(f, &y->wq, "attn_q.weight", l, 2, err, errlen) || bind(f, &y->wk, "attn_k.weight", l, 2, err, errlen) ||
93
+ bind(f, &y->wv, "attn_v.weight", l, 2, err, errlen) || bind(f, &y->wo, "attn_output.weight", l, 2, err, errlen) || bind(f, &y->qn, "attn_q_norm.weight", l, 1, err, errlen) ||
94
+ bind(f, &y->kn, "attn_k_norm.weight", l, 1, err, errlen) || bind(f, &y->ffn_norm, "ffn_norm.weight", l, 1, err, errlen) || bind(f, &y->router, "ffn_gate_inp.weight", l, 2, err, errlen) ||
95
+ bind(f, &y->gate, "ffn_gate_exps.weight", l, 3, err, errlen) || bind(f, &y->up, "ffn_up_exps.weight", l, 3, err, errlen) || bind(f, &y->down, "ffn_down_exps.weight", l, 3, err, errlen)) goto fail;
96
+ const int N = f->n_embd;
97
+ const int QN = f->qk_per_head ? f->head_dim : f->n_q, KN = f->qk_per_head ? f->head_dim : f->n_kvw;
98
+ if (y->attn_norm.cols != N || y->ffn_norm.cols != N || y->qn.cols != QN || y->kn.cols != KN ||
99
+ y->wq.rows != f->n_q || y->wq.cols != N || y->wk.rows != f->n_kvw || y->wk.cols != N || y->wv.rows != f->n_kvw || y->wv.cols != N || y->wo.rows != N || y->wo.cols != f->n_q ||
100
+ y->up.rows != f->n_ff_exp || y->up.cols != N || y->gate.cols != N) {
101
+ snprintf(err, errlen, "layer %d: attention or norm tensor shapes do not match the embedding width %d", l, N); goto fail;
102
+ }
103
+ char nm[200];
104
+ snprintf(nm, sizeof nm, "blk.%d.ffn_gate_exps.weight", l);
105
+ const cl_tensor *tg = find(&f->model, nm);
106
+ if (y->router.rows != f->n_expert || y->router.cols != f->n_embd || (int)tg->dims[2] != f->n_expert || y->up.cols != N || y->gate.rows != f->n_ff_exp || y->down.rows != f->n_embd || y->down.cols != f->n_ff_exp) {
107
+ snprintf(err, errlen, "layer %d: tensor shapes do not match the hyper-parameters", l); goto fail;
108
+ }
109
+ y->gate_slab = (uint64_t)y->gate.rows * cl_row_bytes(y->gate.type, (uint64_t)y->gate.cols);
110
+ y->up_slab = (uint64_t)y->up.rows * cl_row_bytes(y->up.type, (uint64_t)y->up.cols);
111
+ y->down_slab = (uint64_t)y->down.rows * cl_row_bytes(y->down.type, (uint64_t)y->down.cols);
112
+ y->attn_norm_f = to_float(&y->attn_norm, (size_t)f->n_embd); y->ffn_norm_f = to_float(&y->ffn_norm, (size_t)f->n_embd);
113
+ y->qn_f = to_float(&y->qn, (size_t)QN); y->kn_f = to_float(&y->kn, (size_t)KN);
114
+ y->router_f = malloc((size_t)f->n_expert * f->n_embd * sizeof(float));
115
+ if (!y->attn_norm_f || !y->ffn_norm_f || !y->qn_f || !y->kn_f || !y->router_f) { snprintf(err, errlen, "out of memory"); goto fail; }
116
+ for (int e = 0; e < f->n_expert; e++) cl_dequant_row(y->router.type, y->router.p + (size_t)e * cl_row_bytes(y->router.type, (uint64_t)f->n_embd), y->router_f + (size_t)e * f->n_embd, f->n_embd);
117
+ }
118
+ size_t kvn = (size_t)f->n_layer * f->n_ctx_max * f->n_kvw;
119
+ size_t E = (size_t)f->n_embd;
120
+ f->kc = calloc(kvn, sizeof(float)); f->vc = calloc(kvn, sizeof(float));
121
+ f->x = calloc(E, 4); f->xn = calloc(E, 4); f->q = calloc((size_t)f->n_q, 4); f->k = calloc((size_t)f->n_kvw, 4); f->v = calloc((size_t)f->n_kvw, 4); f->att = calloc((size_t)f->n_q, 4);
122
+ f->scores = calloc((size_t)f->n_ctx_max, 4); f->moe = calloc(E, 4); f->hg = calloc((size_t)f->n_ff_exp * f->n_used, 4); f->hu = calloc((size_t)f->n_ff_exp * f->n_used, 4); f->hd = calloc(E * (size_t)f->n_used, 4);
123
+ f->rl = calloc((size_t)f->n_expert, 4); f->rlogit = calloc((size_t)f->n_expert, 4);
124
+ f->xq_ffk = calloc((size_t)f->n_used, sizeof(cl_xq));
125
+ if (!f->xq_ffk || !cl_xq_init(&f->xq_e, f->n_embd) || !cl_xq_init(&f->xq_q, f->n_q) || !cl_xq_init(&f->xq_ff, f->n_ff_exp)) { snprintf(err, errlen, "out of memory"); goto fail; }
126
+ for (int k = 0; k < f->n_used; k++) if (!cl_xq_init(&f->xq_ffk[k], f->n_ff_exp)) { snprintf(err, errlen, "out of memory"); goto fail; }
127
+ if (!f->kc || !f->vc || !f->x || !f->xn || !f->q || !f->k || !f->v || !f->att || !f->scores || !f->moe || !f->hg || !f->hu || !f->hd || !f->rl || !f->rlogit) { snprintf(err, errlen, "out of memory"); goto fail; }
128
+ { long pages = sysconf(_SC_PHYS_PAGES), psz = sysconf(_SC_PAGE_SIZE);
129
+ if ((!opts || !opts->cache_bytes) && pages > 0 && psz > 0 && f->map_size > (size_t)pages * (size_t)psz * 8 / 10) {
130
+ snprintf(err, errlen, "the model file (%.1f GiB) is larger than 80%% of this machine's memory (%.1f GiB): mapping it whole would thrash; stream its experts with --cache-mb N", f->map_size / 1073741824.0, (double)pages * (double)psz / 1073741824.0); goto fail;
131
+ } }
132
+ if (opts && opts->smart.enabled && !opts->cache_bytes) { snprintf(err, errlen, "--smart needs --cache-mb: it substitutes experts that are already in the cache"); goto fail; }
133
+ if (opts && opts->cache_bytes) {
134
+ cl_xs_layer *xl = calloc((size_t)f->n_layer, sizeof *xl);
135
+ if (!xl) { snprintf(err, errlen, "out of memory"); goto fail; }
136
+ for (int l = 0; l < f->n_layer; l++) {
137
+ const cl_fwd_layer *y = &f->L[l];
138
+ xl[l].off[0] = (uint64_t)(y->gate.p - f->map); xl[l].slab[0] = y->gate_slab;
139
+ xl[l].off[1] = (uint64_t)(y->up.p - f->map); xl[l].slab[1] = y->up_slab;
140
+ xl[l].off[2] = (uint64_t)(y->down.p - f->map); xl[l].slab[2] = y->down_slab;
141
+ }
142
+ int stage = opts->stage_slots > 0 ? opts->stage_slots : (opts->prefetch ? 2 * f->n_used : 0);
143
+ f->xs = cl_xs_open(path, xl, f->n_layer, f->n_expert, f->n_used, opts->cache_bytes, stage, opts->policy, opts->io_threads > 0 ? opts->io_threads : 3, opts->os_bypass, err, errlen);
144
+ free(xl);
145
+ if (!f->xs) goto fail;
146
+ f->prefetch = opts->prefetch;
147
+ f->smart = opts->smart;
148
+ f->prefetch_n = opts->prefetch_n > 0 && opts->prefetch_n < f->n_used ? opts->prefetch_n : f->n_used;
149
+ f->prev_sel = malloc((size_t)f->n_layer * f->n_used * sizeof(int));
150
+ if (!f->prev_sel) { snprintf(err, errlen, "out of memory"); goto fail; }
151
+ f->pred_sel = malloc((size_t)f->n_layer * f->n_used * sizeof(int));
152
+ if (!f->pred_sel) { snprintf(err, errlen, "out of memory"); goto fail; }
153
+ for (int i = 0; i < f->n_layer * f->n_used; i++) f->prev_sel[i] = f->pred_sel[i] = -1;
154
+ }
155
+ return 0;
156
+ fail:
157
+ cl_fwd_close(f);
158
+ return 1;
159
+ }
160
+
161
+ void cl_fwd_close(cl_fwd *f) {
162
+ if (f->L) for (int l = 0; l < f->n_layer; l++) { free(f->L[l].attn_norm_f); free(f->L[l].ffn_norm_f); free(f->L[l].qn_f); free(f->L[l].kn_f); free(f->L[l].router_f); }
163
+ free(f->L); free(f->out_norm_f);
164
+ free(f->kc); free(f->vc); free(f->x); free(f->xn); free(f->q); free(f->k); free(f->v); free(f->att); free(f->scores); free(f->moe); free(f->hg); free(f->hu); free(f->hd); free(f->rl); free(f->rlogit);
165
+ cl_xq_free(&f->xq_e); cl_xq_free(&f->xq_q); cl_xq_free(&f->xq_ff);
166
+ if (f->xq_ffk) for (int k = 0; k < f->n_used; k++) cl_xq_free(&f->xq_ffk[k]);
167
+ free(f->xq_ffk);
168
+ cl_xs_close(f->xs); free(f->prev_sel); free(f->pred_sel);
169
+ cl_meta_free(&f->meta);
170
+ if (f->map) munmap(f->map, f->map_size);
171
+ cl_model_close(&f->model);
172
+ memset(f, 0, sizeof *f);
173
+ }
174
+
175
+ void cl_fwd_reset(cl_fwd *f) { f->pos = 0; }
176
+
177
+ static void rmsnorm(float *out, const float *x, const float *w, int n, float eps) {
178
+ double ss = 0;
179
+ for (int i = 0; i < n; i++) ss += (double)x[i] * x[i];
180
+ float s = 1.0f / sqrtf((float)(ss / n) + eps);
181
+ for (int i = 0; i < n; i++) out[i] = x[i] * s * w[i];
182
+ }
183
+
184
+ static void rope_neox(float *v, int hd, int pos, float base) {
185
+ int half = hd / 2;
186
+ for (int i = 0; i < half; i++) {
187
+ float theta = (float)pos * powf(base, -2.0f * (float)i / (float)hd), c = cosf(theta), s = sinf(theta);
188
+ float a = v[i], b = v[i + half];
189
+ v[i] = a * c - b * s; v[i + half] = a * s + b * c;
190
+ }
191
+ }
192
+
193
+ static cl_mv mv(const cl_w *w, const float *xf, const cl_xq *xq, float *out) { cl_mv j = {w->type, w->p, w->rows, w->cols, xf, xq, out}; return j; }
194
+
195
+ static int resident_cb(void *ctx, int expert) {
196
+ struct { cl_xs *xs; int layer; } *c = ctx;
197
+ return cl_xs_resident(c->xs, c->layer, expert);
198
+ }
199
+
200
+ int cl_fwd_step(cl_fwd *f, int token, float *logits, cl_fwd_route *route, char *err, size_t errlen) {
201
+ if (token < 0 || token >= f->n_vocab) { snprintf(err, errlen, "token %d is outside the vocabulary", token); return 1; }
202
+ if (f->pos >= f->n_ctx_max) { snprintf(err, errlen, "the context is full (%d tokens)", f->n_ctx_max); return 1; }
203
+ const int E = f->n_embd, H = f->n_head, KVH = f->n_head_kv, hd = f->head_dim, pos = f->pos, KW = f->n_kvw, GROUP = f->n_head / f->n_head_kv;
204
+ cl_dequant_row(f->tok_embd.type, f->tok_embd.p + (size_t)token * cl_row_bytes(f->tok_embd.type, (uint64_t)E), f->x, E);
205
+ if (f->pred_sel) for (int i = 0; i < f->n_layer * f->n_used; i++) f->pred_sel[i] = -1;
206
+ if (f->xs && f->prefetch) for (int k = 0; k < f->prefetch_n; k++) { cl_xs_prefetch(f->xs, 0, f->prev_sel[k]); f->pred_sel[k] = f->prev_sel[k]; } /* layer 0, as the previous token chose it (nothing else is known yet) */
207
+ for (int l = 0; l < f->n_layer; l++) {
208
+ cl_fwd_layer *y = &f->L[l];
209
+ /* while this layer's attention and experts compute, read the experts the NEXT layer chose for the previous token */
210
+ if (f->xs && f->prefetch == 1 && l + 1 < f->n_layer) for (int k = 0; k < f->prefetch_n; k++) { int e = f->prev_sel[(l + 1) * f->n_used + k]; cl_xs_prefetch(f->xs, l + 1, e); f->pred_sel[(l + 1) * f->n_used + k] = e; }
211
+ rmsnorm(f->xn, f->x, y->attn_norm_f, E, f->rms_eps);
212
+ cl_xq_quantize(&f->xq_e, f->xn);
213
+ { cl_mv j[3] = {mv(&y->wq, f->xn, &f->xq_e, f->q), mv(&y->wk, f->xn, &f->xq_e, f->k), mv(&y->wv, f->xn, &f->xq_e, f->v)}; cl_matvec_batch(j, 3); }
214
+ if (f->qk_per_head) { /* qwen3moe: each head's vector is normalised on its own, with one weight shared by all heads */
215
+ for (int h = 0; h < H; h++) rmsnorm(f->q + h * hd, f->q + h * hd, y->qn_f, hd, f->rms_eps);
216
+ for (int h = 0; h < KVH; h++) rmsnorm(f->k + h * hd, f->k + h * hd, y->kn_f, hd, f->rms_eps);
217
+ } else { /* olmoe: over the whole projected vector, before it is split into heads */
218
+ rmsnorm(f->q, f->q, y->qn_f, f->n_q, f->rms_eps);
219
+ rmsnorm(f->k, f->k, y->kn_f, f->n_kvw, f->rms_eps);
220
+ }
221
+ for (int h = 0; h < H; h++) rope_neox(f->q + h * hd, hd, pos, f->rope_base);
222
+ for (int h = 0; h < KVH; h++) rope_neox(f->k + h * hd, hd, pos, f->rope_base);
223
+ float *kc = f->kc + ((size_t)l * f->n_ctx_max + pos) * KW, *vc = f->vc + ((size_t)l * f->n_ctx_max + pos) * KW;
224
+ memcpy(kc, f->k, (size_t)KW * 4); memcpy(vc, f->v, (size_t)KW * 4);
225
+ const float scale = 1.0f / sqrtf((float)hd);
226
+ for (int h = 0; h < H; h++) {
227
+ const float *qh = f->q + h * hd;
228
+ const int kvo = (h / GROUP) * hd; /* grouped-query attention: GROUP query heads share one key/value head */
229
+ float mx = -INFINITY;
230
+ for (int t = 0; t <= pos; t++) {
231
+ const float *kt = f->kc + ((size_t)l * f->n_ctx_max + t) * KW + kvo;
232
+ float s = 0;
233
+ for (int i = 0; i < hd; i++) s += qh[i] * kt[i];
234
+ f->scores[t] = s * scale;
235
+ if (f->scores[t] > mx) mx = f->scores[t];
236
+ }
237
+ float sum = 0;
238
+ for (int t = 0; t <= pos; t++) { f->scores[t] = expf(f->scores[t] - mx); sum += f->scores[t]; }
239
+ float *oh = f->att + h * hd;
240
+ memset(oh, 0, (size_t)hd * 4);
241
+ for (int t = 0; t <= pos; t++) {
242
+ const float *vt = f->vc + ((size_t)l * f->n_ctx_max + t) * KW + kvo;
243
+ float p = f->scores[t] / sum;
244
+ for (int i = 0; i < hd; i++) oh[i] += p * vt[i];
245
+ }
246
+ }
247
+ cl_xq_quantize(&f->xq_q, f->att);
248
+ { cl_mv j = mv(&y->wo, f->att, &f->xq_q, f->hd); cl_matvec_batch(&j, 1); }
249
+ for (int i = 0; i < E; i++) f->x[i] += f->hd[i];
250
+
251
+ /* router-ahead: apply the NEXT layer's router to this layer's hidden state (the residual stream changes slowly) and start reading what it picks,
252
+ * so the reads overlap this layer's experts */
253
+ if (f->xs && f->prefetch == 2 && l + 1 < f->n_layer) {
254
+ const cl_fwd_layer *nx = &f->L[l + 1];
255
+ rmsnorm(f->xn, f->x, nx->ffn_norm_f, E, f->rms_eps);
256
+ for (int e = 0; e < f->n_expert; e++) { const float *r = nx->router_f + (size_t)e * E; float a = 0; for (int i = 0; i < E; i++) a += r[i] * f->xn[i]; f->rl[e] = a; }
257
+ char taken[4096] = {0};
258
+ for (int k = 0; k < f->prefetch_n; k++) {
259
+ int best = -1;
260
+ for (int e = 0; e < f->n_expert; e++) if (!taken[e] && (best < 0 || f->rl[e] > f->rl[best])) best = e;
261
+ taken[best] = 1; cl_xs_prefetch(f->xs, l + 1, best); f->pred_sel[(l + 1) * f->n_used + k] = best;
262
+ }
263
+ }
264
+
265
+ /* mixture of experts */
266
+ rmsnorm(f->xn, f->x, y->ffn_norm_f, E, f->rms_eps);
267
+ for (int e = 0; e < f->n_expert; e++) {
268
+ const float *r = y->router_f + (size_t)e * E;
269
+ float s = 0;
270
+ for (int i = 0; i < E; i++) s += r[i] * f->xn[i];
271
+ f->rl[e] = s;
272
+ }
273
+ memcpy(f->rlogit, f->rl, (size_t)f->n_expert * sizeof(float));
274
+ if (route && route->logits) memcpy(route->logits + (size_t)l * f->n_expert, f->rl, (size_t)f->n_expert * sizeof(float));
275
+ float mx = f->rl[0];
276
+ for (int e = 1; e < f->n_expert; e++) if (f->rl[e] > mx) mx = f->rl[e];
277
+ double sum = 0;
278
+ for (int e = 0; e < f->n_expert; e++) { f->rl[e] = expf(f->rl[e] - mx); sum += f->rl[e]; }
279
+ for (int e = 0; e < f->n_expert; e++) f->rl[e] = (float)(f->rl[e] / sum);
280
+ int sel[64];
281
+ float wt[64];
282
+ if (f->smart.enabled) {
283
+ /* SMART-ROUTE: the true top-J stay; a slot that would need an expert that is not in memory may take a resident one whose router probability is within tau.
284
+ * A substituted expert is weighted by its OWN router probability. */
285
+ struct { cl_xs *xs; int layer; } rc = {f->xs, l};
286
+ cl_route_result rr;
287
+ if (cl_route(f->rlogit, f->n_expert, f->n_used, &f->smart, resident_cb, &rc, &rr)) { snprintf(err, errlen, "bad SMART-ROUTE settings"); return 1; }
288
+ for (int k = 0; k < f->n_used; k++) { sel[k] = rr.selected[k]; wt[k] = f->rl[sel[k]]; }
289
+ f->subs += (uint64_t)rr.substituted; f->dropped_mass += rr.dropped_mass;
290
+ if (rr.max_delta > f->max_delta) f->max_delta = rr.max_delta;
291
+ } else {
292
+ char used[4096] = {0};
293
+ for (int k = 0; k < f->n_used; k++) { /* top-k by probability; ties go to the lower expert id */
294
+ int best = -1;
295
+ for (int e = 0; e < f->n_expert; e++) if (!used[e] && (best < 0 || f->rl[e] > f->rl[best])) best = e;
296
+ used[best] = 1; sel[k] = best; wt[k] = f->rl[best]; /* OLMoE does not renormalise the chosen weights */
297
+ }
298
+ }
299
+ if (f->pred_sel) for (int k = 0; k < f->prefetch_n; k++) {
300
+ int pe = f->pred_sel[l * f->n_used + k];
301
+ if (pe < 0) continue;
302
+ f->pred_total++;
303
+ for (int j = 0; j < f->n_used; j++) if (sel[j] == pe) { f->pred_hit++; break; }
304
+ }
305
+ if (f->norm_topk) { float z = 0; for (int k = 0; k < f->n_used; k++) z += wt[k]; if (z > 0) for (int k = 0; k < f->n_used; k++) wt[k] /= z; } /* qwen3moe: the chosen weights sum to 1 */
306
+ f->expert_slots += (uint64_t)f->n_used;
307
+ if (route) for (int k = 0; k < f->n_used; k++) { route->experts[l * f->n_used + k] = sel[k]; route->weights[l * f->n_used + k] = wt[k]; }
308
+ memset(f->moe, 0, (size_t)E * 4);
309
+ cl_xq_quantize(&f->xq_e, f->xn); /* every selected expert reads the same normalised input */
310
+ int hnd[64];
311
+ const uint8_t *P[64];
312
+ if (f->xs) { /* ask for what is already in memory first, so a miss cannot evict an expert this layer is about to use */
313
+ for (int pass = 0; pass < 2; pass++) for (int k = 0; k < f->n_used; k++) {
314
+ int here = cl_xs_resident(f->xs, l, sel[k]);
315
+ if ((pass == 0) == (here != 0)) hnd[k] = cl_xs_request(f->xs, l, sel[k]);
316
+ }
317
+ }
318
+ for (int k = 0; k < f->n_used; k++) {
319
+ if (f->xs) {
320
+ P[k] = cl_xs_wait(f->xs, hnd[k]);
321
+ if (!P[k]) { snprintf(err, errlen, "reading expert %d of layer %d from disk failed", sel[k], l); for (int j = 0; j < f->n_used; j++) cl_xs_release(f->xs, hnd[j]); return 1; }
322
+ } else P[k] = y->gate.p + (size_t)sel[k] * y->gate_slab; /* mapped file: the three matrices sit in three fused tensors */
323
+ }
324
+ /* all experts' gate and up products in one batch, then all down products in another: two thread hand-offs per layer, not sixteen */
325
+ cl_mv j[64];
326
+ const size_t FF = (size_t)f->n_ff_exp;
327
+ for (int k = 0; k < f->n_used; k++) {
328
+ const uint8_t *g = f->xs ? P[k] : y->gate.p + (size_t)sel[k] * y->gate_slab, *u = f->xs ? P[k] + y->gate_slab : y->up.p + (size_t)sel[k] * y->up_slab;
329
+ j[2 * k] = (cl_mv){y->gate.type, g, f->n_ff_exp, E, f->xn, &f->xq_e, f->hg + k * FF};
330
+ j[2 * k + 1] = (cl_mv){y->up.type, u, f->n_ff_exp, E, f->xn, &f->xq_e, f->hu + k * FF};
331
+ }
332
+ cl_matvec_batch(j, 2 * f->n_used);
333
+ for (int k = 0; k < f->n_used; k++) {
334
+ float *hg = f->hg + k * FF, *hu = f->hu + k * FF;
335
+ for (size_t i = 0; i < FF; i++) hg[i] = hg[i] / (1.0f + expf(-hg[i])) * hu[i]; /* silu(gate) * up */
336
+ cl_xq_quantize(&f->xq_ffk[k], hg);
337
+ const uint8_t *d = f->xs ? P[k] + y->gate_slab + y->up_slab : y->down.p + (size_t)sel[k] * y->down_slab;
338
+ j[k] = (cl_mv){y->down.type, d, E, f->n_ff_exp, hg, &f->xq_ffk[k], f->hd + (size_t)k * E};
339
+ }
340
+ cl_matvec_batch(j, f->n_used);
341
+ for (int k = 0; k < f->n_used; k++) {
342
+ const float *hd = f->hd + (size_t)k * E;
343
+ for (int i = 0; i < E; i++) f->moe[i] += wt[k] * hd[i];
344
+ if (f->xs) cl_xs_release(f->xs, hnd[k]);
345
+ }
346
+ if (f->prev_sel) memcpy(f->prev_sel + l * f->n_used, sel, (size_t)f->n_used * sizeof(int));
347
+ for (int i = 0; i < E; i++) f->x[i] += f->moe[i];
348
+ }
349
+ rmsnorm(f->xn, f->x, f->out_norm_f, E, f->rms_eps);
350
+ cl_xq_quantize(&f->xq_e, f->xn);
351
+ { cl_mv j = mv(&f->output, f->xn, &f->xq_e, logits); cl_matvec_batch(&j, 1); }
352
+ f->pos++;
353
+ return 0;
354
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * A real forward pass for two mixture-of-experts architectures (llama.cpp's "olmoe" and "qwen3moe"): token embedding, RMSNorm, attention with QK-norm and
3
+ * NeoX rotary embeddings (grouped-query for qwen3moe), a softmax router over every expert, the top-k experts' SwiGLU feed-forwards, and the output projection.
4
+ * Weights are read straight from the memory-mapped GGUF and dequantized block by block; the arithmetic is float32.
5
+ *
6
+ * Every step is checked against llama.cpp (see `make verify-llama`). Routing here is the model's own: the gate sees the real hidden state.
7
+ */
8
+ #ifndef CL_FORWARD_H
9
+ #define CL_FORWARD_H
10
+ #include "gguf.h"
11
+ #include "meta.h"
12
+ #include "quant.h"
13
+ #include "route.h"
14
+ #include "xs.h"
15
+ #include <stddef.h>
16
+ #include <stdint.h>
17
+
18
+ typedef struct { const uint8_t *p; uint32_t type; int rows, cols; } cl_w; /* rows x cols weights of `type`, row-major (cols is the input width) */
19
+
20
+ typedef struct {
21
+ cl_w attn_norm, wq, wk, wv, wo, qn, kn, ffn_norm, router, gate, up, down; /* gate/up/down are the fused expert tensors */
22
+ float *attn_norm_f, *qn_f, *kn_f, *ffn_norm_f, *router_f;
23
+ uint64_t gate_slab, up_slab, down_slab; /* bytes of one expert in each fused tensor */
24
+ } cl_fwd_layer;
25
+
26
+ typedef struct {
27
+ cl_model model;
28
+ cl_meta meta;
29
+ uint8_t *map; size_t map_size;
30
+ char arch[16]; /* "olmoe" or "qwen3moe" */
31
+ int n_layer, n_embd, n_head, n_head_kv, head_dim, n_ff_exp, n_expert, n_used, n_vocab, n_ctx_max;
32
+ int n_q, n_kvw; /* width of the query projection (n_head * head_dim) and of the key / value projections (n_head_kv * head_dim) */
33
+ int qk_per_head; /* qwen3moe: RMSNorm over each head_dim vector with one shared weight; olmoe: over the whole projected vector */
34
+ int norm_topk; /* qwen3moe renormalises the chosen experts' weights to sum to 1; olmoe uses the raw softmax probabilities */
35
+ float rms_eps, rope_base;
36
+ cl_w tok_embd, output;
37
+ float *out_norm_f;
38
+ cl_fwd_layer *L;
39
+ float *kc, *vc; /* [layer][position][n_head_kv * head_dim] */
40
+ float *x, *xn, *q, *k, *v, *att, *scores, *logits_buf, *moe, *hg, *hu, *hd, *rl;
41
+ cl_xq xq_e, xq_q, xq_ff, *xq_ffk; /* the current activation vector, quantized for the integer dot products */
42
+ cl_xs *xs; /* when set, experts come from disk through the cache instead of from the mapped file */
43
+ cl_smart smart;
44
+ uint64_t subs, expert_slots; /* experts used in place of the router's own choice, out of all expert slots filled */
45
+ double dropped_mass, max_delta;
46
+ float *rlogit; /* the router's raw scores for the layer being processed */
47
+ int prefetch_n; /* experts per layer to prefetch */
48
+ int prefetch; /* 0 off; 1 "prev": guess the next layer's experts from the previous token's choices; 2 "ahead": apply the next layer's router to the current hidden state; either way read them early */
49
+ int *pred_sel; /* [layer][prefetch_n]: what was predicted for this token's layer, -1 for none */
50
+ uint64_t pred_total, pred_hit; /* predicted experts, and how many of them the router then chose */
51
+ int *prev_sel; /* [layer][n_used]: the experts each layer chose for the previous token, -1 before the first */
52
+ int pos; /* tokens already in the KV cache */
53
+ } cl_fwd;
54
+
55
+ /* What the router chose for the token just processed: n_layer rows of n_used experts, best first, with the weight each was given. `logits` (if not NULL)
56
+ * receives the router's raw scores for all experts, which is what the streaming engine replays. */
57
+ typedef struct { int *experts; float *weights; float *logits; /* optional: n_layer * n_expert raw router logits */ int n_layer, n_used; } cl_fwd_route;
58
+
59
+ /* Streaming options. cache_bytes == 0 keeps the whole file mapped (fast, but resident memory grows with the model); otherwise experts are read
60
+ * on demand into a cache of that many bytes and the rest of the model stays on disk. */
61
+ typedef struct { uint64_t cache_bytes; int stage_slots, io_threads, os_bypass, prefetch;
62
+ cl_smart smart; /* SMART-ROUTE: needs streaming (cache_bytes > 0), since it asks the cache what is in memory */
63
+ int prefetch_n; /* how many of the previous token's experts per layer to read early (best-ranked first); 0 = all that were used */ cl_policy policy; } cl_fwd_opts;
64
+ int cl_fwd_open(cl_fwd *f, const char *path, int n_ctx, const cl_fwd_opts *opts, char *err, size_t errlen);
65
+ void cl_fwd_close(cl_fwd *f);
66
+ void cl_fwd_reset(cl_fwd *f);
67
+ /* Runs one token at position f->pos (then advances it). Writes n_vocab logits. `route` may be NULL, else it must hold n_layer*n_used entries. */
68
+ int cl_fwd_step(cl_fwd *f, int token, float *logits, cl_fwd_route *route, char *err, size_t errlen);
69
+ #endif