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