ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
spm_tokenizer.h
Go to the documentation of this file.
1#pragma once
2// SentencePiece vocabularies (tokenizer.ggml.model "llama") as llama.cpp b6000 encodes them
3// (llama-vocab.cpp: tokenizer_st_partition, then llm_tokenizer_spm_session on each text
4// fragment), for the chat mode of Gemma 3 GGUFs (chat_tokenizer.h). ember's older SentencePiece
5// path (tokenizer.cpp) approximates merges from scores and stays with the TinyLlama models.
6//
7// - Special tokens: every CONTROL, USER_DEFINED and UNKNOWN token (GGUF token types 3, 4, 2) is
8// cut out of the text before encoding, longest token text first, each occurrence left to right
9// (llama.cpp's parse_special = true: chat prompts carry <bos> and <start_of_turn> as text).
10// - Each remaining fragment: " " -> U+2581 (and a leading U+2581 when
11// tokenizer.ggml.add_space_prefix is true and the fragment starts the text or follows a special
12// token), split into UTF-8 characters, then the adjacent pair whose concatenation is a token with
13// the highest score is merged, ties to the leftmost, until no pair is a token. A final symbol that
14// is not a token is written as its bytes (<0xXX>).
15// - No BOS is added: a chat template writes its own.
16// Decoding: U+2581 -> " ", byte tokens -> their byte, every other token -> its text.
17#include <algorithm>
18#include <cstdint>
19#include <cstdio>
20#include <functional>
21#include <queue>
22#include <stdexcept>
23#include <string>
24#include <unordered_map>
25#include <utility>
26#include <vector>
27
28namespace ember {
29
31public:
32 SpmTokenizer(std::vector<std::string> tokens, std::vector<float> scores, std::vector<int32_t> types,
33 bool add_space_prefix)
34 : tokens_(std::move(tokens)), scores_(std::move(scores)), types_(std::move(types)),
35 add_space_prefix_(add_space_prefix) {
36 if (scores_.size() != tokens_.size()) throw std::runtime_error("SentencePiece GGUF: scores and tokens differ in length");
37 if (types_.size() != tokens_.size()) throw std::runtime_error("SentencePiece GGUF: token types and tokens differ in length");
38 id_.reserve(tokens_.size());
39 for (size_t i = 0; i < tokens_.size(); ++i) id_[tokens_[i]] = static_cast<int>(i); // the last one wins
40 for (size_t i = 0; i < tokens_.size(); ++i)
41 if ((types_[i] == kControl || types_[i] == kUserDefined || types_[i] == kUnknown) && !tokens_[i].empty())
42 specials_.push_back(static_cast<int>(i));
43 // Longest text first (llama.cpp sorts by length with std::sort; among equal lengths the
44 // order only matters for overlapping specials, which the gate against HF would show).
45 std::stable_sort(specials_.begin(), specials_.end(),
46 [&](int a, int b) { return tokens_[a].size() > tokens_[b].size(); });
47 for (int b = 0; b < 256; ++b) {
48 char buf[8];
49 std::snprintf(buf, sizeof buf, "<0x%02X>", b);
50 auto it = id_.find(buf);
51 if (it == id_.end()) it = id_.find(std::string(1, static_cast<char>(b)));
52 byte_id_[b] = it == id_.end() ? -1 : it->second;
53 }
54 }
55
56 std::vector<int> encode(const std::string& text) const {
57 // Fragments: (special token id, or -1 with [offset, offset + length) of text).
58 struct Frag {
59 int id;
60 size_t off, len;
61 };
62 std::vector<Frag> frags = {{-1, 0, text.size()}};
63 for (int sid : specials_) {
64 const std::string& s = tokens_[sid];
65 std::vector<Frag> next;
66 next.reserve(frags.size());
67 for (const Frag& f : frags) {
68 if (f.id >= 0) {
69 next.push_back(f);
70 continue;
71 }
72 size_t at = f.off;
73 const size_t end = f.off + f.len;
74 while (at < end) {
75 const size_t hit = text.find(s, at);
76 if (hit == std::string::npos || hit + s.size() > end) break;
77 if (hit > at) next.push_back({-1, at, hit - at});
78 next.push_back({sid, hit, s.size()});
79 at = hit + s.size();
80 }
81 if (at < end) next.push_back({-1, at, end - at});
82 }
83 frags.swap(next);
84 }
85 std::vector<int> out;
86 bool prev_special = true;
87 for (const Frag& f : frags) {
88 if (f.id >= 0) {
89 out.push_back(f.id);
90 prev_special = true;
91 continue;
92 }
93 if (f.len == 0) continue;
94 std::string piece = add_space_prefix_ && prev_special ? std::string(" ") : std::string();
95 piece += text.substr(f.off, f.len);
97 prev_special = false;
98 }
99 return out;
100 }
101
102 std::string decode(const std::vector<int>& ids) const {
103 std::string s;
104 for (int id : ids) {
105 if (id < 0 || static_cast<size_t>(id) >= tokens_.size()) continue;
106 const std::string& t = tokens_[id];
107 if (types_[id] == kByte && t.size() == 6 && t.compare(0, 3, "<0x") == 0) {
108 s += static_cast<char>(std::stoi(t.substr(3, 2), nullptr, 16));
109 continue;
110 }
111 for (size_t i = 0; i < t.size();) {
112 if (t.compare(i, 3, kSpace) == 0) {
113 s += ' ';
114 i += 3;
115 } else {
116 s += t[i++];
117 }
118 }
119 }
120 return s;
121 }
122
123 size_t size() const { return tokens_.size(); }
124
125private:
126 static constexpr int32_t kUnknown = 2, kControl = 3, kUserDefined = 4, kByte = 6;
127 static constexpr const char* kSpace = "\xe2\x96\x81"; // U+2581
128
129 static std::string escape(const std::string& s) {
130 std::string o;
131 o.reserve(s.size() + s.size() / 2);
132 for (char c : s) {
133 if (c == ' ') o += kSpace;
134 else o += c;
135 }
136 return o;
137 }
138
139 struct Symbol {
140 int prev, next;
141 size_t off, n;
142 };
143 struct Bigram {
145 float score;
146 size_t size;
147 };
148 struct Lower { // llama.cpp's comparator: the highest score on top, ties to the leftmost
149 bool operator()(const Bigram& l, const Bigram& r) const {
150 return l.score < r.score || (l.score == r.score && l.left > r.left);
151 }
152 };
153
154 int lookup(const std::string& s) const {
155 auto it = id_.find(s);
156 return it == id_.end() ? -1 : it->second;
157 }
158
159 void encode_fragment(const std::string& text, std::vector<int>& out) const {
160 static const uint8_t utf8_len[16] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4};
161 std::vector<Symbol> sym;
162 for (size_t off = 0; off < text.size();) {
163 const size_t len = std::min<size_t>(utf8_len[static_cast<uint8_t>(text[off]) >> 4], text.size() - off);
164 const int index = static_cast<int>(sym.size());
165 sym.push_back({index - 1, off + len == text.size() ? -1 : index + 1, off, len});
166 off += len;
167 }
168 std::priority_queue<Bigram, std::vector<Bigram>, Lower> queue;
169 std::unordered_map<std::string, std::pair<int, int>> rev_merge;
170 auto try_add = [&](int left, int right) {
171 if (left == -1 || right == -1) return;
172 const std::string s = text.substr(sym[left].off, sym[left].n + sym[right].n);
173 const int id = lookup(s);
174 if (id < 0) return;
175 queue.push({left, right, scores_[id], s.size()});
176 rev_merge[s] = {left, right};
177 };
178 for (int i = 1; i < static_cast<int>(sym.size()); ++i) try_add(i - 1, i);
179 while (!queue.empty()) {
180 const Bigram b = queue.top();
181 queue.pop();
182 Symbol& l = sym[b.left];
183 Symbol& r = sym[b.right];
184 if (l.n == 0 || r.n == 0 || l.n + r.n != b.size) continue;
185 l.n += r.n;
186 r.n = 0;
187 l.next = r.next;
188 if (r.next >= 0) sym[r.next].prev = b.left;
189 try_add(l.prev, b.left);
190 try_add(b.left, l.next);
191 }
192 std::function<void(const Symbol&)> resegment = [&](const Symbol& s) {
193 const std::string t = text.substr(s.off, s.n);
194 const int id = lookup(t);
195 if (id >= 0) {
196 out.push_back(id);
197 return;
198 }
199 const auto p = rev_merge.find(t);
200 if (p == rev_merge.end()) {
201 for (size_t j = 0; j < s.n; ++j) {
202 const int bid = byte_id_[static_cast<uint8_t>(text[s.off + j])];
203 if (bid < 0) throw std::runtime_error("SentencePiece GGUF has no byte token for a byte of the text");
204 out.push_back(bid);
205 }
206 return;
207 }
208 resegment(sym[p->second.first]);
209 resegment(sym[p->second.second]);
210 };
211 for (int i = sym.empty() ? -1 : 0; i != -1; i = sym[i].next) resegment(sym[i]);
212 }
213
214 std::vector<std::string> tokens_;
215 std::vector<float> scores_;
216 std::vector<int32_t> types_;
218 std::unordered_map<std::string, int> id_;
219 std::vector<int> specials_;
220 int byte_id_[256];
221};
222
223} // namespace ember
std::string decode(const std::vector< int > &ids) const
std::vector< int > specials_
std::unordered_map< std::string, int > id_
int lookup(const std::string &s) const
static constexpr int32_t kControl
std::vector< std::string > tokens_
std::vector< float > scores_
SpmTokenizer(std::vector< std::string > tokens, std::vector< float > scores, std::vector< int32_t > types, bool add_space_prefix)
void encode_fragment(const std::string &text, std::vector< int > &out) const
std::vector< int > encode(const std::string &text) const
static constexpr int32_t kUnknown
size_t size() const
std::vector< int32_t > types_
static constexpr const char * kSpace
static constexpr int32_t kUserDefined
static constexpr int32_t kByte
static std::string escape(const std::string &s)
const float * p[kIrDepthSourcesMax]
int n
std::string text
float c
std::vector< E > a
node out
size_t j
bool operator()(const Bigram &l, const Bigram &r) const