← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
ck_tokenizer.c
Go to the documentation of this file.
1/*
2 * C-Kernel-Engine BPE Tokenizer Implementation [v4 - correctness complete, optimization pending]
3 *
4 * Pure C implementation of Byte-Pair Encoding.
5 * Reads HuggingFace tokenizer.json format.
6 *
7 * Token IDs are dense indices into the embedding table:
8 * embedding[token_id] gives the vector for that token.
9 *
10 * TODO: Performance optimizations (correctness is done, these are for throughput):
11 *
12 * 1. Pre-allocate encode buffer
13 * - ck_tokenizer_encode() mallocs int32_t[] per call (line ~629)
14 * - Add encode_buf + encode_buf_cap to CKTokenizer struct
15 * - Allocate in ck_tokenizer_init(), realloc only if input exceeds cap
16 * - Eliminates malloc/free syscall per encode call
17 *
18 * 2. Linked list instead of array shift in merge loop
19 * - Merge at line ~686 shifts entire tail of tokens[] left by 1: O(n) per merge
20 * - With n initial tokens and m merges, total shifting is O(n * m)
21 * - Doubly-linked list would make each merge O(1)
22 *
23 * 3. Priority queue for best-merge scan
24 * - find_best_merge scans ALL pairs each iteration (line ~673): O(n) per merge
25 * - Min-heap of valid merge candidates: O(n log n) total instead of O(n * m)
26 *
27 * Current profile: tokenizer runs 2x per prompt (not per token). GEMM kernels
28 * dominate at 57% of compute. These optimizations matter for batch/server use.
29 *
30 * By Anthony Shivakumar
31 */
32
33#include "ck_tokenizer.h"
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <ctype.h>
39
40/* Simple JSON parser state */
41typedef struct {
42 const char *data;
43 const char *pos;
44 const char *end;
45} JSONParser;
46
47/* ========================================================================== */
48/* Memory Pool */
49/* ========================================================================== */
50
52 memset(pool, 0, sizeof(*pool));
53}
54
55static CKPoolBlock *pool_new_block(size_t capacity) {
56 CKPoolBlock *block = (CKPoolBlock *)malloc(sizeof(CKPoolBlock));
57 if (!block) return NULL;
58 block->data = (uint8_t *)malloc(capacity);
59 if (!block->data) {
60 free(block);
61 return NULL;
62 }
63 block->used = 0;
64 block->capacity = capacity;
65 block->next = NULL;
66 return block;
67}
68
69void *ck_pool_alloc(CKMemPool *pool, size_t size) {
70 /* Align to 8 bytes */
71 size = (size + 7) & ~7;
72
73 /* Check if current block has space */
74 if (pool->current && pool->current->used + size <= pool->current->capacity) {
75 void *ptr = pool->current->data + pool->current->used;
76 pool->current->used += size;
77 pool->total_allocated += size;
78 return ptr;
79 }
80
81 /* Need new block */
82 size_t block_size = CK_POOL_BLOCK_SIZE;
83 if (size > block_size) block_size = size;
84
85 CKPoolBlock *block = pool_new_block(block_size);
86 if (!block) return NULL;
87
88 block->next = pool->head;
89 pool->head = block;
90 pool->current = block;
91
92 void *ptr = block->data;
93 block->used = size;
94 pool->total_allocated += size;
95 return ptr;
96}
97
98char *ck_pool_strdup(CKMemPool *pool, const char *s, int len) {
99 if (len < 0) len = (int)strlen(s);
100 char *copy = (char *)ck_pool_alloc(pool, len + 1);
101 if (!copy) return NULL;
102 memcpy(copy, s, len);
103 copy[len] = '\0';
104 return copy;
105}
106
108 CKPoolBlock *block = pool->head;
109 while (block) {
110 CKPoolBlock *next = block->next;
111 free(block->data);
112 free(block);
113 block = next;
114 }
115 memset(pool, 0, sizeof(*pool));
116}
117
118/* ========================================================================== */
119/* Hash Functions */
120/* ========================================================================== */
121
122/* FNV-1a hash for strings */
123static uint32_t hash_string(const char *s, int len) {
124 uint32_t hash = 2166136261u;
125 for (int i = 0; i < len; i++) {
126 hash ^= (uint8_t)s[i];
127 hash *= 16777619u;
128 }
129 return hash;
130}
131
132/* Hash for merge pair (left_id, right_id) */
133static uint32_t hash_pair(int32_t left, int32_t right) {
134 uint64_t combined = ((uint64_t)left << 32) | (uint32_t)right;
135 /* MurmurHash3 finalizer */
136 combined ^= combined >> 33;
137 combined *= 0xff51afd7ed558ccdULL;
138 combined ^= combined >> 33;
139 combined *= 0xc4ceb9fe1a85ec53ULL;
140 combined ^= combined >> 33;
141 return (uint32_t)combined;
142}
143
144/* ========================================================================== */
145/* Tokenizer Init/Free */
146/* ========================================================================== */
147
149 memset(tok, 0, sizeof(*tok));
150 ck_pool_init(&tok->pool);
151
152 /* Default special tokens */
153 tok->unk_id = 0;
154 tok->bos_id = 1;
155 tok->eos_id = 2;
156 tok->pad_id = 3;
157
158 /* Allocate vocab hash table */
159 tok->vocab_hash_size = 65536; /* 64K buckets */
160 tok->vocab_hash = (CKVocabEntry **)calloc(tok->vocab_hash_size, sizeof(CKVocabEntry *));
161 if (!tok->vocab_hash) return -1;
162
163 /* Allocate reverse vocab */
164 tok->id_to_token = (char **)calloc(CK_MAX_VOCAB_SIZE, sizeof(char *));
165 if (!tok->id_to_token) {
166 free(tok->vocab_hash);
167 return -1;
168 }
169
170 /* Allocate merge hash table */
171 tok->merge_hash_size = 262144; /* 256K buckets */
172 tok->merge_hash = (int *)malloc(tok->merge_hash_size * sizeof(int));
173 if (!tok->merge_hash) {
174 free(tok->vocab_hash);
175 free(tok->id_to_token);
176 return -1;
177 }
178 memset(tok->merge_hash, -1, tok->merge_hash_size * sizeof(int));
179
180 return 0;
181}
182
184 ck_pool_free(&tok->pool);
185 free(tok->vocab_hash);
186 free(tok->id_to_token);
187 free(tok->merges);
188 free(tok->merge_hash);
189 memset(tok, 0, sizeof(*tok));
190}
191
192/* ========================================================================== */
193/* Vocabulary Operations */
194/* ========================================================================== */
195
196int32_t ck_tokenizer_add_token(CKTokenizer *tok, const char *token, int len) {
197 if (len < 0) len = (int)strlen(token);
198 if (tok->vocab_size >= CK_MAX_VOCAB_SIZE) return -1;
199
200 /* Check if already exists */
201 int32_t existing = ck_tokenizer_lookup(tok, token, len);
202 if (existing != tok->unk_id || (len == 0)) {
203 return existing;
204 }
205
206 /* Create new entry */
207 CKVocabEntry *entry = (CKVocabEntry *)ck_pool_alloc(&tok->pool, sizeof(CKVocabEntry));
208 if (!entry) return -1;
209
210 entry->token = ck_pool_strdup(&tok->pool, token, len);
211 if (!entry->token) return -1;
212 entry->token_len = len;
213 entry->id = tok->vocab_size;
214
215 /* Add to hash table */
216 uint32_t bucket = hash_string(token, len) % tok->vocab_hash_size;
217 entry->next = tok->vocab_hash[bucket];
218 tok->vocab_hash[bucket] = entry;
219
220 /* Add to reverse lookup */
221 tok->id_to_token[tok->vocab_size] = entry->token;
222
223 tok->vocab_size++;
224 return entry->id;
225}
226
227int32_t ck_tokenizer_lookup(const CKTokenizer *tok, const char *token, int len) {
228 if (len < 0) len = (int)strlen(token);
229 uint32_t bucket = hash_string(token, len) % tok->vocab_hash_size;
230
231 for (CKVocabEntry *e = tok->vocab_hash[bucket]; e; e = e->next) {
232 if (e->token_len == len && memcmp(e->token, token, len) == 0) {
233 return e->id;
234 }
235 }
236 return tok->unk_id;
237}
238
239const char *ck_tokenizer_id_to_token(const CKTokenizer *tok, int32_t id) {
240 if (id < 0 || id >= tok->vocab_size) return NULL;
241 return tok->id_to_token[id];
242}
243
244/* ========================================================================== */
245/* Merge Operations */
246/* ========================================================================== */
247
248int ck_tokenizer_add_merge(CKTokenizer *tok, int32_t left, int32_t right, int32_t merged) {
249 int idx = tok->num_merges;
250
251 /* Grow merges array if needed */
252 if (idx % 4096 == 0) {
253 size_t new_cap = (idx + 4096) * sizeof(CKMergeRule);
254 CKMergeRule *new_merges = (CKMergeRule *)realloc(tok->merges, new_cap);
255 if (!new_merges) return -1;
256 tok->merges = new_merges;
257 }
258
259 tok->merges[idx].left = left;
260 tok->merges[idx].right = right;
261 tok->merges[idx].merged = merged;
262 tok->merges[idx].priority = idx; /* Earlier = higher priority */
263
264 /* Add to hash table */
265 uint32_t bucket = hash_pair(left, right) % tok->merge_hash_size;
266 /* Linear probing */
267 while (tok->merge_hash[bucket] >= 0) {
268 bucket = (bucket + 1) % tok->merge_hash_size;
269 }
270 tok->merge_hash[bucket] = idx;
271
272 tok->num_merges++;
273 return 0;
274}
275
276int ck_tokenizer_lookup_merge(const CKTokenizer *tok, int32_t left, int32_t right) {
277 uint32_t bucket = hash_pair(left, right) % tok->merge_hash_size;
278
279 /* Linear probing */
280 int probes = 0;
281 while (tok->merge_hash[bucket] >= 0 && probes < tok->merge_hash_size) {
282 int idx = tok->merge_hash[bucket];
283 if (tok->merges[idx].left == left && tok->merges[idx].right == right) {
284 return idx;
285 }
286 bucket = (bucket + 1) % tok->merge_hash_size;
287 probes++;
288 }
289 return -1;
290}
291
292/* ========================================================================== */
293/* JSON Parser (minimal, just for tokenizer.json) */
294/* ========================================================================== */
295
296static void json_skip_whitespace(JSONParser *p) {
297 while (p->pos < p->end && isspace((unsigned char)*p->pos)) {
298 p->pos++;
299 }
300}
301
302static int json_match_char(JSONParser *p, char c) {
304 if (p->pos < p->end && *p->pos == c) {
305 p->pos++;
306 return 1;
307 }
308 return 0;
309}
310
311static int json_parse_string(JSONParser *p, char *buf, int max_len) {
313 if (p->pos >= p->end || *p->pos != '"') return -1;
314 p->pos++;
315
316 int len = 0;
317 while (p->pos < p->end && *p->pos != '"') {
318 char c = *p->pos++;
319 if (c == '\\' && p->pos < p->end) {
320 c = *p->pos++;
321 switch (c) {
322 case 'n': c = '\n'; break;
323 case 'r': c = '\r'; break;
324 case 't': c = '\t'; break;
325 case '\\': c = '\\'; break;
326 case '"': c = '"'; break;
327 case 'u': {
328 /* Unicode escape \uXXXX */
329 if (p->pos + 4 <= p->end) {
330 char hex[5] = {p->pos[0], p->pos[1], p->pos[2], p->pos[3], 0};
331 unsigned int codepoint = (unsigned int)strtol(hex, NULL, 16);
332 p->pos += 4;
333 /* Convert to UTF-8 */
334 if (codepoint < 0x80) {
335 if (len < max_len - 1) buf[len++] = (char)codepoint;
336 } else if (codepoint < 0x800) {
337 if (len < max_len - 2) {
338 buf[len++] = (char)(0xC0 | (codepoint >> 6));
339 buf[len++] = (char)(0x80 | (codepoint & 0x3F));
340 }
341 } else {
342 if (len < max_len - 3) {
343 buf[len++] = (char)(0xE0 | (codepoint >> 12));
344 buf[len++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
345 buf[len++] = (char)(0x80 | (codepoint & 0x3F));
346 }
347 }
348 continue;
349 }
350 break;
351 }
352 default: break;
353 }
354 }
355 if (len < max_len - 1) buf[len++] = c;
356 }
357 buf[len] = '\0';
358
359 if (p->pos < p->end && *p->pos == '"') p->pos++;
360 return len;
361}
362
363static int json_parse_int(JSONParser *p, int *out) {
365 if (p->pos >= p->end) return -1;
366
367 int neg = 0;
368 if (*p->pos == '-') {
369 neg = 1;
370 p->pos++;
371 }
372
373 if (p->pos >= p->end || !isdigit((unsigned char)*p->pos)) return -1;
374
375 int val = 0;
376 while (p->pos < p->end && isdigit((unsigned char)*p->pos)) {
377 val = val * 10 + (*p->pos - '0');
378 p->pos++;
379 }
380
381 *out = neg ? -val : val;
382 return 0;
383}
384
385static void json_skip_value(JSONParser *p) {
387 if (p->pos >= p->end) return;
388
389 char c = *p->pos;
390 if (c == '"') {
391 char buf[1024];
392 json_parse_string(p, buf, sizeof(buf));
393 } else if (c == '{') {
394 int depth = 1;
395 p->pos++;
396 while (p->pos < p->end && depth > 0) {
397 if (*p->pos == '{') depth++;
398 else if (*p->pos == '}') depth--;
399 else if (*p->pos == '"') {
400 char buf[1024];
401 json_parse_string(p, buf, sizeof(buf));
402 continue;
403 }
404 p->pos++;
405 }
406 } else if (c == '[') {
407 int depth = 1;
408 p->pos++;
409 while (p->pos < p->end && depth > 0) {
410 if (*p->pos == '[') depth++;
411 else if (*p->pos == ']') depth--;
412 else if (*p->pos == '"') {
413 char buf[1024];
414 json_parse_string(p, buf, sizeof(buf));
415 continue;
416 }
417 p->pos++;
418 }
419 } else {
420 /* Number, bool, null */
421 while (p->pos < p->end && !isspace((unsigned char)*p->pos) &&
422 *p->pos != ',' && *p->pos != '}' && *p->pos != ']') {
423 p->pos++;
424 }
425 }
426}
427
428/* ========================================================================== */
429/* Load from tokenizer.json */
430/* ========================================================================== */
431
432int ck_tokenizer_load(CKTokenizer *tok, const char *path) {
433 FILE *f = fopen(path, "rb");
434 if (!f) {
435 fprintf(stderr, "Failed to open tokenizer: %s\n", path);
436 return -1;
437 }
438
439 fseek(f, 0, SEEK_END);
440 long size = ftell(f);
441 fseek(f, 0, SEEK_SET);
442
443 char *data = (char *)malloc(size + 1);
444 if (!data) {
445 fclose(f);
446 return -1;
447 }
448 fread(data, 1, size, f);
449 data[size] = '\0';
450 fclose(f);
451
452 JSONParser parser = {data, data, data + size};
453 JSONParser *p = &parser;
454
455 /* Parse top-level object */
456 if (!json_match_char(p, '{')) {
457 free(data);
458 return -1;
459 }
460
461 char key[256];
462 while (p->pos < p->end && *p->pos != '}') {
463 if (json_parse_string(p, key, sizeof(key)) < 0) break;
464 if (!json_match_char(p, ':')) break;
465
466 if (strcmp(key, "model") == 0) {
467 /* Parse model object */
468 if (!json_match_char(p, '{')) {
470 json_match_char(p, ',');
471 continue;
472 }
473
474 while (p->pos < p->end && *p->pos != '}') {
475 if (json_parse_string(p, key, sizeof(key)) < 0) break;
476 if (!json_match_char(p, ':')) break;
477
478 if (strcmp(key, "vocab") == 0) {
479 /* Parse vocab object: {"token": id, ...} */
480 if (!json_match_char(p, '{')) {
482 json_match_char(p, ',');
483 continue;
484 }
485
487 while (p->pos < p->end && *p->pos != '}') {
488 int token_len = json_parse_string(p, token, sizeof(token));
489 if (token_len < 0) break;
490 if (!json_match_char(p, ':')) break;
491
492 int id;
493 if (json_parse_int(p, &id) < 0) break;
494
495 /* Ensure we have space up to this ID */
496 while (tok->vocab_size <= id) {
497 ck_tokenizer_add_token(tok, "", 0);
498 }
499
500 /* Add/update token */
501 uint32_t bucket = hash_string(token, token_len) % tok->vocab_hash_size;
502 CKVocabEntry *entry = (CKVocabEntry *)ck_pool_alloc(&tok->pool, sizeof(CKVocabEntry));
503 entry->token = ck_pool_strdup(&tok->pool, token, token_len);
504 entry->token_len = token_len;
505 entry->id = id;
506 entry->next = tok->vocab_hash[bucket];
507 tok->vocab_hash[bucket] = entry;
508 tok->id_to_token[id] = entry->token;
509 if (id >= tok->vocab_size) tok->vocab_size = id + 1;
510
511 json_match_char(p, ',');
512 }
513 json_match_char(p, '}');
514
515 } else if (strcmp(key, "merges") == 0) {
516 /* Parse merges array: ["tok1 tok2", ...] */
517 if (!json_match_char(p, '[')) {
519 json_match_char(p, ',');
520 continue;
521 }
522
523 char merge_str[512];
524 while (p->pos < p->end && *p->pos != ']') {
525 int merge_len = json_parse_string(p, merge_str, sizeof(merge_str));
526 if (merge_len < 0) break;
527
528 /* Parse "token1 token2" */
529 char *space = strchr(merge_str, ' ');
530 if (space) {
531 *space = '\0';
532 char *tok1 = merge_str;
533 char *tok2 = space + 1;
534
535 int32_t id1 = ck_tokenizer_lookup(tok, tok1, -1);
536 int32_t id2 = ck_tokenizer_lookup(tok, tok2, -1);
537
538 /* Create merged token */
539 char merged[512];
540 snprintf(merged, sizeof(merged), "%s%s", tok1, tok2);
541 int32_t merged_id = ck_tokenizer_lookup(tok, merged, -1);
542
543 if (merged_id == tok->unk_id) {
544 merged_id = ck_tokenizer_add_token(tok, merged, -1);
545 }
546
547 ck_tokenizer_add_merge(tok, id1, id2, merged_id);
548 }
549
550 json_match_char(p, ',');
551 }
552 json_match_char(p, ']');
553
554 } else {
556 }
557
558 json_match_char(p, ',');
559 }
560 json_match_char(p, '}');
561
562 } else if (strcmp(key, "added_tokens") == 0) {
563 /* Parse added_tokens array for special tokens */
564 if (!json_match_char(p, '[')) {
566 json_match_char(p, ',');
567 continue;
568 }
569
570 while (p->pos < p->end && *p->pos != ']') {
571 if (!json_match_char(p, '{')) {
573 json_match_char(p, ',');
574 continue;
575 }
576
577 char content[256] = "";
578 int id = -1;
579 while (p->pos < p->end && *p->pos != '}') {
580 if (json_parse_string(p, key, sizeof(key)) < 0) break;
581 if (!json_match_char(p, ':')) break;
582
583 if (strcmp(key, "content") == 0) {
584 json_parse_string(p, content, sizeof(content));
585 } else if (strcmp(key, "id") == 0) {
586 json_parse_int(p, &id);
587 } else if (strcmp(key, "special") == 0) {
590 } else {
592 }
593 json_match_char(p, ',');
594 }
595 json_match_char(p, '}');
596
597 if (id >= 0 && content[0]) {
598 /* Identify special tokens */
599 if (strcmp(content, "<unk>") == 0 || strcmp(content, "[UNK]") == 0) {
600 tok->unk_id = id;
601 } else if (strcmp(content, "<s>") == 0 || strcmp(content, "<bos>") == 0 ||
602 strcmp(content, "[BOS]") == 0) {
603 tok->bos_id = id;
604 } else if (strcmp(content, "</s>") == 0 || strcmp(content, "<eos>") == 0 ||
605 strcmp(content, "[EOS]") == 0 || strcmp(content, "<|endoftext|>") == 0) {
606 tok->eos_id = id;
607 } else if (strcmp(content, "<pad>") == 0 || strcmp(content, "[PAD]") == 0) {
608 tok->pad_id = id;
609 }
610 }
611
612 json_match_char(p, ',');
613 }
614 json_match_char(p, ']');
615
616 } else {
618 }
619
620 json_match_char(p, ',');
621 }
622
623 free(data);
624
625 printf("Loaded tokenizer: %d tokens, %d merges\n", tok->vocab_size, tok->num_merges);
626 printf(" UNK=%d BOS=%d EOS=%d PAD=%d\n", tok->unk_id, tok->bos_id, tok->eos_id, tok->pad_id);
627
628 return 0;
629}
630
631/* ========================================================================== */
632/* BPE Encode */
633/* ========================================================================== */
634
636 const char *text,
637 int text_len,
638 int32_t *ids,
639 int max_ids) {
640 if (text_len < 0) text_len = (int)strlen(text);
641
642 /* Pre-tokenize: split on whitespace, keep spaces as tokens */
643 /* For simplicity, treat each byte as initial token, then apply BPE */
644
645 /* Initial tokens: one per byte */
646 int32_t *tokens = (int32_t *)malloc(text_len * sizeof(int32_t));
647 int num_tokens = 0;
648
649 for (int i = 0; i < text_len; i++) {
650 /* Look up single-character token */
651 char c[2] = {text[i], '\0'};
652 int32_t id = ck_tokenizer_lookup(tok, c, 1);
653
654 /* Handle special byte tokens like <0xXX> */
655 if (id == tok->unk_id) {
656 char byte_token[8];
657 snprintf(byte_token, sizeof(byte_token), "<0x%02X>", (unsigned char)text[i]);
658 id = ck_tokenizer_lookup(tok, byte_token, -1);
659 }
660
661 /* Try UTF-8 multi-byte sequences */
662 if (id == tok->unk_id && (unsigned char)text[i] >= 0x80) {
663 int utf8_len = 1;
664 if ((text[i] & 0xE0) == 0xC0) utf8_len = 2;
665 else if ((text[i] & 0xF0) == 0xE0) utf8_len = 3;
666 else if ((text[i] & 0xF8) == 0xF0) utf8_len = 4;
667
668 if (i + utf8_len <= text_len) {
669 id = ck_tokenizer_lookup(tok, text + i, utf8_len);
670 if (id != tok->unk_id) {
671 tokens[num_tokens++] = id;
672 i += utf8_len - 1;
673 continue;
674 }
675 }
676 }
677
678 tokens[num_tokens++] = id;
679 }
680
681 /* Apply BPE merges iteratively */
682 bool changed = true;
683 while (changed && num_tokens > 1) {
684 changed = false;
685
686 /* Find best merge (lowest priority = earliest in merge list) */
687 int best_pos = -1;
688 int best_priority = tok->num_merges;
689
690 for (int i = 0; i < num_tokens - 1; i++) {
691 int merge_idx = ck_tokenizer_lookup_merge(tok, tokens[i], tokens[i + 1]);
692 if (merge_idx >= 0 && tok->merges[merge_idx].priority < best_priority) {
693 best_pos = i;
694 best_priority = tok->merges[merge_idx].priority;
695 }
696 }
697
698 if (best_pos >= 0) {
699 int merge_idx = ck_tokenizer_lookup_merge(tok, tokens[best_pos], tokens[best_pos + 1]);
700 tokens[best_pos] = tok->merges[merge_idx].merged;
701
702 /* Shift remaining tokens */
703 for (int i = best_pos + 1; i < num_tokens - 1; i++) {
704 tokens[i] = tokens[i + 1];
705 }
706 num_tokens--;
707 changed = true;
708 }
709 }
710
711 /* Copy to output */
712 int out_len = 0;
713
714 if (tok->add_bos && out_len < max_ids) {
715 ids[out_len++] = tok->bos_id;
716 }
717
718 for (int i = 0; i < num_tokens && out_len < max_ids; i++) {
719 ids[out_len++] = tokens[i];
720 }
721
722 if (tok->add_eos && out_len < max_ids) {
723 ids[out_len++] = tok->eos_id;
724 }
725
726 free(tokens);
727 return out_len;
728}
729
730/* ========================================================================== */
731/* Decode */
732/* ========================================================================== */
733
735 const int32_t *ids,
736 int num_ids,
737 char *text,
738 int max_len) {
739 int len = 0;
740
741 for (int i = 0; i < num_ids; i++) {
742 /* Skip special tokens */
743 if (ids[i] == tok->bos_id || ids[i] == tok->eos_id || ids[i] == tok->pad_id) {
744 continue;
745 }
746
747 const char *token = ck_tokenizer_id_to_token(tok, ids[i]);
748 if (!token) continue;
749
750 int token_len = (int)strlen(token);
751
752 /* Handle byte tokens <0xXX> */
753 if (token_len == 6 && token[0] == '<' && token[1] == '0' && token[2] == 'x') {
754 char hex[3] = {token[3], token[4], 0};
755 unsigned int byte = (unsigned int)strtol(hex, NULL, 16);
756 if (len < max_len - 1) {
757 text[len++] = (char)byte;
758 }
759 continue;
760 }
761
762 /* Handle tokenizer space prefixes:
763 * GPT/byte-level BPE: Ġ (0xC4 0xA0)
764 * SentencePiece BPE: ▁ (0xE2 0x96 0x81)
765 */
766 const char *src = token;
767 if ((unsigned char)token[0] == 0xC4 && (unsigned char)token[1] == 0xA0) {
768 if (len < max_len - 1) {
769 text[len++] = ' ';
770 }
771 src = token + 2;
772 token_len -= 2;
773 } else if ((unsigned char)token[0] == 0xE2 &&
774 (unsigned char)token[1] == 0x96 &&
775 (unsigned char)token[2] == 0x81) {
776 if (len < max_len - 1) {
777 text[len++] = ' ';
778 }
779 src = token + 3;
780 token_len -= 3;
781 }
782
783 /* Copy token, normalizing any embedded SentencePiece markers too.
784 * Some Gemma tokenizer pieces contain repeated ▁ markers after
785 * indentation or punctuation; those are word-boundary spaces, not
786 * literal output characters.
787 */
788 for (int j = 0; j < token_len && len < max_len - 1; ) {
789 if (j + 2 < token_len &&
790 (unsigned char)src[j] == 0xE2 &&
791 (unsigned char)src[j + 1] == 0x96 &&
792 (unsigned char)src[j + 2] == 0x81) {
793 text[len++] = ' ';
794 j += 3;
795 continue;
796 }
797 text[len++] = src[j++];
798 }
799 }
800
801 text[len] = '\0';
802 return len;
803}
int ck_tokenizer_add_merge(CKTokenizer *tok, int32_t left, int32_t right, int32_t merged)
void ck_pool_init(CKMemPool *pool)
int32_t ck_tokenizer_lookup(const CKTokenizer *tok, const char *token, int len)
int ck_tokenizer_decode(const CKTokenizer *tok, const int32_t *ids, int num_ids, char *text, int max_len)
int ck_tokenizer_init(CKTokenizer *tok)
static uint32_t hash_string(const char *s, int len)
static void json_skip_whitespace(JSONParser *p)
int ck_tokenizer_load(CKTokenizer *tok, const char *path)
static int json_match_char(JSONParser *p, char c)
int ck_tokenizer_encode(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
void * ck_pool_alloc(CKMemPool *pool, size_t size)
static CKPoolBlock * pool_new_block(size_t capacity)
int32_t ck_tokenizer_add_token(CKTokenizer *tok, const char *token, int len)
void ck_pool_free(CKMemPool *pool)
static int json_parse_string(JSONParser *p, char *buf, int max_len)
char * ck_pool_strdup(CKMemPool *pool, const char *s, int len)
void ck_tokenizer_free(CKTokenizer *tok)
int ck_tokenizer_lookup_merge(const CKTokenizer *tok, int32_t left, int32_t right)
const char * ck_tokenizer_id_to_token(const CKTokenizer *tok, int32_t id)
static uint32_t hash_pair(int32_t left, int32_t right)
static void json_skip_value(JSONParser *p)
static int json_parse_int(JSONParser *p, int *out)
#define CK_MAX_VOCAB_SIZE
#define CK_POOL_BLOCK_SIZE
#define CK_MAX_TOKEN_LEN
CKPoolBlock * current
CKPoolBlock * head
size_t total_allocated
int32_t left
int32_t right
int32_t merged
uint8_t * data
struct CKPoolBlock * next
size_t capacity
int32_t bos_id
CKMemPool pool
int32_t unk_id
CKVocabEntry ** vocab_hash
int32_t eos_id
int vocab_hash_size
int merge_hash_size
CKMergeRule * merges
char ** id_to_token
int * merge_hash
int32_t pad_id
struct CKVocabEntry * next
const int32_t * ids
Definition tokenizer.h:444
int32_t id
Definition tokenizer.h:316
const int32_t int num_ids
Definition tokenizer.h:445
const char * text
Definition tokenizer.h:564
const char * token
Definition tokenizer.h:307
const int32_t int int * out_len
Definition tokenizer.h:446
static int utf8_len(unsigned char c)
const int32_t int char int max_len
Definition true_bpe.h:288
const char int text_len
Definition true_bpe.h:270
const char * left
Definition true_bpe.h:138
int32_t int32_t int32_t merged_id
Definition true_bpe.h:122
const char int int32_t int max_ids
Definition true_bpe.h:272
const char const char * right
Definition true_bpe.h:139
uint32_t end
Definition utf8.c:215