ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
state_tokens.h
Go to the documentation of this file.
1#pragma once
2// Per-turn state tokens: the template contract (paperkiln
3// experiments/tiny_chat_long_2026-10/IR_SPEC_DRAFT.md sections 1.2 and 1.5;
4// LOCAL_LLM_STACK.md section 3 point 3 extended). With k = the GGUF's
5// paperkiln.state_tokens.k > 0, every exchange of the served template is
6// <|user|> U <|assistant|> A <|endoftext|> S_0 S_1 .. S_{k-1}
7// with S_j = vocab + j. The ids never come from the tokeniser; the engine
8// inserts them, as the training serialiser does:
9// - after every <|endoftext|> that closes an exchange (an exchange opens at
10// <|user|> and has a reply once an <|assistant|> follows it; an
11// <|assistant|> with no open <|user|> starts nothing), the k state ids;
12// - an assistant reply followed by a <|user|> with no <|endoftext|> between
13// them (a reply that stopped early: max_new_tokens, a stop string, an
14// abort) gets <|endoftext|> and then the k state ids before that <|user|>.
15// Raw ids >= vocab are accepted only as a complete, ordered state block
16// directly after an <|endoftext|> that closes an exchange (kept as given, not
17// doubled); anything else throws std::invalid_argument naming the position.
18#include <stdexcept>
19#include <string>
20#include <vector>
21
22namespace ember {
23
24inline std::vector<int> insert_state_blocks(const std::vector<int>& ids, int vocab, int k, int eot, int user,
25 int assistant) {
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}
79
80} // namespace ember
int n
node out
size_t j
std::vector< int > insert_state_blocks(const std::vector< int > &ids, int vocab, int k, int eot, int user, int assistant)