← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
tokenizer_spm.c File Reference
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <limits.h>
#include "tokenizer/tokenizer.h"
#include "tokenizer/hash_table.h"

Go to the source code of this file.

Macros

#define GGUF_TOKEN_BYTE   6
 
#define GGUF_TOKEN_CONTROL   3
 
#define GGUF_TOKEN_NORMAL   1
 
#define GGUF_TOKEN_UNKNOWN   2
 
#define GGUF_TOKEN_UNUSED   5
 
#define GGUF_TOKEN_USER_DEFINED   4
 

Functions

int ck_tokenizer_encode_spm_dispatch (const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
 
static int ck_tokenizer_encode_spm_impl (const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
 
static int ck_tokenizer_encode_spm_llama_impl (const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
 
static int ck_tokenizer_encode_spm_plain_segment (const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
 
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 int32_t ck_tokenizer_lookup_exact (const CKTokenizer *tok, const char *token)
 
static int32_t ck_tokenizer_lookup_exact_n (const CKTokenizer *tok, const char *text, int text_len)
 
static int preprocess_spm_llama_text (const char *text, int text_len, char *out, int out_max, bool add_space_prefix)
 
static int preprocess_spm_text (const char *text, int text_len, char *out, int out_max, bool add_space_prefix)
 
static void spm_build_byte_lookup (CKTokenizer *tok, const char *strings, const int32_t *offsets, int vocab_size)
 
static int spm_count_unknown_run (const CKTokenizer *tok, const char *text, int text_len, size_t pos)
 
static int spm_encode_byte_fallback (const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
 
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 spm_find_special_token_at_pos (const CKTokenizer *tok, const char *text, int text_len, size_t pos, size_t *match_len)
 
static int32_t spm_get_byte_token (const CKTokenizer *tok, unsigned char byte_val)
 
static bool spm_is_byte_token (const CKTokenizer *tok, int32_t token_id)
 
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 bool spm_token_allowed_in_dp (const CKTokenizer *tok, int32_t token_id)
 
static bool spm_token_is_byte_format (const char *token)
 
static int utf8_len (unsigned char c)
 

Macro Definition Documentation

◆ GGUF_TOKEN_BYTE

#define GGUF_TOKEN_BYTE   6

Definition at line 114 of file tokenizer_spm.c.

◆ GGUF_TOKEN_CONTROL

#define GGUF_TOKEN_CONTROL   3

Definition at line 111 of file tokenizer_spm.c.

◆ GGUF_TOKEN_NORMAL

#define GGUF_TOKEN_NORMAL   1

Definition at line 109 of file tokenizer_spm.c.

◆ GGUF_TOKEN_UNKNOWN

#define GGUF_TOKEN_UNKNOWN   2

Definition at line 110 of file tokenizer_spm.c.

◆ GGUF_TOKEN_UNUSED

#define GGUF_TOKEN_UNUSED   5

Definition at line 113 of file tokenizer_spm.c.

◆ GGUF_TOKEN_USER_DEFINED

#define GGUF_TOKEN_USER_DEFINED   4

Definition at line 112 of file tokenizer_spm.c.

Function Documentation

◆ ck_tokenizer_encode_spm_dispatch()

int ck_tokenizer_encode_spm_dispatch ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
int32_t *  ids,
int  max_ids 
)

Definition at line 807 of file tokenizer_spm.c.

811 {
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}
int32_t bos_id
int32_t eos_id
CKTokenizerConfig config
Definition tokenizer.h:98
const int32_t * ids
Definition tokenizer.h:444
const char * text
Definition tokenizer.h:564
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)
static int ck_tokenizer_encode_spm_plain_segment(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
const char int text_len
Definition true_bpe.h:285
const char int int32_t int max_ids
Definition true_bpe.h:287

References CKTokenizerConfig::add_bos, CKTokenizerConfig::add_eos, CKTokenizer::bos_id, ck_tokenizer_encode_spm_plain_segment(), CKTokenizer::config, CKTokenizer::eos_id, ids, max_ids, spm_find_special_token_at_pos(), text, and text_len.

Referenced by ck_tokenizer_encode().

◆ ck_tokenizer_encode_spm_impl()

static int ck_tokenizer_encode_spm_impl ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
int32_t *  ids,
int  max_ids 
)
static

Definition at line 489 of file tokenizer_spm.c.

493 {
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}
const char * ck_tokenizer_id_to_token(const CKTokenizer *tok, int32_t id)
float * scores
Definition tokenizer.h:112
size_t types_size
Definition tokenizer.h:115
int32_t unk_id
uint8_t * types
Definition tokenizer.h:114
const char * token
Definition tokenizer.h:307
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 bool spm_token_allowed_in_dp(const CKTokenizer *tok, int32_t token_id)
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)
#define GGUF_TOKEN_USER_DEFINED
static int spm_count_unknown_run(const CKTokenizer *tok, const char *text, int text_len, size_t pos)

References CKTokenizerConfig::add_space_prefix, ck_tokenizer_id_to_token(), CKTokenizer::config, GGUF_TOKEN_USER_DEFINED, ids, max_ids, preprocess_spm_text(), CKTokenizer::scores, spm_count_unknown_run(), spm_encode_byte_fallback(), spm_find_candidates_at_pos(), spm_token_allowed_in_dp(), text, text_len, token, CKTokenizer::types, CKTokenizer::types_size, CKTokenizer::unk_id, and CKTokenizer::vocab_size.

Referenced by ck_tokenizer_encode_spm_plain_segment().

◆ ck_tokenizer_encode_spm_llama_impl()

static int ck_tokenizer_encode_spm_llama_impl ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
int32_t *  ids,
int  max_ids 
)
static

Definition at line 291 of file tokenizer_spm.c.

295 {
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}
size_t scores_size
Definition tokenizer.h:113
int32_t float * score
Definition tokenizer.h:328
static int preprocess_spm_llama_text(const char *text, int text_len, char *out, int out_max, bool add_space_prefix)
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)
static int utf8_len(unsigned char c)
const char * left
Definition true_bpe.h:138
const char const char * right
Definition true_bpe.h:139

References CKTokenizerConfig::add_space_prefix, ck_tokenizer_lookup_exact_n(), CKTokenizer::config, ids, left, max_ids, preprocess_spm_llama_text(), right, score, CKTokenizer::scores, CKTokenizer::scores_size, spm_llama_resegment_node(), text, text_len, utf8_len(), and CKTokenizer::vocab_size.

Referenced by ck_tokenizer_encode_spm_plain_segment().

◆ ck_tokenizer_encode_spm_plain_segment()

static int ck_tokenizer_encode_spm_plain_segment ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
int32_t *  ids,
int  max_ids 
)
static

Definition at line 685 of file tokenizer_spm.c.

689 {
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}
CKSpmMode spm_mode
Definition tokenizer.h:85
@ CK_SPM_MODE_LLAMA
Definition tokenizer.h:70
static int ck_tokenizer_encode_spm_llama_impl(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)
static int ck_tokenizer_encode_spm_impl(const CKTokenizer *tok, const char *text, int text_len, int32_t *ids, int max_ids)

References CK_SPM_MODE_LLAMA, ck_tokenizer_encode_spm_impl(), ck_tokenizer_encode_spm_llama_impl(), CKTokenizer::config, ids, max_ids, CKTokenizerConfig::spm_mode, text, and text_len.

Referenced by ck_tokenizer_encode_spm_dispatch().

◆ ck_tokenizer_load_binary_with_scores()

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 
)

Load vocabulary from memory-mapped binary data with scores and types.

This extended version supports SPM (SentencePiece) tokenizers which require token scores for Viterbi/DP encoding.

Parameters
tokTokenizer
vocab_sizeNumber of tokens
offsetsArray of offsets into strings pool
stringsString pool containing null-terminated tokens
scoresArray of token scores (float32), can be NULL
typesArray of token types (uint8), can be NULL
num_mergesNumber of BPE merges
mergesMerge rules as (left, right, merged) triplets
Returns
0 on success, -1 on error

Definition at line 873 of file tokenizer_spm.c.

880 {
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_add_token(CKTokenizer *tok, const char *token, int len)
void ck_tokenizer_reset(CKTokenizer *tok)
Definition tokenizer.c:130
#define GGUF_TOKEN_CONTROL
static void spm_build_byte_lookup(CKTokenizer *tok, const char *strings, const int32_t *offsets, int vocab_size)
#define GGUF_TOKEN_BYTE
#define GGUF_TOKEN_UNKNOWN
#define GGUF_TOKEN_NORMAL
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 uint8_t * types
Definition true_bpe.h:208
int vocab_size
Definition true_bpe.h:197
int const int32_t * offsets
Definition true_bpe.h:198

References ck_tokenizer_add_token(), ck_tokenizer_reset(), GGUF_TOKEN_BYTE, GGUF_TOKEN_CONTROL, GGUF_TOKEN_NORMAL, GGUF_TOKEN_UNKNOWN, merges, num_merges, offsets, score, CKTokenizer::scores, CKTokenizer::scores_size, spm_build_byte_lookup(), strings, token, CKTokenizer::types, types, CKTokenizer::types_size, and vocab_size.

Referenced by ck_tokenizer_load_binary().

◆ ck_tokenizer_lookup_exact()

static int32_t ck_tokenizer_lookup_exact ( const CKTokenizer *  tok,
const char *  token 
)
static

Definition at line 23 of file tokenizer_spm.c.

23 {
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}
void * ck_tokenizer_hash_table_lookup(CKTokenizerHashTable *table, const char *key)
Definition hash_table.c:202
CKTokenizerHashTable * vocab
Definition tokenizer.h:101

References ck_tokenizer_hash_table_lookup(), token, and CKTokenizer::vocab.

Referenced by ck_tokenizer_lookup_exact_n().

◆ ck_tokenizer_lookup_exact_n()

static int32_t ck_tokenizer_lookup_exact_n ( const CKTokenizer *  tok,
const char *  text,
int  text_len 
)
static

Definition at line 30 of file tokenizer_spm.c.

30 {
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}
int32_t id
Definition tokenizer.h:316
static int32_t ck_tokenizer_lookup_exact(const CKTokenizer *tok, const char *token)

References ck_tokenizer_lookup_exact(), id, text, and text_len.

Referenced by ck_tokenizer_encode_spm_llama_impl(), and spm_llama_resegment_node().

◆ preprocess_spm_llama_text()

static int preprocess_spm_llama_text ( const char *  text,
int  text_len,
char *  out,
int  out_max,
bool  add_space_prefix 
)
static

Definition at line 203 of file tokenizer_spm.c.

203 {
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}
bool add_space_prefix
Definition tokenizer.h:253
const int32_t int int * out_len
Definition tokenizer.h:446

References add_space_prefix, out_len, text, and text_len.

Referenced by ck_tokenizer_encode_spm_llama_impl().

◆ preprocess_spm_text()

static int preprocess_spm_text ( const char *  text,
int  text_len,
char *  out,
int  out_max,
bool  add_space_prefix 
)
static

Definition at line 420 of file tokenizer_spm.c.

420 {
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}

References add_space_prefix, out_len, text, and text_len.

Referenced by ck_tokenizer_encode_spm_impl().

◆ spm_build_byte_lookup()

static void spm_build_byte_lookup ( CKTokenizer *  tok,
const char *  strings,
const int32_t *  offsets,
int  vocab_size 
)
static

Definition at line 156 of file tokenizer_spm.c.

156 {
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}
int32_t * byte_token_id
Definition tokenizer.h:118
static bool spm_token_is_byte_format(const char *token)

References CKTokenizer::byte_token_id, GGUF_TOKEN_BYTE, offsets, spm_token_is_byte_format(), strings, token, CKTokenizer::types, and vocab_size.

Referenced by ck_tokenizer_load_binary_with_scores().

◆ spm_count_unknown_run()

static int spm_count_unknown_run ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
size_t  pos 
)
static

Definition at line 773 of file tokenizer_spm.c.

773 {
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}
const int32_t int char int max_len
Definition true_bpe.h:303

References ck_tokenizer_hash_table_lookup(), max_len, spm_token_allowed_in_dp(), text, text_len, CKTokenizer::unk_id, and CKTokenizer::vocab.

Referenced by ck_tokenizer_encode_spm_impl().

◆ spm_encode_byte_fallback()

static int spm_encode_byte_fallback ( const CKTokenizer *  tok,
const char *  text,
int  text_len,
int32_t *  ids,
int  max_ids 
)
static

Definition at line 699 of file tokenizer_spm.c.

701 {
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}
static int32_t spm_get_byte_token(const CKTokenizer *tok, unsigned char byte_val)
const uint8_t size_t count
Definition true_bpe.h:209

References count, ids, max_ids, spm_get_byte_token(), text, text_len, and CKTokenizer::unk_id.

Referenced by ck_tokenizer_encode_spm_impl().

◆ spm_find_candidates_at_pos()

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

Definition at line 722 of file tokenizer_spm.c.

723 {
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}

References ck_tokenizer_hash_table_lookup(), max_len, spm_token_allowed_in_dp(), text, text_len, CKTokenizer::unk_id, and CKTokenizer::vocab.

Referenced by ck_tokenizer_encode_spm_impl().

◆ spm_find_special_token_at_pos()

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 
)
static

Definition at line 46 of file tokenizer_spm.c.

50 {
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}
CKTrie * vocab_trie
Definition tokenizer.h:104
struct CKTrieNode * children[256]
#define CK_TOKENIZER_MAX_TOKEN_LEN
Definition tokenizer.h:44

References CKTrieNode::children, ck_tokenizer_hash_table_lookup(), CK_TOKENIZER_MAX_TOKEN_LEN, CKTrieNode::is_special, max_len, CKTrie::root, text, text_len, CKTrieNode::token_id, CKTokenizer::vocab, and CKTokenizer::vocab_trie.

Referenced by ck_tokenizer_encode_spm_dispatch().

◆ spm_get_byte_token()

static int32_t spm_get_byte_token ( const CKTokenizer *  tok,
unsigned char  byte_val 
)
inlinestatic

Definition at line 136 of file tokenizer_spm.c.

136 {
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}
int32_t ck_tokenizer_lookup(const CKTokenizer *tok, const char *token, int len)

References CKTokenizer::byte_token_id, ck_tokenizer_lookup(), and CKTokenizer::unk_id.

Referenced by spm_encode_byte_fallback(), and spm_llama_resegment_node().

◆ spm_is_byte_token()

static bool spm_is_byte_token ( const CKTokenizer *  tok,
int32_t  token_id 
)
inlinestatic

Definition at line 128 of file tokenizer_spm.c.

128 {
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}

References GGUF_TOKEN_BYTE, CKTokenizer::types, and CKTokenizer::vocab_size.

◆ spm_llama_resegment_node()

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

Definition at line 260 of file tokenizer_spm.c.

265 {
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}

References ck_tokenizer_lookup_exact_n(), ids, max_ids, spm_get_byte_token(), spm_llama_resegment_node(), and CKTokenizer::unk_id.

Referenced by ck_tokenizer_encode_spm_llama_impl(), and spm_llama_resegment_node().

◆ spm_token_allowed_in_dp()

static bool spm_token_allowed_in_dp ( const CKTokenizer *  tok,
int32_t  token_id 
)
inlinestatic

Definition at line 118 of file tokenizer_spm.c.

118 {
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}
#define GGUF_TOKEN_UNUSED

References GGUF_TOKEN_BYTE, GGUF_TOKEN_CONTROL, GGUF_TOKEN_UNUSED, CKTokenizer::types, and CKTokenizer::vocab_size.

Referenced by ck_tokenizer_encode_spm_impl(), spm_count_unknown_run(), and spm_find_candidates_at_pos().

◆ spm_token_is_byte_format()

static bool spm_token_is_byte_format ( const char *  token)
inlinestatic

Definition at line 149 of file tokenizer_spm.c.

149 {
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}

References token.

Referenced by spm_build_byte_lookup().

◆ utf8_len()

static int utf8_len ( unsigned char  c)
inlinestatic

Definition at line 190 of file tokenizer_spm.c.

190 {
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}

Referenced by ck_tokenizer_encode(), and ck_tokenizer_encode_spm_llama_impl().