ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
graph_ir_expr.cpp
Go to the documentation of this file.
1#include "graph_ir_expr.h"
2
3#include <algorithm>
4#include <cctype>
5#include <cmath>
6#include <cstdlib>
7#include <map>
8#include <mutex>
9#include <stdexcept>
10#include <utility>
11
12namespace ember_ir {
13namespace expr {
14
15namespace {
16
17// ---- the parser: vendored from paperkiln src/expr.cpp (1159d63e), messages unchanged ----
18
19struct Fn {
20 const char* name;
22 size_t arity;
23};
24constexpr Fn kFns[] = {
25 {"exp", Kind::EXP, 1}, {"log", Kind::LOG, 1}, {"sqrt", Kind::SQRT, 1},
26 {"tanh", Kind::TANH, 1}, {"sigmoid", Kind::SIGMOID, 1}, {"erf", Kind::ERF, 1},
27 {"abs", Kind::ABS, 1}, {"relu", Kind::RELU, 1}, {"max", Kind::MAX, 2},
28 {"min", Kind::MIN, 2}, {"where", Kind::WHERE, 3}, {"rowsum", Kind::ROWSUM, 1},
29 {"rowmean", Kind::ROWMEAN, 1}, {"rowmax", Kind::ROWMAX, 1},
30};
31constexpr float kPi = 3.14159265358979323846f; // the nearest float to pi
32
33enum class Tok { NUM, NAME, OP, LPAREN, RPAREN, COMMA, END };
34
35struct Token {
36 Tok tok;
37 std::string text;
38 float value = 0.0f;
39 size_t column = 0; // 1-based
40};
41
42class Parser {
43 public:
44 Parser(const std::string& text, size_t n_inputs) : s_(text), n_(n_inputs) { lex(); }
45
46 Node parse() {
47 if (toks_.front().tok == Tok::END) fail("empty expression", 1);
48 Node root = expr(0);
49 if (peek().tok != Tok::END) fail("expected an operator or the end, got '" + peek().text + "'", peek().column);
50 return root;
51 }
52
53 private:
54 [[noreturn]] void fail(const std::string& what, size_t column) const {
55 throw std::invalid_argument("expr: " + what + " at column " + std::to_string(column) + " in \"" + s_ + "\"");
56 }
57
58 void lex() {
59 size_t i = 0;
60 while (i < s_.size()) {
61 const char ch = s_[i];
62 const size_t col = i + 1;
63 if (ch == ' ' || ch == '\t') {
64 ++i;
65 } else if (std::isdigit(static_cast<unsigned char>(ch)) || ch == '.') {
66 // digits [. digits] | . digits, then an optional exponent.
67 size_t j = i, mant = 0;
68 while (j < s_.size() && std::isdigit(static_cast<unsigned char>(s_[j]))) ++j, ++mant;
69 if (j < s_.size() && s_[j] == '.') {
70 ++j;
71 while (j < s_.size() && std::isdigit(static_cast<unsigned char>(s_[j]))) ++j, ++mant;
72 }
73 if (mant == 0) fail("malformed number", col);
74 if (j < s_.size() && (s_[j] == 'e' || s_[j] == 'E')) {
75 size_t k = j + 1;
76 if (k < s_.size() && (s_[k] == '+' || s_[k] == '-')) ++k;
77 const size_t d0 = k;
78 while (k < s_.size() && std::isdigit(static_cast<unsigned char>(s_[k]))) ++k;
79 if (k == d0) fail("malformed number (exponent without digits)", col);
80 j = k;
81 }
82 const std::string lit = s_.substr(i, j - i);
83 const float v = std::strtof(lit.c_str(), nullptr);
84 if (!std::isfinite(v)) fail("number " + lit + " is out of float range", col);
85 toks_.push_back({Tok::NUM, lit, v, col});
86 i = j;
87 } else if (std::isalpha(static_cast<unsigned char>(ch)) || ch == '_') {
88 size_t j = i;
89 while (j < s_.size() && (std::isalnum(static_cast<unsigned char>(s_[j])) || s_[j] == '_')) ++j;
90 toks_.push_back({Tok::NAME, s_.substr(i, j - i), 0.0f, col});
91 i = j;
92 } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '^') {
93 toks_.push_back({Tok::OP, std::string(1, ch), 0.0f, col});
94 ++i;
95 } else if (ch == '(') {
96 toks_.push_back({Tok::LPAREN, "(", 0.0f, col});
97 ++i;
98 } else if (ch == ')') {
99 toks_.push_back({Tok::RPAREN, ")", 0.0f, col});
100 ++i;
101 } else if (ch == ',') {
102 toks_.push_back({Tok::COMMA, ",", 0.0f, col});
103 ++i;
104 } else {
105 fail(std::string("unexpected character '") + ch + "'", col);
106 }
107 }
108 toks_.push_back({Tok::END, "end", 0.0f, s_.size() + 1});
109 }
110
111 const Token& peek() const { return toks_[pos_]; }
112 const Token& next() { return toks_[pos_++]; }
113 bool at_op(char op) const { return peek().tok == Tok::OP && peek().text[0] == op; }
114
115 void enter(size_t depth) const {
116 if (depth > Expr::kMaxDepth) fail("nesting deeper than " + std::to_string(Expr::kMaxDepth), peek().column);
117 }
118
119 static Node binary(Kind k, Node a, Node b, size_t column) {
120 Node n;
121 n.kind = k;
122 n.column = column;
123 n.args.push_back(std::move(a));
124 n.args.push_back(std::move(b));
125 return n;
126 }
127
128 // expr := term (('+' | '-') term)*
129 Node expr(size_t depth) {
130 enter(depth);
131 Node left = term(depth + 1);
132 while (at_op('+') || at_op('-')) {
133 const Token op = next();
134 Node right = term(depth + 1);
135 left = binary(op.text[0] == '+' ? Kind::ADD : Kind::SUB, std::move(left), std::move(right), op.column);
136 }
137 return left;
138 }
139
140 // term := unary (('*' | '/') unary)*
141 Node term(size_t depth) {
142 enter(depth);
143 Node left = unary(depth + 1);
144 while (at_op('*') || at_op('/')) {
145 const Token op = next();
146 Node right = unary(depth + 1);
147 left = binary(op.text[0] == '*' ? Kind::MUL : Kind::DIV, std::move(left), std::move(right), op.column);
148 }
149 return left;
150 }
151
152 // unary := '-' unary | power
153 Node unary(size_t depth) {
154 enter(depth);
155 if (at_op('-')) {
156 const Token op = next();
157 Node n;
158 n.kind = Kind::NEG;
159 n.column = op.column;
160 n.args.push_back(unary(depth + 1));
161 return n;
162 }
163 return power(depth + 1);
164 }
165
166 // power := primary ('^' unary)? (right-associative through unary)
167 Node power(size_t depth) {
168 enter(depth);
169 Node base = primary(depth + 1);
170 if (at_op('^')) {
171 const Token op = next();
172 Node exponent = unary(depth + 1);
173 return binary(Kind::POW, std::move(base), std::move(exponent), op.column);
174 }
175 return base;
176 }
177
178 Node primary(size_t depth) {
179 enter(depth);
180 const Token t = next();
181 Node n;
182 n.column = t.column;
183 if (t.tok == Tok::NUM) {
184 n.kind = Kind::NUMBER;
185 n.value = t.value;
186 return n;
187 }
188 if (t.tok == Tok::LPAREN) {
189 Node inner = expr(depth + 1);
190 if (peek().tok != Tok::RPAREN) fail("expected ')', got '" + peek().text + "'", peek().column);
191 next();
192 return inner;
193 }
194 if (t.tok != Tok::NAME) {
195 fail(t.tok == Tok::END ? "expected an operand, got the end" : "expected an operand, got '" + t.text + "'",
196 t.column);
197 }
198 for (const Fn& f : kFns) {
199 if (t.text != f.name) continue;
200 if (peek().tok != Tok::LPAREN) fail("'" + t.text + "' is a function: expected '('", peek().column);
201 next();
202 n.kind = f.kind;
203 for (;;) {
204 n.args.push_back(expr(depth + 1));
205 if (peek().tok == Tok::COMMA) {
206 next();
207 continue;
208 }
209 if (peek().tok != Tok::RPAREN) fail("expected ',' or ')', got '" + peek().text + "'", peek().column);
210 next();
211 break;
212 }
213 if (n.args.size() != f.arity)
214 fail("'" + t.text + "' takes " + std::to_string(f.arity) + " argument" + (f.arity == 1 ? "" : "s") +
215 ", got " + std::to_string(n.args.size()),
216 t.column);
217 return n;
218 }
219 if (peek().tok == Tok::LPAREN) fail("unknown function '" + t.text + "'", t.column);
220 if (t.text == "pi") {
221 n.kind = Kind::NUMBER;
222 n.value = kPi;
223 return n;
224 }
225 // x is the first input, a the second, b the third, ...
226 size_t idx = n_;
227 if (t.text == "x") {
228 idx = 0;
229 } else if (t.text.size() == 1 && t.text[0] >= 'a' && t.text[0] < static_cast<char>('a' + n_ - 1)) {
230 idx = static_cast<size_t>(t.text[0] - 'a') + 1;
231 }
232 if (idx >= n_) {
233 std::string names = "x";
234 for (size_t k = 1; k < n_; ++k) names += std::string(", ") + static_cast<char>('a' + k - 1);
235 fail("unknown name '" + t.text + "' (inputs: " + names + "; constant: pi)", t.column);
236 }
237 n.kind = Kind::INPUT;
238 n.input = idx;
239 return n;
240 }
241
242 const std::string& s_;
243 size_t n_;
244 std::vector<Token> toks_;
245 size_t pos_ = 0;
246};
247
248// ---- the forward: ember's own, paperkiln's Builder step for step on plain buffers ----
249
250// A subexpression's value: a constant (folded in f32), a column [T, 1] (a
251// row reduction, broadcast only where it meets a full value) or a full
252// [T, w] buffer. The shapes are an implementation detail: the semantics are
253// those of [T, w] throughout.
254struct Value {
255 enum Shape { SCALAR = 0, COL = 1, FULL = 2 } shape = SCALAR;
256 float c = 0.0f;
257 std::vector<float> v; // T (COL) or T * w (FULL) entries
258};
259
260// The primitives, each rounded to f32 (paperkiln's host formulas: ops::sigmoid
261// is 1 / (1 + exp(-a)), ops::relu std::max(0, a), maximum a >= b ? a : b).
262float unary_op(Kind k, float a) {
263 switch (k) {
264 case Kind::NEG: return -a;
265 case Kind::EXP: return std::exp(a);
266 case Kind::LOG: return std::log(a);
267 case Kind::SQRT: return std::sqrt(a);
268 case Kind::TANH: return std::tanh(a);
269 case Kind::SIGMOID: return 1.0f / (1.0f + std::exp(-a));
270 case Kind::ERF: return std::erf(a);
271 case Kind::ABS: return std::fabs(a);
272 case Kind::RELU: return std::max(0.0f, a);
273 default: throw std::logic_error("expr: unary");
274 }
275}
276
277float binary_op(Kind k, float a, float b) {
278 switch (k) {
279 case Kind::ADD: return a + b;
280 case Kind::SUB: return a - b;
281 case Kind::MUL: return a * b;
282 case Kind::DIV: return a / b;
283 case Kind::POW: return std::pow(a, b);
284 case Kind::MAX: return a >= b ? a : b;
285 case Kind::MIN: return a <= b ? a : b;
286 default: throw std::logic_error("expr: binary");
287 }
288}
289
290class Evaluator {
291 public:
292 Evaluator(const std::vector<const float*>& ins, int T, int w) : ins_(ins), T_(T), w_(w) {}
293
294 void result(const Node& root, float* out) {
295 Value r = to(eval(root), Value::FULL);
296 std::copy(r.v.begin(), r.v.end(), out);
297 }
298
299 private:
300 size_t size(Value::Shape s) const {
301 return s == Value::SCALAR ? 1 : static_cast<size_t>(T_) * (s == Value::COL ? 1 : w_);
302 }
303
304 // r brought to `shape` (never narrower than its own).
305 Value to(Value r, Value::Shape shape) const {
306 if (r.shape == shape) return r;
307 Value out;
308 out.shape = shape;
309 out.v.resize(size(shape));
310 if (r.shape == Value::SCALAR) {
311 std::fill(out.v.begin(), out.v.end(), r.c);
312 } else { // COL -> FULL
313 for (int t = 0; t < T_; ++t) std::fill_n(out.v.begin() + static_cast<size_t>(t) * w_, w_, r.v[t]);
314 }
315 return out;
316 }
317
318 static Value constant(float c) {
319 Value out;
320 out.c = c;
321 return out;
322 }
323
324 Value eval(const Node& n) {
325 switch (n.kind) {
326 case Kind::NUMBER: return constant(n.value);
327 case Kind::INPUT: {
328 Value out;
329 out.shape = Value::FULL;
330 out.v.assign(ins_[n.input], ins_[n.input] + size(Value::FULL));
331 return out;
332 }
333 case Kind::NEG: case Kind::EXP: case Kind::LOG: case Kind::SQRT: case Kind::TANH: case Kind::SIGMOID:
334 case Kind::ERF: case Kind::ABS: case Kind::RELU: {
335 Value a = eval(n.args[0]);
336 if (a.shape == Value::SCALAR) return constant(unary_op(n.kind, a.c));
337 for (float& v : a.v) v = unary_op(n.kind, v);
338 return a;
339 }
340 case Kind::ADD: case Kind::SUB: case Kind::MUL: case Kind::DIV: case Kind::POW: case Kind::MAX:
341 case Kind::MIN:
342 return binary(n.kind, eval(n.args[0]), eval(n.args[1]));
343 case Kind::WHERE: {
344 Value c = eval(n.args[0]), a = eval(n.args[1]), b = eval(n.args[2]);
345 if (c.shape == Value::SCALAR && a.shape == Value::SCALAR && b.shape == Value::SCALAR)
346 return constant(c.c > 0.0f ? a.c : b.c);
347 const auto s = std::max({c.shape, a.shape, b.shape});
348 c = to(std::move(c), s), a = to(std::move(a), s), b = to(std::move(b), s);
349 for (size_t i = 0; i < a.v.size(); ++i) a.v[i] = c.v[i] > 0.0f ? a.v[i] : b.v[i];
350 return a;
351 }
352 case Kind::ROWSUM: case Kind::ROWMEAN: case Kind::ROWMAX: {
353 const Value full = to(eval(n.args[0]), Value::FULL);
354 Value col;
355 col.shape = Value::COL;
356 col.v.resize(static_cast<size_t>(T_));
357 for (int t = 0; t < T_; ++t) {
358 const float* row = full.v.data() + static_cast<size_t>(t) * w_;
359 float acc;
360 if (n.kind == Kind::ROWMAX) { // the first largest entry, scanning ascending columns
361 acc = row[0];
362 for (int j = 1; j < w_; ++j) acc = row[j] > acc ? row[j] : acc;
363 } else { // 0 + row[0] + row[1] + ... in ascending columns, as the reference
364 acc = 0.0f;
365 for (int j = 0; j < w_; ++j) acc = acc + row[j];
366 }
367 col.v[t] = acc;
368 }
369 if (n.kind == Kind::ROWMEAN) return binary(Kind::DIV, std::move(col), constant(static_cast<float>(w_)));
370 return col;
371 }
372 }
373 throw std::logic_error("expr: unhandled node");
374 }
375
376 Value binary(Kind k, Value a, Value b) {
377 if (a.shape == Value::SCALAR && b.shape == Value::SCALAR) return constant(binary_op(k, a.c, b.c));
378 const auto s = std::max(a.shape, b.shape);
379 // paperkiln forms x - c as x + (-c): the same IEEE result per element.
380 if (k == Kind::SUB && b.shape == Value::SCALAR) {
381 k = Kind::ADD;
382 b.c = -b.c;
383 }
384 if (a.shape == Value::SCALAR || b.shape == Value::SCALAR) {
385 const bool left = a.shape == Value::SCALAR;
386 Value& t = left ? b : a;
387 const float c = left ? a.c : b.c;
388 for (float& v : t.v) v = left ? binary_op(k, c, v) : binary_op(k, v, c);
389 return std::move(t);
390 }
391 a = to(std::move(a), s), b = to(std::move(b), s);
392 for (size_t i = 0; i < a.v.size(); ++i) a.v[i] = binary_op(k, a.v[i], b.v[i]);
393 return a;
394 }
395
396 const std::vector<const float*>& ins_;
397 int T_, w_;
398};
399
400// ---- the device form (lower) ----
401
402Op op_of(Kind k) {
403 switch (k) {
404 case Kind::NEG: return Op::NEG;
405 case Kind::EXP: return Op::EXP;
406 case Kind::LOG: return Op::LOG;
407 case Kind::SQRT: return Op::SQRT;
408 case Kind::TANH: return Op::TANH;
409 case Kind::SIGMOID: return Op::SIGMOID;
410 case Kind::ERF: return Op::ERF;
411 case Kind::ABS: return Op::ABS;
412 case Kind::RELU: return Op::RELU;
413 case Kind::ADD: return Op::ADD;
414 case Kind::SUB: return Op::SUB;
415 case Kind::MUL: return Op::MUL;
416 case Kind::DIV: return Op::DIV;
417 case Kind::POW: return Op::POW;
418 case Kind::MAX: return Op::MAX;
419 case Kind::MIN: return Op::MIN;
420 case Kind::WHERE: return Op::WHERE;
421 default: throw std::logic_error("expr: no instruction");
422 }
423}
424
425class Lowering {
426 public:
427 explicit Lowering(const Expr& e) : e_(e) {}
428
429 std::vector<Stage> run() {
430 Stage last;
431 emit_stage(e_.root(), last);
432 stages_.push_back(std::move(last));
433 return std::move(stages_);
434 }
435
436 private:
437 // n's value when it is a constant in eval (every leaf a number, no row
438 // reduction), folded with eval's operations in eval's order.
439 static bool constant(const Node& n, float& v) {
440 switch (n.kind) {
441 case Kind::NUMBER: v = n.value; return true;
442 case Kind::INPUT: case Kind::ROWSUM: case Kind::ROWMEAN: case Kind::ROWMAX: return false;
443 case Kind::WHERE: {
444 float c, a, b;
445 if (!constant(n.args[0], c) || !constant(n.args[1], a) || !constant(n.args[2], b)) return false;
446 v = c > 0.0f ? a : b;
447 return true;
448 }
449 default: break;
450 }
451 float a, b;
452 if (!constant(n.args[0], a)) return false;
453 if (n.args.size() == 1) {
454 v = unary_op(n.kind, a);
455 return true;
456 }
457 if (!constant(n.args[1], b)) return false;
458 v = binary_op(n.kind, a, b);
459 return true;
460 }
461
462 void emit_stage(const Node& n, Stage& s) {
463 size_t depth = 0, peak = 0;
464 emit(n, s.code, depth, peak);
465 if (s.code.size() > kMaxCode || peak > kMaxStack)
466 throw std::invalid_argument("expr: \"" + e_.text() + "\" needs " + std::to_string(s.code.size()) +
467 " instructions and a stack of " + std::to_string(peak) + " in one stage (the CUDA form "
468 "takes " + std::to_string(kMaxCode) + " and " + std::to_string(kMaxStack) + ")");
469 }
470
471 void emit(const Node& n, std::vector<Instr>& code, size_t& depth, size_t& peak) {
472 auto push = [&](Op op, int idx = 0, float c = 0.0f) {
473 code.push_back({op, idx, c});
474 peak = std::max(peak, ++depth);
475 };
476 float v;
477 if (constant(n, v)) return push(Op::CONST, 0, v);
478 switch (n.kind) {
479 case Kind::INPUT: return push(Op::INPUT, static_cast<int>(n.input));
480 case Kind::ROWSUM: case Kind::ROWMEAN: case Kind::ROWMAX: {
481 Stage s;
482 s.reduce = n.kind == Kind::ROWMAX ? 2 : 1;
483 emit_stage(n.args[0], s); // its own reductions become earlier stages
484 stages_.push_back(std::move(s));
485 if (stages_.size() > kMaxColumns)
486 throw std::invalid_argument("expr: \"" + e_.text() + "\" has more than " + std::to_string(kMaxColumns) +
487 " row reductions (the CUDA form's limit)");
488 push(Op::COL, static_cast<int>(stages_.size() - 1));
489 if (n.kind == Kind::ROWMEAN) { // eval: the column divided by (float) w
491 code.push_back({Op::DIV, 0, 0.0f});
492 --depth;
493 }
494 return;
495 }
496 default: break;
497 }
498 for (const Node& a : n.args) emit(a, code, depth, peak);
499 code.push_back({op_of(n.kind), 0, 0.0f});
500 depth -= n.args.size() - 1;
501 }
502
503 const Expr& e_;
504 std::vector<Stage> stages_;
505};
506
507} // namespace
508
509std::vector<Stage> lower(const Expr& e) { return Lowering(e).run(); }
510
511Expr Expr::parse(const std::string& text, size_t n_inputs) {
512 if (n_inputs == 0 || n_inputs > kMaxInputs)
513 throw std::invalid_argument("expr: " + std::to_string(n_inputs) + " inputs (1 to " + std::to_string(kMaxInputs) +
514 ")");
515 Expr e;
516 e.text_ = text;
517 e.n_inputs_ = n_inputs;
518 e.root_ = Parser(e.text_, n_inputs).parse();
519 return e;
520}
521
522void eval(const Expr& e, const std::vector<const float*>& ins, int T, int w, float* out) {
523 if (ins.size() != e.n_inputs())
524 throw std::invalid_argument("expr: " + std::to_string(ins.size()) + " inputs given, \"" + e.text() +
525 "\" was parsed for " + std::to_string(e.n_inputs()));
526 if (T <= 0 || w <= 0) throw std::invalid_argument("expr: inputs must be [T, w] with T, w > 0");
527 Evaluator(ins, T, w).result(e.root(), out);
528}
529
530const Expr& cached(const std::string& text, size_t n_inputs) {
531 static std::mutex mu;
532 static std::map<std::pair<std::string, size_t>, Expr> cache;
533 std::lock_guard<std::mutex> lock(mu);
534 auto key = std::make_pair(text, n_inputs);
535 auto it = cache.find(key);
536 if (it == cache.end()) it = cache.emplace(key, Expr::parse(text, n_inputs)).first;
537 return it->second;
538}
539
540} // namespace expr
541} // namespace ember_ir
static constexpr size_t kMaxDepth
static Expr parse(const std::string &text, size_t n_inputs=1)
size_t n_inputs() const
static constexpr size_t kMaxInputs
const std::string & text() const
expr::Instr code[expr::kMaxCode]
const float * col[expr::kMaxColumns]
int n
int w_
std::vector< Stage > stages_
float value
std::string text
size_t arity
size_t n_
const std::string & s_
std::vector< float > v
std::vector< Token > toks_
const std::vector< const float * > & ins_
size_t column
const char * name
size_t pos_
const Expr & e_
enum ember_ir::expr::@33::Value::Shape shape
int T_
Kind kind
Tok tok
float c
std::vector< E > a
node expr
node out
size_t j
std::vector< Stage > lower(const Expr &e)
constexpr size_t kMaxCode
constexpr size_t kMaxColumns
void eval(const Expr &e, const std::vector< const float * > &ins, int T, int w, float *out)
constexpr size_t kMaxStack
const Expr & cached(const std::string &text, size_t n_inputs)
Definition api.h:20