ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
ember Namespace Reference

Namespaces

namespace  detail
 
namespace  nfc
 

Classes

struct  ChatFormat
 
struct  ChatPrompt
 
struct  ChatReply
 
struct  ChatSetup
 
class  ChatTokenizer
 
class  ChatTokenizerOf
 
struct  ChatVocab
 
struct  Conversation
 
class  QwenNfcTokenizer
 
class  ReplyBuilder
 
class  ReplyStream
 
struct  RequestError
 
struct  ScoreItem
 
struct  ScoreModel
 
class  SpmTokenizer
 

Typedefs

using ChatDecode = std::function< std::string(const std::vector< int > &)>
 

Enumerations

enum class  Utf8Part { whole , ill_formed , cut }
 

Functions

ChatVocab chat_vocab_from_gguf (const GGUFData &gd, const std::string &path)
 
std::string strip_reply_ws (const std::string &s)
 
ChatReply reply_of (const std::vector< int > &ids, int call_id, const ChatDecode &decode)
 
template<class Json >
Json tool_calls_json (const std::vector< chat::ToolCall > &calls)
 
chat::json ordered_tools (const std::string &body)
 
Conversation chat_conversation_tools (const nlohmann::json &r, const chat::json &tools)
 
Conversation chat_conversation (const nlohmann::json &r, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
 
std::string openai_content (const nlohmann::json &m, size_t i)
 
Conversation openai_conversation_tools (const nlohmann::json &body, const chat::json &tools)
 
Conversation openai_conversation (const nlohmann::json &body, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
 
template<class Count >
ChatPrompt build_general_prompt (const ChatFormat &f, const Conversation &c, int max_new, int ctx, Count count)
 
template<class Count >
ChatPrompt build_chat_prompt (const ChatFormat &f, const Conversation &c, int max_new, int ctx, Count count)
 
int token_id (const std::vector< std::string > &tokens, const std::string &text)
 
bool has_chat_specials (const std::vector< std::string > &tokens)
 
void control_ids_check (const chat::Template &t, const ChatTokenizer &tok, const std::vector< std::pair< std::string, int > > &want)
 
ChatSetup make_chat_setup (ChatVocab v)
 
bool deterministic ()
 
void deterministic_cublas_env ()
 
template<class F >
void parallel_for (int64_t n, F &&fn)
 
std::string score_state_ids_error (const std::vector< int > &ids, const ScoreModel &m)
 
std::string parse_score_body (const nlohmann::json &body, const ScoreModel &m, std::vector< ScoreItem > &out)
 
std::vector< char > score_banned (const std::vector< std::string > &tokens, size_t vocab)
 
void score_fill (nlohmann::json &rec, const std::vector< int > &c, const float *rows, size_t V, const std::vector< char > &banned)
 
std::vector< int > insert_state_blocks (const std::vector< int > &ids, int vocab, int k, int eot, int user, int assistant)
 
size_t utf8_part (const std::string &s, size_t i, Utf8Part &part)
 
std::string utf8_valid (const std::string &s)
 
size_t utf8_settled (const std::string &s, size_t from)
 
template<class Json >
Json utf8_valid_json (const Json &j)
 
template<class Json >
std::string dump_valid (const Json &j)
 

Typedef Documentation

◆ ChatDecode

using ember::ChatDecode = typedef std::function<std::string(const std::vector<int>&)>

Definition at line 30 of file chat_reply.h.

Enumeration Type Documentation

◆ Utf8Part

Enumerator
whole 
ill_formed 
cut 

Definition at line 26 of file utf8_text.h.

Function Documentation

◆ build_chat_prompt()

template<class Count >
ChatPrompt ember::build_chat_prompt ( const ChatFormat &  f,
const Conversation &  c,
int  max_new,
int  ctx,
Count  count 
)

Definition at line 449 of file chat_request.h.

449 {
450 if (c.general) return build_general_prompt(f, c, max_new, ctx, count);
451 const std::string* sys = c.has_system ? &c.system : nullptr;
452 auto build = [&](size_t from) { return f.prompt(c.turns, from, c.user, sys, c.thinking); };
453 size_t from = 0;
454 ChatPrompt out;
455 out.prompt = build(from);
456 while (from < c.turns.size() && count(out.prompt) + max_new > ctx) out.prompt = build(++from);
457 out.prompt_tokens = count(out.prompt);
458 out.turns_used = static_cast<int>(c.turns.size() - from);
459 if (out.prompt_tokens >= ctx)
460 out.error = "message is " + std::to_string(out.prompt_tokens) + " tokens; the model's context is " +
461 std::to_string(ctx);
462 return out;
463}
float c
node out
size_t j
ChatPrompt build_general_prompt(const ChatFormat &f, const Conversation &c, int max_new, int ctx, Count count)

References build_general_prompt(), c, j, out, and ember::ChatPrompt::prompt.

Referenced by bpe_build_prompt().

◆ build_general_prompt()

template<class Count >
ChatPrompt ember::build_general_prompt ( const ChatFormat &  f,
const Conversation &  c,
int  max_new,
int  ctx,
Count  count 
)

Definition at line 411 of file chat_request.h.

411 {
412 const std::vector<chat::Message>& m = c.messages;
413 const size_t sys = !m.empty() && m[0].role == "system" ? 1 : 0;
414 const auto ex = chat::exchanges(m);
415 auto kept = [&](size_t k) {
416 if (k == 0) return m; // the whole conversation: anything before the first user message is refused by render
417 std::vector<chat::Message> out(m.begin(), m.begin() + static_cast<std::ptrdiff_t>(sys));
418 if (k < ex.size()) out.insert(out.end(), m.begin() + static_cast<std::ptrdiff_t>(ex[k].first), m.end());
419 return out;
420 };
421 auto build = [&](size_t k) { return chat::render(kept(k), f.tmpl, /*generation_prompt=*/true, c.thinking, c.tools); };
422 size_t k = 0;
423 ChatPrompt out;
424 out.prompt = build(k); // the whole conversation first: the record's refusals come from here
425 while (k + 1 < ex.size() && count(out.prompt) + max_new > ctx) out.prompt = build(++k);
426 out.prompt_tokens = count(out.prompt);
427 out.turns_used = ex.empty() ? 0 : static_cast<int>(ex.size() - 1 - k);
428 if (out.prompt_tokens >= ctx) {
429 std::string head; // the pinned system turn (with the tool definitions), as rendered
430 for (const auto& p : chat::render_pieces(kept(k), f.tmpl, true, c.thinking, c.tools))
431 if (p.role == "system" || p.kind == "bos") head += p.text;
432 const int pinned = head.empty() ? 0 : count(head);
433 out.error = pinned > 0 ? "the system turn and the newest exchange are " + std::to_string(out.prompt_tokens) +
434 " tokens (the system turn " + std::to_string(pinned) +
435 "); the model's context is " + std::to_string(ctx) +
436 " (an exchange is never cut)"
437 : "the newest exchange is " + std::to_string(out.prompt_tokens) +
438 " tokens; the model's context is " + std::to_string(ctx) +
439 " (an exchange is never cut)";
440 }
441 return out;
442}
const float * p[kIrDepthSourcesMax]
std::string text
Kind kind
std::vector< Piece > render_pieces(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
std::vector< std::pair< size_t, size_t > > exchanges(const std::vector< Message > &m)
std::string render(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})

References c, microtorch::chat::exchanges(), j, k, out, p, ember::ChatPrompt::prompt, microtorch::chat::render(), and microtorch::chat::render_pieces().

Referenced by build_chat_prompt().

◆ chat_conversation()

Conversation ember::chat_conversation ( const nlohmann::json &  r,
const chat::Template *  t = nullptr,
const chat::json &  tools = chat::json() 
)
inline

Definition at line 210 of file chat_request.h.

211 {
212 if (detail::tool_turns(t)) return chat_conversation_tools(r, tools);
214 if (!r.contains("user_input") || !r["user_input"].is_string())
215 throw RequestError("Missing 'user_input' field in request JSON");
216 c.user = r["user_input"].get<std::string>();
217 if (r.contains("system")) {
218 if (!r["system"].is_string()) throw RequestError("system must be a string");
219 c.system = r["system"].get<std::string>();
220 c.has_system = true;
221 }
222 if (r.contains("history") && r["history"].is_array()) {
223 const auto& hist = r["history"];
224 std::string pending_user;
225 bool have_user = false;
226 for (size_t i = 0; i < hist.size(); ++i) {
227 const auto& h = hist[i];
228 if (!h.is_object()) throw RequestError("history[" + std::to_string(i) + "] must be an object");
229 if (h.contains("role")) {
230 if (!h["role"].is_string()) throw RequestError("history[" + std::to_string(i) + "].role must be a string");
231 const std::string role = h["role"].get<std::string>();
232 detail::refuse_tool_members_v1(h, "history[" + std::to_string(i) + "]");
233 const std::string content = detail::history_content(h, i);
234 if (role == "system") {
235 if (i != 0) throw RequestError("a system message is allowed only as the first history entry");
236 if (c.has_system)
237 throw RequestError("give the system message once: the 'system' field or a leading history entry");
238 c.system = content;
239 c.has_system = true;
240 } else if (role == "user") {
241 if (have_user)
242 throw RequestError("history has two user messages in a row: user and assistant messages "
243 "must alternate (section 3 point 3a)");
244 pending_user = content;
245 have_user = true;
246 } else if (role == "assistant") {
247 if (!have_user)
248 throw RequestError("history has an assistant message without a user message before it: user "
249 "and assistant messages must alternate, starting with user (section 3 "
250 "point 3a)");
251 c.turns.emplace_back(pending_user, content);
252 have_user = false;
253 } else {
254 throw RequestError("history role '" + role +
255 "' is not allowed here (user, assistant; system only first)");
256 }
257 } else {
258 if (have_user)
259 throw RequestError("history has a user message without its assistant reply before a {user, "
260 "assistant} pair");
261 c.turns.push_back(detail::history_pair(h, i));
262 }
263 }
264 if (have_user)
265 throw RequestError("history ends with a user message: it must end with an assistant reply (the new "
266 "message goes in user_input)");
267 }
268 c.has_system = c.has_system && !c.system.empty();
269 if (r.contains("thinking")) c.thinking = detail::thinking_field(r["thinking"], "thinking");
270 detail::refuse_tools_v1(r);
271 return c;
272}
Conversation chat_conversation_tools(const nlohmann::json &r, const chat::json &tools)

References c, chat_conversation_tools(), ember::detail::history_content(), ember::detail::history_pair(), j, ember::detail::refuse_tool_members_v1(), ember::detail::refuse_tools_v1(), ember::detail::thinking_field(), and ember::detail::tool_turns().

Referenced by main().

◆ chat_conversation_tools()

Conversation ember::chat_conversation_tools ( const nlohmann::json &  r,
const chat::json &  tools 
)
inline

Definition at line 146 of file chat_request.h.

146 {
148 c.general = true;
149 std::string system;
150 bool has_system = false;
151 if (r.contains("system")) {
152 if (!r["system"].is_string()) throw RequestError("system must be a string");
153 system = r["system"].get<std::string>();
154 has_system = true;
155 }
156 if (r.contains("history") && r["history"].is_array()) {
157 const auto& hist = r["history"];
158 for (size_t i = 0; i < hist.size(); ++i) {
159 const auto& h = hist[i];
160 if (!h.is_object()) throw RequestError("history[" + std::to_string(i) + "] must be an object");
161 if (h.contains("role")) {
162 if (!h["role"].is_string()) throw RequestError("history[" + std::to_string(i) + "].role must be a string");
163 const std::string role = h["role"].get<std::string>();
164 const std::string content = detail::history_content(h, i, /*calls_may_omit=*/true);
165 if (role == "system") {
166 if (h.contains("tool_calls") || h.contains("tool_call_id"))
167 throw RequestError("history[" + std::to_string(i) +
168 "]: tool_calls belong on assistant messages, tool_call_id on tool messages");
169 if (i != 0) throw RequestError("a system message is allowed only as the first history entry");
170 if (has_system)
171 throw RequestError("give the system message once: the 'system' field or a leading history entry");
172 system = content;
173 has_system = true;
174 } else if (role == "user" || role == "assistant" || role == "tool") {
175 c.messages.push_back(detail::tool_turn_message(role, content, h));
176 } else {
177 throw RequestError("history role '" + role +
178 "' is not allowed here (user, assistant, tool; system only first)");
179 }
180 } else {
181 const auto pr = detail::history_pair(h, i);
182 c.messages.push_back({"user", pr.first});
183 c.messages.push_back({"assistant", pr.second});
184 }
185 }
186 }
187 if (r.contains("user_input")) {
188 if (!r["user_input"].is_string()) throw RequestError("user_input must be a string");
189 c.messages.push_back({"user", r["user_input"].get<std::string>()});
190 } else if (c.messages.empty() || c.messages.back().role != "tool") {
191 throw RequestError("Missing 'user_input' field in request JSON (it may be left out only when the history "
192 "ends on a tool message)");
193 }
194 detail::push_system(c.messages, system);
195 c.tools = detail::tool_definitions(r, tools);
196 if (r.contains("thinking")) c.thinking = detail::thinking_field(r["thinking"], "thinking");
197 return c;
198}

References c, ember::Conversation::general, ember::detail::history_content(), ember::detail::history_pair(), j, ember::detail::push_system(), ember::detail::thinking_field(), ember::detail::tool_definitions(), and ember::detail::tool_turn_message().

Referenced by chat_conversation().

◆ chat_vocab_from_gguf()

ChatVocab ember::chat_vocab_from_gguf ( const GGUFData &  gd,
const std::string &  path 
)
inline

Definition at line 13 of file chat_gguf.h.

13 {
15 auto str = [&](const char* key) {
16 auto it = gd.metadata.find(key);
17 const std::string* s = it == gd.metadata.end() ? nullptr : std::get_if<std::string>(&it->second);
18 return s ? *s : std::string();
19 };
20 v.arch = str("general.architecture");
21 v.chat_template = str("tokenizer.chat_template");
22 v.declared = str("paperkiln.chat_template");
23 auto asp = gd.metadata.find("tokenizer.ggml.add_space_prefix");
24 if (asp != gd.metadata.end())
25 if (const bool* b = std::get_if<bool>(&asp->second)) v.add_space_prefix = *b;
26 // Qwen and SentencePiece vocabularies come from the metadata already parsed:
27 // read_gguf_tokenizer reads the whole file into memory (1.3 GB for Qwen2.5-0.5B f16, 1.1 GB for
28 // Gemma-3-270m f32). paperkiln files keep that reader.
29 const std::string model = str("tokenizer.ggml.model");
30 if ((str("tokenizer.ggml.pre") == "qwen2" || model == "llama") && !gd.tokenizer_tokens.empty()) {
31 v.g.model = model;
32 v.g.pre = str("tokenizer.ggml.pre");
33 v.g.tokens = gd.tokenizer_tokens;
34 v.g.merges = gd.tokenizer_merges;
35 v.g.token_type.assign(gd.tokenizer_token_types.begin(), gd.tokenizer_token_types.end());
36 v.scores = gd.tokenizer_scores;
37 } else {
39 }
40 return v;
41}
std::vector< float > v
std::string str
Definition ir_digest.cpp:66
GgufTokenizer read_gguf_tokenizer(const std::string &path)

References j, microtorch::bpe::read_gguf_tokenizer(), str, and v.

Referenced by tinyllama::TinyLlamaSession::TinyLlamaSession().

◆ control_ids_check()

void ember::control_ids_check ( const chat::Template &  t,
const ChatTokenizer &  tok,
const std::vector< std::pair< std::string, int > > &  want 
)
inline

Definition at line 269 of file chat_tokenizer.h.

270 {
271 for (const auto& [text, id] : want) {
272 const std::vector<int> got = tok.encode(text);
273 if (got.size() != 1 || got[0] != id)
274 throw std::runtime_error("chat template " + t.id() + ": the " + tok.kind() + " tokenizer does not encode '" +
275 text + "' as its control token " + std::to_string(id) + ": the file is refused");
276 }
277}
Tok tok

References id, j, text, and tok.

Referenced by make_chat_setup().

◆ deterministic()

bool ember::deterministic ( )
inline

Definition at line 22 of file determinism.h.

22 {
23 static const bool on = [] {
24 const char* v = std::getenv("EMBER_DETERMINISTIC");
25 return v != nullptr && *v != '\0' && std::strcmp(v, "0") != 0;
26 }();
27 return on;
28}

References j, and v.

Referenced by attention_cuda_optimized(), attention_devpos_cuda(), deterministic_cublas_env(), TinyLlamaModel::initialize_gpu_and_rope(), main(), and rmsnorm_vector_cuda_optimized().

◆ deterministic_cublas_env()

void ember::deterministic_cublas_env ( )
inline

Definition at line 32 of file determinism.h.

32 {
33 if (!deterministic() || std::getenv("CUBLAS_WORKSPACE_CONFIG") != nullptr) return;
34#ifdef _WIN32
35 _putenv_s("CUBLAS_WORKSPACE_CONFIG", ":4096:8");
36#else
37 setenv("CUBLAS_WORKSPACE_CONFIG", ":4096:8", 0);
38#endif
39}
bool deterministic()
Definition determinism.h:22

References deterministic(), and j.

Referenced by TinyLlamaModel::initialize_gpu_and_rope().

◆ dump_valid()

template<class Json >
std::string ember::dump_valid ( const Json &  j)

Definition at line 153 of file utf8_text.h.

153 {
154 try {
155 return j.dump();
156 } catch (const typename Json::type_error&) {
157 return utf8_valid_json(j).dump();
158 }
159}

References j, and utf8_valid_json().

Referenced by json_out().

◆ has_chat_specials()

bool ember::has_chat_specials ( const std::vector< std::string > &  tokens)
inline

Definition at line 239 of file chat_tokenizer.h.

239 {
240 if (tokens.size() < chat::kV1Specials.size()) return false;
241 for (size_t i = 0; i < chat::kV1Specials.size(); ++i)
242 if (tokens[i] != chat::kV1Specials[i]) return false;
243 return true;
244}
const std::array< const char *, 4 > kV1Specials

References j, and microtorch::chat::kV1Specials.

◆ insert_state_blocks()

std::vector< int > ember::insert_state_blocks ( const std::vector< int > &  ids,
int  vocab,
int  k,
int  eot,
int  user,
int  assistant 
)
inline

Definition at line 24 of file state_tokens.h.

25 {
26 if (k <= 0) {
27 for (size_t i = 0; i < ids.size(); ++i)
28 if (ids[i] >= vocab)
29 throw std::invalid_argument("token id " + std::to_string(ids[i]) + " at position " + std::to_string(i) +
30 " is past the vocabulary (" + std::to_string(vocab) + ") and the model has no state tokens");
31 return ids;
32 }
33 std::vector<int> out;
34 out.reserve(ids.size() + 4 * static_cast<size_t>(k));
35 auto block = [&] {
36 for (int j = 0; j < k; ++j) out.push_back(vocab + j);
37 };
38 // paperkiln's serialiser (tools/state_tokens.py, nn::insert_state_ids): an exchange opens at
39 // <|user|>, has a reply once <|assistant|> follows it, and ends at the next <|endoftext|>.
40 enum { kNone, kUser, kReply } phase = kNone;
41 const size_t n = ids.size();
42 for (size_t i = 0; i < n; ++i) {
43 const int id = ids[i];
44 if (id < 0) throw std::invalid_argument("token id " + std::to_string(id) + " at position " + std::to_string(i) + " is negative");
45 if (id >= vocab)
46 throw std::invalid_argument("state id " + std::to_string(id) + " at position " + std::to_string(i) +
47 " is not part of a complete state block directly after an <|endoftext|> that ends "
48 "an assistant reply");
49 if (id == user) {
50 if (phase == kReply) { // the reply stopped early: close it as training does
51 out.push_back(eot);
52 block();
53 }
54 phase = kUser;
55 out.push_back(id);
56 } else if (id == assistant) {
57 if (phase == kUser) phase = kReply;
58 out.push_back(id);
59 } else if (id == eot) {
60 const bool closes = phase == kReply;
61 phase = kNone;
62 out.push_back(id);
63 if (!closes) continue;
64 if (i + 1 < n && ids[i + 1] >= vocab) { // a state block given with the ids: it must be complete
65 for (int j = 0; j < k; ++j)
66 if (i + 1 + j >= n || ids[i + 1 + j] != vocab + j)
67 throw std::invalid_argument("the state block after the <|endoftext|> at position " + std::to_string(i) +
68 " is not the " + std::to_string(k) + " ids " + std::to_string(vocab) + " .. " +
69 std::to_string(vocab + k - 1) + " in order");
70 i += static_cast<size_t>(k);
71 }
72 block();
73 } else {
74 out.push_back(id);
75 }
76 }
77 return out;
78}
int n

References j, k, n, and out.

Referenced by tinyllama::TinyLlamaSession::bpe_prompt_ids().

◆ make_chat_setup()

ChatSetup ember::make_chat_setup ( ChatVocab  v)
inline

Definition at line 279 of file chat_tokenizer.h.

279 {
280 ChatSetup s;
282 try {
283 r = chat::resolve(v.declared, v.g.tokens, v.g.model, v.chat_template, v.arch);
284 } catch (const std::exception& e) {
285 s.why = std::string("paperkiln.chat_template refused (") + e.what() + "): raw completion only";
286 return s;
287 }
288 s.why = r.why;
289 if (!r.chat) return s;
290 s.resolved = true;
291 s.tmpl = r.tmpl;
292 auto& g = v.g;
293 std::vector<std::pair<std::string, int>> control; // tool-turn records: each reserved string and its id
294 if (r.tmpl.has_tool) {
295 try {
296 chat::check_control_tokens(r.tmpl, g.tokens, g.token_type);
297 } catch (const chat::Error& e) {
298 throw std::runtime_error(std::string(e.what()) + ": the file is refused");
299 }
300 for (const auto& x : r.tmpl.reserved) control.emplace_back(x, token_id(g.tokens, x));
301 }
302 const bool bpe = g.model == "bpe";
303 const bool l3 = g.model == "gpt2" && (g.pre == "llama-bpe" || g.pre == "qwen2");
304 const bool spm = g.model == "llama";
305 if (!bpe && !l3 && !spm) {
306 if (g.model != "word") s.why += "; ember has no chat tokenizer for tokenizer.ggml.model '" + g.model + "'";
307 return s;
308 }
309 ChatFormat f;
310 try {
311 f = ChatFormat::of(r.tmpl, g.tokens, r.why);
312 } catch (const chat::Error& e) {
313 s.resolved = false;
314 s.why = r.why + "; " + e.what() + ": raw completion only";
315 return s;
316 }
317 if (bpe) {
318 if (g.merges.empty()) throw std::runtime_error("BPE GGUF has no tokenizer.ggml.merges");
319 s.tok = std::make_unique<ChatTokenizerOf<microtorch::bpe::Tokenizer>>(
320 microtorch::bpe::Tokenizer(std::move(g.tokens), g.merges), "bpe", std::move(f));
321 } else if (l3) {
322 if (g.merges.empty()) throw std::runtime_error(g.pre + " GGUF has no tokenizer.ggml.merges");
323 if (g.pre == "qwen2")
324 s.tok = std::make_unique<ChatTokenizerOf<QwenNfcTokenizer>>(
325 QwenNfcTokenizer(std::move(g.tokens), g.merges, g.token_type), "qwen2_bpe", std::move(f));
326 else
327 s.tok = std::make_unique<ChatTokenizerOf<microtorch::llama3::Tokenizer>>(
328 microtorch::llama3::Tokenizer(std::move(g.tokens), g.merges, g.token_type, 3), "llama3_bpe",
329 std::move(f));
330 } else {
331 s.tok = std::make_unique<ChatTokenizerOf<SpmTokenizer>>(
332 SpmTokenizer(std::move(g.tokens), std::move(v.scores), g.token_type, v.add_space_prefix), "spm",
333 std::move(f));
334 }
335 if (!control.empty()) control_ids_check(s.tmpl, *s.tok, control);
336 return s;
337}
int token_id(const std::vector< std::string > &tokens, const std::string &text)
void control_ids_check(const chat::Template &t, const ChatTokenizer &tok, const std::vector< std::pair< std::string, int > > &want)
void check_control_tokens(const Template &t, const std::vector< std::string > &tokens, const std::vector< int32_t > &types)
Resolution resolve(const std::string &declared, const std::vector< std::string > &tokens, const std::string &model, const std::string &chat_template, const std::string &arch)

References microtorch::chat::check_control_tokens(), control_ids_check(), j, ember::ChatFormat::of(), microtorch::chat::resolve(), token_id(), and v.

Referenced by tinyllama::TinyLlamaSession::TinyLlamaSession().

◆ openai_content()

std::string ember::openai_content ( const nlohmann::json &  m,
size_t  i 
)
inline

Definition at line 275 of file chat_request.h.

275 {
276 const std::string at = "messages[" + std::to_string(i) + "]";
277 if (!m.contains("content") || m["content"].is_null()) throw RequestError(at + " has no content");
278 const auto& c = m["content"];
279 if (c.is_string()) return c.get<std::string>();
280 if (!c.is_array()) throw RequestError(at + ".content must be a string or a list of text parts");
281 std::string s;
282 for (const auto& part : c) {
283 if (!part.is_object() || part.value("type", "") != "text" || !part.contains("text") || !part["text"].is_string())
284 throw RequestError(at + ".content: only text parts are supported (this model takes text only)");
285 s += part["text"].get<std::string>();
286 }
287 return s;
288}

References c, and j.

Referenced by openai_conversation(), and openai_conversation_tools().

◆ openai_conversation()

Conversation ember::openai_conversation ( const nlohmann::json &  body,
const chat::Template *  t = nullptr,
const chat::json &  tools = chat::json() 
)
inline

Definition at line 353 of file chat_request.h.

354 {
355 if (!body.is_object() || !body.contains("messages") || !body["messages"].is_array() || body["messages"].empty())
356 throw RequestError("body must be JSON with a non-empty 'messages' array");
357 if (detail::tool_turns(t)) return openai_conversation_tools(body, tools);
358 const auto& msgs = body["messages"];
360 std::vector<chat::Message> m;
361 for (size_t i = 0; i < msgs.size(); ++i) {
362 const auto& x = msgs[i];
363 if (!x.is_object() || !x.contains("role") || !x["role"].is_string())
364 throw RequestError("messages[" + std::to_string(i) + "] needs a string role");
365 const std::string role = x["role"].get<std::string>();
366 if (role == "tool")
367 throw RequestError("tool messages are not supported: this model's chat template has no tool role");
368 detail::refuse_tool_members_v1(x, "messages[" + std::to_string(i) + "]");
369 if (role == "system" || role == "developer") {
370 if (!m.empty())
371 throw RequestError("a " + role +
372 " message is allowed only before the first user message (this model's chat "
373 "template has one system message, first)");
374 const std::string s = openai_content(x, i);
375 if (s.empty()) continue;
376 c.system += (c.has_system ? "\n\n" : "") + s;
377 c.has_system = true;
378 continue;
379 }
380 if (role != "user" && role != "assistant")
381 throw RequestError("messages[" + std::to_string(i) + "] has unknown role '" + role + "'");
382 m.push_back({role, openai_content(x, i)});
383 }
384 if (m.empty() || m.back().role != "user") throw RequestError("the last message must be from the user");
385 for (size_t i = 0; i < m.size(); ++i)
386 if (m[i].role != (i % 2 == 0 ? "user" : "assistant"))
387 throw RequestError("messages must alternate user and assistant, starting from the user (message " +
388 std::to_string(i) + " is " + m[i].role + ")");
389 for (size_t i = 0; i + 1 < m.size(); i += 2) c.turns.emplace_back(m[i].content, m[i + 1].content);
390 c.user = m.back().content;
391 detail::openai_thinking(body, c);
392 detail::refuse_tools_v1(body);
393 return c;
394}
std::string openai_content(const nlohmann::json &m, size_t i)
Conversation openai_conversation_tools(const nlohmann::json &body, const chat::json &tools)

References body, c, j, openai_content(), openai_conversation_tools(), ember::detail::openai_thinking(), ember::detail::refuse_tool_members_v1(), ember::detail::refuse_tools_v1(), and ember::detail::tool_turns().

Referenced by main().

◆ openai_conversation_tools()

Conversation ember::openai_conversation_tools ( const nlohmann::json &  body,
const chat::json &  tools 
)
inline

Definition at line 305 of file chat_request.h.

305 {
306 const auto& msgs = body["messages"];
308 c.general = true;
309 std::string system;
310 for (size_t i = 0; i < msgs.size(); ++i) {
311 const auto& x = msgs[i];
312 if (!x.is_object() || !x.contains("role") || !x["role"].is_string())
313 throw RequestError("messages[" + std::to_string(i) + "] needs a string role");
314 const std::string role = x["role"].get<std::string>();
315 if (role == "system" || role == "developer") {
316 if (!c.messages.empty())
317 throw RequestError("a " + role +
318 " message is allowed only before the first user message (this model's chat "
319 "template has one system message, first)");
320 if (x.contains("tool_calls") || x.contains("tool_call_id"))
321 throw RequestError("messages[" + std::to_string(i) +
322 "]: tool_calls belong on assistant messages, tool_call_id on tool messages");
323 const std::string s = openai_content(x, i);
324 if (s.empty()) continue;
325 system += (system.empty() ? "" : "\n\n") + s;
326 continue;
327 }
328 if (role != "user" && role != "assistant" && role != "tool")
329 throw RequestError("messages[" + std::to_string(i) + "] has unknown role '" + role + "'");
330 const bool calls = x.contains("tool_calls") && !x["tool_calls"].is_null();
331 const bool no_content = !x.contains("content") || x["content"].is_null();
332 const std::string content = role == "assistant" && calls && no_content ? std::string() : openai_content(x, i);
333 c.messages.push_back(detail::tool_turn_message(role, content, x));
334 }
335 if (c.messages.empty() || (c.messages.back().role != "user" && c.messages.back().role != "tool"))
336 throw RequestError("the last message must be from the user or a tool");
337 detail::push_system(c.messages, system);
338 c.tools = detail::tool_definitions(body, tools);
339 if (body.contains("tool_choice") && !body["tool_choice"].is_null() &&
340 !(body["tool_choice"].is_string() && body["tool_choice"].get<std::string>() == "auto"))
341 throw RequestError("tool_choice is not supported except \"auto\" (the model decides whether to call a tool)");
342 detail::openai_thinking(body, c);
343 return c;
344}

References body, c, ember::Conversation::general, j, openai_content(), ember::detail::openai_thinking(), ember::detail::push_system(), ember::detail::tool_definitions(), and ember::detail::tool_turn_message().

Referenced by openai_conversation().

◆ ordered_tools()

chat::json ember::ordered_tools ( const std::string &  body)
inline

Definition at line 135 of file chat_request.h.

135 {
136 chat::json o = chat::json::parse(body, nullptr, false);
137 if (o.is_discarded() || !o.is_object() || !o.contains("tools")) return chat::json();
138 return o["tools"];
139}
nlohmann::ordered_json json

References body, and j.

Referenced by main().

◆ parallel_for()

template<class F >
void ember::parallel_for ( int64_t  n,
F &&  fn 
)

Definition at line 31 of file parallel_for.h.

31 {
32#pragma omp parallel for schedule(static)
33 for (int64_t i = 0; i < n; ++i) fn(i);
34}

References j, and n.

Referenced by attention_batch_cpu(), attention_bias_batch_cpu(), attention_relkv_batch_cpu(), attention_rows_batch_cpu(), attention_window_batch_cpu(), matmul_f32_f32_batch_cpu(), matmul_rows_f32_batch_cpu(), and matvec_f32_f32_vector_cpu().

◆ parse_score_body()

std::string ember::parse_score_body ( const nlohmann::json &  body,
const ScoreModel &  m,
std::vector< ScoreItem > &  out 
)
inline

Definition at line 92 of file score.h.

92 {
93 if (!body.is_object() || !body.contains("requests") || !body["requests"].is_array())
94 return "body must be {\"requests\": [{\"key\", \"prompt_ids\", \"continuation_ids\"}, ...]}";
95 const nlohmann::json& reqs = body["requests"];
96 out.clear();
97 out.reserve(reqs.size());
98 for (size_t n = 0; n < reqs.size(); ++n) {
99 const nlohmann::json& r = reqs[n];
100 const std::string at = "request " + std::to_string(n);
101 if (!r.is_object()) return at + " is not an object";
102 if (!r.contains("prompt_ids") || !r.contains("continuation_ids"))
103 return at + ": prompt_ids and continuation_ids are required (ember scores token ids; mtstudio's text fields "
104 "are not served)";
106 try {
107 it.prompt = r["prompt_ids"].get<std::vector<int>>();
108 it.cont = r["continuation_ids"].get<std::vector<int>>();
109 } catch (const nlohmann::json::exception& e) {
110 return at + ": prompt_ids and continuation_ids must be lists of integers (" + e.what() + ")";
111 }
112 if (it.prompt.empty() || it.cont.empty()) return at + " has an empty prompt or continuation";
113 for (int t : it.prompt)
114 if (t < 0 || static_cast<size_t>(t) >= m.vocab + m.state_k) return at + ": prompt id out of range";
115 for (int t : it.cont)
116 if (t < 0 || static_cast<size_t>(t) >= m.vocab) return at + ": continuation id out of range";
117 if (m.state_k > 0) {
118 std::vector<int> ids = it.prompt;
119 ids.insert(ids.end(), it.cont.begin(), it.cont.end());
120 const std::string err = score_state_ids_error(ids, m);
121 if (!err.empty()) return at + ": " + err;
122 }
123 it.rec = {{"i", n}, {"prompt_tokens", it.prompt.size()}, {"continuation_ids", it.cont}};
124 if (r.contains("key")) it.rec["key"] = r["key"];
125 it.over_ctx = it.prompt.size() + it.cont.size() > m.ctx + 1;
126 out.push_back(std::move(it));
127 }
128 return "";
129}
std::string score_state_ids_error(const std::vector< int > &ids, const ScoreModel &m)
Definition score.h:58
std::vector< int > prompt
Definition score.h:52

References body, j, n, out, ember::ScoreItem::prompt, and score_state_ids_error().

Referenced by main().

◆ reply_of()

ChatReply ember::reply_of ( const std::vector< int > &  ids,
int  call_id,
const ChatDecode &  decode 
)
inline

Definition at line 45 of file chat_reply.h.

45 {
47 if (call_id >= 0) {
49 if (p.parsed && !p.tool_calls.empty()) {
50 r.content = utf8_valid(strip_reply_ws(p.content));
51 for (auto& c : p.tool_calls) {
52 c.name = utf8_valid(c.name);
53 c.arguments = utf8_valid(c.arguments);
54 r.tool_calls.push_back(std::move(c));
55 }
56 return r;
57 }
58 }
59 r.content = utf8_valid(strip_reply_ws(decode(ids)));
60 return r;
61}
std::string strip_reply_ws(const std::string &s)
Definition chat_reply.h:33
std::string utf8_valid(const std::string &s)
Definition utf8_text.h:74
ParsedReply parse_reply(const std::vector< int > &ids, int call_id, Decode decode)

References c, j, p, microtorch::chat::parse_reply(), strip_reply_ws(), and utf8_valid().

Referenced by bpe_run(), and ember::ReplyBuilder::finish().

◆ score_banned()

std::vector< char > ember::score_banned ( const std::vector< std::string > &  tokens,
size_t  vocab 
)
inline

Definition at line 133 of file score.h.

133 {
134 std::vector<char> banned(vocab, 0);
135 const size_t n = tokens.size() < vocab ? tokens.size() : vocab;
136 for (const char* sp : {"<unk>", "<s>", "</s>", "<pad>"})
137 for (size_t k = 0; k < n; ++k)
138 if (tokens[k] == sp) {
139 banned[k] = 1;
140 break;
141 }
142 return banned;
143}

References j, k, and n.

Referenced by main().

◆ score_fill()

void ember::score_fill ( nlohmann::json &  rec,
const std::vector< int > &  c,
const float *  rows,
size_t  V,
const std::vector< char > &  banned 
)
inline

Definition at line 147 of file score.h.

148 {
149 std::vector<double> lp;
150 std::vector<int> am;
151 lp.reserve(c.size());
152 am.reserve(c.size());
153 double sum = 0.0;
154 for (size_t j = 0; j < c.size(); ++j) {
155 const float* row = rows + j * V;
156 double mx = -1e300;
157 int best = -1;
158 double best_v = -1e300;
159 for (size_t k = 0; k < V; ++k) {
160 const double x = row[k];
161 mx = std::max(mx, x);
162 if (!banned[k] && (best < 0 || x > best_v)) {
163 best = static_cast<int>(k);
164 best_v = x;
165 }
166 }
167 double z = 0.0;
169 const double l = static_cast<double>(row[static_cast<size_t>(c[j])]) - mx - std::log(z);
170 lp.push_back(l);
171 am.push_back(best);
172 sum += l;
173 }
174 rec["over_ctx"] = false;
175 rec["logprobs"] = lp;
176 rec["argmax"] = am;
177 rec["sum"] = sum;
178}

References c, j, and k.

Referenced by main().

◆ score_state_ids_error()

std::string ember::score_state_ids_error ( const std::vector< int > &  ids,
const ScoreModel &  m 
)
inline

Definition at line 58 of file score.h.

58 {
59 enum { NONE, USER, REPLY } phase = NONE;
60 bool closed = false;
61 for (size_t p = 0; p < ids.size(); ++p) {
62 const int id = ids[p];
63 if (id < 0 || static_cast<size_t>(id) >= m.vocab + m.state_k)
64 return "id " + std::to_string(id) + " at position " + std::to_string(p) + " is out of range";
65 if (static_cast<size_t>(id) >= m.vocab) {
66 if (!closed || static_cast<size_t>(id) != m.vocab)
67 return "state id " + std::to_string(id) + " at position " + std::to_string(p) +
68 " does not start a block after an <|endoftext|> that ends a reply";
69 for (size_t j = 0; j < m.state_k; ++j)
70 if (p + j >= ids.size() || static_cast<size_t>(ids[p + j]) != m.vocab + j)
71 return "the state block at position " + std::to_string(p) + " is not the ids " + std::to_string(m.vocab) +
72 " .. " + std::to_string(m.vocab + m.state_k - 1) + " in order";
73 p += m.state_k - 1;
74 closed = false;
75 continue;
76 }
77 closed = false;
78 if (id == m.user) {
79 phase = USER;
80 } else if (id == m.assistant) {
81 if (phase == USER) phase = REPLY;
82 } else if (id == m.eot) {
83 closed = phase == REPLY;
84 phase = NONE;
85 }
86 }
87 return "";
88}

References j, and p.

Referenced by parse_score_body().

◆ strip_reply_ws()

std::string ember::strip_reply_ws ( const std::string &  s)
inline

Definition at line 33 of file chat_reply.h.

33 {
34 const char* ws = " \t\n\v\f\r";
35 const auto a = s.find_first_not_of(ws), b = s.find_last_not_of(ws);
36 return a == std::string::npos ? std::string() : s.substr(a, b - a + 1);
37}
std::vector< E > a

References a, and j.

Referenced by reply_of().

◆ token_id()

int ember::token_id ( const std::vector< std::string > &  tokens,
const std::string &  text 
)
inline

Definition at line 41 of file chat_tokenizer.h.

41 {
42 for (size_t i = tokens.size(); i-- > 0;)
43 if (tokens[i] == text) return static_cast<int>(i);
44 return -1;
45}

References j, and text.

Referenced by make_chat_setup(), and ember::ChatFormat::of().

◆ tool_calls_json()

template<class Json >
Json ember::tool_calls_json ( const std::vector< chat::ToolCall > &  calls)

Definition at line 66 of file chat_reply.h.

66 {
67 Json a = Json::array();
68 for (const auto& c : calls)
69 a.push_back(Json{{"id", c.id}, {"type", "function"}, {"function", Json{{"name", c.name}, {"arguments", c.arguments}}}});
70 return a;
71}

References a, c, and j.

◆ utf8_part()

size_t ember::utf8_part ( const std::string &  s,
size_t  i,
Utf8Part &  part 
)
inline

Definition at line 29 of file utf8_text.h.

29 {
30 const unsigned char c = static_cast<unsigned char>(s[i]);
31 part = Utf8Part::whole;
32 if (c < 0x80) return 1;
33 // continuation bytes needed after the lead, and the range of the first one (Table 3-7)
34 size_t need = 0;
35 unsigned char lo = 0x80, hi = 0xBF;
36 if (c >= 0xC2 && c <= 0xDF) {
37 need = 1;
38 } else if (c == 0xE0) {
39 need = 2;
40 lo = 0xA0;
41 } else if ((c >= 0xE1 && c <= 0xEC) || c == 0xEE || c == 0xEF) {
42 need = 2;
43 } else if (c == 0xED) {
44 need = 2;
45 hi = 0x9F;
46 } else if (c == 0xF0) {
47 need = 3;
48 lo = 0x90;
49 } else if (c >= 0xF1 && c <= 0xF3) {
50 need = 3;
51 } else if (c == 0xF4) {
52 need = 3;
53 hi = 0x8F;
54 } else { // a continuation byte without a lead, C0, C1, F5..FF
55 part = Utf8Part::ill_formed;
56 return 1;
57 }
58 const size_t n = s.size();
59 size_t j = 1;
60 for (; j <= need; ++j) {
61 if (i + j >= n) { // the lead and the continuations that fitted, then the end of s
62 part = Utf8Part::cut;
63 return j;
64 }
65 const unsigned char d = static_cast<unsigned char>(s[i + j]);
66 if (d < (j == 1 ? lo : 0x80) || d > (j == 1 ? hi : 0xBF)) {
67 part = Utf8Part::ill_formed; // the lead and the continuations that fitted: one maximal subpart
68 return j;
69 }
70 }
71 return j;
72}

References c, cut, ill_formed, j, n, and whole.

Referenced by utf8_settled(), and utf8_valid().

◆ utf8_settled()

size_t ember::utf8_settled ( const std::string &  s,
size_t  from 
)
inline

Definition at line 95 of file utf8_text.h.

95 {
96 const size_t n = s.size();
97 size_t i = from;
98 while (i < n) {
100 const size_t len = utf8_part(s, i, part);
101 if (part == Utf8Part::cut) return i;
102 i += len;
103 }
104 return n;
105}
size_t utf8_part(const std::string &s, size_t i, Utf8Part &part)
Definition utf8_text.h:29

References cut, j, n, and utf8_part().

Referenced by ember::ReplyStream::next().

◆ utf8_valid()

std::string ember::utf8_valid ( const std::string &  s)
inline

Definition at line 74 of file utf8_text.h.

74 {
75 const char rep[] = {static_cast<char>(0xEF), static_cast<char>(0xBF), static_cast<char>(0xBD)};
76 std::string out;
77 out.reserve(s.size());
78 const size_t n = s.size();
79 size_t i = 0;
80 while (i < n) {
81 Utf8Part part;
82 const size_t len = utf8_part(s, i, part);
83 if (part == Utf8Part::whole)
84 out.append(s, i, len);
85 else
86 out.append(rep, 3); // ill-formed, or cut by the end of s: one U+FFFD per maximal subpart
87 i += len;
88 }
89 return out;
90}

References j, n, out, utf8_part(), and whole.

Referenced by bpe_run(), tinyllama::TinyLlamaSession::generate(), main(), ember::ReplyStream::next(), PYBIND11_MODULE(), reply_of(), and utf8_valid_json().

◆ utf8_valid_json()

template<class Json >
Json ember::utf8_valid_json ( const Json &  j)

Definition at line 135 of file utf8_text.h.

135 {
136 if (j.is_string()) return Json(utf8_valid(j.template get<std::string>()));
137 if (j.is_array()) {
138 Json a = Json::array();
139 for (const auto& x : j) a.push_back(utf8_valid_json(x));
140 return a;
141 }
142 if (j.is_object()) {
143 Json o = Json::object();
144 for (auto it = j.begin(); it != j.end(); ++it) o[utf8_valid(it.key())] = utf8_valid_json(it.value());
145 return o;
146 }
147 return j;
148}
Json utf8_valid_json(const Json &j)
Definition utf8_text.h:135

References a, j, utf8_valid(), and utf8_valid_json().

Referenced by dump_valid(), and utf8_valid_json().