ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
score.h
Go to the documentation of this file.
1// POST /score (server.cpp): the log-probability of given continuations under the model, with the
2// semantics of paperkiln's `mtstudio score` (tools/mtstudio.cpp score_cmd, the forced-choice scorer of
3// its benchmarks; LOCAL_LLM_STACK.md section 3) on token ids.
4//
5// request: {"requests": [{"key": any, "prompt_ids": [int], "continuation_ids": [int]}, ...]}
6// response: {"results": [{"i", "key", "prompt_tokens", "continuation_ids", "over_ctx",
7// "logprobs", "argmax", "sum"}, ...], "stats": {...}}
8//
9// Each request is one fresh forward over prompt_ids + continuation_ids[:-1] from position 0 (no state
10// is carried between requests; TinyLlamaSession::score_rows). For every continuation token j, at
11// position P + j (P = the prompt's length), from the logits row before it: its log-probability under
12// the softmax at temperature 1, in double from the float logits (max, then the sum of exp(x - max),
13// then x_c - max - log(sum), in vocabulary order), and the argmax with the specials mtstudio bans
14// from generation (<unk>, <s>, </s>, <pad>, by name in the vocabulary) left out, the first maximum
15// winning; "sum" adds the log-probabilities in order. A request with P + C > ctx + 1 ids (ctx =
16// score_context(), mtstudio's T) is reported "over_ctx": true and not scored. Records carry
17// mtstudio's fields: "i" (the request's index), "prompt_tokens", "continuation_ids", "key" when the
18// request has one.
19//
20// Refusals are mtstudio's, whose score fails the whole run on the first bad line: here the whole
21// request is refused (HTTP 400) before anything runs, naming the first bad request. A request must
22// be an object; prompt_ids and continuation_ids are read as mtstudio reads them (nlohmann's
23// get<std::vector<int>>); an empty prompt or continuation, a prompt id outside [0, vocab + k) (k
24// the model's state tokens, 0 without), a continuation id outside [0, vocab), or (state-token
25// models) a prompt whose state ids are not complete blocks after an <|endoftext|> that ends a reply
26// (nn::state_ids_error, ported below) is refused. mtstudio's text fields ("prompt",
27// "continuation", "messages") are not served: ember scores ids only.
28#ifndef EMBER_SCORE_H
29#define EMBER_SCORE_H
30
31#include <algorithm>
32#include <cmath>
33#include <cstddef>
34#include <string>
35#include <vector>
36
37#include <nlohmann/json.hpp>
38
39namespace ember {
40
41// What validation needs to know about the model.
42struct ScoreModel {
43 size_t vocab = 0; // the logits' width: continuation ids lie in [0, vocab)
44 size_t state_k = 0; // per-turn state tokens: prompt ids lie in [0, vocab + state_k)
45 int eot = -1, user = -1, assistant = -1; // the state-token template's markers (state_k > 0)
46 size_t ctx = 0; // positions one forward holds
47};
48
49// One parsed request: mtstudio's record so far, and the ids.
50struct ScoreItem {
51 nlohmann::json rec;
52 std::vector<int> prompt, cont;
53 bool over_ctx = false;
54};
55
56// paperkiln's nn::state_ids_error (src/llama.cpp): "" when every id >= vocab sits in a complete,
57// ordered block vocab .. vocab + k - 1 directly after an <|endoftext|> that closes an exchange.
58inline std::string score_state_ids_error(const std::vector<int>& ids, const ScoreModel& m) {
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}
89
90// Parses and checks a /score body in mtstudio's order (per request: the ids, empty, prompt range,
91// continuation range, state blocks; then over_ctx). Returns "" or the refusal (HTTP 400).
92inline std::string parse_score_body(const nlohmann::json& body, const ScoreModel& m, std::vector<ScoreItem>& out) {
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}
130
131// banned[k]: vocabulary id k is one of the specials mtstudio leaves out of the argmax, found by name
132// among ids 0 .. vocab - 1 (the first id holding the name, as mtstudio's vocabulary map keeps).
133inline std::vector<char> score_banned(const std::vector<std::string>& tokens, size_t vocab) {
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}
144
145// mtstudio's arithmetic on the continuation's rows (rows[j * V ..] precedes continuation token j):
146// sets rec's over_ctx (false), logprobs, argmax and sum.
147inline void score_fill(nlohmann::json& rec, const std::vector<int>& c, const float* rows, size_t V,
148 const std::vector<char>& banned) {
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}
179
180} // namespace ember
181
182#endif // EMBER_SCORE_H
const float * p[kIrDepthSourcesMax]
int n
float c
node out
size_t j
std::string parse_score_body(const nlohmann::json &body, const ScoreModel &m, std::vector< ScoreItem > &out)
Definition score.h:92
void score_fill(nlohmann::json &rec, const std::vector< int > &c, const float *rows, size_t V, const std::vector< char > &banned)
Definition score.h:147
std::vector< char > score_banned(const std::vector< std::string > &tokens, size_t vocab)
Definition score.h:133
std::string score_state_ids_error(const std::vector< int > &ids, const ScoreModel &m)
Definition score.h:58
std::vector< int > cont
Definition score.h:52
bool over_ctx
Definition score.h:53
nlohmann::json rec
Definition score.h:51
std::vector< int > prompt
Definition score.h:52
size_t state_k
Definition score.h:44
size_t vocab
Definition score.h:43