@clutor/cli 0.1.0 → 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.
@@ -0,0 +1,287 @@
1
+ #include "tokenizer.h"
2
+ #include <stdio.h>
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ /* ---- string -> int hash table (open addressing; keys are views into the mapped file or into our own buffer) ---- */
7
+ typedef struct { const char *k; uint32_t len; int32_t v; } slot;
8
+ typedef struct { slot *s; size_t cap; } smap;
9
+
10
+ static uint64_t hash(const char *p, size_t n) { uint64_t h = 1469598103934665603ull; for (size_t i = 0; i < n; i++) { h ^= (uint8_t)p[i]; h *= 1099511628211ull; } return h; }
11
+ static int smap_init(smap *m, size_t n) { m->cap = 1; while (m->cap < n * 2 + 2) m->cap <<= 1; m->s = calloc(m->cap, sizeof(slot)); return m->s != NULL; }
12
+ static void smap_put(smap *m, const char *k, size_t len, int32_t v) {
13
+ size_t i = hash(k, len) & (m->cap - 1);
14
+ while (m->s[i].k) { if (m->s[i].len == len && !memcmp(m->s[i].k, k, len)) return; i = (i + 1) & (m->cap - 1); } /* first definition wins, as in llama.cpp */
15
+ m->s[i].k = k; m->s[i].len = (uint32_t)len; m->s[i].v = v;
16
+ }
17
+ static int32_t smap_get(const smap *m, const char *k, size_t len) {
18
+ size_t i = hash(k, len) & (m->cap - 1);
19
+ while (m->s[i].k) { if (m->s[i].len == len && !memcmp(m->s[i].k, k, len)) return m->s[i].v; i = (i + 1) & (m->cap - 1); }
20
+ return -1;
21
+ }
22
+
23
+ struct cl_tok {
24
+ cl_str *tokens; int32_t *types; uint64_t n;
25
+ smap vocab, merges;
26
+ char *merge_buf;
27
+ int bos, eos, add_bos;
28
+ int pre; /* 0: olmo (GPT-2 style), 1: qwen2 */
29
+ int *special; int n_special; /* ids of control / user-defined tokens, longest text first */
30
+ int byte_cp[256]; /* GPT-2 byte -> code point */
31
+ int cp_byte[512]; /* code point (< 512) -> byte, or -1 */
32
+ };
33
+
34
+ static int put_utf8(char *o, int cp) {
35
+ if (cp < 0x80) { o[0] = (char)cp; return 1; }
36
+ if (cp < 0x800) { o[0] = (char)(0xC0 | (cp >> 6)); o[1] = (char)(0x80 | (cp & 63)); return 2; }
37
+ o[0] = (char)(0xE0 | (cp >> 12)); o[1] = (char)(0x80 | ((cp >> 6) & 63)); o[2] = (char)(0x80 | (cp & 63)); return 3;
38
+ }
39
+ /* Decodes one code point; an invalid byte gives U+FFFD and length 1. */
40
+ static int get_utf8(const uint8_t *p, size_t avail, int *cp) {
41
+ uint8_t c = p[0];
42
+ if (c < 0x80) { *cp = c; return 1; }
43
+ int n = c >= 0xF0 ? 4 : c >= 0xE0 ? 3 : c >= 0xC0 ? 2 : 0;
44
+ if (!n || (size_t)n > avail) { *cp = 0xFFFD; return 1; }
45
+ int v = c & (0x3F >> (n - 1));
46
+ for (int i = 1; i < n; i++) { if ((p[i] & 0xC0) != 0x80) { *cp = 0xFFFD; return 1; } v = (v << 6) | (p[i] & 63); }
47
+ *cp = v;
48
+ return n;
49
+ }
50
+
51
+ /* ---- character classes: exact for ASCII, approximate above it ---- */
52
+ static int is_space(int c) { return c == ' ' || (c >= 9 && c <= 13) || c == 0x85 || c == 0xA0 || (c >= 0x2000 && c <= 0x200A) || c == 0x2028 || c == 0x2029 || c == 0x202F || c == 0x205F || c == 0x3000 || c == 0x1680; }
53
+ static int is_digit(int c) { return (c >= '0' && c <= '9') || c == 0xB2 || c == 0xB3 || c == 0xB9 || (c >= 0xBC && c <= 0xBE) || (c >= 0x660 && c <= 0x669) || (c >= 0xFF10 && c <= 0xFF19); }
54
+ static int is_punct(int c) { /* Unicode P*: ASCII punctuation except the symbols $ + < = > ^ ` | ~ */
55
+ if (c < 0x80) return c && strchr("!\"#%&'()*,-./:;?@[\\]_{}", c) != NULL;
56
+ return c == 0xA1 || c == 0xA7 || c == 0xAB || c == 0xB6 || c == 0xB7 || c == 0xBB || c == 0xBF || (c >= 0x2010 && c <= 0x2027) || (c >= 0x2030 && c <= 0x205E) || (c >= 0x3001 && c <= 0x3003) || (c >= 0x3008 && c <= 0x3011) || (c >= 0xFF01 && c <= 0xFF0F && c != 0xFF04 && c != 0xFF0B);
57
+ }
58
+ static int is_letter(int c) {
59
+ if (c < 0x80) return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
60
+ if (c == 0xAA || c == 0xB5 || c == 0xBA) return 1;
61
+ if (c < 0xC0 || c == 0xD7 || c == 0xF7) return 0; /* Latin-1 symbols */
62
+ if (is_space(c) || is_digit(c) || is_punct(c)) return 0;
63
+ if ((c >= 0x2000 && c <= 0x2BFF) || (c >= 0x3000 && c <= 0x303F) || (c >= 0xFE00 && c <= 0xFE0F) || (c >= 0x1F000 && c <= 0x1FAFF) || (c >= 0xFF00 && c <= 0xFF0F)) return 0; /* punctuation, symbols, emoji */
64
+ return 1;
65
+ }
66
+
67
+ /* ---- the three splitting patterns of the "olmo" pre-tokenizer; each returns the length of the match at s[i] (0 = no match) ---- */
68
+ static int cp_at(const uint8_t *s, size_t i, size_t end, int *cp) { return get_utf8(s + i, end - i, cp); }
69
+
70
+ static size_t run(const uint8_t *s, size_t i, size_t end, int (*in)(int), int negate_all_three) {
71
+ size_t j = i;
72
+ while (j < end) {
73
+ int cp, n = cp_at(s, j, end, &cp);
74
+ int ok = negate_all_three ? !(is_space(cp) || is_letter(cp) || is_digit(cp)) : in(cp);
75
+ if (!ok) break;
76
+ j += (size_t)n;
77
+ }
78
+ return j - i;
79
+ }
80
+ static size_t m_gpt2(const uint8_t *s, size_t i, size_t end) { /* 's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S) */
81
+ static const char *contr[] = {"'s", "'t", "'re", "'ve", "'m", "'ll", "'d"};
82
+ for (size_t c = 0; c < sizeof contr / sizeof *contr; c++) { size_t n = strlen(contr[c]); if (end - i >= n && !memcmp(s + i, contr[c], n)) return n; }
83
+ size_t j = i;
84
+ if (s[j] == ' ') j++;
85
+ if (j < end) {
86
+ size_t n;
87
+ if ((n = run(s, j, end, is_letter, 0))) return j - i + n;
88
+ if ((n = run(s, j, end, is_digit, 0))) return j - i + n;
89
+ if ((n = run(s, j, end, NULL, 1))) return j - i + n;
90
+ }
91
+ /* \s+(?!\S): the longest whitespace run that is followed by whitespace or the end; a run followed by text gives back its last character */
92
+ size_t k = i, last = i;
93
+ while (k < end) { int cp, n = cp_at(s, k, end, &cp); if (!is_space(cp)) break; last = k; k += (size_t)n; }
94
+ if (k == i) return 0;
95
+ if (k == end) return k - i;
96
+ return last > i ? last - i : 0;
97
+ }
98
+
99
+ static int is_nl(int c) { return c == '\r' || c == '\n'; }
100
+ static size_t ws_run_end(const uint8_t *s, size_t i, size_t end) { /* end of the whitespace run starting at i */
101
+ size_t k = i;
102
+ while (k < end) { int cp, n = cp_at(s, k, end, &cp); if (!is_space(cp)) break; k += (size_t)n; }
103
+ return k;
104
+ }
105
+ /* Qwen2's pre-tokenizer: (?i:'s|'t|'re|'ve|'m|'ll|'d) | [^\r\n\p{L}\p{N}]?\p{L}+ | \p{N} | ?[^\s\p{L}\p{N}]+[\r\n]* | \s*[\r\n]+ | \s+(?!\S) | \s+ */
106
+ static size_t m_qwen2(const uint8_t *s, size_t i, size_t end) {
107
+ static const char *contr[] = {"'s", "'t", "'m", "'d", "'re", "'ve", "'ll"};
108
+ for (size_t c = 0; c < sizeof contr / sizeof *contr; c++) {
109
+ size_t n = strlen(contr[c]);
110
+ if (end - i < n) continue;
111
+ size_t k = 0;
112
+ while (k < n && (s[i + k] == (uint8_t)contr[c][k] || (k > 0 && s[i + k] >= 'A' && s[i + k] <= 'Z' && s[i + k] + 32 == (uint8_t)contr[c][k]))) k++;
113
+ if (k == n) return n;
114
+ }
115
+ int cp, n0 = cp_at(s, i, end, &cp);
116
+ if (is_letter(cp)) return run(s, i, end, is_letter, 0); /* the optional leading character is absent */
117
+ if (!is_nl(cp) && !is_digit(cp)) { /* one leading character that is not a letter, digit or newline, then letters */
118
+ size_t m = run(s, i + (size_t)n0, end, is_letter, 0);
119
+ if (m) return (size_t)n0 + m;
120
+ }
121
+ if (is_digit(cp)) return (size_t)n0; /* digits are one token each */
122
+ size_t j = i;
123
+ if (s[j] == ' ') j++;
124
+ if (j < end) {
125
+ size_t m = run(s, j, end, NULL, 1);
126
+ if (m) {
127
+ size_t k = j + m;
128
+ while (k < end && (s[k] == '\r' || s[k] == '\n')) k++;
129
+ return k - i;
130
+ }
131
+ }
132
+ size_t we = ws_run_end(s, i, end);
133
+ if (we == i) return 0;
134
+ size_t last_nl = 0; int has_nl = 0;
135
+ for (size_t k = i; k < we;) { int c2, n = cp_at(s, k, end, &c2); if (is_nl(c2)) { last_nl = k + (size_t)n; has_nl = 1; } k += (size_t)n; }
136
+ if (has_nl) return last_nl - i; /* \s*[\r\n]+ : up to the last newline of the run */
137
+ if (we == end) return we - i; /* \s+(?!\S) at the end of the text */
138
+ size_t lastc = i;
139
+ for (size_t k = i; k < we;) { lastc = k; int c2, n = cp_at(s, k, end, &c2); k += (size_t)n; }
140
+ if (lastc > i) return lastc - i; /* \s+(?!\S) gives the last space back to the next token */
141
+ return we - i; /* \s+ : a single space before text */
142
+ }
143
+
144
+ typedef size_t (*matcher)(const uint8_t *, size_t, size_t);
145
+ typedef struct { size_t a, b; } seg;
146
+ typedef struct { seg *v; size_t n, cap; } segs;
147
+ static int segs_add(segs *l, size_t a, size_t b) {
148
+ if (a >= b) return 1;
149
+ if (l->n == l->cap) { l->cap = l->cap ? l->cap * 2 : 64; l->v = realloc(l->v, l->cap * sizeof(seg)); if (!l->v) return 0; }
150
+ l->v[l->n++] = (seg){a, b};
151
+ return 1;
152
+ }
153
+ /* Applies one pattern to every segment: matches become their own segments, and what lies between matches stays a segment for the next pattern. */
154
+ static int split(const uint8_t *s, const segs *in, segs *out, matcher m) {
155
+ for (size_t k = 0; k < in->n; k++) {
156
+ size_t a = in->v[k].a, end = in->v[k].b, gap = a, i = a;
157
+ while (i < end) {
158
+ size_t n = m(s, i, end);
159
+ if (n) { if (!segs_add(out, gap, i) || !segs_add(out, i, i + n)) return 0; i += n; gap = i; }
160
+ else { int cp; i += (size_t)cp_at(s, i, end, &cp); }
161
+ }
162
+ if (!segs_add(out, gap, end)) return 0;
163
+ }
164
+ return 1;
165
+ }
166
+
167
+ cl_tok *cl_tok_load(const cl_meta *m, char *err, size_t errlen) {
168
+ cl_str mdl, pre;
169
+ if (!cl_meta_str(m, "tokenizer.ggml.model", &mdl) || mdl.len != 4 || memcmp(mdl.p, "gpt2", 4)) { snprintf(err, errlen, "only the gpt2 (byte-level BPE) tokenizer is supported"); return NULL; }
170
+ int pre_kind = -1;
171
+ if (cl_meta_str(m, "tokenizer.ggml.pre", &pre)) { if (pre.len == 4 && !memcmp(pre.p, "olmo", 4)) pre_kind = 0; else if (pre.len == 5 && !memcmp(pre.p, "qwen2", 5)) pre_kind = 1; }
172
+ if (pre_kind < 0) { snprintf(err, errlen, "only the \"olmo\" and \"qwen2\" pre-tokenizers are implemented"); return NULL; }
173
+ cl_tok *t = calloc(1, sizeof *t);
174
+ if (!t) { snprintf(err, errlen, "out of memory"); return NULL; }
175
+ t->pre = pre_kind;
176
+ t->tokens = cl_meta_str_array(m, "tokenizer.ggml.tokens", &t->n);
177
+ uint64_t nt = 0, nm = 0;
178
+ t->types = cl_meta_i32_array(m, "tokenizer.ggml.token_type", &nt);
179
+ cl_str *merges = cl_meta_str_array(m, "tokenizer.ggml.merges", &nm);
180
+ if (!t->tokens || !merges) { snprintf(err, errlen, "the model has no tokenizer tables"); free(merges); cl_tok_free(t); return NULL; }
181
+ if (!smap_init(&t->vocab, t->n) || !smap_init(&t->merges, nm)) { snprintf(err, errlen, "out of memory"); free(merges); cl_tok_free(t); return NULL; }
182
+ for (uint64_t i = 0; i < t->n; i++) smap_put(&t->vocab, t->tokens[i].p, t->tokens[i].len, (int32_t)i);
183
+ for (uint64_t i = 0; i < nm; i++) smap_put(&t->merges, merges[i].p, merges[i].len, (int32_t)i); /* "left right": the rank is the line number */
184
+ free(merges);
185
+ uint64_t v;
186
+ t->bos = cl_meta_u64(m, "tokenizer.ggml.bos_token_id", &v) ? (int)v : -1;
187
+ t->eos = cl_meta_u64(m, "tokenizer.ggml.eos_token_id", &v) ? (int)v : -1;
188
+ t->add_bos = cl_meta_u64(m, "tokenizer.ggml.add_bos_token", &v) ? (int)v : 0;
189
+ int n = 0;
190
+ for (int i = 0; i < 512; i++) t->cp_byte[i] = -1;
191
+ for (int b = 0; b < 256; b++) {
192
+ int printable = (b >= 33 && b <= 126) || (b >= 161 && b <= 172) || (b >= 174 && b <= 255);
193
+ t->byte_cp[b] = printable ? b : 256 + n++;
194
+ t->cp_byte[t->byte_cp[b]] = b;
195
+ }
196
+ t->special = malloc((t->n + 1) * sizeof(int));
197
+ if (t->types && t->special) for (uint64_t i = 0; i < t->n && i < nt; i++) if ((t->types[i] == 3 || t->types[i] == 4) && t->tokens[i].len > 0) t->special[t->n_special++] = (int)i;
198
+ for (int a = 0; a < t->n_special; a++) for (int b = a + 1; b < t->n_special; b++) /* longest first */
199
+ if (t->tokens[t->special[b]].len > t->tokens[t->special[a]].len) { int x = t->special[a]; t->special[a] = t->special[b]; t->special[b] = x; }
200
+ return t;
201
+ }
202
+
203
+ void cl_tok_free(cl_tok *t) {
204
+ if (!t) return;
205
+ free(t->tokens); free(t->types); free(t->vocab.s); free(t->merges.s); free(t->special);
206
+ free(t);
207
+ }
208
+ int cl_tok_vocab_size(const cl_tok *t) { return (int)t->n; }
209
+ int cl_tok_bos(const cl_tok *t) { return t->bos; }
210
+ int cl_tok_eos(const cl_tok *t) { return t->eos; }
211
+ int cl_tok_add_bos(const cl_tok *t) { return t->add_bos; }
212
+
213
+ /* Byte-pair-encodes one pre-token and appends its ids. */
214
+ static int bpe(const cl_tok *t, const uint8_t *s, size_t len, int32_t *out, int cap, int *n) {
215
+ if (len == 0) return 1;
216
+ char *buf = malloc(len * 3 + 1);
217
+ size_t *st = malloc((len + 1) * sizeof(size_t)); /* start offset of each symbol in buf, plus the end */
218
+ if (!buf || !st) { free(buf); free(st); return 0; }
219
+ size_t pos = 0, ns = 0;
220
+ for (size_t i = 0; i < len; i++) { st[ns++] = pos; pos += (size_t)put_utf8(buf + pos, t->byte_cp[s[i]]); }
221
+ st[ns] = pos;
222
+ for (;;) { /* merge the adjacent pair with the lowest rank until none is in the merge table */
223
+ int32_t best = -1; size_t bi = 0;
224
+ for (size_t i = 0; i + 1 < ns; i++) {
225
+ size_t la = st[i + 1] - st[i], lb = st[i + 2] - st[i + 1];
226
+ char key[256];
227
+ if (la + lb + 1 > sizeof key) continue;
228
+ memcpy(key, buf + st[i], la); key[la] = ' '; memcpy(key + la + 1, buf + st[i + 1], lb);
229
+ int32_t r = smap_get(&t->merges, key, la + 1 + lb);
230
+ if (r >= 0 && (best < 0 || r < best)) { best = r; bi = i; }
231
+ }
232
+ if (best < 0) break;
233
+ memmove(&st[bi + 1], &st[bi + 2], (ns - bi - 1) * sizeof(size_t)); /* drop the boundary between the pair */
234
+ ns--;
235
+ }
236
+ for (size_t i = 0; i < ns; i++) {
237
+ int32_t id = smap_get(&t->vocab, buf + st[i], st[i + 1] - st[i]);
238
+ if (id < 0) { free(buf); free(st); return 0; }
239
+ if (*n >= cap) { free(buf); free(st); return -1; }
240
+ out[(*n)++] = id;
241
+ }
242
+ free(buf); free(st);
243
+ return 1;
244
+ }
245
+
246
+ static int encode_plain(const cl_tok *t, const uint8_t *s, size_t len, int32_t *out, int cap, int *n) {
247
+ segs a = {0}, b = {0}, c = {0}, d = {0};
248
+ int rc = 1;
249
+ if (!segs_add(&a, 0, len) || !split(s, &a, &d, t->pre == 1 ? m_qwen2 : m_gpt2)) rc = 0;
250
+ for (size_t i = 0; rc == 1 && i < d.n; i++) rc = bpe(t, s + d.v[i].a, d.v[i].b - d.v[i].a, out, cap, n);
251
+ free(a.v); free(b.v); free(c.v); free(d.v);
252
+ return rc;
253
+ }
254
+
255
+ int cl_tok_encode(const cl_tok *t, const char *text, size_t len, int parse_special, int32_t *out, int cap) {
256
+ const uint8_t *s = (const uint8_t *)text;
257
+ int n = 0;
258
+ size_t from = 0, i = 0;
259
+ while (parse_special && i < len) {
260
+ int hit = -1;
261
+ for (int k = 0; k < t->n_special && hit < 0; k++) {
262
+ const cl_str *tk = &t->tokens[t->special[k]];
263
+ if (len - i >= tk->len && !memcmp(s + i, tk->p, tk->len)) hit = t->special[k];
264
+ }
265
+ if (hit < 0) { i++; continue; }
266
+ int rc = encode_plain(t, s + from, i - from, out, cap, &n);
267
+ if (rc != 1) return -1;
268
+ if (n >= cap) return -1;
269
+ out[n++] = hit;
270
+ i += t->tokens[hit].len; from = i;
271
+ }
272
+ return encode_plain(t, s + from, len - from, out, cap, &n) == 1 ? n : -1;
273
+ }
274
+
275
+ int cl_tok_piece(const cl_tok *t, int id, char *out, int cap) {
276
+ if (id < 0 || (uint64_t)id >= t->n) return 0;
277
+ const cl_str *tk = &t->tokens[id];
278
+ int ctl = t->types && (t->types[id] == 3 || t->types[id] == 4);
279
+ int w = 0;
280
+ for (size_t i = 0; i < tk->len && w < cap;) {
281
+ if (ctl) { out[w++] = tk->p[i++]; continue; }
282
+ int cp, n = get_utf8((const uint8_t *)tk->p + i, tk->len - i, &cp);
283
+ i += (size_t)n;
284
+ out[w++] = (char)(cp < 512 && t->cp_byte[cp] >= 0 ? t->cp_byte[cp] : '?');
285
+ }
286
+ return w;
287
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Byte-level BPE tokenizer (GPT-2 family) built from a GGUF file's own tokenizer tables. The pre-tokenizers implemented are the ones llama.cpp
3
+ * calls "olmo" and "qwen2". Letter, number and punctuation classes are exact for ASCII and approximate above U+007F (see tokenizer.c), so non-English
4
+ * text can tokenize differently from llama.cpp; `make verify-llama` checks agreement on a fixed set of prompts.
5
+ */
6
+ #ifndef CL_TOKENIZER_H
7
+ #define CL_TOKENIZER_H
8
+ #include "meta.h"
9
+ #include <stddef.h>
10
+ #include <stdint.h>
11
+
12
+ typedef struct cl_tok cl_tok;
13
+
14
+ cl_tok *cl_tok_load(const cl_meta *m, char *err, size_t errlen);
15
+ void cl_tok_free(cl_tok *t);
16
+ int cl_tok_vocab_size(const cl_tok *t);
17
+ int cl_tok_bos(const cl_tok *t);
18
+ int cl_tok_eos(const cl_tok *t);
19
+ int cl_tok_add_bos(const cl_tok *t);
20
+ /* Encodes text. With parse_special, control tokens such as <|endoftext|> written in the text become their single token. Returns the number of
21
+ * tokens written, or -1 when `cap` is too small. */
22
+ int cl_tok_encode(const cl_tok *t, const char *text, size_t len, int parse_special, int32_t *out, int cap);
23
+ /* The bytes of one token (control tokens give their text). Returns the length written, at most cap. */
24
+ int cl_tok_piece(const cl_tok *t, int id, char *out, int cap);
25
+ #endif
package/c/src/xs.c ADDED
@@ -0,0 +1,215 @@
1
+ #define _GNU_SOURCE
2
+ #include "xs.h"
3
+ #include <errno.h>
4
+ #include <fcntl.h>
5
+ #include <pthread.h>
6
+ #include <stdio.h>
7
+ #include <stdlib.h>
8
+ #include <string.h>
9
+ #include <time.h>
10
+ #include <unistd.h>
11
+
12
+ enum { S_EMPTY = 0, S_LOADING = 1, S_READY = 2 };
13
+ typedef struct job { int slot, layer, expert, prefetch; struct job *next; } job;
14
+
15
+ struct cl_xs {
16
+ int fd, n_layer, n_expert, n_used, bypass_os;
17
+ const cl_xs_layer *lay;
18
+ cl_xs_layer *lay_copy;
19
+ pthread_mutex_t mu;
20
+ pthread_cond_t cv_work, cv_done;
21
+ job *dq_head, *dq_tail, *pq_head, *pq_tail;
22
+ int shutdown, failed, nth;
23
+ pthread_t th[16];
24
+ cl_cache *cache;
25
+ uint32_t total; /* cache + staging slots */
26
+ int nbypass, bypass_next;
27
+ uint64_t slot_bytes;
28
+ uint8_t *arena;
29
+ int *state;
30
+ cl_xs_stats st;
31
+ };
32
+
33
+ static uint64_t now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; }
34
+
35
+ static void push_job(cl_xs *x, job *j, int demand) {
36
+ job **h = demand ? &x->dq_head : &x->pq_head, **t = demand ? &x->dq_tail : &x->pq_tail;
37
+ j->next = NULL;
38
+ if (*t) (*t)->next = j; else *h = j;
39
+ *t = j;
40
+ pthread_cond_signal(&x->cv_work);
41
+ }
42
+ static job *pop_job(cl_xs *x) {
43
+ job **h = x->dq_head ? &x->dq_head : &x->pq_head, **t = x->dq_head ? &x->dq_tail : &x->pq_tail; /* demand before prefetch */
44
+ job *j = *h;
45
+ if (!j) return NULL;
46
+ *h = j->next;
47
+ if (!*h) *t = NULL;
48
+ return j;
49
+ }
50
+
51
+ static int read_full(cl_xs *x, uint8_t *dst, uint64_t len, uint64_t off) {
52
+ uint64_t done = 0;
53
+ while (done < len) {
54
+ ssize_t n = pread(x->fd, dst + done, (size_t)(len - done), (off_t)(off + done));
55
+ if (n < 0 && errno == EINTR) continue;
56
+ if (n <= 0) return -1;
57
+ done += (uint64_t)n;
58
+ }
59
+ #if defined(__linux__)
60
+ if (x->bypass_os) posix_fadvise(x->fd, (off_t)off, (off_t)len, POSIX_FADV_DONTNEED);
61
+ #endif
62
+ return 0;
63
+ }
64
+
65
+ static void *worker(void *arg) {
66
+ cl_xs *x = arg;
67
+ pthread_mutex_lock(&x->mu);
68
+ for (;;) {
69
+ job *j = pop_job(x);
70
+ if (!j) { if (x->shutdown) break; pthread_cond_wait(&x->cv_work, &x->mu); continue; }
71
+ pthread_mutex_unlock(&x->mu);
72
+ uint64_t t0 = now_ns();
73
+ const cl_xs_layer *L = &x->lay_copy[j->layer];
74
+ uint8_t *dst = x->arena + (size_t)j->slot * x->slot_bytes;
75
+ uint64_t at = 0, bytes = 0;
76
+ int rc = 0;
77
+ for (int i = 0; i < 3 && !rc; i++) { rc = read_full(x, dst + at, L->slab[i], L->off[i] + (uint64_t)j->expert * L->slab[i]); at += L->slab[i]; }
78
+ bytes = at;
79
+ uint64_t dt = now_ns() - t0;
80
+ pthread_mutex_lock(&x->mu);
81
+ x->st.read_ns += dt;
82
+ if (rc) x->failed = 1;
83
+ x->state[j->slot] = rc ? S_EMPTY : S_READY;
84
+ if (j->prefetch) { x->st.prefetch_bytes += bytes; cl_cache_unpin(x->cache, j->slot); } else x->st.demand_bytes += bytes;
85
+ pthread_cond_broadcast(&x->cv_done);
86
+ free(j);
87
+ }
88
+ pthread_mutex_unlock(&x->mu);
89
+ return NULL;
90
+ }
91
+
92
+ cl_xs *cl_xs_open(const char *path, const cl_xs_layer *layers, int n_layer, int n_expert, int n_used, uint64_t cache_bytes, int stage_slots,
93
+ cl_policy policy, int io_threads, int os_cache_bypass, char *err, size_t errlen) {
94
+ cl_xs *x = calloc(1, sizeof *x);
95
+ if (!x) { snprintf(err, errlen, "out of memory"); return NULL; }
96
+ x->n_layer = n_layer; x->n_expert = n_expert; x->n_used = n_used; x->bypass_os = os_cache_bypass;
97
+ x->lay_copy = malloc((size_t)n_layer * sizeof *layers);
98
+ if (!x->lay_copy) { snprintf(err, errlen, "out of memory"); free(x); return NULL; }
99
+ memcpy(x->lay_copy, layers, (size_t)n_layer * sizeof *layers);
100
+ for (int l = 0; l < n_layer; l++) { uint64_t s = layers[l].slab[0] + layers[l].slab[1] + layers[l].slab[2]; if (s > x->slot_bytes) x->slot_bytes = s; }
101
+ x->fd = open(path, O_RDONLY);
102
+ if (x->fd < 0) { snprintf(err, errlen, "cannot open %s: %s", path, strerror(errno)); free(x->lay_copy); free(x); return NULL; }
103
+ #ifdef F_NOCACHE
104
+ if (os_cache_bypass) fcntl(x->fd, F_NOCACHE, 1);
105
+ #endif
106
+ uint32_t cslots = (uint32_t)(cache_bytes / x->slot_bytes);
107
+ if (cslots < (uint32_t)n_used) cslots = (uint32_t)n_used; /* a layer's experts must fit at once */
108
+ uint32_t sslots = stage_slots < 0 ? 0 : (uint32_t)stage_slots;
109
+ x->cache = cl_cache_new(cslots, sslots, (uint32_t)n_layer * (uint32_t)n_expert, policy, 0);
110
+ x->total = x->cache ? cl_cache_total_slots(x->cache) : 0; /* what the cache really manages (it may round) */
111
+ x->nbypass = n_used + 2;
112
+ size_t nbuf = (size_t)x->total + (size_t)x->nbypass;
113
+ x->arena = aligned_alloc(4096, (nbuf * x->slot_bytes + 4095) / 4096 * 4096);
114
+ x->state = calloc(nbuf, sizeof(int));
115
+ if (!x->cache || !x->arena || !x->state) { snprintf(err, errlen, "out of memory"); cl_xs_close(x); return NULL; }
116
+ pthread_mutex_init(&x->mu, NULL); pthread_cond_init(&x->cv_work, NULL); pthread_cond_init(&x->cv_done, NULL);
117
+ x->nth = io_threads < 1 ? 1 : io_threads > 16 ? 16 : io_threads;
118
+ for (int i = 0; i < x->nth; i++) pthread_create(&x->th[i], NULL, worker, x);
119
+ return x;
120
+ }
121
+
122
+ void cl_xs_close(cl_xs *x) {
123
+ if (!x) return;
124
+ if (x->nth) {
125
+ pthread_mutex_lock(&x->mu);
126
+ x->shutdown = 1;
127
+ pthread_cond_broadcast(&x->cv_work);
128
+ pthread_mutex_unlock(&x->mu);
129
+ for (int i = 0; i < x->nth; i++) pthread_join(x->th[i], NULL);
130
+ }
131
+ if (x->cache) cl_cache_free(x->cache);
132
+ if (x->fd > 0) close(x->fd);
133
+ free(x->arena); free(x->state); free(x->lay_copy); free(x);
134
+ }
135
+
136
+ uint64_t cl_xs_slot_bytes(const cl_xs *x) { return x->slot_bytes; }
137
+ uint32_t cl_xs_cache_slots(const cl_xs *x) { return x->total ? cl_cache_total_slots(x->cache) : 0; }
138
+ uint64_t cl_xs_buffer_bytes(const cl_xs *x) { return ((uint64_t)x->total + (uint64_t)x->nbypass) * x->slot_bytes + cl_cache_meta_bytes(x->cache); }
139
+
140
+ int cl_xs_request(cl_xs *x, int layer, int expert) {
141
+ if (layer < 0 || layer >= x->n_layer || expert < 0 || expert >= x->n_expert) return -1;
142
+ pthread_mutex_lock(&x->mu);
143
+ int slot, r = cl_cache_access(x->cache, (uint32_t)(layer * x->n_expert + expert), &slot);
144
+ x->st.accesses++;
145
+ if (r == 1) x->st.hits++;
146
+ else if (r == 2) { x->st.stage_hits++; x->st.hits++; if (x->state[slot] != S_READY) x->st.prefetch_late++; }
147
+ else {
148
+ x->st.misses++;
149
+ if (r == -1) { /* nothing could be evicted: read into a spare buffer that the cache does not manage */
150
+ slot = (int)x->total + (x->bypass_next++ % x->nbypass);
151
+ x->st.bypass++;
152
+ }
153
+ x->state[slot] = S_LOADING;
154
+ job *j = calloc(1, sizeof *j);
155
+ j->slot = slot; j->layer = layer; j->expert = expert; j->prefetch = 0;
156
+ push_job(x, j, 1);
157
+ }
158
+ pthread_mutex_unlock(&x->mu);
159
+ return slot;
160
+ }
161
+
162
+ const uint8_t *cl_xs_wait(cl_xs *x, int h) {
163
+ pthread_mutex_lock(&x->mu);
164
+ uint64_t t0 = 0;
165
+ int waited = 0;
166
+ while (x->state[h] != S_READY && !x->failed) {
167
+ if (!waited) { waited = 1; t0 = now_ns(); }
168
+ pthread_cond_wait(&x->cv_done, &x->mu);
169
+ }
170
+ if (waited) x->st.wait_ns += now_ns() - t0;
171
+ int ok = x->state[h] == S_READY;
172
+ pthread_mutex_unlock(&x->mu);
173
+ return ok ? x->arena + (size_t)h * x->slot_bytes : NULL;
174
+ }
175
+
176
+ void cl_xs_release(cl_xs *x, int h) {
177
+ if (h < 0) return;
178
+ pthread_mutex_lock(&x->mu);
179
+ if ((uint32_t)h < x->total) cl_cache_unpin(x->cache, h);
180
+ else x->state[h] = S_EMPTY;
181
+ pthread_mutex_unlock(&x->mu);
182
+ }
183
+
184
+ int cl_xs_resident(cl_xs *x, int layer, int expert) {
185
+ if (layer < 0 || layer >= x->n_layer || expert < 0 || expert >= x->n_expert) return 0;
186
+ pthread_mutex_lock(&x->mu);
187
+ int slot = cl_cache_find(x->cache, (uint32_t)(layer * x->n_expert + expert));
188
+ int r = slot >= 0 && x->state[slot] == S_READY;
189
+ pthread_mutex_unlock(&x->mu);
190
+ return r;
191
+ }
192
+
193
+ void cl_xs_prefetch(cl_xs *x, int layer, int expert) {
194
+ if (layer < 0 || layer >= x->n_layer || expert < 0 || expert >= x->n_expert) return;
195
+ pthread_mutex_lock(&x->mu);
196
+ int slot = cl_cache_stage(x->cache, (uint32_t)(layer * x->n_expert + expert));
197
+ if (slot >= 0) {
198
+ x->state[slot] = S_LOADING;
199
+ x->st.prefetch_issued++;
200
+ job *j = calloc(1, sizeof *j);
201
+ j->slot = slot; j->layer = layer; j->expert = expert; j->prefetch = 1;
202
+ push_job(x, j, 0);
203
+ }
204
+ pthread_mutex_unlock(&x->mu);
205
+ }
206
+
207
+ cl_xs_stats cl_xs_get_stats(cl_xs *x) {
208
+ pthread_mutex_lock(&x->mu);
209
+ cl_xs_stats s = x->st;
210
+ cl_cache_stats cs = cl_cache_get_stats(x->cache);
211
+ s.admit_rejects = cs.admit_rejects; s.evictions = cs.evictions; s.prefetch_wasted = cs.stage_wasted;
212
+ pthread_mutex_unlock(&x->mu);
213
+ return s;
214
+ }
215
+ void cl_xs_reset_stats(cl_xs *x) { pthread_mutex_lock(&x->mu); memset(&x->st, 0, sizeof x->st); pthread_mutex_unlock(&x->mu); }
package/c/src/xs.h ADDED
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Expert store: serves the experts of a real forward pass from disk through Clutor's cache, so resident memory is bounded by the cache
3
+ * budget and not by the model's size. A slot holds one expert's gate, up and down matrices back to back. Misses are read by a pool of I/O
4
+ * threads (pread, with the OS cache bypassed when asked); a prefetch fills a staging slot that the cache policy cannot be evicted by.
5
+ */
6
+ #ifndef CL_XS_H
7
+ #define CL_XS_H
8
+ #include "cache.h"
9
+ #include <stddef.h>
10
+ #include <stdint.h>
11
+
12
+ typedef struct { uint64_t off[3], slab[3]; } cl_xs_layer; /* file offset of expert 0 and bytes per expert, for gate, up, down */
13
+
14
+ typedef struct {
15
+ uint64_t accesses, hits, stage_hits, misses, bypass, prefetch_issued, prefetch_late, prefetch_wasted, admit_rejects, evictions;
16
+ uint64_t demand_bytes, prefetch_bytes, read_ns, wait_ns;
17
+ } cl_xs_stats;
18
+
19
+ typedef struct cl_xs cl_xs;
20
+
21
+ cl_xs *cl_xs_open(const char *path, const cl_xs_layer *layers, int n_layer, int n_expert, int n_used, uint64_t cache_bytes, int stage_slots,
22
+ cl_policy policy, int io_threads, int os_cache_bypass, char *err, size_t errlen);
23
+ void cl_xs_close(cl_xs *x);
24
+ uint64_t cl_xs_slot_bytes(const cl_xs *x);
25
+ uint32_t cl_xs_cache_slots(const cl_xs *x);
26
+ uint64_t cl_xs_buffer_bytes(const cl_xs *x); /* memory the store holds for expert data and its tables */
27
+
28
+ /* Starts getting an expert. Returns a handle (>= 0) or -1 on failure. Misses are read in the background. */
29
+ int cl_xs_request(cl_xs *x, int layer, int expert);
30
+ /* Blocks until the expert is in memory; returns its bytes (gate, up, down in that order) or NULL if the read failed. */
31
+ const uint8_t *cl_xs_wait(cl_xs *x, int handle);
32
+ void cl_xs_release(cl_xs *x, int handle);
33
+ /* 1 when this expert is in memory right now (in the cache or fully read into staging), so asking for it costs no disk read. */
34
+ int cl_xs_resident(cl_xs *x, int layer, int expert);
35
+ /* A hint: read this expert into staging if there is room and it is not already here. */
36
+ void cl_xs_prefetch(cl_xs *x, int layer, int expert);
37
+ cl_xs_stats cl_xs_get_stats(cl_xs *x);
38
+ void cl_xs_reset_stats(cl_xs *x);
39
+ #endif
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-6WAEQCCP.js";
4
+ } from "./chunk-2FBPYNHP.js";
5
5
  import "./chunk-U5GHVJJO.js";
6
6
 
7
7
  // src/bin.ts
@@ -40,6 +40,7 @@ var prompt = (q) => new Promise((resolve) => {
40
40
  });
41
41
  main(process.argv.slice(2), {
42
42
  out: (l) => console.log(l),
43
+ write: (s) => void process.stdout.write(s),
43
44
  err: (l) => console.error(l),
44
45
  cwd: process.cwd(),
45
46
  ...process.stdin.isTTY ? { prompt } : {}
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { mkdirSync, readFileSync, appendFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport { main } from \"./cli\";\n\n// One readline interface for the whole process, so arrow-key history works across prompts and Ctrl-D ends an interactive session.\nlet rl: Interface | undefined;\nconst historyFile = join(homedir(), \".clutor\", \"history\");\nfunction readline(): Interface {\n if (rl) return rl;\n let history: string[] = [];\n try { history = readFileSync(historyFile, \"utf8\").split(\"\\n\").filter(Boolean).slice(-500).reverse(); } catch { /* no history yet */ }\n rl = createInterface({ input: process.stdin, output: process.stderr, history, historySize: 500 });\n return rl;\n}\nconst prompt = (q: string): Promise<string> => new Promise((resolve) => {\n const r = readline();\n const onClose = (): void => resolve(\"/exit\");\n r.once(\"close\", onClose);\n r.question(q, (a) => {\n r.off(\"close\", onClose);\n if (a.trim()) { try { mkdirSync(join(homedir(), \".clutor\"), { recursive: true, mode: 0o700 }); appendFileSync(historyFile, `${a.replace(/\\n/g, \" \")}\\n`, { mode: 0o600 }); } catch { /* history is a convenience */ } }\n resolve(a);\n });\n});\n\nmain(process.argv.slice(2), {\n out: (l) => console.log(l),\n err: (l) => console.error(l),\n cwd: process.cwd(),\n ...(process.stdin.isTTY ? { prompt } : {}),\n}).then(\n (code) => process.exit(code),\n (e: unknown) => {\n console.error(e instanceof Error ? e.message : String(e));\n process.exit(1);\n },\n);\n"],"mappings":";;;;;;;AACA,SAAS,WAAW,cAAc,sBAAsB;AACxD,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,uBAAuC;AAIhD,IAAI;AACJ,IAAM,cAAc,KAAK,QAAQ,GAAG,WAAW,SAAS;AACxD,SAAS,WAAsB;AAC7B,MAAI,GAAI,QAAO;AACf,MAAI,UAAoB,CAAC;AACzB,MAAI;AAAE,cAAU,aAAa,aAAa,MAAM,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI,EAAE,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAuB;AACpI,OAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,SAAS,aAAa,IAAI,CAAC;AAChG,SAAO;AACT;AACA,IAAM,SAAS,CAAC,MAA+B,IAAI,QAAQ,CAAC,YAAY;AACtE,QAAM,IAAI,SAAS;AACnB,QAAM,UAAU,MAAY,QAAQ,OAAO;AAC3C,IAAE,KAAK,SAAS,OAAO;AACvB,IAAE,SAAS,GAAG,CAAC,MAAM;AACnB,MAAE,IAAI,SAAS,OAAO;AACtB,QAAI,EAAE,KAAK,GAAG;AAAE,UAAI;AAAE,kBAAU,KAAK,QAAQ,GAAG,SAAS,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAG,uBAAe,aAAa,GAAG,EAAE,QAAQ,OAAO,GAAG,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAiC;AAAA,IAAE;AACtN,YAAQ,CAAC;AAAA,EACX,CAAC;AACH,CAAC;AAED,KAAK,QAAQ,KAAK,MAAM,CAAC,GAAG;AAAA,EAC1B,KAAK,CAAC,MAAM,QAAQ,IAAI,CAAC;AAAA,EACzB,KAAK,CAAC,MAAM,QAAQ,MAAM,CAAC;AAAA,EAC3B,KAAK,QAAQ,IAAI;AAAA,EACjB,GAAI,QAAQ,MAAM,QAAQ,EAAE,OAAO,IAAI,CAAC;AAC1C,CAAC,EAAE;AAAA,EACD,CAAC,SAAS,QAAQ,KAAK,IAAI;AAAA,EAC3B,CAAC,MAAe;AACd,YAAQ,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AACxD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { mkdirSync, readFileSync, appendFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createInterface, type Interface } from \"node:readline\";\nimport { main } from \"./cli\";\n\n// One readline interface for the whole process, so arrow-key history works across prompts and Ctrl-D ends an interactive session.\nlet rl: Interface | undefined;\nconst historyFile = join(homedir(), \".clutor\", \"history\");\nfunction readline(): Interface {\n if (rl) return rl;\n let history: string[] = [];\n try { history = readFileSync(historyFile, \"utf8\").split(\"\\n\").filter(Boolean).slice(-500).reverse(); } catch { /* no history yet */ }\n rl = createInterface({ input: process.stdin, output: process.stderr, history, historySize: 500 });\n return rl;\n}\nconst prompt = (q: string): Promise<string> => new Promise((resolve) => {\n const r = readline();\n const onClose = (): void => resolve(\"/exit\");\n r.once(\"close\", onClose);\n r.question(q, (a) => {\n r.off(\"close\", onClose);\n if (a.trim()) { try { mkdirSync(join(homedir(), \".clutor\"), { recursive: true, mode: 0o700 }); appendFileSync(historyFile, `${a.replace(/\\n/g, \" \")}\\n`, { mode: 0o600 }); } catch { /* history is a convenience */ } }\n resolve(a);\n });\n});\n\nmain(process.argv.slice(2), {\n out: (l) => console.log(l),\n write: (s) => void process.stdout.write(s),\n err: (l) => console.error(l),\n cwd: process.cwd(),\n ...(process.stdin.isTTY ? { prompt } : {}),\n}).then(\n (code) => process.exit(code),\n (e: unknown) => {\n console.error(e instanceof Error ? e.message : String(e));\n process.exit(1);\n },\n);\n"],"mappings":";;;;;;;AACA,SAAS,WAAW,cAAc,sBAAsB;AACxD,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,uBAAuC;AAIhD,IAAI;AACJ,IAAM,cAAc,KAAK,QAAQ,GAAG,WAAW,SAAS;AACxD,SAAS,WAAsB;AAC7B,MAAI,GAAI,QAAO;AACf,MAAI,UAAoB,CAAC;AACzB,MAAI;AAAE,cAAU,aAAa,aAAa,MAAM,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI,EAAE,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAuB;AACpI,OAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,SAAS,aAAa,IAAI,CAAC;AAChG,SAAO;AACT;AACA,IAAM,SAAS,CAAC,MAA+B,IAAI,QAAQ,CAAC,YAAY;AACtE,QAAM,IAAI,SAAS;AACnB,QAAM,UAAU,MAAY,QAAQ,OAAO;AAC3C,IAAE,KAAK,SAAS,OAAO;AACvB,IAAE,SAAS,GAAG,CAAC,MAAM;AACnB,MAAE,IAAI,SAAS,OAAO;AACtB,QAAI,EAAE,KAAK,GAAG;AAAE,UAAI;AAAE,kBAAU,KAAK,QAAQ,GAAG,SAAS,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAG,uBAAe,aAAa,GAAG,EAAE,QAAQ,OAAO,GAAG,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAiC;AAAA,IAAE;AACtN,YAAQ,CAAC;AAAA,EACX,CAAC;AACH,CAAC;AAED,KAAK,QAAQ,KAAK,MAAM,CAAC,GAAG;AAAA,EAC1B,KAAK,CAAC,MAAM,QAAQ,IAAI,CAAC;AAAA,EACzB,OAAO,CAAC,MAAM,KAAK,QAAQ,OAAO,MAAM,CAAC;AAAA,EACzC,KAAK,CAAC,MAAM,QAAQ,MAAM,CAAC;AAAA,EAC3B,KAAK,QAAQ,IAAI;AAAA,EACjB,GAAI,QAAQ,MAAM,QAAQ,EAAE,OAAO,IAAI,CAAC;AAC1C,CAAC,EAAE;AAAA,EACD,CAAC,SAAS,QAAQ,KAAK,IAAI;AAAA,EAC3B,CAAC,MAAe;AACd,YAAQ,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AACxD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}