← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
true_bpe.c
Go to the documentation of this file.
1/*
2 * True BPE (Byte-Pair Encoding) Tokenizer [v4 - correctness complete, optimization pending]
3 *
4 * Implements the actual BPE algorithm used by GPT-2, LLaMA, Qwen, etc.
5 * Unlike greedy longest-match, this applies merge rules in priority order.
6 * Achieves 100% token-exact parity with HuggingFace (8/8 tests passing).
7 *
8 * Algorithm:
9 * 1. Split text into initial tokens (characters or bytes)
10 * 2. Find the highest-priority merge that can be applied
11 * 3. Apply that merge (combine two adjacent tokens into one)
12 * 4. Repeat until no more merges possible
13 * 5. Look up final tokens in vocabulary to get IDs
14 *
15 * Data Structures:
16 * - Vocabulary: token string -> token ID (hash table, FNV-1a, 65K buckets)
17 * - Merge rules: (left_id, right_id) -> (merged_id, priority) (hash table, MurmurHash3, 65K buckets)
18 * - Token list: dynamic array for the working token sequence
19 *
20 * TODO: Performance optimizations (correctness is done, these are for throughput):
21 *
22 * 1. Arena allocator for token strings
23 * - token_list_append() and token_list_merge_at() malloc/free per token string
24 * - Replace with scratch arena (~64KB) on CKTrueBPE struct, reset per encode_chunk()
25 * - Strings only live for one encode call, so arena lifetime matches perfectly
26 *
27 * 2. Linked list instead of array shift in merge loop
28 * - token_list_merge_at() shifts entire tail array left by 1: O(n) per merge
29 * - Use doubly-linked list of nodes (allocated from arena): O(1) per merge
30 * - Store only token ID per node, use id_to_token[] for string when needed
31 *
32 * 3. Priority queue for find_best_merge()
33 * - Currently rescans ALL adjacent pairs every iteration: O(n) per merge
34 * - Total merge loop is O(n * m) where m = merges applied
35 * - Use min-heap of candidate merges, pop best, re-insert affected neighbors
36 * - Would reduce to O(n log n) total
37 *
38 * 4. Eliminate redundant string storage
39 * - CKBPEToken stores both .str (malloc'd) and .id
40 * - After initial char->ID lookup, only IDs needed for merge lookup
41 * - Use id_to_token[merged_id] to get string when constructing merged tokens
42 *
43 * Current profile: tokenizer runs 2x per prompt (not per token), so these
44 * matter for batch tokenization / server use / 32K+ context, not for
45 * interactive chat where GEMM kernels dominate at 57% of compute.
46 *
47 * By Anthony Shivakumar
48 */
49
50#include <stdio.h>
51#include <stdlib.h>
52#include <string.h>
53#include <stdint.h>
54#include <stdbool.h>
55#include <limits.h>
56
57#include "tokenizer/true_bpe.h"
58
59/* ═══════════════════════════════════════════════════════════════════════════════
60 * Constants
61 * ═══════════════════════════════════════════════════════════════════════════════ */
62
63#define MERGE_HASH_SIZE 65536 /* Size of merge lookup hash table */
64#define INITIAL_TOKEN_CAPACITY 256 /* Initial capacity for token list */
65#define MAX_TOKEN_LEN 128 /* Maximum length of a single token string */
66
67/* ═══════════════════════════════════════════════════════════════════════════════
68 * Data Structures
69 * ═══════════════════════════════════════════════════════════════════════════════ */
70
71/* A single merge rule */
72typedef struct {
73 int32_t left_id; /* Left token ID */
74 int32_t right_id; /* Right token ID */
75 int32_t merged_id; /* Resulting merged token ID */
76 int32_t priority; /* Lower = higher priority (applied first) */
77} CKBPEMerge;
78
79/* Hash table entry for merge lookup */
80typedef struct CKMergeEntry {
81 uint64_t key; /* Hash key: (left_id << 32) | right_id */
82 CKBPEMerge merge; /* The merge rule */
83 struct CKMergeEntry *next; /* Chain for collision handling */
84} CKMergeEntry;
85
86/* Merge lookup hash table */
87typedef struct {
88 CKMergeEntry **buckets;
89 size_t num_buckets;
90 size_t num_entries;
91} CKMergeTable;
92
93/* A working token during BPE encoding */
94typedef struct {
95 char *str; /* Token string */
96 int32_t id; /* Token ID (-1 if not yet looked up) */
97 uint16_t len; /* String length */
98 bool is_merged; /* True if this is result of a merge */
99} CKBPEToken;
100
101/* Dynamic token list for BPE processing */
102typedef struct {
103 CKBPEToken *tokens;
104 size_t count;
105 size_t capacity;
106} CKBPETokenList;
107
108/* Special token entry for pre-BPE matching */
109typedef struct {
110 char *token; /* Token string to match */
111 int32_t id; /* Token ID to output */
112 int len; /* Length of token string (for faster matching) */
113} CKSpecialToken;
114
115/* Main True BPE tokenizer structure */
116struct CKTrueBPE {
117 /* Vocabulary: token string -> token ID */
119
120 /* Reverse vocabulary: token ID -> token string */
121 char **id_to_token;
122 size_t vocab_size;
123 size_t vocab_capacity;
124
125 /* Merge rules: (left_id, right_id) -> merged_id with priority */
126 CKMergeTable *merges;
127 int32_t num_merges;
128
129 /* Special token IDs */
130 int32_t unk_id;
131 int32_t bos_id;
132 int32_t eos_id;
133 int32_t pad_id;
134
135 /* Special tokens to match before BPE (sorted by length, longest first) */
136 CKSpecialToken *special_tokens;
137 int num_special_tokens;
138 size_t special_tokens_capacity;
139
140 /* Configuration */
142 bool suppress_dummy_prefix;
143
144 /* String buffer for token operations */
145 char *str_buffer;
146 size_t str_buffer_size;
147};
148
149/* ═══════════════════════════════════════════════════════════════════════════════
150 * Merge Table Operations
151 * ═══════════════════════════════════════════════════════════════════════════════ */
152
153static uint64_t merge_key(int32_t left_id, int32_t right_id) {
154 return ((uint64_t)left_id << 32) | (uint32_t)right_id;
155}
156
157static size_t merge_hash(uint64_t key, size_t num_buckets) {
158 /* Simple hash mixing */
159 key ^= key >> 33;
160 key *= 0xff51afd7ed558ccdULL;
161 key ^= key >> 33;
162 key *= 0xc4ceb9fe1a85ec53ULL;
163 key ^= key >> 33;
164 return key % num_buckets;
165}
166
167static CKMergeTable *merge_table_create(size_t num_buckets) {
168 CKMergeTable *table = (CKMergeTable *)malloc(sizeof(CKMergeTable));
169 if (!table) return NULL;
170
171 table->buckets = (CKMergeEntry **)calloc(num_buckets, sizeof(CKMergeEntry *));
172 if (!table->buckets) {
173 free(table);
174 return NULL;
175 }
176
177 table->num_buckets = num_buckets;
178 table->num_entries = 0;
179 return table;
180}
181
182static void merge_table_free(CKMergeTable *table) {
183 if (!table) return;
184
185 for (size_t i = 0; i < table->num_buckets; i++) {
186 CKMergeEntry *entry = table->buckets[i];
187 while (entry) {
188 CKMergeEntry *next = entry->next;
189 free(entry);
190 entry = next;
191 }
192 }
193
194 free(table->buckets);
195 free(table);
196}
197
198static int merge_table_insert(CKMergeTable *table, const CKBPEMerge *merge) {
199 uint64_t key = merge_key(merge->left_id, merge->right_id);
200 size_t bucket = merge_hash(key, table->num_buckets);
201
202 /* Check if already exists */
203 CKMergeEntry *entry = table->buckets[bucket];
204 while (entry) {
205 if (entry->key == key) {
206 /* Update existing */
207 entry->merge = *merge;
208 return 0;
209 }
210 entry = entry->next;
211 }
212
213 /* Create new entry */
214 entry = (CKMergeEntry *)malloc(sizeof(CKMergeEntry));
215 if (!entry) return -1;
216
217 entry->key = key;
218 entry->merge = *merge;
219 entry->next = table->buckets[bucket];
220 table->buckets[bucket] = entry;
221 table->num_entries++;
222
223 return 0;
224}
225
226static const CKBPEMerge *merge_table_lookup(const CKMergeTable *table, int32_t left_id, int32_t right_id) {
227 uint64_t key = merge_key(left_id, right_id);
228 size_t bucket = merge_hash(key, table->num_buckets);
229
230 CKMergeEntry *entry = table->buckets[bucket];
231 while (entry) {
232 if (entry->key == key) {
233 return &entry->merge;
234 }
235 entry = entry->next;
236 }
237
238 return NULL;
239}
240
241/* ═══════════════════════════════════════════════════════════════════════════════
242 * Token List Operations
243 * ═══════════════════════════════════════════════════════════════════════════════ */
244
245static CKBPETokenList *token_list_create(size_t initial_capacity) {
246 CKBPETokenList *list = (CKBPETokenList *)malloc(sizeof(CKBPETokenList));
247 if (!list) return NULL;
248
249 list->tokens = (CKBPEToken *)calloc(initial_capacity, sizeof(CKBPEToken));
250 if (!list->tokens) {
251 free(list);
252 return NULL;
253 }
254
255 list->count = 0;
256 list->capacity = initial_capacity;
257 return list;
258}
259
260static void token_list_free(CKBPETokenList *list) {
261 if (!list) return;
262
263 for (size_t i = 0; i < list->count; i++) {
264 if (list->tokens[i].str) {
265 free(list->tokens[i].str);
266 }
267 }
268
269 free(list->tokens);
270 free(list);
271}
272
273static void token_list_clear(CKBPETokenList *list) {
274 for (size_t i = 0; i < list->count; i++) {
275 if (list->tokens[i].str) {
276 free(list->tokens[i].str);
277 list->tokens[i].str = NULL;
278 }
279 }
280 list->count = 0;
281}
282
283static int token_list_append(CKBPETokenList *list, const char *str, size_t len, int32_t id) {
284 if (list->count >= list->capacity) {
285 size_t new_cap = list->capacity * 2;
286 CKBPEToken *new_tokens = (CKBPEToken *)realloc(list->tokens, new_cap * sizeof(CKBPEToken));
287 if (!new_tokens) return -1;
288 list->tokens = new_tokens;
289 list->capacity = new_cap;
290 /* Zero new entries */
291 memset(list->tokens + list->count, 0, (new_cap - list->count) * sizeof(CKBPEToken));
292 }
293
294 CKBPEToken *tok = &list->tokens[list->count];
295 tok->str = (char *)malloc(len + 1);
296 if (!tok->str) return -1;
297
298 memcpy(tok->str, str, len);
299 tok->str[len] = '\0';
300 tok->len = (uint16_t)len;
301 tok->id = id;
302 tok->is_merged = false;
303
304 list->count++;
305 return 0;
306}
307
308/* Merge tokens at positions i and i+1 into a single token */
309static int token_list_merge_at(CKBPETokenList *list, size_t pos, const char *merged_str, size_t merged_len, int32_t merged_id) {
310 if (pos + 1 >= list->count) return -1;
311
312 /* Free old strings */
313 free(list->tokens[pos].str);
314 free(list->tokens[pos + 1].str);
315
316 /* Create merged token */
317 list->tokens[pos].str = (char *)malloc(merged_len + 1);
318 if (!list->tokens[pos].str) return -1;
319
320 memcpy(list->tokens[pos].str, merged_str, merged_len);
321 list->tokens[pos].str[merged_len] = '\0';
322 list->tokens[pos].len = (uint16_t)merged_len;
323 list->tokens[pos].id = merged_id;
324 list->tokens[pos].is_merged = true;
325
326 /* Shift remaining tokens left */
327 for (size_t i = pos + 1; i < list->count - 1; i++) {
328 list->tokens[i] = list->tokens[i + 1];
329 }
330 list->count--;
331
332 /* Clear the now-unused last slot */
333 list->tokens[list->count].str = NULL;
334
335 return 0;
336}
337
338/* ═══════════════════════════════════════════════════════════════════════════════
339 * True BPE Tokenizer API
340 * ═══════════════════════════════════════════════════════════════════════════════ */
341
342CKTrueBPE *ck_true_bpe_create(void) {
343 CKTrueBPE *bpe = (CKTrueBPE *)calloc(1, sizeof(CKTrueBPE));
344 if (!bpe) return NULL;
345
346 /* Create vocabulary hash table */
348 if (!bpe->vocab) {
349 free(bpe);
350 return NULL;
351 }
352
353 /* Create merge table */
354 bpe->merges = merge_table_create(MERGE_HASH_SIZE);
355 if (!bpe->merges) {
356 ck_tokenizer_hash_table_free(bpe->vocab, true);
357 free(bpe);
358 return NULL;
359 }
360
361 /* Initialize reverse vocabulary */
362 bpe->vocab_capacity = 4096;
363 bpe->id_to_token = (char **)calloc(bpe->vocab_capacity, sizeof(char *));
364 if (!bpe->id_to_token) {
365 merge_table_free(bpe->merges);
366 ck_tokenizer_hash_table_free(bpe->vocab, true);
367 free(bpe);
368 return NULL;
369 }
370
371 /* String buffer for token operations */
372 bpe->str_buffer_size = 4096;
373 bpe->str_buffer = (char *)malloc(bpe->str_buffer_size);
374 if (!bpe->str_buffer) {
375 free(bpe->id_to_token);
376 merge_table_free(bpe->merges);
377 ck_tokenizer_hash_table_free(bpe->vocab, true);
378 free(bpe);
379 return NULL;
380 }
381
382 /* Default special token IDs */
383 bpe->unk_id = 0;
384 bpe->bos_id = -1;
385 bpe->eos_id = -1;
386 bpe->pad_id = -1;
387
388 /* Special tokens grow with the publisher vocabulary's declared types. */
389 bpe->num_special_tokens = 0;
390 bpe->special_tokens = NULL;
391 bpe->special_tokens_capacity = 0;
392
393 /* Default config */
394 bpe->config.add_bos = false;
395 bpe->config.add_eos = false;
396 bpe->config.byte_fallback = true;
397 bpe->config.space_prefix_style = CK_SPACE_PREFIX_AUTO;
398 bpe->config.pretokenizer = CK_BPE_PRETOKENIZER_GPT2;
399
400 return bpe;
401}
402
403void ck_true_bpe_free(CKTrueBPE *bpe) {
404 if (!bpe) return;
405
406 if (bpe->vocab) {
407 ck_tokenizer_hash_table_free(bpe->vocab, true);
408 }
409
410 if (bpe->merges) {
411 merge_table_free(bpe->merges);
412 }
413
414 if (bpe->id_to_token) {
415 for (size_t i = 0; i < bpe->vocab_size; i++) {
416 if (bpe->id_to_token[i]) {
417 free(bpe->id_to_token[i]);
418 }
419 }
420 free(bpe->id_to_token);
421 }
422
423 if (bpe->str_buffer) {
424 free(bpe->str_buffer);
425 }
426
427 /* Free special tokens */
428 for (int i = 0; i < bpe->num_special_tokens; i++) {
429 if (bpe->special_tokens[i].token) {
430 free(bpe->special_tokens[i].token);
431 }
432 }
433 free(bpe->special_tokens);
434
435 free(bpe);
436}
437
438/* ═══════════════════════════════════════════════════════════════════════════════
439 * Vocabulary Management
440 * ═══════════════════════════════════════════════════════════════════════════════ */
441
442/* Token info stored in vocab hash table */
443typedef struct {
444 int32_t id;
445 float score;
446} BPETokenInfo;
447
448int ck_true_bpe_add_token(CKTrueBPE *bpe, const char *token, int32_t id, float score) {
449 if (!bpe || !token) return -1;
450
451 /* Ensure reverse vocab has space */
452 if (id >= (int32_t)bpe->vocab_capacity) {
453 size_t new_cap = bpe->vocab_capacity * 2;
454 while (new_cap <= (size_t)id) new_cap *= 2;
455
456 char **new_array = (char **)realloc(bpe->id_to_token, new_cap * sizeof(char *));
457 if (!new_array) return -1;
458
459 memset(new_array + bpe->vocab_capacity, 0, (new_cap - bpe->vocab_capacity) * sizeof(char *));
460 bpe->id_to_token = new_array;
461 bpe->vocab_capacity = new_cap;
462 }
463
464 /* Check if token exists */
465 BPETokenInfo *existing = (BPETokenInfo *)ck_tokenizer_hash_table_lookup(bpe->vocab, token);
466 if (existing) {
467 existing->id = id;
468 existing->score = score;
469 if (bpe->id_to_token[id]) free(bpe->id_to_token[id]);
470 bpe->id_to_token[id] = strdup(token);
471 return 0;
472 }
473
474 /* Create new token info */
475 BPETokenInfo *info = (BPETokenInfo *)malloc(sizeof(BPETokenInfo));
476 if (!info) return -1;
477
478 info->id = id;
479 info->score = score;
480
481 if (ck_tokenizer_hash_table_insert(bpe->vocab, token, info) != 0) {
482 free(info);
483 return -1;
484 }
485
486 if (id >= (int32_t)bpe->vocab_size) {
487 bpe->vocab_size = id + 1;
488 }
489
490 if (bpe->id_to_token[id]) free(bpe->id_to_token[id]);
491 bpe->id_to_token[id] = strdup(token);
492
493 return 0;
494}
495
496int ck_true_bpe_add_merge(CKTrueBPE *bpe, int32_t left_id, int32_t right_id, int32_t merged_id, int32_t priority) {
497 if (!bpe) return -1;
498
499 CKBPEMerge merge = {
500 .left_id = left_id,
501 .right_id = right_id,
502 .merged_id = merged_id,
503 .priority = priority
504 };
505
506 int ret = merge_table_insert(bpe->merges, &merge);
507 if (ret == 0) {
508 bpe->num_merges++;
509 }
510 return ret;
511}
512
513int ck_true_bpe_add_merge_by_tokens(CKTrueBPE *bpe, const char *left, const char *right, int32_t priority) {
514 if (!bpe || !left || !right) return -1;
515
516 /* Look up token IDs */
517 BPETokenInfo *left_info = (BPETokenInfo *)ck_tokenizer_hash_table_lookup(bpe->vocab, left);
518 BPETokenInfo *right_info = (BPETokenInfo *)ck_tokenizer_hash_table_lookup(bpe->vocab, right);
519
520 if (!left_info || !right_info) {
521 return -1; /* Tokens not in vocabulary */
522 }
523
524 /* Create merged token string */
525 size_t left_len = strlen(left);
526 size_t right_len = strlen(right);
527 size_t merged_len = left_len + right_len;
528
529 if (merged_len >= bpe->str_buffer_size) {
530 return -1; /* Too long */
531 }
532
533 memcpy(bpe->str_buffer, left, left_len);
534 memcpy(bpe->str_buffer + left_len, right, right_len);
535 bpe->str_buffer[merged_len] = '\0';
536
537 /* Look up or create merged token */
538 BPETokenInfo *merged_info = (BPETokenInfo *)ck_tokenizer_hash_table_lookup(bpe->vocab, bpe->str_buffer);
539 int32_t merged_id;
540
541 if (merged_info) {
542 merged_id = merged_info->id;
543 } else {
544 /* Merged token should already exist in vocabulary */
545 return -1;
546 }
547
548 return ck_true_bpe_add_merge(bpe, left_info->id, right_info->id, merged_id, priority);
549}
550
551void ck_true_bpe_set_special_ids(CKTrueBPE *bpe, int32_t unk, int32_t bos, int32_t eos, int32_t pad) {
552 if (!bpe) return;
553 bpe->unk_id = unk;
554 bpe->bos_id = bos;
555 bpe->eos_id = eos;
556 bpe->pad_id = pad;
557}
558
559void ck_true_bpe_set_config(CKTrueBPE *bpe, const CKBPEConfig *config) {
560 if (!bpe || !config) return;
561 bpe->config = *config;
562}
563
565 if (bpe) bpe->suppress_dummy_prefix = suppress;
566}
567
568static int reserve_special_tokens(CKTrueBPE *bpe, size_t needed) {
569 if (!bpe || needed > INT_MAX || needed > SIZE_MAX / sizeof(CKSpecialToken)) return -1;
570 if (needed <= bpe->special_tokens_capacity) return 0;
571 size_t capacity = bpe->special_tokens_capacity ? bpe->special_tokens_capacity : 32;
572 while (capacity < needed) {
573 if (capacity > INT_MAX / 2) {
574 capacity = needed;
575 break;
576 }
577 capacity *= 2;
578 }
579 CKSpecialToken *items = realloc(bpe->special_tokens, capacity * sizeof(*items));
580 if (!items) return -1;
581 bpe->special_tokens = items;
582 bpe->special_tokens_capacity = capacity;
583 return 0;
584}
585
586int ck_true_bpe_add_special_token(CKTrueBPE *bpe, const char *token, int32_t id) {
587 if (!bpe || !token || id < 0) return -1;
588
589 int token_len = (int)strlen(token);
590 if (token_len == 0) return -1;
591
592 /* Check if already exists */
593 for (int i = 0; i < bpe->num_special_tokens; i++) {
594 if (bpe->special_tokens[i].token &&
595 strcmp(bpe->special_tokens[i].token, token) == 0) {
596 /* Update ID for existing token */
597 bpe->special_tokens[i].id = id;
598 return 0;
599 }
600 }
601 if (reserve_special_tokens(bpe, (size_t)bpe->num_special_tokens + 1) != 0) return -1;
602
603 char *copy = strdup(token);
604 if (!copy) return -1;
605
606 /* Find insertion point (keep sorted by length, longest first) */
607 int insert_idx = bpe->num_special_tokens;
608 for (int i = 0; i < bpe->num_special_tokens; i++) {
609 if (token_len > bpe->special_tokens[i].len) {
610 insert_idx = i;
611 break;
612 }
613 }
614
615 /* Shift existing entries down */
616 for (int i = bpe->num_special_tokens; i > insert_idx; i--) {
617 bpe->special_tokens[i] = bpe->special_tokens[i - 1];
618 }
619
620 /* Insert new entry */
621 bpe->special_tokens[insert_idx].token = copy;
622 bpe->special_tokens[insert_idx].id = id;
623 bpe->special_tokens[insert_idx].len = token_len;
624 bpe->num_special_tokens++;
625
626 return 0;
627}
628
629int ck_true_bpe_load_binary(CKTrueBPE *bpe,
630 int vocab_size,
631 const int32_t *offsets,
632 const char *strings,
633 int num_merges,
634 const int32_t *merges) {
635 if (!bpe || !offsets || !strings || vocab_size <= 0) return -1;
636
637 for (int i = 0; i < vocab_size; i++) {
638 const char *token = strings + offsets[i];
639 if (ck_true_bpe_add_token(bpe, token, i, 0.0f) != 0) {
640 return -1;
641 }
642 }
643
644 if (merges && num_merges > 0) {
645 for (int i = 0; i < num_merges; i++) {
646 int32_t left = merges[i * 3 + 0];
647 int32_t right = merges[i * 3 + 1];
648 int32_t merged = merges[i * 3 + 2];
649 if (left < 0 || right < 0 || merged < 0) {
650 continue;
651 }
652 if (ck_true_bpe_add_merge(bpe, left, right, merged, i) != 0) {
653 return -1;
654 }
655 }
656 }
657
658 return 0;
659}
660
662 const uint8_t *types,
663 size_t count) {
664 if (!bpe || !types || count != bpe->vocab_size) return -1;
665 size_t needed = 0;
666 for (size_t id = 0; id < count; id++) {
667 if (types[id] != 3 && types[id] != 4) continue;
668 const char *token = bpe->id_to_token[id];
669 if (!token || !*token) return -1;
670 needed++;
671 }
672 if (needed > (size_t)INT_MAX - (size_t)bpe->num_special_tokens ||
673 reserve_special_tokens(bpe, needed + (size_t)bpe->num_special_tokens) != 0) return -1;
674 for (size_t id = 0; id < count; id++) {
675 if (types[id] != 3 && types[id] != 4) continue;
676 if (ck_true_bpe_add_special_token(bpe, bpe->id_to_token[id], (int32_t)id) != 0) return -1;
677 }
678 return 0;
679}
680
681static int32_t lookup_token_exact(const CKTrueBPE *bpe, const char *token) {
682 if (!bpe || !token) return -1;
683 BPETokenInfo *info = (BPETokenInfo *)ck_tokenizer_hash_table_lookup(bpe->vocab, token);
684 return info ? info->id : -1;
685}
686
687int32_t ck_true_bpe_lookup(const CKTrueBPE *bpe, const char *token) {
688 int32_t id = lookup_token_exact(bpe, token);
689 if (id >= 0) return id;
690 return bpe ? bpe->unk_id : -1;
691}
692
693const char *ck_true_bpe_id_to_token(const CKTrueBPE *bpe, int32_t id) {
694 if (!bpe || id < 0 || id >= (int32_t)bpe->vocab_size) return NULL;
695 return bpe->id_to_token[id];
696}
697
698/* ═══════════════════════════════════════════════════════════════════════════════
699 * Space Prefix Detection
700 * ═══════════════════════════════════════════════════════════════════════════════ */
701
703 if (!bpe) return CK_SPACE_PREFIX_GPT2;
704
705 if (bpe->config.space_prefix_style != CK_SPACE_PREFIX_AUTO) {
706 return bpe->config.space_prefix_style;
707 }
708
709 /* Count tokens starting with each style */
710 int gpt2_count = 0; /* Ġ (0xC4 0xA0) */
711 int spm_count = 0; /* ▁ (0xE2 0x96 0x81) */
712
713 for (size_t i = 0; i < bpe->vocab_size; i++) {
714 const char *token = bpe->id_to_token[i];
715 if (!token) continue;
716
717 unsigned char c0 = (unsigned char)token[0];
718 unsigned char c1 = (unsigned char)token[1];
719
720 if (c0 == 0xC4 && c1 == 0xA0) {
721 gpt2_count++;
722 } else if (c0 == 0xE2 && c1 == 0x96 && (unsigned char)token[2] == 0x81) {
723 spm_count++;
724 }
725 }
726
727 CKSpacePrefixStyle detected;
728 if (spm_count > gpt2_count * 2 && spm_count > 0) {
729 detected = CK_SPACE_PREFIX_SPM;
730 } else if (gpt2_count > 0) {
731 detected = CK_SPACE_PREFIX_GPT2;
732 } else {
733 /* No Ġ/▁ markers found: treat vocab as ASCII identity BPE. */
734 detected = CK_SPACE_PREFIX_ASCII;
735 }
736 bpe->config.space_prefix_style = detected;
737
738 return detected;
739}
740
741/* ═══════════════════════════════════════════════════════════════════════════════
742 * True BPE Encoding
743 * ═══════════════════════════════════════════════════════════════════════════════ */
744
745/*
746 * GPT-2 Byte-Level BPE Character Mapping
747 *
748 * GPT-2 uses a byte-level encoding where certain bytes are mapped to
749 * special Unicode characters to avoid issues with control characters:
750 *
751 * - Space (0x20) → Ġ (U+0120, bytes 0xC4 0xA0)
752 * - Newline (0x0A) → Ċ (U+010A, bytes 0xC4 0x8A)
753 * - Tab (0x09) → ĉ (U+0109, bytes 0xC4 0x89)
754 * - Carriage return (0x0D) → č (U+010D, bytes 0xC4 0x8D)
755 *
756 * Bytes represented directly by Unicode keep their value. The remaining 68
757 * bytes map, in byte order, to U+0100..U+0143. This is the exact GPT-2 table;
758 * it is not equivalent to adding 0x100 to the original byte.
759 */
760
761static bool gpt2_byte_is_identity(unsigned int byte) {
762 return (byte >= 0x21 && byte <= 0x7E) ||
763 (byte >= 0xA1 && byte <= 0xAC) ||
764 (byte >= 0xAE && byte <= 0xFF);
765}
766
767static unsigned int gpt2_byte_to_codepoint(unsigned int byte) {
768 if (gpt2_byte_is_identity(byte)) return byte;
769
770 unsigned int mapped_index = 0;
771 for (unsigned int candidate = 0; candidate < byte; candidate++) {
772 if (!gpt2_byte_is_identity(candidate)) mapped_index++;
773 }
774 return 0x100 + mapped_index;
775}
776
777/* Convert a byte to GPT-2 byte-level BPE representation */
778static int byte_to_gpt2(unsigned char byte, char *out) {
779 unsigned int codepoint = gpt2_byte_to_codepoint(byte);
780
781 /* Encode as UTF-8 */
782 if (codepoint < 0x80) {
783 out[0] = (char)codepoint;
784 return 1;
785 } else if (codepoint < 0x800) {
786 out[0] = (char)(0xC0 | (codepoint >> 6));
787 out[1] = (char)(0x80 | (codepoint & 0x3F));
788 return 2;
789 } else {
790 out[0] = (char)(0xE0 | (codepoint >> 12));
791 out[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
792 out[2] = (char)(0x80 | (codepoint & 0x3F));
793 return 3;
794 }
795}
796
797/* Preprocess text: convert to byte-level BPE representation */
798static int preprocess_text(const CKTrueBPE *bpe, const char *text, int text_len, char *out, int out_max) {
799 CKSpacePrefixStyle style = bpe->config.space_prefix_style;
800 int out_len = 0;
801
803 if (text_len > out_max) return -1;
804 memcpy(out, text, (size_t)text_len);
805 return text_len;
806 }
807
808 /* For SentencePiece, add ▁ at start */
809 if (style == CK_SPACE_PREFIX_SPM && !bpe->suppress_dummy_prefix &&
810 text_len > 0 && text[0] != ' ') {
811 if (out_len + 3 > out_max) return -1;
812 out[out_len++] = (char)0xE2;
813 out[out_len++] = (char)0x96;
814 out[out_len++] = (char)0x81;
815 }
816
817 for (int i = 0; i < text_len; i++) {
818 unsigned char byte = (unsigned char)text[i];
819
820 if (style == CK_SPACE_PREFIX_SPM) {
821 /* SentencePiece style: only convert spaces */
822 if (byte == ' ') {
823 if (out_len + 3 > out_max) return -1;
824 out[out_len++] = (char)0xE2;
825 out[out_len++] = (char)0x96;
826 out[out_len++] = (char)0x81;
827 } else {
828 if (out_len + 1 > out_max) return -1;
829 out[out_len++] = (char)byte;
830 }
831 } else {
832 /* GPT-2 style: full byte-level encoding */
833 char encoded[4];
834 int enc_len = byte_to_gpt2(byte, encoded);
835 if (out_len + enc_len > out_max) return -1;
836 for (int j = 0; j < enc_len; j++) {
837 out[out_len++] = encoded[j];
838 }
839 }
840 }
841
842 return out_len;
843}
844
845/* Get UTF-8 character length */
846static int utf8_char_len(unsigned char c) {
847 if ((c & 0x80) == 0) return 1; /* 0xxxxxxx */
848 if ((c & 0xE0) == 0xC0) return 2; /* 110xxxxx */
849 if ((c & 0xF0) == 0xE0) return 3; /* 1110xxxx */
850 if ((c & 0xF8) == 0xF0) return 4; /* 11110xxx */
851 return 1; /* Invalid, treat as single byte */
852}
853
854/* ═══════════════════════════════════════════════════════════════════════════════
855 * GPT-2 Style Pretokenizer
856 *
857 * The GPT-2 pretokenizer uses a regex to split text into chunks BEFORE BPE:
858 * (?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}|
859 * ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+
860 *
861 * Key behaviors:
862 * - Words get an optional leading space attached: "hello world" -> ["hello", " world"]
863 * - Multiple spaces before a word: " hello" -> [" ", " hello"] (n-1 spaces, then space+word)
864 * - Numbers stay together: "123" -> ["123"]
865 * - Punctuation may get leading space: " ," -> [" ,"]
866 *
867 * This implementation provides a simplified version that handles common cases.
868 * ═══════════════════════════════════════════════════════════════════════════════ */
869
870/* Character classification helpers */
871static bool is_letter(unsigned char c) {
872 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
873}
874
875static bool is_digit(unsigned char c) {
876 return c >= '0' && c <= '9';
877}
878
879static bool is_whitespace(unsigned char c) {
880 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
881}
882
883/* Check if this is the GPT-2 Ġ character (0xC4 0xA0) */
884static bool is_gpt2_space(const char *s, int len) {
885 return len >= 2 && (unsigned char)s[0] == 0xC4 && (unsigned char)s[1] == 0xA0;
886}
887
888/* Check if this is a GPT-2 encoded letter (regular ASCII letter in byte-level) */
889static bool is_bpe_letter(const char *s, int len) {
890 if (len == 1) {
891 unsigned char c = (unsigned char)s[0];
892 return is_letter(c);
893 }
894 /* Keep mapped UTF-8 bytes in one word chunk so BPE merges can reconstruct
895 * accented and non-ASCII letters. Exclude GPT-2 whitespace controls. */
896 if (len >= 2 && (unsigned char)s[0] == 0xC4) {
897 unsigned char c1 = (unsigned char)s[1];
898 if (c1 == 0xA0 || c1 == 0x8A || c1 == 0x89 || c1 == 0x8D) {
899 return false; /* space, newline, tab, carriage return */
900 }
901 }
902 return true;
903}
904
905/* Check if this is a GPT-2 encoded digit */
906static bool is_bpe_digit(const char *s, int len) {
907 if (len == 1) {
908 unsigned char c = (unsigned char)s[0];
909 return is_digit(c);
910 }
911 return false;
912}
913
914/* The isolated Unicode Split profile keeps a contraction on the end of its
915 * word match. GPT-2 instead gives contractions their own pretokenizer chunk. */
916static int isolated_contraction_len(const char *s, int available) {
917 static const char *suffixes[] = {"s", "t", "re", "ve", "m", "ll", "d"};
918 if (available < 2 || s[0] != '\'') return 0;
919 for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); ++i) {
920 const char *suffix = suffixes[i];
921 size_t length = strlen(suffix);
922 if ((size_t)available < length + 1) continue;
923 bool matches = true;
924 for (size_t j = 0; j < length; ++j) {
925 unsigned char c = (unsigned char)s[j + 1];
926 if (c >= 'A' && c <= 'Z') c = (unsigned char)(c + ('a' - 'A'));
927 if (c != (unsigned char)suffix[j]) {
928 matches = false;
929 break;
930 }
931 }
932 if (matches) return (int)length + 1;
933 }
934 return 0;
935}
936
937/* Pretokenizer chunk types */
938typedef enum {
939 CHUNK_WORD, /* Letters (with optional leading space) */
940 CHUNK_NUMBER, /* Digits */
941 CHUNK_WHITESPACE, /* Whitespace (not attached to word) */
942 CHUNK_OTHER /* Punctuation, etc. */
944
945/* A chunk from pretokenization */
946typedef struct {
947 const char *start;
948 int len;
949 ChunkType type;
950} PretokChunk;
951
952/* Check if character is a newline (Ċ = 0xC4 0x8A in byte-level BPE) */
953static bool is_bpe_newline(const char *s, int len) {
954 return len >= 2 && (unsigned char)s[0] == 0xC4 && (unsigned char)s[1] == 0x8A;
955}
956
957/* Check if this is a non-letter, non-digit, non-newline character that can prefix a word */
958static bool is_word_prefix_char(const char *s, int len) {
959 /* In GPT-2 regex: [^\r\n\p{L}\p{N}]? matches any char except newline, letter, digit */
960 if (len == 1) {
961 unsigned char c = (unsigned char)s[0];
962 return !is_letter(c) && !is_digit(c) && c != '\n' && c != '\r';
963 }
964 /* Multi-byte: check if it's not a letter/digit (newline is Ċ which we handle separately) */
965 if (is_gpt2_space(s, len)) return true; /* Space can prefix */
966 if (is_bpe_newline(s, len)) return false; /* Newline cannot prefix */
967 return true; /* Other multi-byte chars can prefix */
968}
969
970/* Check if character is punctuation (not space, not letter, not digit) */
971static bool is_bpe_punct(const char *s, int len) {
972 if (len == 1) {
973 unsigned char c = (unsigned char)s[0];
974 return !is_letter(c) && !is_digit(c) && c != ' ' && c != '\t' && c != '\n' && c != '\r';
975 }
976 /* Multi-byte: not Ġ (space) or Ċ (newline) */
977 if (is_gpt2_space(s, len)) return false;
978 if (len >= 2 && (unsigned char)s[0] == 0xC4) {
979 unsigned char c1 = (unsigned char)s[1];
980 /* Ċ (newline), ĉ (tab), č (CR) etc. are not punctuation */
981 if (c1 == 0x8A || c1 == 0x89 || c1 == 0x8D) return false;
982 }
983 return true;
984}
985
986/*
987 * GPT-2 Pretokenizer
988 *
989 * Splits byte-level encoded text into chunks for independent BPE processing.
990 *
991 * The GPT-2 regex pattern (in order of matching):
992 * 1. (?i:'s|'t|'re|'ve|'m|'ll|'d) - Contractions
993 * 2. [^\r\n\p{L}\p{N}]?\p{L}+ - Words with optional prefix
994 * 3. \p{N} - Single digit
995 * 4. ?[^\s\p{L}\p{N}]+[\r\n]* - Optional space + punctuation + newlines
996 * 5. \s*[\r\n]+ - Whitespace + newlines
997 * 6. \s+(?!\S) - Trailing whitespace
998 * 7. \s+ - Whitespace
999 *
1000 * Returns array of chunks (caller provides buffer).
1001 */
1002static int gpt2_pretokenize(const char *text, int text_len, PretokChunk *chunks, int max_chunks,
1003 CKBPEPretokenizer pretokenizer) {
1004 int num_chunks = 0;
1005 int pos = 0;
1006
1007 while (pos < text_len && num_chunks < max_chunks) {
1008 int chunk_start = pos;
1009 int char_len = utf8_char_len((unsigned char)text[pos]);
1010 if (pos + char_len > text_len) char_len = text_len - pos;
1011
1012 /* Pattern 2: [^\r\n\p{L}\p{N}]?\p{L}+ - Words with optional punctuation prefix */
1013 /* This pattern MUST be checked before pattern 4 (punctuation) */
1014 /* Check if we have: letter, or punctuation followed by letter */
1015 bool is_word = false;
1016 int word_start = pos;
1017 int prefix_len = 0;
1018
1019 if (is_bpe_letter(text + pos, char_len)) {
1020 /* Word without prefix */
1021 is_word = true;
1022 } else if (is_word_prefix_char(text + pos, char_len) &&
1023 !is_gpt2_space(text + pos, text_len - pos)) {
1024 /* Check if punctuation is followed by a letter */
1025 int after = pos + char_len;
1026 if (after < text_len) {
1027 int next_len = utf8_char_len((unsigned char)text[after]);
1028 if (is_bpe_letter(text + after, next_len)) {
1029 /* This is pattern 2: punctuation + letters */
1030 is_word = true;
1031 prefix_len = char_len;
1032 }
1033 }
1034 }
1035
1036 if (is_word) {
1037 /* Collect the word (with optional prefix) */
1038 pos = word_start + prefix_len; /* Skip prefix if any */
1039 while (pos < text_len) {
1040 int clen = utf8_char_len((unsigned char)text[pos]);
1041 if (is_bpe_letter(text + pos, clen)) {
1042 pos += clen;
1043 } else {
1044 break;
1045 }
1046 }
1048 pos += isolated_contraction_len(text + pos, text_len - pos);
1049 chunks[num_chunks].start = text + chunk_start;
1050 chunks[num_chunks].len = pos - chunk_start;
1051 chunks[num_chunks].type = CHUNK_WORD;
1052 num_chunks++;
1053 continue;
1054 }
1055
1056 /* Pattern 3: \p{N} - Single digit */
1057 if (is_bpe_digit(text + pos, char_len)) {
1058 pos += char_len;
1059 /* The isolated Split regex groups up to three digits; GPT-2 does not. */
1060 if (pretokenizer == CK_BPE_PRETOKENIZER_UNICODE_SPLIT_ISOLATED) {
1061 for (int digits = 1; digits < 3 && pos < text_len; ++digits) {
1062 int next_len = utf8_char_len((unsigned char)text[pos]);
1063 if (pos + next_len > text_len ||
1064 !is_bpe_digit(text + pos, next_len)) break;
1065 pos += next_len;
1066 }
1067 }
1068 chunks[num_chunks].start = text + chunk_start;
1069 chunks[num_chunks].len = pos - chunk_start;
1070 chunks[num_chunks].type = CHUNK_NUMBER;
1071 num_chunks++;
1072 continue;
1073 }
1074
1075 /* Pattern 4: ?[^\s\p{L}\p{N}]+[\r\n]* - Optional space + punctuation + newlines */
1076 /* At this point, we know the current char is NOT followed by letters (checked above) */
1077 /* Check for space (Ġ) followed by punctuation, OR just punctuation */
1078 bool has_leading_space = is_gpt2_space(text + pos, text_len - pos);
1079 int punct_start = has_leading_space ? pos + 2 : pos;
1080
1081 if (punct_start < text_len) {
1082 int pchar_len = utf8_char_len((unsigned char)text[punct_start]);
1083 if (is_bpe_punct(text + punct_start, pchar_len)) {
1084 /* This matches pattern 4: space? + punctuation + newlines? */
1085 if (has_leading_space) {
1086 pos += 2; /* Include the leading space */
1087 }
1088 /* Collect punctuation characters */
1089 /* Pattern 4 collects ALL consecutive punctuation, NOT stopping before letters */
1090 /* The key insight: if we have " (" before "int", the " (" is pattern 4, */
1091 /* and "(int" would be pattern 2, but pattern 4 matches first since " (" */
1092 /* is a complete match for pattern 4 (space + one punctuation). */
1093 while (pos < text_len) {
1094 int clen = utf8_char_len((unsigned char)text[pos]);
1095 if (is_bpe_punct(text + pos, clen)) {
1096 pos += clen;
1097 } else {
1098 break;
1099 }
1100 }
1101 /* The isolated Split pattern allows slashes after line breaks;
1102 * GPT-2's punctuation branch only includes line breaks. */
1103 while (pos < text_len) {
1104 if (is_bpe_newline(text + pos, text_len - pos)) {
1105 pos += 2;
1106 } else if (pretokenizer == CK_BPE_PRETOKENIZER_UNICODE_SPLIT_ISOLATED &&
1107 text[pos] == '/') {
1108 pos++;
1109 } else {
1110 break;
1111 }
1112 }
1113 chunks[num_chunks].start = text + chunk_start;
1114 chunks[num_chunks].len = pos - chunk_start;
1115 chunks[num_chunks].type = CHUNK_OTHER;
1116 num_chunks++;
1117 continue;
1118 }
1119 }
1120
1121 /* Pattern 5/6/7: Whitespace handling */
1122 if (is_gpt2_space(text + pos, text_len - pos)) {
1123 /* Count consecutive spaces */
1124 int space_count = 0;
1125 int space_end = pos;
1126 while (space_end < text_len && is_gpt2_space(text + space_end, text_len - space_end)) {
1127 space_count++;
1128 space_end += 2;
1129 }
1130
1131 /* Check if spaces are followed by newlines (pattern 5) */
1132 if (space_end < text_len && is_bpe_newline(text + space_end, text_len - space_end)) {
1133 /* Whitespace + newlines */
1134 while (space_end < text_len && is_bpe_newline(text + space_end, text_len - space_end)) {
1135 space_end += 2;
1136 }
1137 chunks[num_chunks].start = text + pos;
1138 chunks[num_chunks].len = space_end - pos;
1139 chunks[num_chunks].type = CHUNK_WHITESPACE;
1140 num_chunks++;
1141 pos = space_end;
1142 continue;
1143 }
1144
1145 /* Check what follows the spaces */
1146 if (space_end < text_len) {
1147 int next_len = utf8_char_len((unsigned char)text[space_end]);
1148 if (space_count > 1 && is_bpe_punct(text + space_end, next_len)) {
1149 /* The optional space in the punctuation branch belongs to
1150 * the punctuation chunk; preceding spaces form their own
1151 * whitespace chunk. */
1152 chunks[num_chunks].start = text + pos;
1153 chunks[num_chunks].len = (space_count - 1) * 2;
1154 chunks[num_chunks].type = CHUNK_WHITESPACE;
1155 num_chunks++;
1156 pos += (space_count - 1) * 2;
1157 continue;
1158 }
1159 if (is_bpe_letter(text + space_end, next_len)) {
1160 /* Letters follow - output (n-1) spaces, then space+word (pattern 2) */
1161 if (space_count > 1) {
1162 chunks[num_chunks].start = text + pos;
1163 chunks[num_chunks].len = (space_count - 1) * 2;
1164 chunks[num_chunks].type = CHUNK_WHITESPACE;
1165 num_chunks++;
1166 pos += (space_count - 1) * 2;
1167 if (num_chunks >= max_chunks) break;
1168 }
1169 /* Collect space + word */
1170 chunk_start = pos;
1171 pos += 2; /* Skip the Ġ */
1172 while (pos < text_len) {
1173 int clen = utf8_char_len((unsigned char)text[pos]);
1174 if (is_bpe_letter(text + pos, clen)) {
1175 pos += clen;
1176 } else {
1177 break;
1178 }
1179 }
1181 pos += isolated_contraction_len(text + pos, text_len - pos);
1182 chunks[num_chunks].start = text + chunk_start;
1183 chunks[num_chunks].len = pos - chunk_start;
1184 chunks[num_chunks].type = CHUNK_WORD;
1185 num_chunks++;
1186 continue;
1187 } else if (pretokenizer == CK_BPE_PRETOKENIZER_GPT2 &&
1188 is_bpe_digit(text + space_end, next_len)) {
1189 /* Digit follows - output (n-1) spaces, then space+digit */
1190 if (space_count > 1) {
1191 chunks[num_chunks].start = text + pos;
1192 chunks[num_chunks].len = (space_count - 1) * 2;
1193 chunks[num_chunks].type = CHUNK_WHITESPACE;
1194 num_chunks++;
1195 pos += (space_count - 1) * 2;
1196 if (num_chunks >= max_chunks) break;
1197 }
1198 /* Space + single digit */
1199 chunk_start = pos;
1200 pos += 2 + 1; /* Ġ + digit */
1201 chunks[num_chunks].start = text + chunk_start;
1202 chunks[num_chunks].len = pos - chunk_start;
1203 chunks[num_chunks].type = CHUNK_NUMBER;
1204 num_chunks++;
1205 continue;
1206 }
1207 /* Pattern 4 would have caught space+punct, so this is trailing space before something else */
1208 }
1209
1210 /* Just whitespace */
1211 chunks[num_chunks].start = text + pos;
1212 chunks[num_chunks].len = space_count * 2;
1213 chunks[num_chunks].type = CHUNK_WHITESPACE;
1214 num_chunks++;
1215 pos = space_end;
1216 continue;
1217 }
1218
1219 /* Pattern 5: Newlines (Ċ) */
1220 if (is_bpe_newline(text + pos, text_len - pos)) {
1221 while (pos < text_len && is_bpe_newline(text + pos, text_len - pos)) {
1222 pos += 2;
1223 }
1224 chunks[num_chunks].start = text + chunk_start;
1225 chunks[num_chunks].len = pos - chunk_start;
1226 chunks[num_chunks].type = CHUNK_OTHER;
1227 num_chunks++;
1228 continue;
1229 }
1230
1231 /* Fallback: single character chunk */
1232 pos += char_len;
1233 chunks[num_chunks].start = text + chunk_start;
1234 chunks[num_chunks].len = pos - chunk_start;
1235 chunks[num_chunks].type = CHUNK_OTHER;
1236 num_chunks++;
1237 }
1238
1239 return num_chunks;
1240}
1241
1242/* Initialize token list from preprocessed text */
1243static int init_tokens_from_text(CKTrueBPE *bpe, CKBPETokenList *list, const char *text, int text_len) {
1244 token_list_clear(list);
1245
1246 CKSpacePrefixStyle style = bpe->config.space_prefix_style;
1247 if (style == CK_SPACE_PREFIX_AUTO) {
1249 }
1250
1251 int pos = 0;
1252 while (pos < text_len) {
1253 int char_len = (style == CK_SPACE_PREFIX_ASCII)
1254 ? 1
1255 : utf8_char_len((unsigned char)text[pos]);
1256 if (pos + char_len > text_len) {
1257 char_len = text_len - pos; /* Truncated UTF-8 */
1258 }
1259
1260 /* Look up this character/byte in vocabulary */
1261 char char_buf[8];
1262 memcpy(char_buf, text + pos, char_len);
1263 char_buf[char_len] = '\0';
1264
1265 int32_t id = lookup_token_exact(bpe, char_buf);
1266
1267 if (token_list_append(list, char_buf, char_len, id) != 0) {
1268 return -1;
1269 }
1270
1271 pos += char_len;
1272 }
1273
1274 return 0;
1275}
1276
1277/* Find the best (highest priority = lowest number) merge in the token list */
1278static int find_best_merge(const CKTrueBPE *bpe, const CKBPETokenList *list,
1279 size_t *best_pos, const CKBPEMerge **best_merge) {
1280 *best_pos = 0;
1281 *best_merge = NULL;
1282 int32_t best_priority = INT32_MAX;
1283
1284 for (size_t i = 0; i + 1 < list->count; i++) {
1285 int32_t left_id = list->tokens[i].id;
1286 int32_t right_id = list->tokens[i + 1].id;
1287
1288 if (left_id < 0 || right_id < 0) continue; /* Unknown tokens can't merge */
1289
1290 const CKBPEMerge *merge = merge_table_lookup(bpe->merges, left_id, right_id);
1291 if (merge && merge->priority < best_priority) {
1292 best_priority = merge->priority;
1293 *best_pos = i;
1294 *best_merge = merge;
1295 }
1296 }
1297
1298 return (*best_merge != NULL) ? 0 : -1;
1299}
1300
1301/* Apply BPE merges until no more possible */
1302static int apply_bpe_merges(CKTrueBPE *bpe, CKBPETokenList *list) {
1303 char merged_buf[MAX_TOKEN_LEN * 2];
1304
1305 while (list->count > 1) {
1306 size_t best_pos;
1307 const CKBPEMerge *best_merge;
1308
1309 if (find_best_merge(bpe, list, &best_pos, &best_merge) != 0) {
1310 break; /* No more merges possible */
1311 }
1312
1313 /* Get merged token string */
1314 const char *merged_str = bpe->id_to_token[best_merge->merged_id];
1315 if (!merged_str) {
1316 /* Construct from left + right */
1317 size_t left_len = list->tokens[best_pos].len;
1318 size_t right_len = list->tokens[best_pos + 1].len;
1319
1320 if (left_len + right_len >= sizeof(merged_buf)) {
1321 return -1; /* Cannot encode this merge completely */
1322 }
1323
1324 memcpy(merged_buf, list->tokens[best_pos].str, left_len);
1325 memcpy(merged_buf + left_len, list->tokens[best_pos + 1].str, right_len);
1326 merged_buf[left_len + right_len] = '\0';
1327 merged_str = merged_buf;
1328 }
1329
1330 /* Apply the merge */
1331 if (token_list_merge_at(list, best_pos, merged_str, strlen(merged_str), best_merge->merged_id) != 0) {
1332 return -1;
1333 }
1334 }
1335
1336 return 0;
1337}
1338
1339/*
1340 * Encode a single chunk (after pretokenization) using BPE
1341 */
1342static int encode_chunk(CKTrueBPE *bpe, const char *chunk, int chunk_len,
1343 int32_t *ids, int max_ids, CKBPETokenList *list) {
1344 if (chunk_len <= 0) return 0;
1345
1346 /* First, try to look up the entire chunk as a single token */
1347 char chunk_buf[256];
1348 if (chunk_len < (int)sizeof(chunk_buf)) {
1349 memcpy(chunk_buf, chunk, chunk_len);
1350 chunk_buf[chunk_len] = '\0';
1351 int32_t chunk_id = lookup_token_exact(bpe, chunk_buf);
1352 if (chunk_id >= 0) {
1353 /* Entire chunk is a single token */
1354 if (max_ids >= 1) {
1355 ids[0] = chunk_id;
1356 return 1;
1357 }
1358 return -1;
1359 }
1360 }
1361
1362 /* Initialize token list from chunk characters */
1363 if (init_tokens_from_text(bpe, list, chunk, chunk_len) != 0) {
1364 return -1;
1365 }
1366
1367 /* Apply BPE merges to this chunk */
1368 if (apply_bpe_merges(bpe, list) != 0) return -1;
1369
1370 /* Extract token IDs from this chunk */
1371 int out_idx = 0;
1372 CKSpacePrefixStyle style = bpe->config.space_prefix_style;
1373 for (size_t i = 0; i < list->count; i++) {
1374 int32_t id = list->tokens[i].id;
1375
1376 /* Handle unknown tokens */
1377 if (id < 0) {
1378 if (bpe->config.byte_fallback) {
1379 /* Output each byte as separate token (byte fallback) */
1380 for (size_t j = 0; j < list->tokens[i].len; j++) {
1381 if (out_idx >= max_ids) return -1;
1382 unsigned char raw_b = (unsigned char)list->tokens[i].str[j];
1383 int32_t byte_id = -1;
1384
1386 char raw_tok[2] = { (char)raw_b, '\0' };
1387 byte_id = lookup_token_exact(bpe, raw_tok);
1388 }
1389
1390 /*
1391 * Keep legacy <0xHH> fallback for older tokenizers,
1392 * then try GPT-2 byte-level piece fallback for modern BPE vocabs.
1393 */
1394 if (byte_id < 0) {
1395 char byte_token[8];
1396 snprintf(byte_token, sizeof(byte_token), "<0x%02X>", raw_b);
1397 byte_id = lookup_token_exact(bpe, byte_token);
1398 }
1399
1400 if (byte_id < 0) {
1401 char mapped[8];
1402 int mapped_len = byte_to_gpt2(raw_b, mapped);
1403 if (mapped_len > 0 && mapped_len < (int)sizeof(mapped)) {
1404 mapped[mapped_len] = '\0';
1405 byte_id = lookup_token_exact(bpe, mapped);
1406 }
1407 }
1408
1409 ids[out_idx++] = (byte_id >= 0) ? byte_id : bpe->unk_id;
1410 }
1411 } else {
1412 if (out_idx >= max_ids) return -1;
1413 ids[out_idx++] = bpe->unk_id;
1414 }
1415 } else {
1416 if (out_idx >= max_ids) return -1;
1417 ids[out_idx++] = id;
1418 }
1419 }
1420
1421 return out_idx;
1422}
1423
1424/*
1425 * Helper: Encode a segment of text (no special tokens) using BPE
1426 */
1427static int encode_text_segment(CKTrueBPE *bpe, const char *text, int text_len,
1428 int32_t *ids, int max_ids) {
1429 if (text_len <= 0) return 0;
1430
1431 /* Preprocess text (byte-level encoding) */
1432 /* GPT-2 byte mapping can expand one input byte to three UTF-8 bytes. */
1433 if (text_len > (INT_MAX - 4) / 3) return -1;
1434 size_t pp_capacity = (size_t)text_len * 3 + 4;
1435 char *preprocessed = malloc(pp_capacity);
1436 if (!preprocessed) return -1;
1437 int pp_len = preprocess_text(bpe, text, text_len, preprocessed, (int)pp_capacity - 1);
1438 if (pp_len < 0) {
1439 free(preprocessed);
1440 return -1;
1441 }
1442 preprocessed[pp_len] = '\0';
1443
1444 int out_idx = 0;
1445 CKSpacePrefixStyle style = bpe->config.space_prefix_style;
1446
1447 /* For GPT-2 style, use pretokenizer to split into chunks */
1449
1450 /* Pretokenize */
1451 /* Every chunk consumes at least one preprocessed byte. */
1452 if ((size_t)pp_len > SIZE_MAX / sizeof(PretokChunk)) {
1453 free(preprocessed);
1454 return -1;
1455 }
1456 PretokChunk *chunks = malloc((size_t)pp_len * sizeof(*chunks));
1457 if (!chunks) {
1458 free(preprocessed);
1459 return -1;
1460 }
1461 int num_chunks = gpt2_pretokenize(
1462 preprocessed, pp_len, chunks, pp_len, bpe->config.pretokenizer);
1463
1464 /* Create reusable token list */
1465 CKBPETokenList *list = token_list_create(INITIAL_TOKEN_CAPACITY);
1466 if (!list) {
1467 free(chunks);
1468 free(preprocessed);
1469 return -1;
1470 }
1471
1472 /* Process each chunk independently with BPE */
1473 for (int c = 0; c < num_chunks; c++) {
1474 int chunk_ids = encode_chunk(bpe, chunks[c].start, chunks[c].len,
1475 ids + out_idx, max_ids - out_idx, list);
1476 if (chunk_ids < 0) {
1477 token_list_free(list);
1478 free(chunks);
1479 free(preprocessed);
1480 return -1;
1481 }
1482 out_idx += chunk_ids;
1483 }
1484
1485 token_list_free(list);
1486 free(chunks);
1487 } else {
1488 /* SentencePiece style: no pretokenization, process entire text */
1489 CKBPETokenList *list = token_list_create(INITIAL_TOKEN_CAPACITY);
1490 if (!list) {
1491 free(preprocessed);
1492 return -1;
1493 }
1494
1495 int chunk_ids = encode_chunk(bpe, preprocessed, pp_len,
1496 ids + out_idx, max_ids - out_idx, list);
1497 if (chunk_ids < 0) {
1498 token_list_free(list);
1499 free(preprocessed);
1500 return -1;
1501 }
1502 out_idx += chunk_ids;
1503
1504 token_list_free(list);
1505 }
1506
1507 free(preprocessed);
1508 return out_idx;
1509}
1510
1511/*
1512 * Check if text at position matches a special token
1513 * Returns: matched special token index, or -1 if no match
1514 */
1515static int match_special_token(const CKTrueBPE *bpe, const char *text, int text_len, int pos) {
1516 int remaining = text_len - pos;
1517 const char *cur = text + pos;
1518
1519 /* Special tokens are sorted longest first, so first match is best */
1520 for (int i = 0; i < bpe->num_special_tokens; i++) {
1521 int tok_len = bpe->special_tokens[i].len;
1522 if (tok_len <= remaining &&
1523 memcmp(cur, bpe->special_tokens[i].token, tok_len) == 0) {
1524 return i;
1525 }
1526 }
1527 return -1;
1528}
1529
1530int ck_true_bpe_encode(CKTrueBPE *bpe, const char *text, int text_len, int32_t *ids, int max_ids) {
1531 if (!bpe || !text || !ids || max_ids <= 0) return -1;
1532 if (text_len < 0) {
1533 size_t inferred_len = strlen(text);
1534 if (inferred_len > INT_MAX) return -1;
1535 text_len = (int)inferred_len;
1536 }
1537 if (text_len == 0) return 0;
1538
1539 /* Auto-detect space style if needed */
1540 if (bpe->config.space_prefix_style == CK_SPACE_PREFIX_AUTO) {
1542 }
1543
1544 int out_idx = 0;
1545
1546 /* A publisher template may already spell out BOS. Do not prepend it a
1547 * second time when it is the first exact special token in the prompt. */
1548 int leading_special = match_special_token(bpe, text, text_len, 0);
1549 bool explicit_bos = leading_special >= 0 &&
1550 bpe->special_tokens[leading_special].id == bpe->bos_id;
1551 if (bpe->config.add_bos && bpe->bos_id >= 0 && !explicit_bos) {
1552 if (out_idx >= max_ids) return -1;
1553 ids[out_idx++] = bpe->bos_id;
1554 }
1555
1556 /* If no special tokens registered, use fast path */
1557 if (bpe->num_special_tokens == 0) {
1558 int segment_count = encode_text_segment(bpe, text, text_len, ids + out_idx, max_ids - out_idx);
1559 if (segment_count < 0) return -1;
1560 out_idx += segment_count;
1561 } else {
1562 /* Scan for special tokens and encode segments between them */
1563 int pos = 0;
1564 int segment_start = 0;
1565
1566 while (pos < text_len) {
1567 int match = match_special_token(bpe, text, text_len, pos);
1568
1569 if (match >= 0) {
1570 /* Found special token - first encode any text before it */
1571 if (pos > segment_start) {
1572 int seg_len = pos - segment_start;
1573 int segment_count = encode_text_segment(bpe, text + segment_start, seg_len,
1574 ids + out_idx, max_ids - out_idx);
1575 if (segment_count < 0) return -1;
1576 out_idx += segment_count;
1577 }
1578
1579 /* Output the special token ID */
1580 if (out_idx >= max_ids) return -1;
1581 ids[out_idx++] = bpe->special_tokens[match].id;
1582
1583 /* Advance past the special token */
1584 pos += bpe->special_tokens[match].len;
1585 segment_start = pos;
1586 } else {
1587 /* No special token here, advance to next character */
1588 pos++;
1589 }
1590 }
1591
1592 /* Encode any remaining text after last special token */
1593 if (segment_start < text_len) {
1594 int segment_count = encode_text_segment(bpe, text + segment_start, text_len - segment_start,
1595 ids + out_idx, max_ids - out_idx);
1596 if (segment_count < 0) return -1;
1597 out_idx += segment_count;
1598 }
1599 }
1600
1601 /* Add EOS token if configured */
1602 if (bpe->config.add_eos && bpe->eos_id >= 0) {
1603 if (out_idx >= max_ids) return -1;
1604 ids[out_idx++] = bpe->eos_id;
1605 }
1606
1607 return out_idx;
1608}
1609
1610/* ═══════════════════════════════════════════════════════════════════════════════
1611 * Decoding
1612 * ═══════════════════════════════════════════════════════════════════════════════ */
1613
1614/* Decode one UTF-8 scalar from s[0..len) and report codepoint + bytes consumed. */
1615static int decode_utf8_scalar(const unsigned char *s, int len, int *out_cp, int *out_used) {
1616 if (!s || len <= 0 || !out_cp || !out_used) return -1;
1617 unsigned char c0 = s[0];
1618 if ((c0 & 0x80) == 0) {
1619 *out_cp = (int)c0;
1620 *out_used = 1;
1621 return 0;
1622 }
1623 if ((c0 & 0xE0) == 0xC0) {
1624 if (len < 2) return -1;
1625 if ((s[1] & 0xC0) != 0x80) return -1;
1626 *out_cp = ((int)(c0 & 0x1F) << 6) | (int)(s[1] & 0x3F);
1627 *out_used = 2;
1628 return 0;
1629 }
1630 if ((c0 & 0xF0) == 0xE0) {
1631 if (len < 3) return -1;
1632 if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80) return -1;
1633 *out_cp = ((int)(c0 & 0x0F) << 12) |
1634 ((int)(s[1] & 0x3F) << 6) |
1635 (int)(s[2] & 0x3F);
1636 *out_used = 3;
1637 return 0;
1638 }
1639 if ((c0 & 0xF8) == 0xF0) {
1640 if (len < 4) return -1;
1641 if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80 || (s[3] & 0xC0) != 0x80) return -1;
1642 *out_cp = ((int)(c0 & 0x07) << 18) |
1643 ((int)(s[1] & 0x3F) << 12) |
1644 ((int)(s[2] & 0x3F) << 6) |
1645 (int)(s[3] & 0x3F);
1646 *out_used = 4;
1647 return 0;
1648 }
1649 return -1;
1650}
1651
1652/* Invert byte_to_gpt2(): convert mapped codepoint back to original byte if possible. */
1653static int gpt2_codepoint_to_byte(int cp) {
1654 if (cp >= 0 && cp <= 0xFF && gpt2_byte_is_identity((unsigned int)cp)) {
1655 return cp;
1656 }
1657 if (cp >= 0x100 && cp <= 0x143) {
1658 int mapped_index = cp - 0x100;
1659 for (int byte = 0; byte <= 0xFF; byte++) {
1660 if (gpt2_byte_is_identity((unsigned int)byte)) continue;
1661 if (mapped_index-- == 0) return byte;
1662 }
1663 }
1664 return -1;
1665}
1666
1667int ck_true_bpe_decode(const CKTrueBPE *bpe, const int32_t *ids, int num_ids, char *text, int max_len) {
1668 if (!bpe || !ids || !text || max_len <= 0) return 0;
1669
1670 if (bpe->config.space_prefix_style == CK_SPACE_PREFIX_AUTO) {
1671 ck_true_bpe_detect_space_style((CKTrueBPE *)bpe);
1672 }
1673 CKSpacePrefixStyle style = bpe->config.space_prefix_style;
1674
1675 int len = 0;
1676 for (int i = 0; i < num_ids && len < max_len - 1; i++) {
1677 int32_t id = ids[i];
1678 if (id < 0) continue;
1679
1680 /* Skip special tokens */
1681 if (id == bpe->bos_id || id == bpe->eos_id || id == bpe->pad_id) {
1682 continue;
1683 }
1684
1685 const char *token = ck_true_bpe_id_to_token(bpe, id);
1686 if (!token) continue;
1687
1689 int token_len_ascii = (int)strlen(token);
1690 for (int j = 0; j < token_len_ascii && len < max_len - 1; ) {
1691 if (j + 2 < token_len_ascii &&
1692 (unsigned char)token[j] == 0xE2 &&
1693 (unsigned char)token[j + 1] == 0x96 &&
1694 (unsigned char)token[j + 2] == 0x81) {
1695 text[len++] = ' ';
1696 j += 3;
1697 continue;
1698 }
1699 text[len++] = token[j++];
1700 }
1701 continue;
1702 }
1703
1704 int token_len = (int)strlen(token);
1705
1706 /* Check for SentencePiece space marker ▁ (U+2581) at start */
1707 if (token_len >= 3 &&
1708 (unsigned char)token[0] == 0xE2 &&
1709 (unsigned char)token[1] == 0x96 &&
1710 (unsigned char)token[2] == 0x81) {
1711 /* ▁ -> space */
1712 if (len < max_len - 1) text[len++] = ' ';
1713 token += 3;
1714 token_len -= 3;
1715 }
1716
1717 /* Process rest of token, decoding GPT-2 byte-level encoding */
1718 int pos = 0;
1719 while (pos < token_len && len < max_len - 1) {
1720 int cp = 0;
1721 int used = 0;
1722 if (decode_utf8_scalar((const unsigned char *)token + pos, token_len - pos, &cp, &used) == 0) {
1723 if (cp == 0x2581) {
1724 text[len++] = ' ';
1725 } else {
1726 int decoded = gpt2_codepoint_to_byte(cp);
1727 if (decoded >= 0) {
1728 text[len++] = (char)decoded;
1729 } else {
1730 /* Not part of byte-level mapping: preserve original UTF-8 bytes. */
1731 for (int j = 0; j < used && pos + j < token_len && len < max_len - 1; j++) {
1732 text[len++] = token[pos + j];
1733 }
1734 }
1735 }
1736 pos += used;
1737 } else {
1738 /* Invalid UTF-8 in token string: copy one byte to avoid stalling. */
1739 text[len++] = token[pos];
1740 pos += 1;
1741 }
1742 }
1743 }
1744
1745 text[len] = '\0';
1746 return len;
1747}
1748
1749/* ═══════════════════════════════════════════════════════════════════════════════
1750 * Statistics
1751 * ═══════════════════════════════════════════════════════════════════════════════ */
1752
1753size_t ck_true_bpe_vocab_size(const CKTrueBPE *bpe) {
1754 return bpe ? bpe->vocab_size : 0;
1755}
1756
1757int32_t ck_true_bpe_num_merges(const CKTrueBPE *bpe) {
1758 return bpe ? bpe->num_merges : 0;
1759}
#define CK_TOKENIZER_HT_BUCKETS_LARGE
Definition hash_table.h:142
void ck_tokenizer_hash_table_free(CKTokenizerHashTable *table, bool free_values)
Definition hash_table.c:144
int ck_tokenizer_hash_table_insert(CKTokenizerHashTable *table, const char *key, void *value)
Definition hash_table.c:162
CKTokenizerHashTable * ck_tokenizer_hash_table_create(size_t bucket_count)
Definition hash_table.c:84
void * ck_tokenizer_hash_table_lookup(CKTokenizerHashTable *table, const char *key)
Definition hash_table.c:202
const int32_t * ids
Definition tokenizer.h:444
int32_t id
Definition tokenizer.h:316
CKSpacePrefixStyle
Definition tokenizer.h:60
@ CK_SPACE_PREFIX_AUTO
Definition tokenizer.h:61
@ CK_SPACE_PREFIX_SPM
Definition tokenizer.h:63
@ CK_SPACE_PREFIX_GPT2
Definition tokenizer.h:62
@ CK_SPACE_PREFIX_ASCII
Definition tokenizer.h:64
const int32_t int num_ids
Definition tokenizer.h:445
const char * text
Definition tokenizer.h:564
const char * token
Definition tokenizer.h:307
int32_t float * score
Definition tokenizer.h:328
int32_t unk
Definition tokenizer.h:230
int32_t int32_t int32_t eos
Definition tokenizer.h:232
int32_t int32_t int32_t int32_t pad
Definition tokenizer.h:233
CKSpacePrefixStyle style
Definition tokenizer.h:288
int32_t int32_t bos
Definition tokenizer.h:231
const int32_t int int * out_len
Definition tokenizer.h:446
static bool is_bpe_punct(const char *s, int len)
Definition true_bpe.c:971
static bool gpt2_byte_is_identity(unsigned int byte)
Definition true_bpe.c:761
static const CKBPEMerge * merge_table_lookup(const CKMergeTable *table, int32_t left_id, int32_t right_id)
Definition true_bpe.c:226
static int gpt2_pretokenize(const char *text, int text_len, PretokChunk *chunks, int max_chunks, CKBPEPretokenizer pretokenizer)
Definition true_bpe.c:1002
int ck_true_bpe_decode(const CKTrueBPE *bpe, const int32_t *ids, int num_ids, char *text, int max_len)
Definition true_bpe.c:1667
static bool is_gpt2_space(const char *s, int len)
Definition true_bpe.c:884
int ck_true_bpe_encode(CKTrueBPE *bpe, const char *text, int text_len, int32_t *ids, int max_ids)
Definition true_bpe.c:1530
static bool is_letter(unsigned char c)
Definition true_bpe.c:871
int ck_true_bpe_register_token_types(CKTrueBPE *bpe, const uint8_t *types, size_t count)
Definition true_bpe.c:661
static int encode_text_segment(CKTrueBPE *bpe, const char *text, int text_len, int32_t *ids, int max_ids)
Definition true_bpe.c:1427
static CKMergeTable * merge_table_create(size_t num_buckets)
Definition true_bpe.c:167
void ck_true_bpe_set_config(CKTrueBPE *bpe, const CKBPEConfig *config)
Definition true_bpe.c:559
static void merge_table_free(CKMergeTable *table)
Definition true_bpe.c:182
#define MAX_TOKEN_LEN
Definition true_bpe.c:65
CKSpacePrefixStyle ck_true_bpe_detect_space_style(CKTrueBPE *bpe)
Definition true_bpe.c:702
void ck_true_bpe_set_suppress_dummy_prefix(CKTrueBPE *bpe, bool suppress)
Definition true_bpe.c:564
static CKBPETokenList * token_list_create(size_t initial_capacity)
Definition true_bpe.c:245
void ck_true_bpe_free(CKTrueBPE *bpe)
Definition true_bpe.c:403
void ck_true_bpe_set_special_ids(CKTrueBPE *bpe, int32_t unk, int32_t bos, int32_t eos, int32_t pad)
Definition true_bpe.c:551
static bool is_bpe_letter(const char *s, int len)
Definition true_bpe.c:889
static int init_tokens_from_text(CKTrueBPE *bpe, CKBPETokenList *list, const char *text, int text_len)
Definition true_bpe.c:1243
ChunkType
Definition true_bpe.c:938
@ CHUNK_NUMBER
Definition true_bpe.c:940
@ CHUNK_OTHER
Definition true_bpe.c:942
@ CHUNK_WHITESPACE
Definition true_bpe.c:941
@ CHUNK_WORD
Definition true_bpe.c:939
static bool is_bpe_newline(const char *s, int len)
Definition true_bpe.c:953
static bool is_bpe_digit(const char *s, int len)
Definition true_bpe.c:906
int ck_true_bpe_add_merge(CKTrueBPE *bpe, int32_t left_id, int32_t right_id, int32_t merged_id, int32_t priority)
Definition true_bpe.c:496
static void token_list_clear(CKBPETokenList *list)
Definition true_bpe.c:273
static int encode_chunk(CKTrueBPE *bpe, const char *chunk, int chunk_len, int32_t *ids, int max_ids, CKBPETokenList *list)
Definition true_bpe.c:1342
CKTrueBPE * ck_true_bpe_create(void)
Definition true_bpe.c:342
static int utf8_char_len(unsigned char c)
Definition true_bpe.c:846
static void token_list_free(CKBPETokenList *list)
Definition true_bpe.c:260
static int preprocess_text(const CKTrueBPE *bpe, const char *text, int text_len, char *out, int out_max)
Definition true_bpe.c:798
static int token_list_append(CKBPETokenList *list, const char *str, size_t len, int32_t id)
Definition true_bpe.c:283
static bool is_whitespace(unsigned char c)
Definition true_bpe.c:879
int32_t ck_true_bpe_num_merges(const CKTrueBPE *bpe)
Definition true_bpe.c:1757
int ck_true_bpe_add_special_token(CKTrueBPE *bpe, const char *token, int32_t id)
Definition true_bpe.c:586
const char * ck_true_bpe_id_to_token(const CKTrueBPE *bpe, int32_t id)
Definition true_bpe.c:693
static int decode_utf8_scalar(const unsigned char *s, int len, int *out_cp, int *out_used)
Definition true_bpe.c:1615
#define MERGE_HASH_SIZE
Definition true_bpe.c:63
static int match_special_token(const CKTrueBPE *bpe, const char *text, int text_len, int pos)
Definition true_bpe.c:1515
int ck_true_bpe_load_binary(CKTrueBPE *bpe, int vocab_size, const int32_t *offsets, const char *strings, int num_merges, const int32_t *merges)
Definition true_bpe.c:629
static size_t merge_hash(uint64_t key, size_t num_buckets)
Definition true_bpe.c:157
static int apply_bpe_merges(CKTrueBPE *bpe, CKBPETokenList *list)
Definition true_bpe.c:1302
static int reserve_special_tokens(CKTrueBPE *bpe, size_t needed)
Definition true_bpe.c:568
static int isolated_contraction_len(const char *s, int available)
Definition true_bpe.c:916
static int find_best_merge(const CKTrueBPE *bpe, const CKBPETokenList *list, size_t *best_pos, const CKBPEMerge **best_merge)
Definition true_bpe.c:1278
static bool is_word_prefix_char(const char *s, int len)
Definition true_bpe.c:958
static int merge_table_insert(CKMergeTable *table, const CKBPEMerge *merge)
Definition true_bpe.c:198
size_t ck_true_bpe_vocab_size(const CKTrueBPE *bpe)
Definition true_bpe.c:1753
int ck_true_bpe_add_token(CKTrueBPE *bpe, const char *token, int32_t id, float score)
Definition true_bpe.c:448
static uint64_t merge_key(int32_t left_id, int32_t right_id)
Definition true_bpe.c:153
static int gpt2_codepoint_to_byte(int cp)
Definition true_bpe.c:1653
static bool is_digit(unsigned char c)
Definition true_bpe.c:875
static int32_t lookup_token_exact(const CKTrueBPE *bpe, const char *token)
Definition true_bpe.c:681
static int byte_to_gpt2(unsigned char byte, char *out)
Definition true_bpe.c:778
static unsigned int gpt2_byte_to_codepoint(unsigned int byte)
Definition true_bpe.c:767
static int token_list_merge_at(CKBPETokenList *list, size_t pos, const char *merged_str, size_t merged_len, int32_t merged_id)
Definition true_bpe.c:309
int32_t ck_true_bpe_lookup(const CKTrueBPE *bpe, const char *token)
Definition true_bpe.c:687
int ck_true_bpe_add_merge_by_tokens(CKTrueBPE *bpe, const char *left, const char *right, int32_t priority)
Definition true_bpe.c:513
#define INITIAL_TOKEN_CAPACITY
Definition true_bpe.c:64
const CKBPEConfig * config
Definition true_bpe.h:179
int const int32_t const char int num_merges
Definition true_bpe.h:200
int const int32_t const char * strings
Definition true_bpe.h:199
int const int32_t const char int const int32_t * merges
Definition true_bpe.h:201
int32_t int32_t int32_t int32_t priority
Definition true_bpe.h:123
const int32_t int char int max_len
Definition true_bpe.h:303
bool suppress
Definition true_bpe.h:183
int32_t left_id
Definition true_bpe.h:120
const uint8_t * types
Definition true_bpe.h:208
const char int text_len
Definition true_bpe.h:285
CKBPEPretokenizer
Definition true_bpe.h:53
@ CK_BPE_PRETOKENIZER_UNICODE_SPLIT_ISOLATED
Definition true_bpe.h:55
@ CK_BPE_PRETOKENIZER_GPT2
Definition true_bpe.h:54
int vocab_size
Definition true_bpe.h:197
int32_t int32_t right_id
Definition true_bpe.h:121
int const int32_t * offsets
Definition true_bpe.h:198
const char * left
Definition true_bpe.h:138
const uint8_t size_t count
Definition true_bpe.h:209
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:287
const char const char * right
Definition true_bpe.h:139
uint32_t start
Definition utf8.c:214