ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
llama3_tokenizer.h
Go to the documentation of this file.
1// Vendored verbatim from paperkiln (microtorch) include/microtorch/llama3_tokenizer.hpp
2// at paperkiln a8510d93 (include names .hpp -> .h). ember.cpp uses it for paperkiln chat GGUFs whose tokenizer.ggml.model
3// is "gpt2" with pre "llama-bpe" (the llama.cpp-compatible vocabulary, chat_tokenizer.h), and with
4// max_digits 1 for Qwen2/Qwen3 GGUFs (pre "qwen2").
5// Do not edit here: change it in paperkiln (its tests check C++/Python
6// parity) and re-vendor.
7#pragma once
8// Llama 3 byte-level BPE, encoding exactly as llama.cpp does for a GGUF with
9// tokenizer.ggml.model "gpt2" and tokenizer.ggml.pre "llama-bpe" (the
10// vocabulary scheme of paperkiln models from the 21 Oct 2026 decision, so
11// llama.cpp, Ollama and LM Studio tokenise them natively).
12//
13// Ported from llama.cpp b6000 (MIT License, Copyright (c) 2023-2024 The ggml
14// authors): src/unicode.cpp (UTF-8 decoding, the GPT-2 byte-to-unicode map,
15// unicode_regex_split_custom_llama3, a hand-written splitter for the llama3
16// regex) and src/llama-vocab.cpp (special-token partition, the BPE merge
17// queue with ignore_merges). The category tables come from
18// src/unicode-data.cpp via tools/gen_llama3_tables.py.
19//
20// Encoding: the text is cut at special tokens (token types control,
21// user-defined and unknown, longest first); each remaining fragment is split
22// by the llama3 pre-tokeniser regex
23// (?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}|
24// ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+
25// each word is byte-encoded (GPT-2 map) and, unless the whole word is a
26// token, merged by rank (lowest rank first, leftmost on ties). As in
27// llama.cpp, invalid UTF-8 becomes U+FFFD before splitting, so only valid
28// UTF-8 round-trips. No BOS or EOS is added.
29//
30// Qwen2 (tokenizer.ggml.pre "qwen2", llama.cpp's LLAMA_VOCAB_PRE_TYPE_QWEN2)
31// uses the same regex with \p{N} for \p{N}{1,3}: every digit its own word.
32// max_digits (split_words, Tokenizer) selects it: 3 is llama3, 1 is qwen2.
33#include <algorithm>
34#include <cstdint>
35#include <queue>
36#include <stdexcept>
37#include <string>
38#include <unordered_map>
39#include <utility>
40#include <vector>
41
42#include "bpe_tokenizer.h"
44
45namespace microtorch {
46namespace llama3 {
47
48struct CptFlags {
49 bool number = false, letter = false, whitespace = false;
50 bool any = false; // llama.cpp's as_uint() != 0: a code point inside the fragment
51};
52
54 if (cpt >= 0x110000) return 0x0001; // undefined
55 uint32_t lo = 0, hi = tables::kRangeCount - 1; // kRangeStart[lo] <= cpt < kRangeStart[hi]
56 while (hi - lo > 1) {
57 const uint32_t mid = (lo + hi) / 2;
58 (tables::kRangeStart[mid] <= cpt ? lo : hi) = mid;
59 }
60 return tables::kRangeFlags[lo];
61}
62
64 const uint16_t c = category_bits(cpt);
65 CptFlags f;
66 f.number = c & 0x0002;
67 f.letter = c & 0x0004;
68 for (uint32_t i = 0; i < tables::kWhitespaceCount; ++i)
69 if (tables::kWhitespace[i] == cpt) f.whitespace = true;
70 f.any = true;
71 return f;
72}
73
74// llama.cpp's unicode_cpts_from_utf8: an invalid byte is skipped and becomes U+FFFD.
75inline std::vector<uint32_t> cpts_from_utf8(const std::string& s) {
76 std::vector<uint32_t> out;
77 out.reserve(s.size());
78 const auto b = [&](size_t i) { return static_cast<unsigned char>(s[i]); };
79 const auto cont = [&](size_t i) { return (b(i) & 0xC0) == 0x80; };
80 size_t i = 0;
81 while (i < s.size()) {
82 const unsigned char c = b(i);
83 if (!(c & 0x80)) {
84 out.push_back(c);
85 i += 1;
86 } else if (!(c & 0x40)) {
87 out.push_back(0xFFFD);
88 i += 1;
89 } else if (!(c & 0x20)) {
90 if (i + 1 >= s.size() || !cont(i + 1)) {
91 out.push_back(0xFFFD);
92 i += 1;
93 } else {
94 out.push_back(((c & 0x1Fu) << 6) | (b(i + 1) & 0x3Fu));
95 i += 2;
96 }
97 } else if (!(c & 0x10)) {
98 if (i + 2 >= s.size() || !cont(i + 1) || !cont(i + 2)) {
99 out.push_back(0xFFFD);
100 i += 1;
101 } else {
102 out.push_back(((c & 0x0Fu) << 12) | ((b(i + 1) & 0x3Fu) << 6) | (b(i + 2) & 0x3Fu));
103 i += 3;
104 }
105 } else if (!(c & 0x08)) {
106 if (i + 3 >= s.size() || !cont(i + 1) || !cont(i + 2) || !cont(i + 3)) {
107 out.push_back(0xFFFD);
108 i += 1;
109 } else {
110 out.push_back(((c & 0x07u) << 18) | ((b(i + 1) & 0x3Fu) << 12) | ((b(i + 2) & 0x3Fu) << 6) |
111 (b(i + 3) & 0x3Fu));
112 i += 4;
113 }
114 } else {
115 out.push_back(0xFFFD);
116 i += 1;
117 }
118 }
119 return out;
120}
121
122inline void append_utf8(uint32_t cpt, std::string& out) {
123 if (cpt <= 0x7F) {
124 out.push_back(static_cast<char>(cpt));
125 } else if (cpt <= 0x7FF) {
126 out.push_back(static_cast<char>(0xC0 | ((cpt >> 6) & 0x1F)));
127 out.push_back(static_cast<char>(0x80 | (cpt & 0x3F)));
128 } else if (cpt <= 0xFFFF) {
129 out.push_back(static_cast<char>(0xE0 | ((cpt >> 12) & 0x0F)));
130 out.push_back(static_cast<char>(0x80 | ((cpt >> 6) & 0x3F)));
131 out.push_back(static_cast<char>(0x80 | (cpt & 0x3F)));
132 } else if (cpt <= 0x10FFFF) {
133 out.push_back(static_cast<char>(0xF0 | ((cpt >> 18) & 0x07)));
134 out.push_back(static_cast<char>(0x80 | ((cpt >> 12) & 0x3F)));
135 out.push_back(static_cast<char>(0x80 | ((cpt >> 6) & 0x3F)));
136 out.push_back(static_cast<char>(0x80 | (cpt & 0x3F)));
137 } else {
138 throw std::invalid_argument("invalid codepoint");
139 }
140}
141
142// The llama3 pre-tokeniser (unicode_regex_split_custom_llama3) on one
143// fragment with no special tokens in it: the words, as UTF-8. max_digits 1
144// gives the qwen2 regex (\p{N} in place of \p{N}{1,3}).
145inline std::vector<std::string> split_words(const std::string& text, size_t max_digits = 3) {
146 const std::vector<uint32_t> cp = cpts_from_utf8(text);
147 const size_t n = cp.size();
148 constexpr uint32_t kOut = 0xFFFFFFFF;
149 const auto get = [&](size_t p) { return p < n ? cp[p] : kOut; };
150 const auto fl = [&](size_t p) { return p < n ? flags_of(cp[p]) : CptFlags{}; };
151 // Only ASCII capitals lowercase to s, t, r, e, v, m, l or d in llama.cpp's
152 // table (checked by tools/gen_llama3_tables.py's source), so ASCII suffices.
153 const auto lower = [](uint32_t c) { return c >= 'A' && c <= 'Z' ? c + 32 : c; };
154 std::vector<std::string> words;
155 size_t prev = 0;
156 const auto add = [&](size_t end) {
157 const size_t len = end - prev;
158 if (len > 0) {
159 words.emplace_back();
160 for (size_t i = prev; i < end; ++i) append_utf8(cp[i], words.back());
161 }
162 prev = end;
163 return len;
164 };
165 for (size_t pos = 0; pos < n;) {
166 const uint32_t c = get(pos);
167 const CptFlags f = fl(pos);
168 // (?i:'s|'t|'re|'ve|'m|'ll|'d)
169 if (c == '\'' && pos + 1 < n) {
170 const uint32_t c1 = lower(get(pos + 1));
171 if (c1 == 's' || c1 == 't' || c1 == 'm' || c1 == 'd') {
172 pos += add(pos + 2);
173 continue;
174 }
175 if (pos + 2 < n) {
176 const uint32_t c2 = lower(get(pos + 2));
177 if ((c1 == 'r' && c2 == 'e') || (c1 == 'v' && c2 == 'e') || (c1 == 'l' && c2 == 'l')) {
178 pos += add(pos + 3);
179 continue;
180 }
181 }
182 }
183 // [^\r\n\p{L}\p{N}]?\p{L}+
184 if (!(c == '\r' || c == '\n' || f.number)) {
185 if (f.letter || fl(pos + 1).letter) {
186 ++pos;
187 while (fl(pos).letter) ++pos;
188 add(pos);
189 continue;
190 }
191 }
192 // \p{N}{1,3} (\p{N} for qwen2)
193 if (f.number) {
194 size_t ini = pos;
195 while (fl(pos).number) {
196 if (++pos - ini >= max_digits) {
197 add(pos);
198 ini = pos;
199 }
200 }
201 add(pos);
202 continue;
203 }
204 // <space>?[^\s\p{L}\p{N}]+[\r\n]*
205 CptFlags f2 = c == ' ' ? fl(pos + 1) : f;
206 if (!(f2.whitespace || f2.letter || f2.number) && f.any) {
207 pos += c == ' ' ? 1 : 0;
208 while (!(f2.whitespace || f2.letter || f2.number) && f2.any) f2 = fl(++pos);
209 uint32_t c2 = get(pos);
210 while (c2 == '\r' || c2 == '\n') c2 = get(++pos);
211 add(pos);
212 continue;
213 }
214 size_t nws = 0, last_rn = 0;
215 while (fl(pos + nws).whitespace) {
216 const uint32_t c2 = get(pos + nws);
217 if (c2 == '\r' || c2 == '\n') last_rn = pos + nws + 1;
218 ++nws;
219 }
220 if (last_rn > 0) { // \s*[\r\n]+
221 pos = last_rn;
222 add(pos);
223 continue;
224 }
225 if (nws > 1 && get(pos + nws) != kOut) { // \s+(?!\S)
226 pos += nws - 1;
227 add(pos);
228 continue;
229 }
230 if (nws > 0) { // \s+
231 pos += nws;
232 add(pos);
233 continue;
234 }
235 add(++pos); // no match
236 }
237 return words;
238}
239
240// GPT-2's bytes_to_unicode: printable bytes map to themselves, the rest to
241// U+0100 upward in byte order.
242inline const std::vector<std::string>& byte_to_unicode() {
243 static const std::vector<std::string> m = [] {
244 std::vector<std::string> v(256);
245 std::vector<bool> direct(256, false);
246 for (int c = 0x21; c <= 0x7E; ++c) direct[c] = true;
247 for (int c = 0xA1; c <= 0xAC; ++c) direct[c] = true;
248 for (int c = 0xAE; c <= 0xFF; ++c) direct[c] = true;
249 uint32_t n = 0;
251 return v;
252 }();
253 return m;
254}
255
256inline std::string byte_encode(const std::string& word) {
257 std::string out;
258 for (unsigned char c : word) out += byte_to_unicode()[c];
259 return out;
260}
261
263public:
264 // tokens: tokenizer.ggml.tokens (byte-encoded strings); merges: "a b"
265 // in rank order; token_type: tokenizer.ggml.token_type (1 normal,
266 // 2 unknown, 3 control, 4 user-defined, ...), empty = all normal;
267 // max_digits: the pre-tokeniser's digit run (3 llama3, 1 qwen2).
268 Tokenizer(std::vector<std::string> tokens, const std::vector<std::string>& merges,
269 const std::vector<int32_t>& token_type, size_t max_digits = 3)
271 // A repeated string maps to its last id, as llama.cpp's token_to_id.
272 for (size_t i = 0; i < tokens_.size(); ++i) id_[tokens_[i]] = static_cast<int>(i);
273 for (size_t r = 0; r < merges.size(); ++r) rank_.emplace(merges[r], static_cast<int>(r));
274 special_.assign(tokens_.size(), false);
275 for (size_t i = 0; i < token_type.size() && i < tokens_.size(); ++i) {
276 const int t = token_type[i];
277 if (t == 2 || t == 3 || t == 4) {
278 special_[i] = true;
279 specials_.push_back(static_cast<int>(i));
280 }
281 }
282 // Longest first, as llama.cpp's special-token cache (ties by id).
283 std::stable_sort(specials_.begin(), specials_.end(),
284 [&](int a, int b) { return tokens_[a].size() > tokens_[b].size(); });
285 for (int c = 0; c < 256; ++c) unbyte_.emplace(byte_to_unicode()[c], static_cast<unsigned char>(c));
286 }
287
288 std::vector<int> encode(const std::string& text) const {
289 std::vector<std::pair<std::string, int>> frags = {{text, -1}};
290 for (int sid : specials_) {
291 const std::string& s = tokens_[sid];
292 if (s.empty()) continue;
293 std::vector<std::pair<std::string, int>> next;
294 for (auto& [t, id] : frags) {
295 if (id >= 0) {
296 next.emplace_back(std::move(t), id);
297 continue;
298 }
299 size_t at = 0;
300 for (size_t m; (m = t.find(s, at)) != std::string::npos; at = m + s.size()) {
301 if (m > at) next.emplace_back(t.substr(at, m - at), -1);
302 next.emplace_back(std::string(), sid);
303 }
304 if (at < t.size()) next.emplace_back(t.substr(at), -1);
305 }
306 frags = std::move(next);
307 }
308 std::vector<int> out;
309 for (const auto& [t, id] : frags) {
310 if (id >= 0) {
311 out.push_back(id);
312 continue;
313 }
314 for (const std::string& w : split_words(t, max_digits_)) encode_word(byte_encode(w), out);
315 }
316 return out;
317 }
318
319 // Raw bytes: special tokens as their text, others byte-decoded.
320 std::string decode(const std::vector<int>& ids) const {
321 std::string s;
322 for (int id : ids) {
323 if (id < 0 || static_cast<size_t>(id) >= tokens_.size()) continue;
324 if (special_[id]) {
325 s += tokens_[id];
326 continue;
327 }
328 const std::string& t = tokens_[id];
329 for (size_t i = 0; i < t.size();) {
330 const size_t len = utf8_len(static_cast<unsigned char>(t[i]));
331 const auto it = unbyte_.find(t.substr(i, len));
332 if (it != unbyte_.end()) s.push_back(static_cast<char>(it->second));
333 i += len;
334 }
335 }
336 return s;
337 }
338
339 size_t size() const { return tokens_.size(); }
340 const std::vector<std::string>& tokens() const { return tokens_; }
341
342private:
343 static size_t utf8_len(unsigned char c) {
344 static const size_t lookup[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4};
345 return lookup[c >> 4];
346 }
347
348 struct Symbol {
349 int prev, next;
350 size_t start, n;
351 };
352 struct Bigram {
354 std::string text;
355 };
356 struct Later { // llama.cpp's comparator: lowest rank first, then leftmost
357 bool operator()(const Bigram& l, const Bigram& r) const {
358 return l.rank > r.rank || (l.rank == r.rank && l.left > r.left);
359 }
360 };
361
362 // llm_tokenizer_bpe_session::tokenize for one byte-encoded word.
363 void encode_word(const std::string& word, std::vector<int>& out) const {
364 std::vector<Symbol> sym;
365 if (id_.count(word)) { // ignore_merges: a whole-word token wins
366 sym.push_back({-1, -1, 0, word.size()});
367 } else {
368 for (size_t off = 0; off < word.size();) {
369 const size_t len = std::min(word.size() - off, utf8_len(static_cast<unsigned char>(word[off])));
370 const int idx = static_cast<int>(sym.size());
371 sym.push_back({idx - 1, off + len == word.size() ? -1 : idx + 1, off, len});
372 off += len;
373 }
374 }
375 std::priority_queue<Bigram, std::vector<Bigram>, Later> q;
376 const auto text = [&](int i) { return word.substr(sym[i].start, sym[i].n); };
377 const auto push = [&](int l, int r) {
378 if (l < 0 || r < 0) return;
379 const std::string a = text(l), b = text(r);
380 const auto it = rank_.find(a + " " + b);
381 if (it != rank_.end()) q.push({l, r, it->second, a + b});
382 };
383 for (int i = 1; i < static_cast<int>(sym.size()); ++i) push(i - 1, i);
384 while (!q.empty()) {
385 const Bigram bg = q.top();
386 q.pop();
387 Symbol& L = sym[bg.left];
388 Symbol& R = sym[bg.right];
389 if (L.n == 0 || R.n == 0 || text(bg.left) + text(bg.right) != bg.text) continue; // outdated
390 L.n += R.n;
391 R.n = 0;
392 L.next = R.next;
393 if (R.next >= 0) sym[R.next].prev = bg.left;
394 push(L.prev, bg.left);
395 push(bg.left, L.next);
396 }
397 for (const Symbol& s : sym) {
398 if (s.n == 0) continue;
399 const std::string t = word.substr(s.start, s.n);
400 const auto it = id_.find(t);
401 if (it != id_.end()) {
402 out.push_back(it->second);
403 continue;
404 }
405 for (char c : t) { // llama.cpp's fallback: single bytes that are tokens
406 const auto b = id_.find(std::string(1, c));
407 if (b != id_.end()) out.push_back(b->second);
408 }
409 }
410 }
411
412 std::vector<std::string> tokens_;
413 size_t max_digits_ = 3;
414 std::unordered_map<std::string, int> id_, rank_;
415 std::vector<bool> special_;
416 std::vector<int> specials_;
417 std::unordered_map<std::string, unsigned char> unbyte_;
418};
419
420// A llama.cpp-compatible vocabulary GGUF: model "gpt2", pre "llama-bpe"
421// (paperkiln's) or "qwen2" (single digits).
422inline Tokenizer load_gguf(const std::string& path) {
424 if (g.model != "gpt2" || (g.pre != "llama-bpe" && g.pre != "qwen2"))
425 throw std::runtime_error(path + ": tokenizer is '" + g.model + "'/'" + g.pre +
426 "', not 'gpt2'/'llama-bpe' or 'gpt2'/'qwen2'");
427 return Tokenizer(std::move(g.tokens), g.merges, g.token_type, g.pre == "qwen2" ? 1 : 3);
428}
429
430} // namespace llama3
431} // namespace microtorch
Tokenizer(std::vector< std::string > tokens, const std::vector< std::string > &merges, const std::vector< int32_t > &token_type, size_t max_digits=3)
std::unordered_map< std::string, unsigned char > unbyte_
std::unordered_map< std::string, int > id_
std::unordered_map< std::string, int > rank_
static size_t utf8_len(unsigned char c)
const std::vector< std::string > & tokens() const
std::vector< std::string > tokens_
void encode_word(const std::string &word, std::vector< int > &out) const
std::string decode(const std::vector< int > &ids) const
std::vector< int > encode(const std::string &text) const
const float * p[kIrDepthSourcesMax]
int n
std::string text
std::vector< float > v
float c
std::vector< E > a
node out
node add
size_t j
double number
Definition ir_digest.cpp:64
GgufTokenizer read_gguf_tokenizer(const std::string &path)
constexpr uint32_t kRangeStart[kRangeCount]
constexpr uint16_t kRangeFlags[kRangeCount]
constexpr uint32_t kWhitespace[kWhitespaceCount]
const std::vector< std::string > & byte_to_unicode()
std::vector< uint32_t > cpts_from_utf8(const std::string &s)
CptFlags flags_of(uint32_t cpt)
void append_utf8(uint32_t cpt, std::string &out)
Tokenizer load_gguf(const std::string &path)
std::vector< std::string > split_words(const std::string &text, size_t max_digits=3)
std::string byte_encode(const std::string &word)
uint16_t category_bits(uint32_t cpt)
std::vector< std::string > merges
std::vector< std::string > tokens
std::vector< int32_t > token_type
bool operator()(const Bigram &l, const Bigram &r) const