ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
draft_table.h
Go to the documentation of this file.
1#pragma once
2// Corpus draft table for speculative decoding (EMBER_DRAFT=table, experiments/draft_2026-10-27).
3//
4// An n-gram table of the most frequent next token, built offline from chat text by
5// experiments/draft_2026-10-27/dr_table.py: for each order o = 1..orders and each context c (the last o tokens)
6// it holds the most frequent next token and its share of c's occurrences (as round(255 p)). A draft walks it:
7// from the history plus the draft so far, the longest order whose context is in the table gives the next token;
8// the draft stops when the product of the shares falls below theta, or at k tokens. It needs no model: the
9// verification pass decides every token, so the replies are plain decode's whatever the table holds.
10//
11// Keys (dr_table.py has the same): t0 is the most recent token; for o <= 3 the key is t0 | t1 << 16 | t2 << 32;
12// for o = 4 it is mix64(t0 | t1 << 16 | t2 << 32 | t3 << 48) >> 16 (splitmix64's finaliser); for o = 5..8
13// (experiments/spec_r1_2026-10-28, r1_sim.py) the order-4 hash continued: h = mix64(t0 | ... | t3 << 48), then
14// h = mix64(h ^ (t_j + 1) * 0x9E3779B97F4A7C15) for j = 4..o-1, the key h >> 16.
15// File (little-endian): "EMBERDT1", u32 vocab_size, u32 orders, u64 entries[orders]; then per order ascending
16// keys u64[n], next u16[n], conf u8[n], each array zero-padded to 8 bytes.
17
18#include <algorithm>
19#include <cstdint>
20#include <cstring>
21#include <fstream>
22#include <string>
23#include <vector>
24
25namespace ember_draft {
26
28 public:
29 // Reads `path`; false (with `why` set) when the file is unreadable or malformed, or was built for another
30 // vocabulary size than `vocab_size`.
31 bool load(const std::string& path, int vocab_size, std::string* why) {
32 std::ifstream f(path, std::ios::binary);
33 if (!f) return fail(why, "cannot open " + path);
34 buf_.assign(std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>());
35 if (buf_.size() < 16 || std::memcmp(buf_.data(), "EMBERDT1", 8) != 0) return fail(why, "not a draft table");
36 uint32_t vocab = 0, orders = 0;
37 std::memcpy(&vocab, buf_.data() + 8, 4);
38 std::memcpy(&orders, buf_.data() + 12, 4);
39 if (orders < 1 || orders > 8) return fail(why, "orders must be 1..8");
40 if (static_cast<int>(vocab) != vocab_size)
41 return fail(why, "built for vocabulary size " + std::to_string(vocab) + ", the model has " +
42 std::to_string(vocab_size));
43 size_t p = 16 + 8 * static_cast<size_t>(orders);
44 if (buf_.size() < p) return fail(why, "truncated header");
45 std::vector<uint64_t> ns(orders);
46 std::memcpy(ns.data(), buf_.data() + 16, 8 * orders);
47 tabs_.assign(orders, Order{});
48 auto pad8 = [](size_t b) { return (b + 7) / 8 * 8; };
49 for (uint32_t o = 0; o < orders; ++o) {
50 const size_t n = ns[o];
51 const size_t need = pad8(8 * n) + pad8(2 * n) + pad8(n);
52 if (buf_.size() - p < need) return fail(why, "truncated tables");
53 tabs_[o].n = n;
54 tabs_[o].keys.resize(n);
55 tabs_[o].next.resize(n);
56 tabs_[o].conf.resize(n);
57 std::memcpy(tabs_[o].keys.data(), buf_.data() + p, 8 * n);
58 p += pad8(8 * n);
59 std::memcpy(tabs_[o].next.data(), buf_.data() + p, 2 * n);
60 p += pad8(2 * n);
61 std::memcpy(tabs_[o].conf.data(), buf_.data() + p, n);
62 p += pad8(n);
63 for (size_t i = 0; i < n; ++i)
64 if (tabs_[o].next[i] >= vocab || (i > 0 && tabs_[o].keys[i] <= tabs_[o].keys[i - 1]))
65 return fail(why, "entries out of range or not ascending");
66 }
67 if (p != buf_.size()) return fail(why, "trailing bytes");
68 buf_.clear();
69 buf_.shrink_to_fit();
70 return true;
71 }
72
73 int orders() const { return static_cast<int>(tabs_.size()); }
74
75 size_t entries() const {
76 size_t s = 0;
77 for (const auto& t : tabs_) s += t.n;
78 return s;
79 }
80
81 // Up to k tokens continuing h (see the header comment). Empty when nothing qualifies.
82 std::vector<int> draft(const std::vector<int>& h, int k, double theta) const {
83 std::vector<int> d;
84 if (tabs_.empty() || k <= 0) return d;
85 const int no = orders();
86 std::vector<int> ctx(h.end() - std::min<size_t>(h.size(), static_cast<size_t>(no)), h.end());
87 double p = 1.0;
88 while (static_cast<int>(d.size()) < k) {
89 int tok = -1;
90 double c = 0.0;
91 if (!lookup(ctx, &tok, &c)) break;
92 p *= c;
93 if (p < theta) break;
94 d.push_back(tok);
95 ctx.push_back(tok);
96 if (static_cast<int>(ctx.size()) > no) ctx.erase(ctx.begin());
97 }
98 return d;
99 }
100
102 x ^= x >> 30;
103 x *= 0xBF58476D1CE4E5B9ULL;
104 x ^= x >> 27;
105 x *= 0x94D049BB133111EBULL;
106 x ^= x >> 31;
107 return x;
108 }
109
110 private:
111 struct Order {
112 size_t n = 0;
113 std::vector<uint64_t> keys;
114 std::vector<uint16_t> next;
115 std::vector<uint8_t> conf;
116 };
117
118 // The longest order whose context (the last o tokens of ctx) is in the table.
119 bool lookup(const std::vector<int>& ctx, int* tok, double* c) const {
120 const int len = static_cast<int>(ctx.size());
121 for (int o = std::min(orders(), len); o >= 1; --o) {
122 const Order& t = tabs_[o - 1];
123 if (t.n == 0) continue;
124 uint64_t key = 0;
125 bool ok = true;
126 for (int j = 0; j < o; ++j) {
127 const int v = ctx[len - 1 - j];
128 if (v < 0 || v > 0xFFFF) { ok = false; break; }
129 if (j < 4) key |= static_cast<uint64_t>(v) << (16 * j);
130 }
131 if (!ok) continue;
132 if (o >= 4) {
133 key = mix64(key);
134 for (int j = 4; j < o; ++j)
135 key = mix64(key ^ ((static_cast<uint64_t>(ctx[len - 1 - j]) + 1) * 0x9E3779B97F4A7C15ULL));
136 key >>= 16;
137 }
138 const auto it = std::lower_bound(t.keys.begin(), t.keys.end(), key);
139 if (it != t.keys.end() && *it == key) {
140 const size_t i = static_cast<size_t>(it - t.keys.begin());
141 *tok = t.next[i];
142 *c = static_cast<double>(t.conf[i]) / 255.0;
143 return true;
144 }
145 }
146 return false;
147 }
148
149 static bool fail(std::string* why, const std::string& w) {
150 if (why) *why = w;
151 return false;
152 }
153
154 std::vector<char> buf_;
155 std::vector<Order> tabs_;
156};
157
158} // namespace ember_draft
bool lookup(const std::vector< int > &ctx, int *tok, double *c) const
bool load(const std::string &path, int vocab_size, std::string *why)
Definition draft_table.h:31
std::vector< int > draft(const std::vector< int > &h, int k, double theta) const
Definition draft_table.h:82
std::vector< Order > tabs_
static uint64_t mix64(uint64_t x)
std::vector< char > buf_
size_t entries() const
Definition draft_table.h:75
static bool fail(std::string *why, const std::string &w)
std::string why
Definition graph_ir.cpp:280
const float * p[kIrDepthSourcesMax]
int n
std::vector< float > v
Tok tok
float c
size_t j
std::vector< uint16_t > next
std::vector< uint64_t > keys
std::vector< uint8_t > conf