← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
tokenizer_spm.c
Go to the documentation of this file.
1/*
2 * SentencePiece-specific tokenizer implementation split from tokenizer.c
3 */
4
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8#include <stdint.h>
9#include <stdbool.h>
10
11#include "tokenizer/tokenizer.h"
13
14/* Token info stored in hash table value */
15typedef struct {
16 int32_t id;
17 float score;
18 bool is_special;
19} TokenInfo;
20
21/* Internal exact lookup (returns -1 if token string is not in vocab). */
22static int32_t ck_tokenizer_lookup_exact(const CKTokenizer *tok, const char *token) {
23 if (!tok || !token) return -1;
24 TokenInfo *info = (TokenInfo *)ck_tokenizer_hash_table_lookup(tok->vocab, token);
25 return info ? info->id : -1;
26}
27
28/* Internal exact lookup for non-null-terminated text slices. */
29static int32_t ck_tokenizer_lookup_exact_n(const CKTokenizer *tok, const char *text, int text_len) {
30 if (!tok || !text || text_len <= 0) return -1;
31 char stack_buf[512];
32 char *tmp = stack_buf;
33 if (text_len >= (int)sizeof(stack_buf)) {
34 tmp = (char *)malloc((size_t)text_len + 1);
35 if (!tmp) return -1;
36 }
37 memcpy(tmp, text, (size_t)text_len);
38 tmp[text_len] = '\0';
39 int32_t id = ck_tokenizer_lookup_exact(tok, tmp);
40 if (tmp != stack_buf) free(tmp);
41 return id;
42}
43
44/* Find the longest registered special token starting at a byte offset. */
46 const char *text,
47 int text_len,
48 size_t pos,
49 size_t *match_len) {
50 if (match_len) *match_len = 0;
51 if (!tok || !text || text_len <= 0 || pos >= (size_t)text_len) return -1;
52
53 if (tok->vocab_trie && tok->vocab_trie->root) {
54 CKTrieNode *node = tok->vocab_trie->root;
55 CKTrieNode *best_node = NULL;
56 size_t best_len = 0;
57 size_t cur = pos;
58
59 while (cur < (size_t)text_len && node) {
60 unsigned char c = (unsigned char)text[cur];
61 if (!node->children[c]) break;
62 node = node->children[c];
63 cur++;
64 if (node->token_id >= 0 && node->is_special) {
65 best_node = node;
66 best_len = cur - pos;
67 }
68 }
69
70 if (best_node) {
71 if (match_len) *match_len = best_len;
72 return best_node->token_id;
73 }
74 }
75
77 if (pos + (size_t)max_len > (size_t)text_len) {
78 max_len = (int)((size_t)text_len - pos);
79 }
80 char tmp[CK_TOKENIZER_MAX_TOKEN_LEN + 1];
81 for (int len = max_len; len >= 1; len--) {
82 memcpy(tmp, text + pos, (size_t)len);
83 tmp[len] = '\0';
84 TokenInfo *info = (TokenInfo *)ck_tokenizer_hash_table_lookup(tok->vocab, tmp);
85 if (info && info->id >= 0 && info->is_special) {
86 if (match_len) *match_len = (size_t)len;
87 return info->id;
88 }
89 }
90 return -1;
91}
92
93/* ============================================================================
94 * SPM (SentencePiece) Tokenization with Viterbi/DP
95 * ============================================================================ */
96
97/*
98 * GGUF Token Type enum values (from llama.cpp gguf/constants.py):
99 * NORMAL = 1
100 * UNKNOWN = 2
101 * CONTROL = 3
102 * USER_DEFINED = 4
103 * UNUSED = 5
104 * BYTE = 6
105 *
106 * IMPORTANT: These must match exactly or token filtering will be incorrect!
107 */
108#define GGUF_TOKEN_NORMAL 1
109#define GGUF_TOKEN_UNKNOWN 2
110#define GGUF_TOKEN_CONTROL 3
111#define GGUF_TOKEN_USER_DEFINED 4
112#define GGUF_TOKEN_UNUSED 5
113#define GGUF_TOKEN_BYTE 6
114
115/* Check if token type allows inclusion in DP path (exclude CONTROL, UNUSED, BYTE)
116 * Note: UNKNOWN tokens are allowed because they're needed for unknown content */
117static inline bool spm_token_allowed_in_dp(const CKTokenizer *tok, int32_t token_id) {
118 if (!tok->types || token_id < 0 || token_id >= (int32_t)tok->vocab_size) {
119 return true; /* No type info, allow all */
120 }
121 uint8_t t = tok->types[token_id];
122 /* Reject CONTROL, UNUSED, and BYTE tokens (but allow UNKNOWN for fallback) */
123 return t != GGUF_TOKEN_CONTROL && t != GGUF_TOKEN_UNUSED && t != GGUF_TOKEN_BYTE;
124}
125
126/* Check if token is a byte token (for identification) */
127static inline bool spm_is_byte_token(const CKTokenizer *tok, int32_t token_id) {
128 if (!tok->types || token_id < 0 || token_id >= (int32_t)tok->vocab_size) {
129 return false;
130 }
131 return tok->types[token_id] == GGUF_TOKEN_BYTE;
132}
133
134/* Find byte token ID using fast lookup table (primary) or <0xXX> fallback */
135static inline int32_t spm_get_byte_token(const CKTokenizer *tok, unsigned char byte_val) {
136 /* Try fast lookup table first */
137 if (tok->byte_token_id && tok->byte_token_id[byte_val] >= 0) {
138 return tok->byte_token_id[byte_val];
139 }
140 /* Fallback to <0xXX> format */
141 char byte_token[16];
142 int len = snprintf(byte_token, sizeof(byte_token), "<0x%02X>", byte_val);
143 if (len <= 0) return tok->unk_id;
144 return ck_tokenizer_lookup(tok, byte_token);
145}
146
147/* Check if a token string represents a byte token (<0xXX> format) */
148static inline bool spm_token_is_byte_format(const char *token) {
149 return token && token[0] == '<' && token[1] == '0' &&
150 token[2] == 'x' && token[3] >= '0' && token[3] <= 'F' &&
151 token[4] >= '0' && token[4] <= 'F' && token[5] == '>';
152}
153
154/* Build byte token lookup table from vocab (called during load) */
155static void spm_build_byte_lookup(CKTokenizer *tok, const char *strings, const int32_t *offsets, int vocab_size) {
156 /* Reuse existing array or allocate new one */
157 if (!tok->byte_token_id) {
158 tok->byte_token_id = (int32_t *)malloc(256 * sizeof(int32_t));
159 if (!tok->byte_token_id) return;
160 }
161
162 /* Initialize all entries to -1 */
163 for (int i = 0; i < 256; i++) {
164 tok->byte_token_id[i] = -1;
165 }
166
167 /* Scan vocab for byte tokens */
168 for (int i = 0; i < vocab_size; i++) {
169 if (!tok->types || tok->types[i] != GGUF_TOKEN_BYTE) continue;
170
171 const char *token = strings + offsets[i];
172 size_t len = strlen(token);
173
174 if (len == 1) {
175 /* Raw byte token (single byte) */
176 unsigned char byte_val = (unsigned char)token[0];
177 tok->byte_token_id[byte_val] = i;
178 } else if (spm_token_is_byte_format(token)) {
179 /* <0xXX> format - parse the hex value */
180 unsigned int byte_val;
181 if (sscanf(token, "<0x%02X>", &byte_val) == 1 && byte_val < 256) {
182 tok->byte_token_id[byte_val] = i;
183 }
184 }
185 }
186}
187
188/* Get length of UTF-8 codepoint starting at c (0 if invalid) */
189static inline int utf8_len(unsigned char c) {
190 if ((c & 0x80) == 0x00) return 1; /* ASCII */
191 if ((c & 0xE0) == 0xC0) return 2; /* 2-byte sequence */
192 if ((c & 0xF0) == 0xE0) return 3; /* 3-byte sequence */
193 if ((c & 0xF8) == 0xF0) return 4; /* 4-byte sequence */
194 return 1; /* Invalid, treat as 1 byte */
195}
196
197/* llama.cpp SPM whitespace handling:
198 * - Optional dummy prefix as ASCII space
199 * - Replace each ASCII space with ▁ (U+2581)
200 * - Do not trim or collapse whitespace
201 */
202static int preprocess_spm_llama_text(const char *text, int text_len, char *out, int out_max, bool add_space_prefix) {
203 int out_len = 0;
204 if (text_len < 0) text_len = (int)strlen(text);
205
206 if (add_space_prefix && text_len > 0) {
207 if (out_len + 3 > out_max) return -1;
208 out[out_len++] = (char)0xE2;
209 out[out_len++] = (char)0x96;
210 out[out_len++] = (char)0x81;
211 }
212
213 for (int i = 0; i < text_len;) {
214 if (text[i] == ' ') {
215 int j = i;
216 while (j < text_len && text[j] == ' ') {
217 j++;
218 }
219 int run = j - i;
220
221 /* Match llama.cpp behavior for this GGUF family:
222 * single separators map to ▁, but multi-space runs remain literal. */
223 if (run == 1) {
224 if (out_len + 3 > out_max) return -1;
225 out[out_len++] = (char)0xE2;
226 out[out_len++] = (char)0x96;
227 out[out_len++] = (char)0x81;
228 } else {
229 if (out_len + run > out_max) return -1;
230 for (int k = 0; k < run; k++) {
231 out[out_len++] = ' ';
232 }
233 }
234 i = j;
235 } else {
236 if (out_len + 1 > out_max) return -1;
237 out[out_len++] = text[i++];
238 }
239 }
240
241 return out_len;
242}
243
244typedef struct {
245 int prev;
246 int next;
247 const char *text;
248 int n;
249 int node_id;
250} SpmLlamaSymbol;
251
252typedef struct {
253 const char *text;
254 int n;
255 int left;
256 int right;
257} SpmLlamaNode;
258
260 const SpmLlamaNode *nodes,
261 int node_id,
262 int32_t *ids,
263 int max_ids,
264 int out_idx) {
265 if (!tok || !nodes || node_id < 0 || !ids || out_idx >= max_ids) {
266 return out_idx;
267 }
268
269 const SpmLlamaNode *node = &nodes[node_id];
270 int32_t token_id = ck_tokenizer_lookup_exact_n(tok, node->text, node->n);
271 if (token_id >= 0) {
272 ids[out_idx++] = token_id;
273 return out_idx;
274 }
275
276 if (node->left >= 0 && node->right >= 0) {
277 out_idx = spm_llama_resegment_node(tok, nodes, node->left, ids, max_ids, out_idx);
278 out_idx = spm_llama_resegment_node(tok, nodes, node->right, ids, max_ids, out_idx);
279 return out_idx;
280 }
281
282 for (int i = 0; i < node->n && out_idx < max_ids; i++) {
283 int32_t byte_token = spm_get_byte_token(tok, (unsigned char)node->text[i]);
284 ids[out_idx++] = (byte_token >= 0) ? byte_token : tok->unk_id;
285 }
286 return out_idx;
287}
288
289/* llama.cpp merge-style SPM path (LLAMA_VOCAB_TYPE_SPM). */
291 const char *text,
292 int text_len,
293 int32_t *ids,
294 int max_ids) {
295 if (!tok || !text || !ids || max_ids <= 0) return 0;
296 if (text_len < 0) text_len = (int)strlen(text);
297 if (text_len == 0) return 0;
298
299 char preprocessed[8192];
300 int pp_len = preprocess_spm_llama_text(text, text_len, preprocessed, (int)sizeof(preprocessed) - 1,
302 if (pp_len < 0) return 0;
303 preprocessed[pp_len] = '\0';
304
305 int num_symbols = 0;
306 for (int offs = 0; offs < pp_len;) {
307 int char_len = utf8_len((unsigned char)preprocessed[offs]);
308 if (char_len <= 0) char_len = 1;
309 if (offs + char_len > pp_len) char_len = pp_len - offs;
310 offs += char_len;
311 num_symbols++;
312 }
313 if (num_symbols <= 0) return 0;
314
315 SpmLlamaSymbol *symbols = (SpmLlamaSymbol *)calloc((size_t)num_symbols, sizeof(SpmLlamaSymbol));
316 int node_cap = 2 * num_symbols + 1;
317 SpmLlamaNode *nodes = (SpmLlamaNode *)calloc((size_t)node_cap, sizeof(SpmLlamaNode));
318 if (!symbols || !nodes) {
319 if (symbols) free(symbols);
320 if (nodes) free(nodes);
321 return 0;
322 }
323
324 int index = 0;
325 for (int offs = 0; offs < pp_len && index < num_symbols;) {
326 int char_len = utf8_len((unsigned char)preprocessed[offs]);
327 if (char_len <= 0) char_len = 1;
328 if (offs + char_len > pp_len) char_len = pp_len - offs;
329
330 symbols[index].text = preprocessed + offs;
331 symbols[index].n = char_len;
332 symbols[index].prev = index - 1;
333 symbols[index].next = (index + 1 < num_symbols) ? (index + 1) : -1;
334 symbols[index].node_id = index;
335
336 nodes[index].text = preprocessed + offs;
337 nodes[index].n = char_len;
338 nodes[index].left = -1;
339 nodes[index].right = -1;
340
341 offs += char_len;
342 index++;
343 }
344
345 int node_count = num_symbols;
346 for (;;) {
347 int best_left = -1;
348 int best_right = -1;
349 float best_score = -1e30f;
350
351 for (int left = 0; left != -1; left = symbols[left].next) {
352 int right = symbols[left].next;
353 if (right < 0) continue;
354
355 int pair_len = symbols[left].n + symbols[right].n;
356 int32_t token_id = ck_tokenizer_lookup_exact_n(tok, symbols[left].text, pair_len);
357 if (token_id < 0 || token_id >= (int32_t)tok->vocab_size) continue;
358
359 float score = 0.0f;
360 if (tok->scores && token_id >= 0 && token_id < (int32_t)tok->scores_size) {
361 score = tok->scores[token_id];
362 }
363
364 if (best_left < 0 || score > best_score || (score == best_score && left < best_left)) {
365 best_left = left;
366 best_right = right;
367 best_score = score;
368 }
369 }
370
371 if (best_left < 0 || best_right < 0) break;
372 if (node_count >= node_cap) break;
373
374 SpmLlamaSymbol *left = &symbols[best_left];
375 SpmLlamaSymbol *right = &symbols[best_right];
376
377 int new_node_id = node_count++;
378 nodes[new_node_id].text = left->text;
379 nodes[new_node_id].n = left->n + right->n;
380 nodes[new_node_id].left = left->node_id;
381 nodes[new_node_id].right = right->node_id;
382
383 left->n += right->n;
384 left->node_id = new_node_id;
385 left->next = right->next;
386 if (right->next >= 0) {
387 symbols[right->next].prev = best_left;
388 }
389
390 right->n = 0;
391 right->prev = -1;
392 right->next = -1;
393 }
394
395 int out_idx = 0;
396 for (int i = 0; i != -1 && out_idx < max_ids; i = symbols[i].next) {
397 out_idx = spm_llama_resegment_node(tok, nodes, symbols[i].node_id, ids, max_ids, out_idx);
398 }
399
400 free(symbols);
401 free(nodes);
402 return out_idx;
403}
404
405/* Replace spaces with SentencePiece underscore (U+2581)
406 * Also normalize whitespace similarly to SPM:
407 * - Leading spaces: consume them (SPM adds dummy prefix)
408 * - Multiple spaces: collapse to single space
409 * - Trailing spaces: consume them
410 */
411static int preprocess_spm_text(const char *text, int text_len, char *out, int out_max, bool add_space_prefix) {
412 int out_len = 0;
413
414 /* Count leading spaces */
415 int lead_spaces = 0;
416 while (lead_spaces < text_len && text[lead_spaces] == ' ') {
417 lead_spaces++;
418 }
419
420 /* Count trailing spaces */
421 int trail_spaces = 0;
422 while (trail_spaces < text_len - lead_spaces &&
423 text[text_len - 1 - trail_spaces] == ' ') {
424 trail_spaces++;
425 }
426
427 /* Add ▁ at start if there's any non-space content AND text doesn't already start with ▁ */
428 int content_len = text_len - lead_spaces - trail_spaces;
429 int starts_with_prefix = (text_len >= 3 &&
430 (unsigned char)text[0] == 0xE2 &&
431 (unsigned char)text[1] == 0x96 &&
432 (unsigned char)text[2] == 0x81);
433 int inserted_prefix = 0;
434 if (content_len > 0 && !starts_with_prefix && add_space_prefix) {
435 if (out_len + 3 > out_max) return -1;
436 out[out_len++] = (char)0xE2;
437 out[out_len++] = (char)0x96;
438 out[out_len++] = (char)0x81;
439 inserted_prefix = 1;
440 }
441
442 /* Process middle content: collapse multiple spaces to single ▁ */
443 int i = lead_spaces;
444 int last_was_space = (starts_with_prefix || inserted_prefix) ? 1 : 0;
445 while (i < text_len - trail_spaces) {
446 if (text[i] == ' ') {
447 if (!last_was_space) {
448 /* First space after content - add ▁ */
449 if (out_len + 3 > out_max) return -1;
450 out[out_len++] = (char)0xE2;
451 out[out_len++] = (char)0x96;
452 out[out_len++] = (char)0x81;
453 last_was_space = 1;
454 }
455 /* Skip additional consecutive spaces */
456 } else {
457 if (out_len + 1 > out_max) return -1;
458 out[out_len++] = text[i];
459 last_was_space = 0;
460 }
461 i++;
462 }
463
464 return out_len;
465}
466
467/* Forward declaration for SPM Viterbi */
468static int spm_find_candidates_at_pos(const CKTokenizer *tok, const char *text, int text_len,
469 size_t pos, int32_t *candidates, int max_candidates);
470
471/* Forward declaration for unknown run counting */
472static int spm_count_unknown_run(const CKTokenizer *tok, const char *text, int text_len, size_t pos);
473
474/* Forward declaration for byte fallback */
475static int spm_encode_byte_fallback(const CKTokenizer *tok,
476 const char *text, int text_len,
477 int32_t *ids, int max_ids);
478
479/* SPM Viterbi/DP encoding - finds best token sequence using token scores */
481 const char *text,
482 int text_len,
483 int32_t *ids,
484 int max_ids) {
485 if (!tok || !text || !ids || max_ids <= 0) return 0;
486 if (text_len < 0) text_len = (int)strlen(text);
487 if (text_len == 0) return 0;
488 const int dbg = getenv("CK_DEBUG_SPM_ENCODE") ? 1 : 0;
489 if (dbg) {
490 fprintf(stderr, "[SPM] encode start: text_len=%d max_ids=%d\n", text_len, max_ids);
491 }
492
493 /* Preprocess: replace spaces with ▁ */
494 char preprocessed[8192];
495 int pp_len = preprocess_spm_text(text, text_len, preprocessed, sizeof(preprocessed) - 1,
497 if (pp_len < 0) return 0;
498 preprocessed[pp_len] = '\0';
499 if (dbg) {
500 fprintf(stderr, "[SPM] preprocessed len=%d: \"%.*s\"\n", pp_len, pp_len, preprocessed);
501 }
502
503 /* DP arrays - use malloc for large inputs */
504 size_t n = (size_t)pp_len + 1;
505 float *best_score = (float *)malloc(n * sizeof(float));
506 int32_t *best_prev = (int32_t *)malloc(n * sizeof(int32_t));
507 int32_t *best_token = (int32_t *)malloc(n * sizeof(int32_t));
508 if (dbg) {
509 fprintf(stderr, "[SPM] DP alloc n=%zu\n", n);
510 }
511
512 if (!best_score || !best_prev || !best_token) {
513 if (best_score) free(best_score);
514 if (best_prev) free(best_prev);
515 if (best_token) free(best_token);
516 return 0;
517 }
518
519 /* Initialize DP */
520 const float neg_inf = -1e30f;
521 const float unknown_penalty = -10.0f; /* SentencePiece-style UNK penalty */
522 for (size_t i = 0; i < n; i++) {
523 best_score[i] = neg_inf;
524 best_prev[i] = -1;
525 best_token[i] = -1;
526 }
527 best_score[0] = 0.0f;
528
529 /* DP: for each position, find best way to reach it */
530 for (size_t pos = 0; pos < n; pos++) {
531 if (best_score[pos] == neg_inf) continue;
532
533 /* Find all tokens that match at this position */
534 int32_t candidates[64];
535 int num_cand = spm_find_candidates_at_pos(tok, preprocessed, pp_len, pos, candidates, 64);
536 if (dbg && pos < 8) {
537 fprintf(stderr, "[SPM] pos=%zu cand=%d\n", pos, num_cand);
538 }
539
540 for (int c = 0; c < num_cand; c++) {
541 int32_t token_id = candidates[c];
542
543 /* Skip disallowed token types in DP */
544 if (!spm_token_allowed_in_dp(tok, token_id)) {
545 continue;
546 }
547
548 /* Get token string and length */
549 const char *token = ck_tokenizer_id_to_token(tok, token_id);
550 if (!token) continue;
551
552 /* Calculate token length in bytes */
553 int token_len = (int)strlen(token);
554
555 /* For UNK token, use the unknown run length to cover all consecutive unknown bytes */
556 if (token_id == tok->unk_id) {
557 token_len = spm_count_unknown_run(tok, preprocessed, pp_len, pos);
558 if (token_len == 0) token_len = 1; /* At least 1 byte */
559 }
560
561 size_t next_pos = pos + token_len;
562
563 if (next_pos >= n) continue;
564
565 /* Get token score for Viterbi */
566 float token_score = 0.0f;
567 if (tok->scores && token_id >= 0 && token_id < (int32_t)tok->vocab_size) {
568 token_score = tok->scores[token_id];
569 }
570
571 /* USER_DEFINED tokens get score 0 (like llama.cpp) */
572 if (tok->types && token_id >= 0 && token_id < (int32_t)tok->types_size) {
573 if (tok->types[token_id] == GGUF_TOKEN_USER_DEFINED) {
574 token_score = 0.0f;
575 }
576 }
577 /* Apply UNK penalty (SentencePiece behavior) */
578 if (token_id == tok->unk_id) {
579 token_score += unknown_penalty;
580 }
581
582 /* Transition: score = best_score[pos] + token_score */
583 float new_score = best_score[pos] + token_score;
584
585 if (new_score > best_score[next_pos]) {
586 best_score[next_pos] = new_score;
587 best_prev[next_pos] = (int32_t)pos;
588 best_token[next_pos] = token_id;
589 }
590 }
591 }
592
593 /* Backtrack to find best token sequence */
594 int32_t *reverse_ids = (int32_t *)malloc(max_ids * sizeof(int32_t));
595 if (!reverse_ids) {
596 free(best_score);
597 free(best_prev);
598 free(best_token);
599 return 0;
600 }
601
602 int num_tokens = 0;
603 int32_t curr = (int32_t)(n - 1);
604
605 /* Handle trailingUNK by finding valid end */
606 while (curr > 0 && best_token[curr] < 0) {
607 curr = best_prev[curr];
608 }
609
610 /* Backtrack from end to start, collecting tokens.
611 * We track the token's start position to avoid duplicates. */
612 int last_start = -1; /* Track the start position of last added token */
613 while (curr > 0 && num_tokens < max_ids) {
614 int32_t token_id = best_token[curr];
615 if (token_id >= 0) {
616 /* Use the DP backpointer as the true token start */
617 int token_start = best_prev[curr];
618
619 /* Only add if this is a new token (different start position) */
620 if (token_start != last_start) {
621 reverse_ids[num_tokens++] = token_id;
622 last_start = token_start;
623 }
624 }
625 curr = best_prev[curr];
626 }
627 if (dbg) {
628 fprintf(stderr, "[SPM] backtrack tokens=%d curr=%d\n", num_tokens, curr);
629 }
630
631 /* Free DP arrays before using reverse_ids */
632 free(best_score);
633 free(best_prev);
634 free(best_token);
635
636 if (num_tokens > max_ids) num_tokens = max_ids;
637
638 /* Backtracking collected tokens in reverse order, so reverse once */
639 for (int i = 0; i < num_tokens / 2; i++) {
640 int32_t tmp = reverse_ids[i];
641 reverse_ids[i] = reverse_ids[num_tokens - 1 - i];
642 reverse_ids[num_tokens - 1 - i] = tmp;
643 }
644
645 /* Copy to output and merge consecutive UNK tokens (SPM behavior) */
646 int out_idx = 0;
647 for (int i = 0; i < num_tokens && out_idx < max_ids; i++) {
648 int32_t token_id = reverse_ids[i];
649
650 /* Merge consecutive UNK tokens into one */
651 if (token_id == tok->unk_id && out_idx > 0 && ids[out_idx - 1] == tok->unk_id) {
652 continue; /* Skip - already have UNK */
653 }
654 ids[out_idx++] = token_id;
655 }
656 if (dbg) {
657 fprintf(stderr, "[SPM] encode done: out=%d\n", out_idx);
658 }
659
660 free(reverse_ids);
661
662 /* If DP failed to produce valid tokens, use byte-fallback */
663 if (num_tokens == 0) {
665 }
666
667 return out_idx;
668}
669
671 const char *text,
672 int text_len,
673 int32_t *ids,
674 int max_ids) {
675 if (!tok || !text || text_len <= 0 || !ids || max_ids <= 0) return 0;
676 if (tok->config.spm_mode == CK_SPM_MODE_LLAMA) {
678 }
680}
681
682/* Fallback: encode using byte tokens for any unmatched content.
683 * Uses the ORIGINAL text (not preprocessed), mapping each byte to a byte token. */
685 const char *text, int text_len,
686 int32_t *ids, int max_ids) {
687 if (!tok || !text || !ids || max_ids <= 0) return 0;
688 if (text_len < 0) text_len = (int)strlen(text);
689 if (text_len == 0) return 0;
690
691 int count = 0;
692 for (int i = 0; i < text_len && count < max_ids; i++) {
693 unsigned char byte_val = (unsigned char)text[i];
694 int32_t byte_token = spm_get_byte_token(tok, byte_val);
695
696 /* If we have a byte token, use it; otherwise use UNK */
697 if (byte_token >= 0 && byte_token != tok->unk_id) {
698 ids[count++] = byte_token;
699 } else {
700 ids[count++] = tok->unk_id;
701 }
702 }
703 return count;
704}
705
706/* Find all candidate tokens matching at position */
707static int spm_find_candidates_at_pos(const CKTokenizer *tok, const char *text, int text_len,
708 size_t pos, int32_t *candidates, int max_candidates) {
709 if (!tok || !text || pos >= (size_t)text_len) return 0;
710
711 int num_found = 0;
712 int max_len = 64;
713 if (pos + max_len > (size_t)text_len) max_len = (int)(text_len - pos);
714
715 /* Iterate from longest to shortest to find all matches */
716 char tmp[65];
717 for (int len = max_len; len >= 1 && num_found < max_candidates; len--) {
718 memcpy(tmp, text + pos, len);
719 tmp[len] = '\0';
720
721 TokenInfo *info = (TokenInfo *)ck_tokenizer_hash_table_lookup(tok->vocab, tmp);
722 if (info && info->id >= 0 && info->id != tok->unk_id) {
723 /* Skip disallowed token types */
724 if (!spm_token_allowed_in_dp(tok, info->id)) {
725 continue;
726 }
727
728 /* Check if already added */
729 int dup = 0;
730 for (int j = 0; j < num_found; j++) {
731 if (candidates[j] == info->id) {
732 dup = 1;
733 break;
734 }
735 }
736 if (!dup) {
737 candidates[num_found++] = info->id;
738 }
739 }
740 }
741
742 /* If no candidates found, add UNK token as fallback.
743 * For SPM, UNK should cover all consecutive unknown bytes until a known token or end.
744 * We handle this by adding UNK with a special marker - we'll check at runtime
745 * how far we can extend it. */
746 if (num_found == 0 && tok->unk_id >= 0 && max_candidates > 0) {
747 /* Only add UNK if it's allowed in DP */
748 if (spm_token_allowed_in_dp(tok, tok->unk_id)) {
749 candidates[num_found++] = tok->unk_id;
750 }
751 }
752
753 return num_found;
754}
755
756/* Count how many consecutive bytes at text[pos] are not start of any vocab token.
757 * Also stop at UTF-8 encoded '▁' (U+2581 = 0xE2 0x96 0x81) since that's a known token. */
758static int spm_count_unknown_run(const CKTokenizer *tok, const char *text, int text_len, size_t pos) {
759 int run = 0;
760 while (pos + run < (size_t)text_len) {
761 /* Stop at '▁' (U+2581 = 0xE2 0x96 0x81) since that's a known token */
762 if (pos + run + 3 <= (size_t)text_len &&
763 (unsigned char)text[pos + run] == 0xE2 &&
764 (unsigned char)text[pos + run + 1] == 0x96 &&
765 (unsigned char)text[pos + run + 2] == 0x81) {
766 break;
767 }
768
769 /* Check if any vocab token matches at this position */
770 int max_len = 64;
771 if (pos + run + max_len > (size_t)text_len) {
772 max_len = (int)(text_len - pos - run);
773 }
774 int found = 0;
775 for (int len = max_len; len >= 1; len--) {
776 char tmp[65];
777 memcpy(tmp, text + pos + run, len);
778 tmp[len] = '\0';
779 TokenInfo *info = (TokenInfo *)ck_tokenizer_hash_table_lookup(tok->vocab, tmp);
780 if (info && info->id >= 0 && info->id != tok->unk_id && spm_token_allowed_in_dp(tok, info->id)) {
781 found = 1;
782 break;
783 }
784 }
785 if (found) break;
786 run++;
787 }
788 return run;
789}
790
791/* Encode text to token IDs using SentencePiece paths only. */
793 const char *text,
794 int text_len,
795 int32_t *ids,
796 int max_ids) {
797 if (!tok || !text || !ids || max_ids <= 0) return 0;
798 if (text_len < 0) text_len = (int)strlen(text);
799
800 int out_idx = 0;
801 if (tok->config.add_bos && tok->bos_id >= 0 && out_idx < max_ids) {
802 ids[out_idx++] = tok->bos_id;
803 }
804 if (text_len == 0) {
805 if (tok->config.add_eos && tok->eos_id >= 0 && out_idx < max_ids) {
806 ids[out_idx++] = tok->eos_id;
807 }
808 return out_idx;
809 }
810
811 int segment_start = 0;
812 for (int pos = 0; pos < text_len && out_idx < max_ids;) {
813 size_t special_len = 0;
814 int32_t special_id = spm_find_special_token_at_pos(tok, text, text_len, (size_t)pos, &special_len);
815 if (special_id < 0 || special_len == 0) {
816 pos++;
817 continue;
818 }
819
820 if (segment_start < pos) {
822 tok,
823 text + segment_start,
824 pos - segment_start,
825 ids + out_idx,
826 max_ids - out_idx
827 );
828 if (n <= 0) return n;
829 out_idx += n;
830 if (out_idx >= max_ids) break;
831 }
832
833 ids[out_idx++] = special_id;
834 pos += (int)special_len;
835 segment_start = pos;
836 }
837
838 if (segment_start < text_len && out_idx < max_ids) {
840 tok,
841 text + segment_start,
842 text_len - segment_start,
843 ids + out_idx,
844 max_ids - out_idx
845 );
846 if (n <= 0) return n;
847 out_idx += n;
848 }
849
850 if (tok->config.add_eos && tok->eos_id >= 0 && out_idx < max_ids) {
851 ids[out_idx++] = tok->eos_id;
852 }
853
854 return out_idx;
855}
856
857/* Load vocabulary from memory-mapped binary data with scores and types */
859 int vocab_size,
860 const int32_t *offsets,
861 const char *strings,
862 const float *scores,
863 const uint8_t *types,
864 int num_merges,
865 const int32_t *merges) {
866 if (!tok || !offsets || !strings) return -1;
868
869 /* Free any existing scores/types arrays before reallocating */
870 if (tok->scores) {
871 free(tok->scores);
872 tok->scores = NULL;
873 tok->scores_size = 0;
874 }
875 if (tok->types) {
876 free(tok->types);
877 tok->types = NULL;
878 tok->types_size = 0;
879 }
880
881 /* Allocate scores and types arrays if provided */
882 if (scores && vocab_size > 0) {
883 tok->scores = (float *)malloc(vocab_size * sizeof(float));
884 if (!tok->scores) return -1;
885 memcpy(tok->scores, scores, vocab_size * sizeof(float));
886 tok->scores_size = (size_t)vocab_size;
887 }
888 if (types && vocab_size > 0) {
889 tok->types = (uint8_t *)malloc(vocab_size * sizeof(uint8_t));
890 if (!tok->types) {
891 if (tok->scores) {
892 free(tok->scores);
893 tok->scores = NULL;
894 }
895 return -1;
896 }
897 memcpy(tok->types, types, vocab_size * sizeof(uint8_t));
898 tok->types_size = (size_t)vocab_size;
899 }
900
901 for (int i = 0; i < vocab_size; i++) {
902 const char *token = strings + offsets[i];
903 float score = scores ? scores[i] : 0.0f;
905 }
906
907 /* Build byte token lookup table if types are available */
908 if (types && vocab_size > 0) {
910
911 /* Log token type statistics */
912 int count_normal = 0, count_unknown = 0, count_control = 0, count_byte = 0, count_other = 0;
913 int max_type = 0;
914 for (int i = 0; i < vocab_size; i++) {
915 uint8_t t = tok->types[i];
916 if (t > max_type) max_type = t;
917 switch (t) {
918 case GGUF_TOKEN_NORMAL: count_normal++; break;
919 case GGUF_TOKEN_UNKNOWN: count_unknown++; break;
920 case GGUF_TOKEN_CONTROL: count_control++; break;
921 case GGUF_TOKEN_BYTE: count_byte++; break;
922 default: count_other++; break;
923 }
924 }
925 fprintf(stderr, "[TOKENIZER] Loaded %d tokens: normal=%d, unknown=%d, control=%d, byte=%d, other=%d\n",
926 vocab_size, count_normal, count_unknown, count_control, count_byte, count_other);
927 if (max_type > GGUF_TOKEN_BYTE) {
928 fprintf(stderr, "[TOKENIZER] Warning: Unexpected token type %d\n", max_type);
929 }
930 }
931
932 /* TODO: Merges */
933 (void)num_merges; (void)merges;
934 return 0;
935}
int32_t ck_tokenizer_lookup(const CKTokenizer *tok, const char *token, int len)
int32_t ck_tokenizer_add_token(CKTokenizer *tok, const char *token, int len)
const char * ck_tokenizer_id_to_token(const CKTokenizer *tok, int32_t id)
void * ck_tokenizer_hash_table_lookup(CKTokenizerHashTable *table, const char *key)
Definition hash_table.c:202
CKSpmMode spm_mode
Definition tokenizer.h:85
int32_t bos_id
float * scores
Definition tokenizer.h:112
size_t types_size
Definition tokenizer.h:115
int32_t * byte_token_id
Definition tokenizer.h:118
int32_t unk_id
CKTrie * vocab_trie
Definition tokenizer.h:104
int32_t eos_id
CKTokenizerHashTable * vocab
Definition tokenizer.h:101
uint8_t * types
Definition tokenizer.h:114
size_t scores_size
Definition tokenizer.h:113
CKTokenizerConfig config
Definition tokenizer.h:98
struct CKTrieNode * children[256]
void ck_tokenizer_reset(CKTokenizer *tok)
Definition tokenizer.c:130
const int32_t * ids
Definition tokenizer.h:444
int32_t id
Definition tokenizer.h:316
const char * text
Definition tokenizer.h:564
bool add_space_prefix
Definition tokenizer.h:253
const char * token
Definition tokenizer.h:307
int32_t float * score
Definition tokenizer.h:328
@ CK_SPM_MODE_LLAMA
Definition tokenizer.h:70
#define CK_TOKENIZER_MAX_TOKEN_LEN
Definition tokenizer.h:44
const int32_t int int * out_len
Definition tokenizer.h:446
static int spm_find_candidates_at_pos(const CKTokenizer *tok, const char *text, int text_len, size_t pos, int32_t *candidates, int max_candidates)
static int32_t ck_tokenizer_lookup_exact(const CKTokenizer *tok, const char *token)
static int preprocess_spm_llama_text(const char *text, int text_len, char *out, int out_max, bool add_space_prefix)
static bool spm_token_is_byte_format(const char *token)
int ck_tokenizer_load_binary_with_scores(CKTokenizer *tok, int vocab_size, const int32_t *offsets, const char *strings, const float *scores, const uint8_t *types, int num_merges, const int32_t *merges)
static int ck_tokenizer_encode_spm_llama_impl(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
static bool spm_token_allowed_in_dp(const CKTokenizer *tok, int32_t token_id)
#define GGUF_TOKEN_CONTROL
static void spm_build_byte_lookup(CKTokenizer *tok, const char *strings, const int32_t *offsets, int vocab_size)
static int preprocess_spm_text(const char *text, int text_len, char *out, int out_max, bool add_space_prefix)
static int spm_encode_byte_fallback(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
int ck_tokenizer_encode_spm_dispatch(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
static int spm_llama_resegment_node(const CKTokenizer *tok, const SpmLlamaNode *nodes, int node_id, int32_t *ids, int max_ids, int out_idx)
static int32_t ck_tokenizer_lookup_exact_n(const CKTokenizer *tok, const char *text, int text_len)
#define GGUF_TOKEN_USER_DEFINED
static int utf8_len(unsigned char c)
static int32_t spm_find_special_token_at_pos(const CKTokenizer *tok, const char *text, int text_len, size_t pos, size_t *match_len)
#define GGUF_TOKEN_UNUSED
static bool spm_is_byte_token(const CKTokenizer *tok, int32_t token_id)
static int32_t spm_get_byte_token(const CKTokenizer *tok, unsigned char byte_val)
#define GGUF_TOKEN_BYTE
static int spm_count_unknown_run(const CKTokenizer *tok, const char *text, int text_len, size_t pos)
#define GGUF_TOKEN_UNKNOWN
static int ck_tokenizer_encode_spm_impl(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
#define GGUF_TOKEN_NORMAL
static int ck_tokenizer_encode_spm_plain_segment(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
int const int32_t const char int num_merges
Definition true_bpe.h:196
int const int32_t const char * strings
Definition true_bpe.h:195
int const int32_t const char int const int32_t * merges
Definition true_bpe.h:197
const int32_t int char int max_len
Definition true_bpe.h:288
const char int text_len
Definition true_bpe.h:270
int vocab_size
Definition true_bpe.h:193
int const int32_t * offsets
Definition true_bpe.h:194
const char * left
Definition true_bpe.h:138
const char int int32_t int max_ids
Definition true_bpe.h:272
const char const char * right
Definition true_bpe.h:139