ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
bpe_tokenizer.h
Go to the documentation of this file.
1// Vendored verbatim from paperkiln (microtorch) include/microtorch/bpe_tokenizer.hpp
2// at paperkiln e70f75d44f6e (include names .hpp -> .h). ember.cpp uses it for GGUFs whose tokenizer.ggml.model
3// is "bpe" (paperkiln byte-level BPE: merges are token-id pairs).
4// Do not edit here: change it in paperkiln (its tests check C++/Python
5// parity) and re-vendor.
6#pragma once
7// Byte-level BPE encoder/decoder: the C++ side of tools/bpe.py (the
8// reference, which also trains). Byte-identical to it: tests/test_bpe.py
9// encodes a corpus with both and compares ids.
10//
11// Pre-tokenization works on bytes so no Unicode tables are needed: a letter
12// is an ASCII letter, an apostrophe or any byte >= 0x80 (a multibyte
13// character stays inside its word); a digit is 0-9 and is always its own
14// chunk; whitespace is the C isspace set. Chunks: a letter run with at most
15// one leading space, a single digit, a run of whitespace (leaving a space
16// that precedes a letter to the next word), a run of other bytes, or a
17// special token. Merges never cross a chunk; within a chunk the
18// lowest-ranked merge present is applied until none applies.
19//
20// Lossless: decode(encode(s)) == s for every byte string, and there is no
21// <unk> in normal use.
22#include <cstdint>
23#include <cstring>
24#include <fstream>
25#include <stdexcept>
26#include <functional>
27#include <iterator>
28#include <map>
29#include <string>
30#include <unordered_map>
31#include <utility>
32#include <vector>
33
34namespace microtorch {
35namespace bpe {
36
37inline const std::vector<std::string>& specials() {
38 static const std::vector<std::string> s = {"<unk>", "<|endoftext|>", "<|user|>", "<|assistant|>"};
39 return s;
40}
41
42class Tokenizer {
43public:
44 // tokens: the GGUF tokenizer.ggml.tokens list (specials, 256 bytes,
45 // then one per merge); merges: "a b" token-id pairs in rank order.
46 Tokenizer(std::vector<std::string> tokens, const std::vector<std::string>& merges) : tokens_(std::move(tokens)) {
47 const int base = static_cast<int>(specials().size()) + 256;
48 for (size_t r = 0; r < merges.size(); ++r) {
49 const auto sp = merges[r].find(' ');
50 const int a = std::stoi(merges[r].substr(0, sp)), b = std::stoi(merges[r].substr(sp + 1));
51 rank_[key(a, b)] = {static_cast<int>(r), base + static_cast<int>(r)};
52 }
53 }
54
55 std::vector<int> encode(const std::string& text) const {
56 std::vector<int> out;
57 for (const auto& [chunk, special] : pretokenize(text)) {
58 if (special >= 0) {
59 out.push_back(special);
60 continue;
61 }
63 }
64 return out;
65 }
66
67 std::string decode(const std::vector<int>& ids) const {
68 std::string s;
69 for (int id : ids)
70 if (id >= 0 && static_cast<size_t>(id) < tokens_.size()) s += tokens_[id];
71 return s;
72 }
73
74 size_t size() const { return tokens_.size(); }
75 const std::vector<std::string>& tokens() const { return tokens_; }
76
77 static bool letter(unsigned char c) {
78 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '\'' || c >= 0x80;
79 }
80 static bool digit(unsigned char c) { return c >= '0' && c <= '9'; }
81 static bool space(unsigned char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
82
83 // (chunk, special id or -1), in order.
84 static std::vector<std::pair<std::string, int>> pretokenize(const std::string& b) {
85 std::vector<std::pair<std::string, int>> out;
86 const size_t n = b.size();
87 auto special_at = [&](size_t i) -> int {
88 for (size_t k = 1; k < specials().size(); ++k)
89 if (b.compare(i, specials()[k].size(), specials()[k]) == 0) return static_cast<int>(k);
90 return -1;
91 };
92 size_t i = 0;
93 while (i < n) {
94 const int sp = special_at(i);
95 if (sp >= 0) {
96 out.emplace_back(specials()[sp], sp);
97 i += specials()[sp].size();
98 continue;
99 }
100 const unsigned char c = b[i];
101 const size_t j = (c == ' ' && i + 1 < n && letter(b[i + 1])) ? i + 1 : i;
102 size_t k;
103 if (letter(b[j])) {
104 k = j;
105 while (k < n && letter(b[k])) ++k;
106 } else if (digit(c)) {
107 k = i + 1;
108 } else if (space(c)) {
109 k = i;
110 while (k < n && space(b[k]) && !(b[k] == ' ' && k + 1 < n && letter(b[k + 1]))) ++k;
111 if (k == i) k = i + 1;
112 } else {
113 k = i;
114 while (k < n && !(space(b[k]) || letter(b[k]) || digit(b[k]) || special_at(k) >= 0)) ++k;
115 if (k == i) k = i + 1;
116 }
117 out.emplace_back(b.substr(i, k - i), -1);
118 i = k;
119 }
120 return out;
121 }
122
123private:
124 struct Merge {
125 int rank, id;
126 };
127 static uint64_t key(int a, int b) { return (static_cast<uint64_t>(a) << 32) | static_cast<uint32_t>(b); }
128
129 void encode_chunk(const std::string& chunk, std::vector<int>& out) const {
130 const int off = static_cast<int>(specials().size());
131 std::vector<int> ids;
132 for (unsigned char c : chunk) ids.push_back(off + c);
133 while (ids.size() > 1) {
134 int best = -1, at = -1, id = -1;
135 for (size_t k = 0; k + 1 < ids.size(); ++k) {
136 const auto it = rank_.find(key(ids[k], ids[k + 1]));
137 if (it != rank_.end() && (best < 0 || it->second.rank < best)) {
138 best = it->second.rank;
139 at = static_cast<int>(k);
140 id = it->second.id;
141 }
142 }
143 if (best < 0) break;
144 ids[at] = id;
145 ids.erase(ids.begin() + at + 1);
146 }
147 out.insert(out.end(), ids.begin(), ids.end());
148 }
149
150 std::vector<std::string> tokens_;
151 std::unordered_map<uint64_t, Merge> rank_;
152};
153
155 std::string model;
156 std::vector<std::string> tokens, merges;
157 std::string pre; // tokenizer.ggml.pre ("" when absent)
158 std::vector<int32_t> token_type; // tokenizer.ggml.token_type (empty when absent)
159};
160
161// The tokenizer block of any GGUF (tokenizer.ggml.model, pre, tokens,
162// token_type, merges), unvalidated: a vocabulary file from tools/bpe.py or a
163// model export.
164inline GgufTokenizer read_gguf_tokenizer(const std::string& path) {
165 std::ifstream f(path, std::ios::binary);
166 if (!f) throw std::runtime_error("cannot open " + path);
167 std::string buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
168 size_t p = 0;
169 auto need = [&](size_t n) {
170 if (p + n > buf.size()) throw std::runtime_error(path + ": truncated GGUF");
171 };
172 auto u32 = [&]() { need(4); uint32_t v; std::memcpy(&v, buf.data() + p, 4); p += 4; return v; };
173 auto u64 = [&]() { need(8); uint64_t v; std::memcpy(&v, buf.data() + p, 8); p += 8; return v; };
174 auto str = [&]() { const uint64_t n = u64(); need(n); std::string s = buf.substr(p, n); p += n; return s; };
175 if (u32() != 0x46554747u) throw std::runtime_error(path + ": not GGUF");
176 u32();
177 u64();
178 const uint64_t nkv = u64();
179 std::vector<std::string> tokens, merges;
180 std::string model, pre;
181 std::vector<int32_t> token_type;
182 std::function<void(uint32_t)> skip = [&](uint32_t t) {
183 static const size_t sz[] = {1, 1, 2, 2, 4, 4, 4, 1, 0, 0, 8, 8, 8};
184 if (t == 8) {
185 str();
186 } else if (t == 9) {
187 const uint32_t et = u32();
188 const uint64_t n = u64();
189 for (uint64_t i = 0; i < n; ++i) skip(et);
190 } else if (t < 13) {
191 need(sz[t]);
192 p += sz[t];
193 } else {
194 throw std::runtime_error(path + ": unknown GGUF type");
195 }
196 };
197 for (uint64_t i = 0; i < nkv; ++i) {
198 const std::string k = str();
199 const uint32_t t = u32();
200 if ((k == "tokenizer.ggml.tokens" || k == "tokenizer.ggml.merges") && t == 9) {
201 const uint32_t et = u32();
202 const uint64_t n = u64();
203 if (et != 8) throw std::runtime_error(path + ": " + k + " is not a string array");
204 auto& dst = k == "tokenizer.ggml.tokens" ? tokens : merges;
205 for (uint64_t j = 0; j < n; ++j) dst.push_back(str());
206 } else if (k == "tokenizer.ggml.token_type" && t == 9) {
207 const uint32_t et = u32();
208 const uint64_t n = u64();
209 for (uint64_t j = 0; j < n; ++j) { // another element type is skipped, not an error
210 if (et == 5)
211 token_type.push_back(static_cast<int32_t>(u32()));
212 else
213 skip(et);
214 }
215 } else if (k == "tokenizer.ggml.model" && t == 8) {
216 model = str();
217 } else if (k == "tokenizer.ggml.pre" && t == 8) {
218 pre = str();
219 } else {
220 skip(t);
221 }
222 }
223 return {std::move(model), std::move(tokens), std::move(merges), std::move(pre), std::move(token_type)};
224}
225
226// Reads a tokenizer GGUF written by tools/bpe.py (or a BPE model export):
227// tokenizer.ggml.model must be "bpe"; tokens and merges are taken from
228// their arrays.
229inline Tokenizer load_gguf(const std::string& path) {
231 if (g.model != "bpe") throw std::runtime_error(path + ": tokenizer.ggml.model is '" + g.model + "', not 'bpe'");
232 return Tokenizer(std::move(g.tokens), g.merges);
233}
234
235} // namespace bpe
236} // namespace microtorch
static bool digit(unsigned char c)
void encode_chunk(const std::string &chunk, std::vector< int > &out) const
static bool letter(unsigned char c)
Tokenizer(std::vector< std::string > tokens, const std::vector< std::string > &merges)
std::vector< int > encode(const std::string &text) const
static uint64_t key(int a, int b)
std::string decode(const std::vector< int > &ids) const
static std::vector< std::pair< std::string, int > > pretokenize(const std::string &b)
std::vector< std::string > tokens_
static bool space(unsigned char c)
std::unordered_map< uint64_t, Merge > rank_
const std::vector< std::string > & tokens() const
const float * p[kIrDepthSourcesMax]
int n
std::string text
std::vector< float > v
float c
std::vector< E > a
node out
size_t j
std::string str
Definition ir_digest.cpp:66
GgufTokenizer read_gguf_tokenizer(const std::string &path)
Tokenizer load_gguf(const std::string &path)
const std::vector< std::string > & specials()
std::vector< std::string > merges
std::vector< std::string > tokens
std::vector< int32_t > token_type