ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
graph_ir.cpp
Go to the documentation of this file.
1#include "graph_ir.h"
2
3#include <algorithm>
4#include <charconv>
5#include <cmath>
6#include <cstdlib>
7#include <cstring>
8#include <fstream>
9#include <limits>
10#include <optional>
11#include <set>
12#include <sstream>
13#include <stdexcept>
14#include <tuple>
15#include <type_traits>
16#include <variant>
17
18#include "cpu_attention.h"
19#include "determinism.h"
20#include "gguf_structs.h"
21#include "graph_ir_expr.h"
22#include "graph_ir_mamba.h"
23#include "graph_ir_ops.h"
24#include "graph_ir_recurrence.h"
25#include "graph_ir_validate.h"
26#include "ir_digest.h"
27#include "kv_quant.h"
28#include "logger.h"
29#include "quantization.h"
30#include "utils.h"
31#include "weight_management.h"
32#ifdef HAS_CUDA
33#include "cuda_kernels.h"
34#include "graph_ir_cuda.h"
35#endif
36
37using nlohmann::json;
38
39namespace ember_ir {
40namespace {
41
42const std::set<std::string> kCpuOps = {"embedding", "pos_embedding", "rmsnorm", "layernorm", "linear", "split", "rope",
43 "attention", "add", "mul", "silu", "gelu", "moe_ffn",
44 "linear_attention", "ssm_scan",
45 // Mamba-1 (graph_ir_mamba.h)
46 "conv1d_causal", "selective_scan",
47 // gated and attention-residual families (graph_ir_ops.h)
48 "sub", "relu", "sigmoid", "affine", "mul_col", "rms_row", "list", "append", "depth_attend",
49 // runtime-defined elementwise ops (graph_ir_expr.h) and the per-feature gain
50 "expr", "scale",
51 // Mixture-of-Recursions' control node (forward's mor_step runs it)
52 "depth_route",
53 // multi-head latent attention (IR_SPEC 5.20, revision 1.5; graph_ir_ops.h)
54 "mla_attention",
55 // a recurrent op defined in the document (graph_ir_recurrence.h); no CUDA
56 // kernel: a CUDA build serves its documents on the CPU (device_supported)
57 "recurrence"};
58const std::set<std::string> kRecurrentOps = {"linear_attention", "ssm_scan", "conv1d_causal", "selective_scan",
59 "recurrence"};
60// The ops run_nodes_device implements.
61const std::set<std::string> kCudaOps = {"embedding", "pos_embedding", "rmsnorm", "layernorm", "linear", "split", "rope",
62 "attention", "add", "mul", "silu", "gelu", "moe_ffn",
63 "linear_attention", "ssm_scan", "conv1d_causal", "selective_scan",
64 "sub", "relu", "sigmoid", "affine", "mul_col", "rms_row", "list", "append", "depth_attend",
65 "expr", "scale",
66 // Mixture-of-Recursions' control node (run_device's mor_step_device runs it)
67 "depth_route",
68 // multi-head latent attention (IR_SPEC 5.20; graph_ir_cuda.h ir_mla_attention_cuda)
69 "mla_attention"};
70const std::set<std::string> kRowWiseOps = {"rmsnorm", "layernorm", "linear", "add", "mul", "silu", "gelu",
71 "sub", "relu", "sigmoid", "affine", "mul_col", "rms_row", "list", "append", "depth_attend",
72 "expr", "scale"};
73
74std::vector<std::string> names(const json& j) {
75 if (j.is_string()) return {j.get<std::string>()};
76 std::vector<std::string> v;
77 for (const auto& x : j) v.push_back(x.get<std::string>());
78 return v;
79}
80
81std::vector<Node> parse_nodes(const json& sub, const char* where) {
82 std::vector<Node> nodes;
83 for (const auto& n : sub.at("nodes")) {
84 Node node{n.at("op").get<std::string>(), names(n.at("in")), names(n.at("out")), n};
85 if (!kCpuOps.count(node.op))
86 throw std::runtime_error(std::string("IR op '") + node.op + "' (" + where +
87 ") has no CPU kernel in ember's interpreter");
88 nodes.push_back(std::move(node));
89 }
90 return nodes;
91}
92
93bool has(const json& n, const char* key) { return n.contains(key) && !n.at(key).is_null(); }
94
95// A per-layer attribute (paperkiln ir.hpp): {"per_layer": [v_0, ..., v_{L-1}]}, block i reading v_i.
96bool is_per_layer(const json& v) { return v.is_object() && v.contains("per_layer"); }
97const json& at_layer(const json& v, int layer) { return is_per_layer(v) ? v.at("per_layer").at(layer) : v; }
98
99// A block node runs in application `layer` unless its apply (IR_SPEC 3.8, revision 1.6) is false there; outside
100// the block (layer < 0) every node runs (apply there is rule 43's).
101bool applies(const Node& n, int layer) {
102 if (layer < 0 || !n.attrs.contains("apply")) return true;
103 const json& a = n.attrs.at("apply");
104 const json& v = is_per_layer(a) && a.at("per_layer").is_array() && layer < static_cast<int>(a.at("per_layer").size())
105 ? a.at("per_layer").at(layer)
106 : a;
107 return !v.is_boolean() || v.get<bool>();
108}
109
110// IR_SPEC 10.1 rule 37, checked when the rows arrive (section 10.1.1): the widths a node reads must fit it.
111[[noreturn]] void binary_shapes(const std::string& op, int a, int b) {
112 throw std::runtime_error("IR_SPEC 5.10 (rule 37): " + op + " operands of two shapes, [T, " + std::to_string(a) +
113 "] and [T, " + std::to_string(b) + "]");
114}
115// rope's q is heads x head_dim wide and its k kv_heads x head_dim (kv_heads defaults to heads), in application layer.
116void rope_widths(const Node& n, int layer, int qw, int kw) {
117 const int L = layer < 0 ? 0 : layer;
118 const int hd = at_layer(n.attrs.at("head_dim"), L).get<int>(), h = at_layer(n.attrs.at("heads"), L).get<int>();
119 const int hkv = n.attrs.contains("kv_heads") ? at_layer(n.attrs.at("kv_heads"), L).get<int>() : h;
120 if (qw != h * hd || kw != hkv * hd)
121 throw std::runtime_error("IR_SPEC 5.14 (rule 37): rope q is " + std::to_string(qw) + " wide and k " +
122 std::to_string(kw) + ", not heads x head_dim = " + std::to_string(h * hd) +
123 " and kv_heads x head_dim = " + std::to_string(hkv * hd));
124}
125
126// An integer attribute that is non-zero in some block.
127bool any_nonzero(const json& n, const char* key) {
128 if (!has(n, key)) return false;
129 const json& v = n.at(key);
130 if (!is_per_layer(v)) return v.get<int>() != 0;
131 for (const auto& e : v.at("per_layer"))
132 if (e.get<int>() != 0) return true;
133 return false;
134}
135
136// ssm_scan names its tensors by attributes a, b, c, d.
137std::string ssm_weight(const json& n, const char* part) {
138 if (!has(n, part)) throw std::runtime_error(std::string("IR ssm_scan names no '") + part + "' tensor");
139 return n.at(part).get<std::string>();
140}
141
142// rope on q or k ([T, heads * hd]) for the forms apply_rope_batch_cpu does not
143// cover: pairing "neox" (x[j], x[j + n_rot/2]) and partial rotary (the first
144// n_rot dims of each head; the rest pass through). freqs: [max_len][n_rot/2]
145// (cos, sin), as the interleaved table. Same per-pair arithmetic as
146// apply_rope_batch_cpu.
147void rope_rows_cpu(std::vector<float>& x, int T, int heads, int hd, int n_rot, bool neox, int start_pos,
148 const std::vector<std::pair<float, float>>& freqs) {
149 const int half = n_rot / 2;
150 for (int t = 0; t < T; ++t) {
151 const auto* f = freqs.data() + static_cast<size_t>(start_pos + t) * half;
152 for (int h = 0; h < heads; ++h) {
153 float* r = x.data() + (static_cast<size_t>(t) * heads + h) * hd;
154 for (int i = 0; i < half; ++i) {
155 const size_t i0 = neox ? i : 2 * i, i1 = neox ? i + half : 2 * i + 1;
156 const float v0 = r[i0], v1 = r[i1];
157 r[i0] = v0 * f[i].first - v1 * f[i].second;
158 r[i1] = v0 * f[i].second + v1 * f[i].first;
159 }
160 }
161 }
162}
163
164// ---- Third-party IR builder (build_ir_from_gguf) ----
165
166std::optional<double> meta_number(const GGUFData& g, const std::string& key) {
167 auto it = g.metadata.find(key);
168 if (it == g.metadata.end()) return std::nullopt;
169 return std::visit(
170 [](const auto& v) -> std::optional<double> {
171 using V = std::decay_t<decltype(v)>;
172 if constexpr (std::is_arithmetic_v<V> && !std::is_same_v<V, bool>)
173 return static_cast<double>(v);
174 else
175 return std::nullopt;
176 },
177 it->second);
178}
179
180std::optional<std::string> meta_string(const GGUFData& g, const std::string& key) {
181 auto it = g.metadata.find(key);
182 if (it == g.metadata.end()) return std::nullopt;
183 if (const auto* s = std::get_if<std::string>(&it->second)) return *s;
184 return std::nullopt;
185}
186
187std::optional<bool> meta_bool(const GGUFData& g, const std::string& key) {
188 auto it = g.metadata.find(key);
189 if (it == g.metadata.end()) return std::nullopt;
190 if (const auto* b = std::get_if<bool>(&it->second)) return *b;
191 return std::nullopt;
192}
193
194// The value a float32 holds, as the shortest decimal that round-trips it,
195// read back as a double (paperkiln's ir::f32 / gguf_ir.py f32 convention).
196double f32_value(double v) {
197 char buf[64];
198 const auto r = std::to_chars(buf, buf + sizeof(buf), static_cast<float>(v));
199 return std::strtod(std::string(buf, r.ptr).c_str(), nullptr);
200}
201
202// elu(x) + 1 as KimiLinearAttention::feature_map: x + 1 for x > 0, else
203// (exp(x) - 1) + 1, each step rounded to f32.
204inline float elu_plus_one(float x) {
205 float r = x;
206 if (r <= 0.f) r = std::exp(r) - 1.f;
207 r += 1.f;
208 return r;
209}
210
211} // namespace
212
213void linear_attention_row_cpu(const float* q, const float* k, const float* v, float* num, float* den, float* out,
214 int width, float den_floor) {
215 // num and den take token t before out[t] is formed (token t sees keys 0..t).
216 for (int c = 0; c < width; ++c) {
217 const float pk = elu_plus_one(k[c]);
218 num[c] = num[c] + pk * v[c];
219 den[c] = den[c] + pk;
220 out[c] = elu_plus_one(q[c]) * num[c] / std::max(den[c], den_floor);
221 }
222}
223
224void ssm_scan_row_cpu(const float* u, const float* A, const float* B, const float* C, float D, float* s,
225 float* scratch, float* y, int n) {
226 // ops::ssm_scan: all of s_t from s_{t-1}, then y from s_t.
227 for (int i = 0; i < n; ++i) {
228 float acc = 0;
229 for (int j = 0; j < n; ++j) acc += A[static_cast<size_t>(i) * n + j] * s[j];
230 acc += B[i] * u[i];
231 scratch[i] = acc;
232 }
233 std::memcpy(s, scratch, static_cast<size_t>(n) * sizeof(float));
234 for (int i = 0; i < n; ++i) y[i] = C[i] * s[i] + D * u[i];
235}
236
237bool has_recurrent_state(const std::string& ir_json) {
238 const json j = json::parse(ir_json);
239 if (!j.contains("block")) return false;
240 for (const auto& s : j.at("block").value("state", json::array()))
241 if (s.value("kind", "") == "recurrent") return true;
242 return false;
243}
244
245bool has_windowed_attention(const std::string& ir_json) {
246 const json j = json::parse(ir_json);
247 if (!j.contains("block")) return false;
248 const json& blk = j.at("block");
249 for (const auto& s : blk.value("state", json::array()))
250 if (any_nonzero(s, "window")) return true;
251 for (const auto& n : blk.value("nodes", json::array()))
252 if ((n.value("op", "") == "attention" || n.value("op", "") == "mla_attention") && any_nonzero(n, "window"))
253 return true;
254 return false;
255}
256
257bool has_state_tokens(const std::string& ir_json) {
258 const json j = json::parse(ir_json);
259 for (const char* sub : {"embed", "block", "head"}) {
260 if (!j.contains(sub)) continue;
261 for (const auto& n : j.at(sub).value("nodes", json::array())) {
262 const std::string op = n.value("op", "");
263 if (op == "embedding" && has(n, "state_weight")) return true;
264 if (op == "attention" && n.contains("mask") && n.at("mask") != "causal") return true;
265 }
266 }
267 if (j.contains("block"))
268 for (const auto& s : j.at("block").value("state", json::array()))
269 if (has(s, "evict")) return true;
270 return false;
271}
272
273namespace {
274
275// The structural match of tuned_llama_match (graph_ir.h): node by node against the graph the tuned llama path
276// implements, tracking the role of every value name (so names are free, the data flow is not).
277class LlamaMatch {
278 public:
279 LlamaMatch(const json& doc, const ModelConfig& cfg) : doc_(doc), cfg_(cfg) {}
280 std::string why;
281
282 bool run() {
283 const json& m = doc_.at("model");
284 d_ = m.at("d").get<int>();
285 if (d_ != cfg_.hidden_size) return fail("model.d " + std::to_string(d_) + " != the file's embedding_length");
286 if (m.at("n_layers").get<int>() != cfg_.num_hidden_layers) return fail("n_layers differs from the file's block_count");
287 if (cfg_.hidden_act != "silu") return fail("the file's hidden_act is " + cfg_.hidden_act);
288 const json& blk = doc_.at("block");
289 if (blk.contains("weight_index")) return fail("weight_index");
290 const json& state = blk.value("state", json::array());
291 if (state.size() != 1 || state[0].at("kind") != "kv") return fail("the block's state is not one kv slot");
292 const json& s = state[0];
293 if (!keys(s, {"name", "kind", "heads", "kv_heads", "head_dim", "max_len", "window", "sinks", "dtype"}, "kv slot"))
294 return false;
295 H_ = s.at("heads").is_number_integer() ? s.at("heads").get<int>() : -1;
296 KV_ = s.at("kv_heads").is_number_integer() ? s.at("kv_heads").get<int>() : -1;
297 hd_ = s.at("head_dim").is_number_integer() ? s.at("head_dim").get<int>() : -1;
300 return fail("kv slot heads / kv_heads / head_dim " + std::to_string(H_) + " / " + std::to_string(KV_) + " / " +
301 std::to_string(hd_) + " differ from the file's " + std::to_string(cfg_.num_attention_heads) + " / " +
302 std::to_string(cfg_.num_key_value_heads) + " (head_dim = d / heads)");
303 if (s.at("window") != 0 || s.at("sinks") != 0) return fail("a windowed kv slot");
304 slot_ = s.at("name").get<std::string>();
305 // embed
306 const json& emb = doc_.at("embed").at("nodes");
307 if (emb.size() != 1) return fail("embed is not one embedding");
308 if (!node(emb[0], "embedding", {"tokens"}, {"x_in"}, {"weight"}) || !weight(emb[0], "weight", "token_embd.weight"))
309 return false;
310 x_ = emb[0].at("out").get<std::string>();
311 // block
312 const json& n = blk.at("nodes");
313 const bool moe = cfg_.num_experts > 0;
314 if (n.size() != (moe ? 11u : 15u)) return fail("the block has " + std::to_string(n.size()) + " nodes");
315 if (!norm(n[0], "x_in", "n1", "blk.{i}.attn_norm.weight") || !lin(n[1], "n1", "q", "blk.{i}.attn_q.weight") ||
316 !lin(n[2], "n1", "k", "blk.{i}.attn_k.weight") || !lin(n[3], "n1", "v", "blk.{i}.attn_v.weight") ||
317 !rope(n[4]) || !attention(n[5]) || !lin(n[6], "a", "o", "blk.{i}.attn_output.weight") ||
318 !node(n[7], "add", {"x_in", "o"}, {"x_mid"}, {}, true) || !norm(n[8], "x_mid", "n2", "blk.{i}.ffn_norm.weight"))
319 return false;
320 if (moe) {
321 if (!moe_ffn(n[9]) || !node(n[10], "add", {"x_mid", "f"}, {"x_out"}, {}, true)) return false;
322 } else if (!lin(n[9], "n2", "g", "blk.{i}.ffn_gate.weight") || !lin(n[10], "n2", "u", "blk.{i}.ffn_up.weight") ||
323 !node(n[11], "silu", {"g"}, {"g_act"}, {}) || !node(n[12], "mul", {"g_act", "u"}, {"gu"}, {}, true) ||
324 !lin(n[13], "gu", "f", "blk.{i}.ffn_down.weight") ||
325 !node(n[14], "add", {"x_mid", "f"}, {"x_out"}, {}, true)) {
326 return false;
327 }
328 if (n.back().at("out") != x_) return fail("the block does not write the residual stream back to '" + x_ + "'");
329 // head: the next application and the head read x_out under the embedding's name
330 role_.clear();
331 role_[x_] = "x_in";
332 const json& h = doc_.at("head").at("nodes");
333 if (h.size() != 2 || !norm(h[0], "x_in", "n3", "output_norm.weight")) return h.size() == 2 ? false : fail("head");
334 if (!node(h[1], "linear", {"n3"}, {"logits"}, {"weight", "bias", "tied_to"}) || !weight(h[1], "weight", "output.weight") ||
335 !h[1].value("bias", json()).is_null() || (h[1].contains("tied_to") && h[1].at("tied_to") != "token_embd.weight"))
336 return fail("head linear: output.weight, no bias, tied to token_embd.weight or not");
337 if (h[1].at("out") != "logits") return fail("head output is not logits");
338 return true;
339 }
340
341 private:
342 bool fail(const std::string& m) {
343 if (why.empty()) why = m;
344 return false;
345 }
346 bool keys(const json& o, std::initializer_list<const char*> allowed, const std::string& where) {
347 for (const auto& kv : o.items()) {
348 if (!kv.key().empty() && kv.key()[0] == '_') continue;
349 bool ok = false;
350 for (const char* a : allowed) ok = ok || kv.key() == a;
351 if (!ok) return fail(where + ": '" + kv.key() + "'");
352 }
353 return true;
354 }
355 bool weight(const json& n, const char* key, const std::string& name) {
356 return n.value(key, json()) == name ? true : fail(n.at("op").get<std::string>() + " " + key + " is not " + name);
357 }
358 bool eps(const json& n) {
359 return n.at("eps").is_number() && n.at("eps").get<float>() == cfg_.rms_norm_eps
360 ? true
361 : fail("an rmsnorm eps differs from the file's layer_norm_rms_epsilon");
362 }
363 // op, input roles (commutative: either order of two), output roles, the keys besides op / in / out.
364 bool node(const json& n, const char* op, std::vector<std::string> ins, std::vector<std::string> outs,
365 std::initializer_list<const char*> attrs, bool commutative = false) {
366 const std::string where = std::string(op) + " node";
367 if (n.at("op") != op) return fail("expected " + where + ", found " + n.at("op").get<std::string>());
368 std::vector<const char*> allowed = {"op", "in", "out"};
369 allowed.insert(allowed.end(), attrs.begin(), attrs.end());
370 for (const auto& kv : n.items()) {
371 if (!kv.key().empty() && kv.key()[0] == '_') continue;
372 bool ok = false;
373 for (const char* a : allowed) ok = ok || kv.key() == a;
374 if (!ok) return fail(where + ": '" + kv.key() + "'");
375 }
376 std::vector<std::string> got;
377 for (const auto& x : n.at("in")) {
378 const auto it = role_.find(x.get<std::string>());
379 got.push_back(it == role_.end() ? "?" : it->second);
380 }
381 const bool same = got == ins || (commutative && ins.size() == 2 && got == std::vector<std::string>{ins[1], ins[0]});
382 if (!same) return fail(where + " reads the wrong values");
383 const std::vector<std::string> on = names(n.at("out"));
384 if (on.size() != outs.size()) return fail(where + " writes the wrong number of values");
385 for (size_t i = 0; i < on.size(); ++i) role_[on[i]] = outs[i];
386 return true;
387 }
388 bool norm(const json& n, const char* in, const char* out, const std::string& w) {
389 return node(n, "rmsnorm", {in}, {out}, {"weight", "eps"}) && weight(n, "weight", w) && eps(n);
390 }
391 bool lin(const json& n, const char* in, const char* out, const std::string& w) {
392 if (!node(n, "linear", {in}, {out}, {"weight", "bias"}) || !weight(n, "weight", w)) return false;
393 return n.value("bias", json()).is_null() ? true : fail("a biased linear");
394 }
395 bool rope(const json& n) {
396 if (!node(n, "rope", {"q", "k", "pos"}, {"q_rot", "k_rot"},
397 {"heads", "kv_heads", "head_dim", "theta", "rope_heads", "pairing", "n_rot", "freq_scale"}))
398 return false;
399 if (n.at("heads") != H_ || n.at("head_dim") != hd_ || n.value("kv_heads", json(KV_)) != KV_ ||
400 n.value("n_rot", json(hd_)) != hd_ || n.at("rope_heads") != "all" || n.at("pairing") != "interleaved" ||
401 !n.value("freq_scale", json(1.0)).is_number() || n.value("freq_scale", json(1.0)).get<double>() != 1.0)
402 return fail("rope is not interleaved on every head, n_rot = head_dim, freq_scale 1");
403 if (!n.at("theta").is_number() || n.at("theta").get<float>() != cfg_.rope_theta)
404 return fail("rope theta differs from the file's rope.freq_base");
405 return true;
406 }
407 bool attention(const json& n) {
408 if (!node(n, "attention", {"q_rot", "k_rot", "v"}, {"a"},
409 {"state", "heads", "kv_heads", "head_dim", "scale", "causal", "window", "sinks", "mask", "softmax"}))
410 return false;
411 if (n.at("state") != slot_ || n.at("heads") != H_ || n.at("kv_heads") != KV_ || n.at("head_dim") != hd_ ||
412 n.at("causal") != true || n.at("window") != 0 || n.at("sinks") != 0 || n.value("mask", json("causal")) != "causal")
413 return fail("attention is not causal over the kv slot with window 0, sinks 0");
414 if (!n.at("scale").is_number() || n.at("scale").get<float>() != 1.0f / std::sqrt(static_cast<float>(hd_)))
415 return fail("attention scale is not 1/sqrt(head_dim)");
416 return true;
417 }
418 bool moe_ffn(const json& n) {
419 if (!node(n, "moe_ffn", {"n2"}, {"f"}, {"router", "experts", "n_experts", "top_k", "gate_norm", "activation", "routing"}))
420 return false;
421 const json& e = n.at("experts");
422 if (n.at("router") != "blk.{i}.ffn_gate_inp.weight" || e.value("gate", "") != "blk.{i}.ffn_gate_exps.weight" ||
423 e.value("up", "") != "blk.{i}.ffn_up_exps.weight" || e.value("down", "") != "blk.{i}.ffn_down_exps.weight")
424 return fail("moe_ffn tensors are not llama.cpp's ffn_gate_inp / ffn_*_exps");
425 if (n.at("n_experts") != cfg_.num_experts || n.at("top_k") != cfg_.num_experts_used || n.at("gate_norm") != "renorm" ||
426 n.at("activation") != "silu")
427 return fail("moe_ffn: n_experts / top_k differ from the file's expert_count / expert_used_count, or not renorm silu");
428 return true;
429 }
430
431 const json& doc_;
432 const ModelConfig cfg_; // a copy: the caller's may be a temporary
433 std::map<std::string, std::string> role_ = {{"tokens", "tokens"}, {"pos", "pos"}};
434 std::string x_, slot_;
435 int d_ = 0, H_ = 0, KV_ = 0, hd_ = 0;
436};
437
438} // namespace
439
440bool tuned_llama_match(const std::string& ir_json, const GGUFData& gguf, std::string* why) {
441 auto no = [&](const std::string& m) {
442 if (why) *why = m;
443 return false;
444 };
445 json j;
446 try {
447 j = parse_ir_document(ir_json); // rule 35 too: the interpreter then refuses the document
448 } catch (const std::exception& e) {
449 return no(std::string("the document does not parse: ") + e.what());
450 }
451 if (!j.is_object() || j.value("family_hint", json()) != "llama") return no("family_hint is not llama");
452 try {
453 validate_ir(j, gguf); // an invalid document goes to the interpreter, which refuses it
454 LlamaMatch m(j, parse_model_config_from_gguf(gguf));
455 if (!m.run()) return no(m.why);
456 } catch (const std::exception& e) {
457 return no(e.what());
458 }
459 if (why) why->clear();
460 return true;
461}
462
463bool device_supported(const std::string& ir_json) {
464 // Per-turn state tokens (state_weight, mask turn_state, evict turn_state) run on either path.
465 const json j = json::parse(ir_json);
466 // depth_attend reads at most every tensor ever put in a list (list inputs
467 // and appends, the block's once per repeat) plus its own extra inputs.
468 // Per-layer attributes (the ones the interpreter accepts: attention and
469 // kv-state window and sinks, RoPE theta and freq_scale) run on either path.
470 // Mixture-of-Recursions (depth_route) runs a step on its active rows at their own positions: rope and
471 // attention take them (mor_step_device); a pos_embedding in its block stays on the CPU.
472 size_t list_tensors = 0, extra = 0;
473 bool mor = false, block_pos_embedding = false, softcap = false, turn_state = false;
474 for (const char* sub : {"embed", "block", "head"}) {
475 if (!j.contains(sub)) continue;
476 const size_t times = std::string(sub) == "block" ? j.at(sub).value("repeat", 1) : 1;
477 for (const auto& n : j.at(sub).value("nodes", json::array())) {
478 const std::string op = n.value("op", "");
479 if (!kCudaOps.count(op)) return false;
480 if (std::string(sub) == "block") {
481 mor = mor || op == "depth_route";
482 block_pos_embedding = block_pos_embedding || op == "pos_embedding";
483 }
484 if (op == "attention" && n.contains("position_bias")) return false; // CPU only (IR_SPEC 5.15, revision 1.3)
485 if (op == "attention" && n.contains("relative_kv")) return false; // CPU only (IR_SPEC 5.15, revision 1.4)
486 // revision 1.6's apply and DeepSeek-V3's router have no device path: a CUDA build serves the document on the CPU
487 if (n.contains("apply")) return false;
488 if (op == "moe_ffn" && (n.contains("scoring") || n.contains("selection_bias") || n.contains("groups"))) return false;
489 // softcap (revision 1.5) runs in the window kernel; with turn_state or depth_route it stays on the CPU
490 if (op == "attention" && n.contains("softcap")) softcap = true;
491 if (op == "attention" && n.contains("mask") && n.at("mask") != "causal") turn_state = true;
492 if (op == "expr") { // the CUDA form's limits (graph_ir_expr.h lower)
493 try {
494 expr::lower(expr::cached(n.at("expr").get<std::string>(), n.at("in").size()));
495 } catch (const std::exception&) {
496 return false;
497 }
498 }
499 const size_t ins = n.contains("in") && n.at("in").is_array() ? n.at("in").size() : 1;
500 if (op == "list") list_tensors += ins * times;
501 if (op == "append") list_tensors += times;
502 if (op == "depth_attend") extra = std::max(extra, ins - 1);
503#ifdef HAS_CUDA
504 if (op == "ssm_scan" && n.value("n", 0) > kIrSsmMax) return false;
505#endif
506 }
507 }
508 if (mor && block_pos_embedding) return false;
509 // A window under depth_route (IR_SPEC 5.15, v1.4.2) counts positions over an application's compact cache: the
510 // CPU interpreter's (attention under attn_row_); a CUDA build serves such a document on the CPU.
511 if (mor && has_windowed_attention(ir_json)) return false;
512 if (softcap && (mor || turn_state)) return false;
513#ifdef HAS_CUDA
514 if (list_tensors + extra > static_cast<size_t>(kIrDepthSourcesMax)) return false;
515#endif
516 return true;
517}
518
519bool has_ir_builder(const std::string& arch) {
520 static const std::set<std::string> kBuilt = {"llama", "mistral", "qwen2", "qwen3", "gpt2", "gptneox", "gemma3",
521 "mamba", "qwen2moe", "qwen3moe", "deepseek2", "phi3", "gemma2"};
522 return kBuilt.count(arch) > 0;
523}
524
525std::string find_ir(const GGUFData& gguf) {
526 if (const char* path = std::getenv("EMBER_IR")) {
527 std::ifstream f(path);
528 if (!f) throw std::runtime_error(std::string("EMBER_IR: cannot read ") + path);
529 std::stringstream ss;
530 ss << f.rdbuf();
531 return ss.str();
532 }
533 auto it = gguf.metadata.find("paperkiln.ir");
534 if (it != gguf.metadata.end()) {
535 if (const auto* s = std::get_if<std::string>(&it->second)) return *s;
536 }
537 // No IR in the file: every built architecture gets one from its metadata,
538 // except that llama and mistral without a sliding window keep the llama path
539 // unless EMBER_IR_BUILD=1 (a window needs the IR: the llama path has none).
540 const std::string arch = meta_string(gguf, "general.architecture").value_or("llama");
541 if (arch == "llama" || arch == "mistral") {
542 const char* b = std::getenv("EMBER_IR_BUILD");
543 const bool windowed = meta_number(gguf, arch + ".attention.sliding_window").value_or(0) > 0;
544 return windowed || (b && std::string(b) == "1") ? build_ir_from_gguf(gguf) : std::string();
545 }
546 if (has_ir_builder(arch)) return build_ir_from_gguf(gguf);
547 throw std::runtime_error("ember cannot run general.architecture = '" + arch +
548 "': the GGUF has no paperkiln.ir and ember has no IR builder for that architecture "
549 "(built: llama, mistral, qwen2, qwen3, gpt2, gptneox, gemma3, mamba, qwen2moe, qwen3moe, "
550 "deepseek2, phi3, gemma2)");
551}
552
553std::string build_ir_from_gguf(const GGUFData& g, bool log) {
554 const std::string arch = meta_string(g, "general.architecture").value_or("");
555 if (!has_ir_builder(arch)) throw std::runtime_error("no IR builder for general.architecture = '" + arch + "'");
556 const std::string p = arch + ".";
557 auto get = [&](const std::string& key, std::optional<double> dflt = std::nullopt) -> double {
558 auto v = meta_number(g, p + key);
559 if (!v) v = dflt;
560 if (!v) throw std::runtime_error("IR builder: " + arch + " file has no " + p + key);
561 return *v;
562 };
563 auto shape_has = [&](const std::string& t) { return g.tensor_infos_map.count(t) > 0; };
564 auto no_rope_scaling = [&]() {
565 const auto scaling = meta_string(g, p + "rope.scaling.type");
566 if ((scaling && *scaling != "none") || shape_has("rope_freqs.weight"))
567 throw std::runtime_error("IR builder: " + arch + " with RoPE scaling is not built yet");
568 };
569 const int d = static_cast<int>(get("embedding_length"));
570 const int n_layers = static_cast<int>(get("block_count"));
571 int vocab = static_cast<int>(g.tokenizer_tokens.size());
572 if (vocab == 0) {
573 auto it = g.tensor_infos_map.find("token_embd.weight");
574 if (it == g.tensor_infos_map.end() || it->second.shape.size() < 2)
575 throw std::runtime_error("IR builder: no tokens and no token_embd.weight");
576 vocab = static_cast<int>(it->second.shape[1]);
577 }
578 const std::string B = "blk.{i}.";
579
580 auto linear = [](const std::string& x, const std::string& w, const std::string& out, const std::string& b = "",
581 const std::string& tied = "") {
582 json n = {{"op", "linear"}, {"in", {x}}, {"weight", w}, {"bias", nullptr}, {"out", out}};
583 if (!b.empty()) n["bias"] = b;
584 if (!tied.empty()) n["tied_to"] = tied;
585 return n;
586 };
587 auto rmsnorm = [](const std::string& x, const std::string& w, double eps, const std::string& out, int group = 0) {
588 json n = {{"op", "rmsnorm"}, {"in", {x}}, {"weight", w}, {"eps", f32_value(eps)}, {"out", out}};
589 if (group) n["group"] = group;
590 return n;
591 };
592 auto layernorm = [](const std::string& x, const std::string& name, double eps, const std::string& out) {
593 return json{{"op", "layernorm"}, {"in", {x}}, {"weight", name + ".weight"},
594 {"bias", name + ".bias"}, {"eps", f32_value(eps)}, {"out", out}};
595 };
596 auto op = [](const std::string& name, std::vector<std::string> in, const json& out, json attrs = json::object()) {
597 attrs["op"] = name;
598 attrs["in"] = in;
599 attrs["out"] = out;
600 return attrs;
601 };
602 auto rope = [](int H, int KV, int hd, int n_rot, const json& theta, const std::string& pairing,
603 const json& freq_scale = nullptr) {
604 json n = {{"op", "rope"}, {"in", {"q", "k", "pos"}}, {"heads", H}, {"kv_heads", KV},
605 {"head_dim", hd}, {"n_rot", n_rot}, {"theta", theta}, {"rope_heads", "all"},
606 {"pairing", pairing}, {"out", {"q", "k"}}};
607 if (!freq_scale.is_null()) n["freq_scale"] = freq_scale;
608 return n;
609 };
610 auto attention = [](int H, int KV, int hd, double scale, const json& window = 0) {
611 return json{{"op", "attention"}, {"in", {"q", "k", "v"}}, {"state", "kv"}, {"heads", H},
612 {"kv_heads", KV}, {"head_dim", hd}, {"causal", true}, {"scale", f32_value(scale)},
613 {"window", window}, {"sinks", 0}, {"softmax", "max-shifted, masked entries exactly zero"},
614 {"out", "a"}};
615 };
616 auto kv_state = [](int H, int KV, int hd, int ctx, const json& window = 0) {
617 return json{{"name", "kv"}, {"kind", "kv"}, {"heads", H}, {"kv_heads", KV}, {"head_dim", hd},
618 {"max_len", ctx}, {"window", window}, {"sinks", 0}, {"dtype", "f32"}};
619 };
620 const bool tied = !shape_has("output.weight");
621 auto tied_head = [&](json norm_nodes) {
622 norm_nodes.push_back(linear("h", "output.weight", "logits", "", tied ? "token_embd.weight" : ""));
623 return norm_nodes;
624 };
625 json embed = json::array({{{"op", "embedding"}, {"in", {"tokens"}}, {"weight", "token_embd.weight"}, {"out", "x"}}});
627 int ctx = 0;
628 std::string summary;
629
630 // A truth value (IR_SPEC D.1): a bool, or a number that is true when non-zero.
631 auto truth = [&](const std::string& key, bool dflt) {
632 if (const auto b = meta_bool(g, p + key)) return *b;
633 if (const auto v = meta_number(g, p + key)) return *v != 0.0;
634 return dflt;
635 };
636 // moe_ffn in llama.cpp's expert layout (IR_SPEC D.3, D.10), its gates normalised as gate_norm says.
637 auto moe_node = [&](const std::string& gate_norm) {
638 return op("moe_ffn", {"h2"}, "f",
639 {{"router", B + "ffn_gate_inp.weight"},
640 {"experts", {{"gate", B + "ffn_gate_exps.weight"}, {"up", B + "ffn_up_exps.weight"},
641 {"down", B + "ffn_down_exps.weight"}}},
642 {"n_experts", static_cast<int>(get("expert_count"))},
643 {"top_k", static_cast<int>(get("expert_used_count"))},
644 {"gate_norm", gate_norm},
645 {"activation", "silu"}});
646 };
647 // The shared expert's SwiGLU over ffn_{gate,up,down}_shexp (IR_SPEC D.10, D.11) -> sf.
648 auto shared_expert = [&](json& nodes) {
649 for (json n : {linear("h2", B + "ffn_gate_shexp.weight", "sg"), linear("h2", B + "ffn_up_shexp.weight", "su"),
650 op("silu", {"sg"}, "sg"), op("mul", {"sg", "su"}, "sgu"),
651 linear("sgu", B + "ffn_down_shexp.weight", "sf")})
652 nodes.push_back(n);
653 };
654 const bool qwen_moe = arch == "qwen2moe" || arch == "qwen3moe";
655
656 if (arch == "llama" || arch == "mistral" || arch == "qwen2" || arch == "qwen3" || qwen_moe) {
657 const int H = static_cast<int>(get("attention.head_count"));
658 const int KV = static_cast<int>(get("attention.head_count_kv", H));
659 const int hd = static_cast<int>(get("attention.key_length", d / H));
660 const int n_rot = static_cast<int>(get("rope.dimension_count", hd));
661 ctx = static_cast<int>(get("context_length"));
662 const double eps = get("attention.layer_norm_rms_epsilon");
663 const double theta = get("rope.freq_base", 10000.0);
665 // HF Mistral's window (key j visible from query p iff p - w < j <= p); qwen2's
666 // GGUF does not say which layers a window would cover, so none.
667 int window = 0;
668 if (arch == "llama" || arch == "mistral")
669 window = static_cast<int>(meta_number(g, p + "attention.sliding_window").value_or(0));
670 const bool bias = shape_has("blk.0.attn_q.bias"), qk_norm = shape_has("blk.0.attn_q_norm.weight");
671 const std::string pairing = arch == "qwen2" || arch == "qwen3" || qwen_moe ? "neox" : "interleaved";
672 auto proj = [&](const std::string& name, const std::string& out) {
673 return linear("h", B + name + ".weight", out, bias ? B + name + ".bias" : "");
674 };
675 // Mixtral's layout (llama.cpp's llama graph; IR_SPEC 9.3, revision 1.3): llama.expert_count and ffn_gate_inp
676 // put a moe_ffn in place of the SwiGLU.
677 const bool moe = arch == "llama" && meta_number(g, p + "expert_count").value_or(0) > 0 &&
678 shape_has("blk.0.ffn_gate_inp.weight");
679 nodes = json::array({rmsnorm("x", B + "attn_norm.weight", eps, "h"), proj("attn_q", "q"), proj("attn_k", "k"),
680 proj("attn_v", "v")});
681 if (qk_norm) {
682 nodes.push_back(rmsnorm("q", B + "attn_q_norm.weight", eps, "q", hd));
683 nodes.push_back(rmsnorm("k", B + "attn_k_norm.weight", eps, "k", hd));
684 }
685 for (json n : {rope(H, KV, hd, n_rot, f32_value(theta), pairing),
686 attention(H, KV, hd, 1.0 / std::sqrt(static_cast<double>(hd)), window),
687 linear("a", B + "attn_output.weight", "o"), op("add", {"x", "o"}, "x"),
688 rmsnorm("x", B + "ffn_norm.weight", eps, "h2")})
689 nodes.push_back(n);
690 // Qwen-MoE (IR_SPEC D.10, revision 1.5): qwen2's / qwen3's attention, moe_ffn with gate_norm raw for qwen2moe
691 // (llama.cpp b6000's graph ignores norm_topk_prob) and renorm for qwen3moe; qwen2moe's shared expert times
692 // sigmoid(ffn_gate_inp_shexp h2) per token, added to f.
693 const bool shexp = arch == "qwen2moe" && shape_has("blk.0.ffn_gate_inp_shexp.weight");
694 if (moe) {
695 nodes.push_back(moe_node("renorm"));
696 } else if (qwen_moe) {
697 nodes.push_back(moe_node(arch == "qwen2moe" ? "raw" : "renorm"));
698 if (shexp) {
700 for (json n : {linear("h2", B + "ffn_gate_inp_shexp.weight", "sgate"), op("sigmoid", {"sgate"}, "sgate"),
701 op("mul_col", {"sf", "sgate"}, "sf"), op("add", {"f", "sf"}, "f")})
702 nodes.push_back(n);
703 }
704 } else {
705 for (json n : {linear("h2", B + "ffn_gate.weight", "g"), linear("h2", B + "ffn_up.weight", "u"),
706 op("silu", {"g"}, "g"), op("mul", {"g", "u"}, "gu"), linear("gu", B + "ffn_down.weight", "f")})
707 nodes.push_back(n);
708 }
709 nodes.push_back(op("add", {"x", "f"}, "x"));
710 head = tied_head(json::array({rmsnorm("x", "output_norm.weight", eps, "h")}));
711 state = json::array({kv_state(H, KV, hd, ctx, window)});
712 summary = "heads " + std::to_string(H) + "/" + std::to_string(KV) + " x " + std::to_string(hd) + ", n_rot " +
713 std::to_string(n_rot) + ", " + pairing + " RoPE" + (window ? ", window " + std::to_string(window) : "") +
714 (bias ? ", q/k/v biases" : "") + (qk_norm ? ", per-head q/k RMSNorm" : "") +
715 (moe || qwen_moe ? ", moe_ffn " + std::to_string(static_cast<int>(get("expert_count"))) + " experts" : "") +
716 (shexp ? " + a gated shared expert" : "");
717 } else if (arch == "gpt2" || arch == "gptneox") {
718 // LayerNorm blocks over a fused, biased qkv (q | k | v as llama.cpp's convert
719 // writes it; for gptneox it has already de-interleaved HF's per-head [q k v] rows).
720 const bool neox = arch == "gptneox";
721 const int H = static_cast<int>(get("attention.head_count"));
722 const int KV = static_cast<int>(get("attention.head_count_kv", H));
723 const int hd = static_cast<int>(get("attention.key_length", d / H));
724 ctx = static_cast<int>(get("context_length"));
725 const double eps = get("attention.layer_norm_epsilon");
726 if (!neox)
727 embed.push_back({{"op", "pos_embedding"}, {"in", {"x", "pos"}}, {"weight", "position_embd.weight"}, {"out", "x"}});
728 nodes = json::array({layernorm("x", B + "attn_norm", eps, "h"),
729 linear("h", B + "attn_qkv.weight", "qkv", B + "attn_qkv.bias"),
730 op("split", {"qkv"}, {"q", "k", "v"}, {{"sizes", {H * hd, KV * hd, KV * hd}}})});
731 int n_rot = 0;
732 if (neox) {
734 n_rot = static_cast<int>(get("rope.dimension_count", hd));
735 nodes.push_back(rope(H, KV, hd, n_rot, f32_value(get("rope.freq_base", 10000.0)), "neox"));
736 }
737 nodes.push_back(attention(H, KV, hd, 1.0 / std::sqrt(static_cast<double>(hd))));
738 nodes.push_back(linear("a", B + "attn_output.weight", "o", B + "attn_output.bias"));
739 const json ffn = json::array({layernorm("x", B + "ffn_norm", eps, "h2"),
740 linear("h2", B + "ffn_up.weight", "u", B + "ffn_up.bias"),
741 op("gelu", {"u"}, "u", {{"approx", neox ? "none" : "tanh"}}),
742 linear("u", B + "ffn_down.weight", "f", B + "ffn_down.bias")});
743 const bool parallel = neox && meta_bool(g, p + "use_parallel_residual").value_or(true);
744 if (parallel) {
745 // llama.cpp: cur = ffn(ln2 x) + x, then + attn_out; HF sums mlp + attn + x (ROUNDING apart).
746 for (const auto& n : ffn) nodes.push_back(n);
747 nodes.push_back(op("add", {"f", "x"}, "x"));
748 nodes.push_back(op("add", {"x", "o"}, "x"));
749 } else {
750 nodes.push_back(op("add", {"x", "o"}, "x"));
751 for (const auto& n : ffn) nodes.push_back(n);
752 nodes.push_back(op("add", {"x", "f"}, "x"));
753 }
754 head = json::array({layernorm("x", "output_norm", eps, "h")});
755 if (neox)
756 head.push_back(linear("h", "output.weight", "logits"));
757 else
759 state = json::array({kv_state(H, KV, hd, ctx)});
760 summary = "heads " + std::to_string(H) + "/" + std::to_string(KV) + " x " + std::to_string(hd) + ", LayerNorm" +
761 (neox ? ", NEOX RoPE n_rot " + std::to_string(n_rot) + (parallel ? ", parallel residual" : "")
762 : std::string(", learned positions"));
763 } else if (arch == "gemma3") {
764 // llama.cpp b6000 hparams.set_swa_pattern(6): layer i is local (windowed,
765 // theta 10000) iff i % 6 < 5; the rest are global (the file's theta, linear scaling).
766 constexpr int kSwaPattern = 6;
767 constexpr double kLocalTheta = 10000.0;
768 const int H = static_cast<int>(get("attention.head_count"));
769 const int KV = static_cast<int>(get("attention.head_count_kv", H));
770 const int hd = static_cast<int>(get("attention.key_length", d / H));
771 const int n_rot = static_cast<int>(get("rope.dimension_count", hd));
772 ctx = static_cast<int>(get("context_length"));
773 const double eps = get("attention.layer_norm_rms_epsilon");
774 if (shape_has("rope_freqs.weight")) throw std::runtime_error("IR builder: gemma3 with rope_freqs is not built yet");
775 const auto stype = meta_string(g, p + "rope.scaling.type");
776 if (stype && *stype != "none" && *stype != "linear")
777 throw std::runtime_error("IR builder: gemma3 RoPE scaling '" + *stype + "' is not built yet");
778 double factor = 1.0;
779 if (stype && *stype == "linear") {
780 factor = meta_number(g, p + "rope.scaling.factor").value_or(1.0);
781 if (factor == 0.0) factor = 1.0;
782 }
783 const int win = static_cast<int>(get("attention.sliding_window"));
784 const double theta_global = get("rope.freq_base");
785 json windows = json::array(), thetas = json::array(), scales = json::array();
786 for (int i = 0; i < n_layers; ++i) {
787 const bool local = i % kSwaPattern < kSwaPattern - 1;
788 windows.push_back(local ? win : 0);
790 scales.push_back(local ? 1.0 : f32_value(1.0 / factor));
791 }
792 windows = {{"per_layer", windows}};
793 thetas = {{"per_layer", thetas}};
794 scales = {{"per_layer", scales}};
795 // llama.cpp: f_attention_scale = 1/sqrt(n_embd_head_k), except 1/sqrt(n_embd / n_head) for the 27B.
796 const double scale = n_layers == 62 ? 1.0 / std::sqrt(static_cast<double>(d) / H)
797 : 1.0 / std::sqrt(static_cast<double>(hd));
798 embed.push_back(op("affine", {"x"}, "x", {{"scale", f32_value(std::sqrt(static_cast<double>(d)))}, {"shift", 0.0}}));
799 nodes = json::array({rmsnorm("x", B + "attn_norm.weight", eps, "h"), linear("h", B + "attn_q.weight", "q"),
800 linear("h", B + "attn_k.weight", "k"), linear("h", B + "attn_v.weight", "v"),
801 rmsnorm("q", B + "attn_q_norm.weight", eps, "q", hd),
802 rmsnorm("k", B + "attn_k_norm.weight", eps, "k", hd),
803 rope(H, KV, hd, n_rot, thetas, "neox", factor != 1.0 ? scales : json(nullptr)),
804 attention(H, KV, hd, scale, windows), linear("a", B + "attn_output.weight", "o"),
805 rmsnorm("o", B + "post_attention_norm.weight", eps, "o"), op("add", {"x", "o"}, "x"),
806 rmsnorm("x", B + "ffn_norm.weight", eps, "h2"), linear("h2", B + "ffn_gate.weight", "g"),
807 linear("h2", B + "ffn_up.weight", "u"), op("gelu", {"g"}, "g", {{"approx", "tanh"}}),
808 op("mul", {"g", "u"}, "gu"), linear("gu", B + "ffn_down.weight", "f"),
809 rmsnorm("f", B + "post_ffw_norm.weight", eps, "f"), op("add", {"x", "f"}, "x")});
810 head = tied_head(json::array({rmsnorm("x", "output_norm.weight", eps, "h")}));
811 state = json::array({kv_state(H, KV, hd, ctx, windows)});
812 summary = "heads " + std::to_string(H) + "/" + std::to_string(KV) + " x " + std::to_string(hd) +
813 ", sandwich RMSNorms, GeGLU, window " + std::to_string(win) + " on 5 of every 6 layers" +
814 (factor != 1.0 ? ", global RoPE scaled 1/" + std::to_string(factor) : "");
815 } else if (arch == "deepseek2") {
816 // DeepSeek-V2 and V3 in llama.cpp's MLA layout (IR_SPEC Appendix D.11; revision 1.6's leading dense blocks, V3's
817 // router and YaRN): the query from attn_q (V2-Lite) or the low-rank attn_q_a / attn_q_b; attn_kv_a_mqa h split
818 // into the latent c and the shared rotary key kr; mla_attention over a kv slot holding [c | kr]; the first
819 // leading_dense_block_count applications a dense SwiGLU, the rest moe_ffn (times expert_weights_scale) plus
820 // ungated shared experts, the two branches selected by apply. Files converted before llama.cpp's MLA keys,
821 // other gating functions and other RoPE scaling build no graph.
822 const int H = static_cast<int>(get("attention.head_count"));
823 ctx = static_cast<int>(get("context_length"));
824 const double eps = get("attention.layer_norm_rms_epsilon");
825 if (!shape_has("blk.0.attn_k_b.weight") || !meta_number(g, p + "attention.key_length_mla"))
826 throw std::runtime_error("IR builder: deepseek2 file without attn_k_b / attention.key_length_mla (converted "
827 "before llama.cpp's MLA layout) builds no graph");
828 double gating = get("expert_gating_func", 1.0);
829 if (gating == 0.0) gating = 1.0;
830 if (gating != 1.0 && gating != 2.0)
831 throw std::runtime_error("IR builder: deepseek2 with expert_gating_func " + std::to_string(gating) +
832 " (neither softmax 1 nor sigmoid 2) builds no graph");
833 const auto stype = meta_string(g, p + "rope.scaling.type");
834 if (shape_has("rope_freqs.weight") || (stype && *stype != "none" && *stype != "yarn"))
835 throw std::runtime_error("IR builder: deepseek2 with RoPE scaling other than YaRN builds no graph");
836 const bool yarn = stype && *stype == "yarn";
837 const int k = static_cast<int>(get("leading_dense_block_count", 0.0));
838 if (k < 0 || k > n_layers)
839 throw std::runtime_error("IR builder: deepseek2 leading_dense_block_count " + std::to_string(k) + " exceeds block_count");
840 const std::string M = "blk." + std::to_string(k) + "."; // the first mixture application's tensors
841 const int dr = static_cast<int>(get("rope.dimension_count"));
842 const int dn = static_cast<int>(get("attention.key_length_mla")) - dr;
843 const int dv = static_cast<int>(get("attention.value_length_mla"));
844 const int r = static_cast<int>(get("attention.kv_lora_rank"));
845 const double theta = get("rope.freq_base", 10000.0);
846 const std::string gate_norm = truth("expert_weights_norm", false) ? "renorm" : "raw";
847 double s = get("expert_weights_scale", 1.0);
848 if (s == 0.0) s = 1.0;
849 // YaRN (IR_SPEC 5.14, revision 1.6): pair j's divisor f_j = 1 / ((1 - e_j) / c + e_j), e_j = 1 - ramp(j), the
850 // ramp linear from the correction dimension of beta_fast = 32 rotations over the original context (floor) to
851 // that of beta_slow = 1 (ceiling), within 0 .. dr - 1; in binary64, each value rounded to f32. The softmax
852 // temperature m = 1 + yarn_log_multiplier ln(c) enters the attention scale as m^2.
853 json yarn_f = nullptr;
854 double mscale = 1.0;
855 if (yarn) {
856 const double c = get("rope.scaling.factor"), o = get("rope.scaling.original_context_length");
857 const double pi = 3.14159265358979323846;
858 auto corr = [&](double beta) { return dr * std::log(o / (beta * 2.0 * pi)) / (2.0 * std::log(theta)); };
859 double lo = std::max(std::floor(corr(32.0)), 0.0), hi = std::min(std::ceil(corr(1.0)), static_cast<double>(dr - 1));
860 if (lo == hi) hi += 0.001;
861 yarn_f = json::array();
862 for (int j = 0; j < dr / 2; ++j) {
863 const double ramp = std::min(std::max((j - lo) / (hi - lo), 0.0), 1.0);
864 const double e = 1.0 - ramp;
865 yarn_f.push_back(f32_value(1.0 / ((1.0 - e) / c + e)));
866 }
867 mscale = 1.0 + get("rope.scaling.yarn_log_multiplier", 0.0) * std::log(c);
868 }
869 nodes = json::array({rmsnorm("x", B + "attn_norm.weight", eps, "h")});
870 const bool q_lora = shape_has("blk.0.attn_q_a.weight");
871 if (q_lora) {
872 for (json n : {linear("h", B + "attn_q_a.weight", "qa"), rmsnorm("qa", B + "attn_q_a_norm.weight", eps, "qa"),
873 linear("qa", B + "attn_q_b.weight", "q")})
874 nodes.push_back(n);
875 } else {
876 nodes.push_back(linear("h", B + "attn_q.weight", "q"));
877 }
878 json rope_node = json{{"op", "rope"}, {"in", {"qr", "kr", "pos"}}, {"heads", H}, {"kv_heads", 1}, {"head_dim", dr},
879 {"n_rot", dr}, {"theta", f32_value(theta)}, {"rope_heads", "all"}, {"pairing", "interleaved"},
880 {"out", {"qr", "kr"}}};
881 if (yarn) rope_node["freq_factors"] = yarn_f;
882 for (json n : {op("split", {"q"}, {"qn", "qr"}, {{"sizes", {dn, dr}}, {"groups", H}}),
883 linear("h", B + "attn_kv_a_mqa.weight", "ckr"), op("split", {"ckr"}, {"c", "kr"}, {{"sizes", {r, dr}}}),
884 rmsnorm("c", B + "attn_kv_a_norm.weight", eps, "c"), rope_node,
885 json{{"op", "mla_attention"}, {"in", {"qn", "qr", "c", "kr"}}, {"state", "kv"}, {"heads", H},
886 {"kv_rank", r}, {"qk_nope", dn}, {"qk_rope", dr}, {"v_head_dim", dv},
887 {"k_b", B + "attn_k_b.weight"}, {"v_b", B + "attn_v_b.weight"},
888 {"scale", f32_value(mscale * mscale / std::sqrt(static_cast<double>(dn + dr)))},
889 {"causal", true}, {"window", 0}, {"sinks", 0}, {"out", "a"}},
890 linear("a", B + "attn_output.weight", "o"), op("add", {"x", "o"}, "x"),
891 rmsnorm("x", B + "ffn_norm.weight", eps, "h2")})
892 nodes.push_back(n);
893 // apply (IR_SPEC 3.8): with 0 < k < L the dense branch runs in applications i < k, the mixture in i >= k.
894 const bool split_layers = k > 0 && k < n_layers;
895 auto applied = [&](json n, bool dense) {
896 if (split_layers) {
897 json pl = json::array();
898 for (int i = 0; i < n_layers; ++i) pl.push_back(dense ? i < k : i >= k);
899 n["apply"] = {{"per_layer", pl}};
900 }
901 return n;
902 };
903 if (k > 0)
904 for (json n : {linear("h2", B + "ffn_gate.weight", "g"), linear("h2", B + "ffn_up.weight", "u"),
905 op("silu", {"g"}, "g"), op("mul", {"g", "u"}, "gu"), linear("gu", B + "ffn_down.weight", "f")})
906 nodes.push_back(applied(n, true));
907 const bool shexp = k < n_layers && shape_has(M + "ffn_gate_shexp.weight");
908 std::string router_note;
909 if (k < n_layers) {
911 if (gating == 2.0) moe["scoring"] = "sigmoid";
912 if (shape_has(M + "exp_probs_b.bias")) moe["selection_bias"] = B + "exp_probs_b.bias";
913 const int gcount = static_cast<int>(get("expert_group_count", 1.0));
914 if (gcount > 1) moe["groups"] = {{"n", gcount}, {"top", static_cast<int>(get("expert_group_used_count", 1.0))}};
915 router_note = (moe.contains("scoring") ? ", sigmoid scores" : "") +
916 std::string(moe.contains("selection_bias") ? ", selection bias" : "") +
917 (moe.contains("groups") ? ", " + std::to_string(gcount) + " groups" : std::string());
918 nodes.push_back(applied(moe, false));
919 if (s != 1.0) nodes.push_back(applied(op("affine", {"f"}, "f", {{"scale", f32_value(s)}, {"shift", 0.0}}), false));
920 if (shexp) {
921 json sh = json::array();
923 for (const auto& n : sh) nodes.push_back(applied(n, false));
924 nodes.push_back(applied(op("add", {"f", "sf"}, "f"), false));
925 }
926 }
927 nodes.push_back(op("add", {"x", "f"}, "x"));
928 head = tied_head(json::array({rmsnorm("x", "output_norm.weight", eps, "h")}));
929 state = json::array({{{"name", "kv"}, {"kind", "kv"}, {"heads", H}, {"kv_heads", 1}, {"head_dim", r + dr},
930 {"max_len", ctx}, {"window", 0}, {"sinks", 0}, {"dtype", "f32"}}});
931 summary = "MLA: heads " + std::to_string(H) + ", kv_rank " + std::to_string(r) + ", qk_nope " + std::to_string(dn) +
932 ", qk_rope " + std::to_string(dr) + ", v_head_dim " + std::to_string(dv) +
933 (q_lora ? ", low-rank query" : "") + (yarn ? ", YaRN" : "") +
934 (k > 0 ? ", " + std::to_string(k) + " leading dense blocks" : std::string()) +
935 (k < n_layers ? ", moe_ffn " + std::to_string(static_cast<int>(get("expert_count"))) + " experts (" +
936 gate_norm + router_note + (s != 1.0 ? ", x " + json(f32_value(s)).dump() : std::string()) +
937 ")" + (shexp ? " + shared experts" : "")
938 : std::string());
939 } else if (arch == "phi3") {
940 // IR_SPEC Appendix D.8 (revision 1.5): pre-RMSNorm, fused (or separate) q/k/v, NEOX RoPE over
941 // rope.dimension_count dims with LongRoPE's factor sets when the file has them, HF's sliding window, SwiGLU
942 // from the fused ffn_up (gate | up), output or the tied embedding.
943 const int H = static_cast<int>(get("attention.head_count"));
944 const int KV = static_cast<int>(get("attention.head_count_kv", H));
945 const int hd = static_cast<int>(get("attention.key_length", d / H));
946 const int n_rot = static_cast<int>(get("rope.dimension_count", hd));
947 const int ff = static_cast<int>(get("feed_forward_length"));
948 ctx = static_cast<int>(get("context_length"));
949 const double eps = get("attention.layer_norm_rms_epsilon");
950 const double theta = get("rope.freq_base", 10000.0);
951 const int window = static_cast<int>(get("attention.sliding_window", 0.0));
953 if (shape_has("blk.0.ffn_gate_inp.weight"))
954 throw std::runtime_error("IR builder: phi3 with a mixture of experts (Phi-3.5-MoE's layout) is not built");
955 const bool fs = shape_has("rope_factors_short.weight"), fl = shape_has("rope_factors_long.weight");
956 if (fs != fl)
957 throw std::runtime_error("IR builder: phi3 with one LongRoPE factor set (rope_factors_short / _long) builds no graph");
958 json r = rope(H, KV, hd, n_rot, f32_value(theta), "neox");
959 double m = 1.0;
960 if (fs) {
961 r["freq_factors"] = {{"short", "rope_factors_short.weight"},
962 {"long", "rope_factors_long.weight"},
963 {"original_ctx", static_cast<int>(get("rope.scaling.original_context_length"))}};
964 m = meta_number(g, p + "rope.scaling.attn_factor").value_or(1.0);
965 if (m == 0.0) m = 1.0;
966 if (m != 1.0) r["attn_factor"] = f32_value(m);
967 }
968 auto bias = [&](const std::string& name) { return shape_has("blk.0." + name + ".bias") ? B + name + ".bias" : ""; };
969 nodes = json::array({rmsnorm("x", B + "attn_norm.weight", eps, "h")});
970 if (shape_has("blk.0.attn_qkv.weight")) {
971 nodes.push_back(linear("h", B + "attn_qkv.weight", "qkv", bias("attn_qkv")));
972 nodes.push_back(op("split", {"qkv"}, {"q", "k", "v"}, {{"sizes", {H * hd, KV * hd, KV * hd}}}));
973 } else {
974 for (const char* t : {"q", "k", "v"})
975 nodes.push_back(linear("h", B + "attn_" + t + ".weight", t, bias(std::string("attn_") + t)));
976 }
977 for (json n : {r, attention(H, KV, hd, 1.0 / std::sqrt(static_cast<double>(hd)), window),
978 linear("a", B + "attn_output.weight", "o", bias("attn_output")), op("add", {"x", "o"}, "x"),
979 rmsnorm("x", B + "ffn_norm.weight", eps, "h2"), linear("h2", B + "ffn_up.weight", "gu2"),
980 op("split", {"gu2"}, {"g", "u"}, {{"sizes", {ff, ff}}}), op("silu", {"g"}, "g"),
981 op("mul", {"g", "u"}, "gu"), linear("gu", B + "ffn_down.weight", "f"), op("add", {"x", "f"}, "x")})
982 nodes.push_back(n);
983 head = tied_head(json::array({rmsnorm("x", "output_norm.weight", eps, "h")}));
984 state = json::array({kv_state(H, KV, hd, ctx, window)});
985 summary = "heads " + std::to_string(H) + "/" + std::to_string(KV) + " x " + std::to_string(hd) + ", NEOX RoPE n_rot " +
986 std::to_string(n_rot) + (fs ? ", LongRoPE (original_ctx " + r["freq_factors"]["original_ctx"].dump() + ")" : "") +
987 (m != 1.0 ? ", attn_factor " + std::to_string(m) : "") + (window ? ", window " + std::to_string(window) : "");
988 } else if (arch == "gemma2") {
989 // IR_SPEC Appendix D.9 (revision 1.5): gemma3's block without q/k norms; one RoPE base; local applications
990 // i % 2 == 0 windowed; the attention scores soft-capped at attn_logit_softcapping, the logits through
991 // tanh(x / C) * C with C = final_logit_softcapping.
992 const int H = static_cast<int>(get("attention.head_count"));
993 const int KV = static_cast<int>(get("attention.head_count_kv", H));
994 const int hd = static_cast<int>(get("attention.key_length", d / H));
995 const int n_rot = static_cast<int>(get("rope.dimension_count", hd));
996 ctx = static_cast<int>(get("context_length"));
997 const double eps = get("attention.layer_norm_rms_epsilon");
998 const double theta = get("rope.freq_base", 10000.0);
1000 auto nonzero = [&](const std::string& key, double dflt) {
1001 const double v = get(key, dflt);
1002 return v == 0.0 ? dflt : v;
1003 };
1004 const int win = static_cast<int>(nonzero("attention.sliding_window", 4096.0));
1005 const double cap = nonzero("attn_logit_softcapping", 50.0), fcap = nonzero("final_logit_softcapping", 30.0);
1006 json windows = json::array();
1007 for (int i = 0; i < n_layers; ++i) windows.push_back(i % 2 == 0 ? win : 0);
1008 windows = {{"per_layer", windows}};
1009 const double scale = n_layers == 46 ? 1.0 / std::sqrt(static_cast<double>(d) / H)
1010 : 1.0 / std::sqrt(static_cast<double>(hd));
1012 att["softcap"] = f32_value(cap);
1013 // C: the shortest decimal that reads back as the f32 nearest fcap, written as Python's repr of that float
1014 // (an integral value with ".0").
1015 std::string C;
1016 {
1017 char buf[64];
1018 const auto res = std::to_chars(buf, buf + sizeof(buf), f32_value(fcap));
1019 C.assign(buf, res.ptr);
1020 if (C.find_first_of(".eEn") == std::string::npos) C += ".0";
1021 }
1022 embed.push_back(op("affine", {"x"}, "x", {{"scale", f32_value(std::sqrt(static_cast<double>(d)))}, {"shift", 0.0}}));
1023 nodes = json::array({rmsnorm("x", B + "attn_norm.weight", eps, "h"), linear("h", B + "attn_q.weight", "q"),
1024 linear("h", B + "attn_k.weight", "k"), linear("h", B + "attn_v.weight", "v"),
1025 rope(H, KV, hd, n_rot, f32_value(theta), "neox"), att,
1026 linear("a", B + "attn_output.weight", "o"),
1027 rmsnorm("o", B + "post_attention_norm.weight", eps, "o"), op("add", {"x", "o"}, "x"),
1028 rmsnorm("x", B + "ffn_norm.weight", eps, "h2"), linear("h2", B + "ffn_gate.weight", "g"),
1029 linear("h2", B + "ffn_up.weight", "u"), op("gelu", {"g"}, "g", {{"approx", "tanh"}}),
1030 op("mul", {"g", "u"}, "gu"), linear("gu", B + "ffn_down.weight", "f"),
1031 rmsnorm("f", B + "post_ffw_norm.weight", eps, "f"), op("add", {"x", "f"}, "x")});
1032 head = json::array({rmsnorm("x", "output_norm.weight", eps, "h"),
1033 linear("h", "output.weight", "z", "", tied ? "token_embd.weight" : ""),
1034 op("expr", {"z"}, "logits", {{"expr", "tanh(x / " + C + ") * " + C}})});
1035 state = json::array({kv_state(H, KV, hd, ctx, windows)});
1036 summary = "heads " + std::to_string(H) + "/" + std::to_string(KV) + " x " + std::to_string(hd) +
1037 ", sandwich RMSNorms, GeGLU, window " + std::to_string(win) + " on even layers, attention softcap " +
1038 std::to_string(cap) + ", final softcap " + C;
1039 } else { // mamba
1040 const int K = static_cast<int>(get("ssm.conv_kernel")), Di = static_cast<int>(get("ssm.inner_size"));
1041 const int N = static_cast<int>(get("ssm.state_size")), R = static_cast<int>(get("ssm.time_step_rank"));
1042 const double eps = get("attention.layer_norm_rms_epsilon");
1043 ctx = static_cast<int>(get("context_length", 0.0));
1044 if (ctx == 0) ctx = 1048576; // llama.cpp writes 1M: a recurrence has no length limit
1045 if (!shape_has("blk.0.ssm_dt.weight") || !shape_has("blk.0.ssm_d"))
1046 throw std::runtime_error("IR builder: mamba file lacks ssm_dt.weight or ssm_d");
1047 if (meta_bool(g, p + "ssm.dt_b_c_rms").value_or(false) || shape_has("blk.0.ssm_dt_norm.weight"))
1048 throw std::runtime_error("IR builder: mamba with dt/B/C RMSNorms (Falcon-Mamba) is not built yet");
1049 nodes = json::array(
1050 {rmsnorm("x", B + "attn_norm.weight", eps, "h"), linear("h", B + "ssm_in.weight", "xz"),
1051 op("split", {"xz"}, {"xs", "z"}, {{"sizes", {Di, Di}}}),
1052 op("conv1d_causal", {"xs"}, "xs",
1053 {{"state", "conv"}, {"weight", B + "ssm_conv1d.weight"}, {"bias", B + "ssm_conv1d.bias"},
1054 {"channels", Di}, {"kernel", K}}),
1055 op("silu", {"xs"}, "xs"), linear("xs", B + "ssm_x.weight", "xdb"),
1056 op("split", {"xdb"}, {"dt", "bm", "cm"}, {{"sizes", {R, N, N}}}),
1057 linear("dt", B + "ssm_dt.weight", "dt", B + "ssm_dt.bias"),
1058 op("selective_scan", {"xs", "dt", "bm", "cm"}, "y",
1059 {{"state", "ssm"}, {"a", B + "ssm_a"}, {"d", B + "ssm_d"}, {"channels", Di}, {"d_state", N},
1060 {"dt_softplus", true}}),
1061 op("silu", {"z"}, "z"), op("mul", {"y", "z"}, "y"), linear("y", B + "ssm_out.weight", "o"),
1062 op("add", {"x", "o"}, "x")});
1063 state = json::array({{{"name", "conv"}, {"kind", "recurrent"}, {"op", "conv1d_causal"}, {"dtype", "f32"},
1064 {"tensors", json::array({{{"name", "x"}, {"shape", {K - 1, Di}}, {"init", 0}}})}},
1065 {{"name", "ssm"}, {"kind", "recurrent"}, {"op", "selective_scan"}, {"dtype", "f32"},
1066 {"tensors", json::array({{{"name", "s"}, {"shape", {Di, N}}, {"init", 0}}})}}});
1067 head = tied_head(json::array({rmsnorm("x", "output_norm.weight", eps, "h")}));
1068 summary = "selective scan d_inner " + std::to_string(Di) + ", d_state " + std::to_string(N) + ", dt_rank " +
1069 std::to_string(R) + ", conv kernel " + std::to_string(K);
1070 }
1071
1072 const auto eos = meta_number(g, "tokenizer.ggml.eos_token_id");
1073 const auto bos = meta_number(g, "tokenizer.ggml.bos_token_id");
1074 json doc;
1075 doc["_comment"] = "built by ember's build_ir_from_gguf (graph_ir.cpp; paperkiln tools/gguf_ir.py's rules) from "
1076 "general.architecture = " + arch + " and its metadata";
1077 doc["ir_version"] = 1; // IR_SPEC 2.3: as tools/gguf_ir.py's IR_VERSION from paperkiln 5349ee19 on
1078 doc["family_hint"] = arch;
1079 doc["model"] = {{"vocab", vocab}, {"d", d}, {"n_layers", n_layers}, {"ctx", ctx}, {"dtype", "f32"}};
1080 doc["tokenizer"] = {{"type", "gguf"},
1081 {"source", "gguf"},
1082 {"specials", eos ? json{{"eot", static_cast<int64_t>(*eos)}} : json::object()},
1083 {"bos", bos ? json(static_cast<int64_t>(*bos)) : json(nullptr)}};
1084 doc["inputs"] = json::array({{{"name", "tokens"}, {"kind", "token_ids"}}, {{"name", "pos"}, {"kind", "positions"}}});
1085 if (arch == "mamba") doc["inputs"] = json::array({{{"name", "tokens"}, {"kind", "token_ids"}}});
1086 doc["embed"] = {{"nodes", embed}};
1087 doc["block"] = {{"repeat", n_layers}, {"state", state}, {"nodes", nodes}};
1088 doc["head"] = {{"nodes", head}};
1089 doc["outputs"] = json::array({{{"name", "logits"}, {"kind", "logits"}}});
1090 doc["modes"] = {{"prefill", "all nodes over T tokens"},
1091 {"decode", "the same graph with T = 1, attention reads and appends to the kv state, a recurrent op "
1092 "reads and replaces its recurrent state"}};
1093 if (log)
1094 Logger::info("[IR] built the IR for general.architecture = " + arch + " from its metadata: d " + std::to_string(d) +
1095 ", " + std::to_string(n_layers) + " layers, " + summary + (tied ? ", tied head" : ""));
1096 return doc.dump();
1097}
1098
1099Interpreter::Interpreter(const std::string& ir_json, const GGUFData& gguf, bool use_gpu) {
1100 const json j = parse_ir_document(ir_json); // a member name twice is refused (IR_SPEC 3.2, rule 35)
1101 try {
1103 } catch (const json::exception&) {
1104 // ember's own checks read the document as its schema shapes it: a document that is not is
1105 // refused naming the rules it breaks (IR_SPEC 10.1), not a JSON accessor's message.
1106 validate_ir(j, gguf);
1107 throw;
1108 }
1109}
1110
1111void Interpreter::init(const json& j, const std::string& ir_json, const GGUFData& gguf, bool use_gpu) {
1112 const int version = j.at("ir_version").get<int>();
1113 // 1 is spec/IR_SPEC.md's first normative text of the same document as 0 (paperkiln, 25 Oct 2026)
1114 if (version != 0 && version != 1)
1115 throw std::runtime_error("IR version " + std::to_string(version) + " is not supported (ember reads 0 and 1)");
1116 family_hint_ = j.value("family_hint", "");
1117 embed_ = parse_nodes(j.at("embed"), "embed");
1118 block_ = parse_nodes(j.at("block"), "block");
1119 head_ = parse_nodes(j.at("head"), "head");
1120 for (const auto& n : head_)
1121 if (!kRowWiseOps.count(n.op)) throw std::runtime_error("IR head op '" + n.op + "' is not row-wise");
1122 n_layers_ = j.at("block").at("repeat").get<int>();
1123 if (n_layers_ < 1) throw std::runtime_error("IR_SPEC 3.3: block.repeat " + std::to_string(n_layers_) + " is not a positive integer");
1124 // Tied-block recursion (paperkiln ir.hpp weight_index): application i reads
1125 // stored layer m_i's tensors; its state slots, kv cache and per-layer
1126 // attributes stay its own (indexed by i). Stored layers are 0 .. max m_i, each
1127 // used, and their count is the file's <arch>.block_count.
1128 if (j.at("block").contains("weight_index")) {
1129 const json& wi = j.at("block").at("weight_index");
1130 if (!is_per_layer(wi) || !wi.at("per_layer").is_array())
1131 throw std::runtime_error("IR block weight_index must be {\"per_layer\": [m_0, ..]}");
1132 for (const auto& e : wi.at("per_layer")) {
1133 if (!e.is_number_integer() || e.get<int>() < 0)
1134 throw std::runtime_error("IR block weight_index entries must be integers >= 0");
1135 weight_map_.push_back(e.get<int>());
1136 }
1137 if (static_cast<int>(weight_map_.size()) != n_layers_)
1138 throw std::runtime_error("IR block weight_index has " + std::to_string(weight_map_.size()) + " entries for " +
1139 std::to_string(n_layers_) + " block applications");
1140 const int stored = *std::max_element(weight_map_.begin(), weight_map_.end()) + 1;
1141 std::vector<bool> used(static_cast<size_t>(stored), false);
1142 for (int m : weight_map_) used[m] = true;
1143 for (int m = 0; m < stored; ++m)
1144 if (!used[m]) throw std::runtime_error("IR block weight_index never uses stored layer " + std::to_string(m));
1145 const std::string arch = meta_string(gguf, "general.architecture").value_or("");
1146 if (const auto bc = meta_number(gguf, arch + ".block_count"); bc && static_cast<int>(*bc) != stored)
1147 throw std::runtime_error("IR block weight_index uses " + std::to_string(stored) + " stored layers, the file's " +
1148 arch + ".block_count is " + std::to_string(static_cast<int>(*bc)));
1149 std::string m;
1150 for (int v : weight_map_) m += (m.empty() ? "" : ", ") + std::to_string(v);
1151 Logger::info("[IR] tied-block recursion: " + std::to_string(n_layers_) + " applications of " +
1152 std::to_string(stored) + " stored layers, weight_index [" + m + "]");
1153 }
1154 vocab_ = j.at("model").at("vocab").get<int>();
1155 max_len_ = j.at("model").value("ctx", 0);
1156
1157 bool has_kv = false, kv_rows_active = false, evict_declared = false;
1158 for (const auto& s : j.at("block").value("state", json::array())) {
1159 const std::string kind = s.at("kind").get<std::string>();
1160 if (kind == "recurrent") {
1161 RecSlot slot{s.at("name").get<std::string>(), s.at("op").get<std::string>(), {}};
1162 if (!kRecurrentOps.count(slot.op))
1163 throw std::runtime_error("IR recurrent state '" + slot.name + "': op '" + slot.op + "' is not a recurrent op");
1164 if (s.value("dtype", "f32") != "f32")
1165 throw std::runtime_error("IR recurrent state '" + slot.name + "': dtype must be f32");
1166 if (!s.contains("tensors") || s.at("tensors").empty())
1167 throw std::runtime_error("IR recurrent state '" + slot.name + "' declares no tensors");
1168 for (const auto& t : s.at("tensors")) {
1169 RecTensor rt;
1170 rt.name = t.at("name").get<std::string>();
1171 rt.shape = t.at("shape").get<std::vector<int64_t>>();
1172 rt.floats = 1;
1173 for (int64_t e : rt.shape) {
1174 if (e <= 0) throw std::runtime_error("IR recurrent state '" + slot.name + "': bad shape for " + rt.name);
1175 rt.floats *= static_cast<size_t>(e);
1176 }
1177 // init 0, or "body" for op recurrence: the node's init assigns it (IR_SPEC 6.10.1; set after binding)
1178 if (t.contains("init") && t.at("init").is_string()) {
1179 if (slot.op != "recurrence" || t.at("init") != "body")
1180 throw std::runtime_error("IR recurrent state '" + slot.name + "': init " + t.at("init").dump() + " for " +
1181 rt.name + " (0, or \"body\" for op recurrence; IR_SPEC 6.3, 10.1 rule 13)");
1182 rt.init = 0.0f;
1183 } else {
1184 rt.init = t.value("init", 0.0f);
1185 }
1186 rt.offset = rec_floats_;
1187 rec_floats_ += rt.floats;
1188 slot.tensors.push_back(std::move(rt));
1189 }
1190 for (const auto& other : rec_slots_)
1191 if (other.name == slot.name) throw std::runtime_error("IR recurrent state '" + slot.name + "' is declared twice");
1192 rec_slots_.push_back(std::move(slot));
1193 continue;
1194 }
1195 if (kind != "kv") throw std::runtime_error("IR state kind '" + kind + "' has no CPU implementation in ember");
1196 has_kv = true;
1197 kv_slot_ = s.value("name", "kv");
1198 if (s.contains("kv_from")) throw std::runtime_error("IR kv state: kv_from (recursive KV sharing) is not implemented in ember");
1199 if (s.contains("kv_rows")) {
1200 if (s.at("kv_rows") != "active")
1201 throw std::runtime_error("IR kv state: kv_rows '" + s.at("kv_rows").dump() + "' is not implemented in ember");
1202 kv_rows_active = true;
1203 }
1204 if (has(s, "evict")) {
1205 if (s.at("evict") != "turn_state")
1206 throw std::runtime_error("IR kv state: evict " + s.at("evict").dump() + " is not implemented in ember (turn_state only)");
1207 evict_declared = true;
1208 }
1209 for (const char* k : {"heads", "kv_heads", "head_dim", "max_len"})
1210 if (has(s, k) && is_per_layer(s.at(k)))
1211 throw std::runtime_error(std::string("IR kv state: a per-layer '") + k + "' is not implemented in ember");
1212 kv_heads_ = s.at("kv_heads").get<int>();
1213 head_dim_ = s.at("head_dim").get<int>();
1214 max_len_ = s.at("max_len").get<int>();
1215 // Sliding window + sinks (paperkiln swa_attention). The cache keeps every
1216 // position up to max_len, which holds the first `sinks` and the last
1217 // `window` rows the rule needs.
1218 if (has(s, "window") && is_per_layer(s.at("window"))) {
1219 window_layer_ = s.at("window").at("per_layer").get<std::vector<int>>();
1220 if (static_cast<int>(window_layer_.size()) != n_layers_)
1221 throw std::runtime_error("IR kv state: per-layer window has " + std::to_string(window_layer_.size()) +
1222 " entries for " + std::to_string(n_layers_) + " blocks");
1223 window_ = *std::max_element(window_layer_.begin(), window_layer_.end());
1224 } else {
1225 window_ = s.value("window", 0);
1226 }
1227 if (has(s, "sinks") && is_per_layer(s.at("sinks")))
1228 throw std::runtime_error("IR kv state: per-layer sinks are not implemented in ember");
1229 sinks_ = s.value("sinks", 0);
1230 }
1231 for (const auto& n : block_) {
1232 if (n.op != "attention" && n.op != "mla_attention") continue;
1233 if (n.op == "mla_attention" && !(n.attrs.contains("causal") && n.attrs.at("causal") == json(true)))
1234 throw std::runtime_error("IR_SPEC 5.20 (rule 14): mla_attention causal must be true");
1235 if (!n.attrs.value("causal", true))
1236 throw std::runtime_error("IR attention with causal=false is not implemented in ember");
1237 for (int l = 0; l < n_layers_; ++l) {
1238 int w = 0, s = 0;
1239 attention_window(n, l, w, s);
1240 if (w < 0 || s < 0) throw std::runtime_error("IR attention window/sinks must be non-negative");
1241 // A windowed kv state declares what its cache holds; an attention node
1242 // reading it with another window or sink count is an exporter error.
1243 const int kvw = window_layer_.empty() ? window_ : window_layer_[l];
1244 if (kvw > 0 && (w != kvw || s != sinks_))
1245 throw std::runtime_error("IR attention window/sinks (" + std::to_string(w) + ", " + std::to_string(s) +
1246 ") in block " + std::to_string(l) + " differ from its kv state's (" +
1247 std::to_string(kvw) + ", " + std::to_string(sinks_) + ")");
1248 if (w > 0 && (l == 0 || window_layer_.size() > 0))
1249 Logger::info("[IR] sliding-window attention: block " + std::to_string(l) + " window " + std::to_string(w) +
1250 ", sinks " + std::to_string(s));
1251 }
1252 }
1253 for (const auto* sub : {&embed_, &block_, &head_})
1254 for (const auto& n : *sub)
1255 if (n.op == "gelu" && n.attrs.value("approx", "none") != "tanh" && n.attrs.value("approx", "none") != "none")
1256 throw std::runtime_error("IR gelu approx '" + n.attrs.value("approx", "") + "' is not implemented in ember");
1257 // The gating ops' attributes and arities (paperkiln ir.hpp): checked here so
1258 // a malformed node fails at load, not mid-generation.
1259 for (const auto* sub : {&embed_, &block_, &head_})
1260 for (const auto& n : *sub) {
1261 auto arity = [&](size_t k, bool at_least) {
1262 if (at_least ? n.in.size() < k : n.in.size() != k)
1263 throw std::runtime_error("IR " + n.op + " needs " + (at_least ? "at least " : "") + std::to_string(k) +
1264 " input(s), has " + std::to_string(n.in.size()));
1265 if (n.out.size() != 1) throw std::runtime_error("IR " + n.op + " needs one output");
1266 };
1267 if (n.op == "sub" || n.op == "mul_col" || n.op == "append") arity(2, false);
1268 if (n.op == "relu" || n.op == "sigmoid" || n.op == "affine" || n.op == "rms_row" || n.op == "scale") arity(1, false);
1269 if (n.op == "scale" && !has(n.attrs, "weight")) throw std::runtime_error("IR scale needs its weight [d]");
1270 if (n.op == "list" || n.op == "depth_attend") arity(1, true);
1271 if (n.op == "affine" && (!has(n.attrs, "scale") || !has(n.attrs, "shift")))
1272 throw std::runtime_error("IR affine needs float attributes scale and shift");
1273 if (n.op == "rms_row" && !has(n.attrs, "eps")) throw std::runtime_error("IR rms_row needs its eps attribute");
1274 for (const auto& kv : n.attrs.items()) { // per-layer attributes: block nodes, the attributes ember reads per block
1275 if (!is_per_layer(kv.value())) continue;
1276 const bool ok = sub == &block_ && (((n.op == "attention" || n.op == "mla_attention") &&
1277 (kv.key() == "window" || kv.key() == "sinks")) ||
1278 (n.op == "attention" && kv.key() == "softcap") ||
1279 (n.op == "rope" && (kv.key() == "theta" || kv.key() == "freq_scale" ||
1280 kv.key() == "attn_factor")) ||
1281 (n.op == "affine" && (kv.key() == "scale" || kv.key() == "shift")) ||
1282 (n.op == "depth_route" && kv.key() == "step") ||
1283 (n.op == "moe_ffn" && kv.key() == "scoring") ||
1284 kv.key() == "apply"); // revision 1.6 (IR_SPEC 3.8): any stateless node
1285 if (ok && kv.key() == "apply") // its entries are true or false (IR_SPEC 3.8, rule 43, revision 1.6)
1286 for (const auto& e : kv.value().at("per_layer"))
1287 if (!e.is_boolean())
1288 throw std::runtime_error("IR_SPEC 3.8 (rule 43): block " + n.op + ": apply entry " + e.dump() +
1289 " is not true or false");
1290 if (!ok && n.op == "rope" && kv.key() == "freq_factors") // structural (IR_SPEC 3.8, revision 1.5)
1291 throw std::runtime_error("IR_SPEC 3.8 (rule 6): rope freq_factors is structural and cannot be per-layer");
1292 if (!ok)
1293 throw std::runtime_error("IR " + n.op + ": a per-layer '" + kv.key() + "' is not implemented in ember");
1294 if (static_cast<int>(kv.value().at("per_layer").size()) != n_layers_)
1295 throw std::runtime_error("IR " + n.op + ": per-layer '" + kv.key() + "' has " +
1296 std::to_string(kv.value().at("per_layer").size()) + " entries for " +
1297 std::to_string(n_layers_) + " blocks");
1298 }
1299 if (n.op == "expr") { // a runtime-defined op: parsed here, so a bad string fails at load naming its column
1300 arity(1, true);
1301 if (!n.attrs.contains("expr") || !n.attrs.at("expr").is_string())
1302 throw std::runtime_error("IR expr needs its string attribute expr");
1303 try {
1304 expr::cached(n.attrs.at("expr").get<std::string>(), n.in.size());
1305 } catch (const std::invalid_argument& e) {
1306 throw std::runtime_error(std::string("IR ") + e.what());
1307 }
1308 }
1309 }
1310#ifdef HAS_CUDA
1312 throw std::runtime_error("IR uses an op or a shape that has no CUDA kernel (EMBER_IR_DEVICE=cpu runs it)");
1313#endif
1314 // Multi-head latent attention (IR_SPEC 5.20): its kv slot holds [c | kr] per position (kv_heads 1, head_dim
1315 // r + dr), and its rope rotates qr and kr, dr wide per head, not the slot's head_dim.
1316 bool mla = false;
1317 for (const auto& n : block_) mla = mla || n.op == "mla_attention";
1318 for (const auto& n : block_) {
1319 if (n.op == "mla_attention") { // IR_SPEC 5.20: the slot holds c and kr, r + dr values per position (rule 12)
1320 for (const char* k : {"heads", "kv_rank", "qk_nope", "qk_rope", "v_head_dim"})
1321 if (!n.attrs.contains(k) || !n.attrs.at(k).is_number_integer() || n.attrs.at(k).get<int>() <= 0)
1322 throw std::runtime_error(std::string("IR_SPEC 5.20 (rule 8): mla_attention needs a positive integer ") + k);
1323 for (const char* k : {"k_b", "v_b"})
1324 if (!n.attrs.contains(k) || !n.attrs.at(k).is_string())
1325 throw std::runtime_error(std::string("IR_SPEC 5.20 (rule 8): mla_attention needs its tensor ") + k);
1326 const int r = n.attrs.at("kv_rank").get<int>(), dr = n.attrs.at("qk_rope").get<int>();
1327 if (kv_heads_ != 1 || head_dim_ != r + dr)
1328 throw std::runtime_error("IR_SPEC 5.20 (rule 12): mla_attention reads a kv slot of kv_heads " +
1329 std::to_string(kv_heads_) + " and head_dim " + std::to_string(head_dim_) +
1330 "; it holds c and kr: kv_heads 1, head_dim kv_rank + qk_rope = " + std::to_string(r + dr));
1331 }
1332 }
1333 for (const auto& n : block_) {
1334 if (n.op == "rope") {
1335 const std::string pairing = n.attrs.value("pairing", "interleaved");
1336 if (pairing != "interleaved" && pairing != "neox")
1337 throw std::runtime_error("IR rope pairing '" + pairing + "' is not implemented in ember");
1338 if (n.attrs.value("rope_heads", "all") != "all")
1339 throw std::runtime_error("IR rope_heads '" + n.attrs.value("rope_heads", "") + "' is not implemented in ember");
1340 if (kv_heads_ > 0 && has(n.attrs, "kv_heads") && n.attrs.at("kv_heads").get<int>() != kv_heads_)
1341 throw std::runtime_error("IR rope kv_heads differs from its kv state's");
1342 // The table is the kv state's head_dim wide, as before, unless the node
1343 // names its own n_rot (partial rotary) or NEOX pairing: then n_rot
1344 // (default head_dim) must fit the kv state's heads.
1345 const int hd = n.attrs.at("head_dim").get<int>();
1346 int n_rot = head_dim_;
1347 if (has(n.attrs, "n_rot") || pairing == "neox" || mla) {
1348 n_rot = has(n.attrs, "n_rot") ? n.attrs.at("n_rot").get<int>() : hd;
1350 throw std::runtime_error("IR rope: n_rot " + std::to_string(n_rot) + " / head_dim " + std::to_string(hd) +
1351 " (kv state head_dim " + std::to_string(head_dim_) + ") is not usable");
1352 }
1353 std::vector<float> thetas(static_cast<size_t>(n_layers_));
1354 std::vector<float> scales(static_cast<size_t>(n_layers_), 1.0f), attn(static_cast<size_t>(n_layers_), 1.0f);
1355 for (int l = 0; l < n_layers_; ++l) {
1356 thetas[l] = at_layer(n.attrs.at("theta"), l).get<float>();
1357 if (has(n.attrs, "freq_scale")) scales[l] = at_layer(n.attrs.at("freq_scale"), l).get<float>();
1358 if (!(scales[l] > 0.0f)) throw std::runtime_error("IR rope freq_scale must be positive");
1359 // LongRoPE's / YaRN's attention scaling (IR_SPEC 5.14, revision 1.5): the table holds cos * m and sin * m.
1360 if (has(n.attrs, "attn_factor") && at_layer(n.attrs.at("attn_factor"), l).is_number())
1361 attn[l] = at_layer(n.attrs.at("attn_factor"), l).get<float>();
1362 }
1363 const json ff = has(n.attrs, "freq_factors") ? n.attrs.at("freq_factors") : json();
1364 if (rope_dim_ != 0 && (rope_theta_layer_ != thetas || rope_scale_layer_ != scales || rope_attn_layer_ != attn ||
1365 rope_dim_ != n_rot || rope_neox_ != (pairing == "neox") || rope_ff_ != ff))
1366 throw std::runtime_error(
1367 "IR rope nodes with different theta, freq_scale, attn_factor, freq_factors, n_rot or pairing are not "
1368 "implemented in ember");
1370 rope_scale_layer_ = scales;
1372 rope_ff_ = ff; // its form is rule 41's (validate_ir below); the tables are built after it
1373 rope_theta_ = thetas[0];
1374 rope_freq_scale_ = scales[0];
1375 rope_dim_ = n_rot;
1376 rope_neox_ = pairing == "neox";
1377 }
1378 }
1379 // The rmsnorm groups (per-head q/k norm) and the attention head counts.
1380 for (const auto& n : block_) {
1381 if (n.op == "rmsnorm" && has(n.attrs, "group") && n.attrs.at("group").get<int>() <= 0)
1382 throw std::runtime_error("IR rmsnorm group must be positive");
1383 if (n.op == "attention") {
1384 const int hq = n.attrs.at("heads").get<int>(), hkv = n.attrs.at("kv_heads").get<int>();
1385 if (hkv <= 0 || hq % hkv != 0)
1386 throw std::runtime_error("IR attention: heads " + std::to_string(hq) + " is not a multiple of kv_heads " +
1387 std::to_string(hkv));
1388 }
1389 }
1390 if (mla) {
1391 for (const auto& n : block_)
1392 if (n.op == "depth_route")
1393 throw std::runtime_error("IR mla_attention under depth_route is not implemented in ember");
1394 }
1395 // Mixture-of-Recursions (ir.hpp depth_route, llama_mor): one depth_route node, the block's
1396 // first, outside it nowhere; its steps one consecutive run of equal-length steps 0 .. R-1; causal
1397 // token choice; a kv state with kv_rows "active"; no recurrent state. A window and sinks count
1398 // positions over each application's rows (IR_SPEC 5.15 and 7.5, v1.4.2, Q16).
1399 for (const auto* sub : {&embed_, &head_})
1400 for (const auto& n : *sub)
1401 if (n.op == "depth_route") throw std::runtime_error("IR depth_route outside the block");
1402 for (size_t k = 0; k < block_.size(); ++k) {
1403 const Node& n = block_[k];
1404 if (n.op != "depth_route") continue;
1405 if (k != 0) throw std::runtime_error("IR depth_route must be the block's first node");
1406 if (n.in.size() != 1 || n.out.size() != 1) throw std::runtime_error("IR depth_route reads [x] and writes one output");
1407 if (n.attrs.value("route", "") != "token_choice")
1408 throw std::runtime_error("IR depth_route route '" + n.attrs.value("route", "") +
1409 "' is not implemented in ember (causal token choice only)");
1410 const std::string gate = n.attrs.value("gate", "");
1411 if (gate != "score" && gate != "none") throw std::runtime_error("IR depth_route gate '" + gate + "' is not score or none");
1412 mor_R_ = n.attrs.at("n_rec").get<int>();
1413 if (mor_R_ < 1) throw std::runtime_error("IR depth_route n_rec must be at least 1");
1414 if (!is_per_layer(n.attrs.at("step"))) throw std::runtime_error("IR depth_route step must be {\"per_layer\": [...]}");
1415 mor_step_ = n.attrs.at("step").at("per_layer").get<std::vector<int>>();
1416 if (static_cast<int>(mor_step_.size()) != n_layers_)
1417 throw std::runtime_error("IR depth_route step has " + std::to_string(mor_step_.size()) + " entries for " +
1418 std::to_string(n_layers_) + " block applications");
1419 // One run of applications with steps 0, 0, .., 1, 1, .., R-1, all steps the same length.
1420 int first = -1, last = -1;
1421 for (int i = 0; i < n_layers_; ++i)
1422 if (mor_step_[i] >= 0) {
1423 if (first < 0) first = i;
1424 last = i;
1425 }
1426 const int span = first < 0 ? 0 : last - first + 1;
1427 if (first < 0 || span % mor_R_ != 0) throw std::runtime_error("IR depth_route step: the recursion is not R equal steps");
1428 for (int i = 0; i < n_layers_; ++i) {
1429 const int want = i < first || i > last ? -1 : (i - first) / (span / mor_R_);
1430 if (mor_step_[i] != want)
1431 throw std::runtime_error("IR depth_route step: application " + std::to_string(i) + " has step " +
1432 std::to_string(mor_step_[i]) + ", expected " + std::to_string(want) +
1433 " (consecutive, ascending, equal-length steps)");
1434 }
1435 mor_gate_score_ = gate == "score";
1436 mor_x_ = n.in[0];
1437 mor_out_ = n.out[0];
1438 mor_router_ = n.attrs.at("weight").get<std::string>();
1439 if (mor_router_.find("{i}") != std::string::npos)
1440 throw std::runtime_error("IR depth_route weight names its tensor in full (no {i})");
1441 }
1442 if (!mor_step_.empty()) {
1443 if (!kv_rows_active) throw std::runtime_error("IR depth_route needs a kv state with kv_rows \"active\"");
1444 if (!rec_slots_.empty()) throw std::runtime_error("IR depth_route with recurrent state is not implemented in ember");
1445 for (const auto& n : block_)
1446 if (n.op == "attention" && has(n.attrs, "position_bias"))
1447 throw std::runtime_error("IR depth_route with attention position_bias is not implemented in ember");
1448 else if (n.op == "attention" && has(n.attrs, "relative_kv"))
1449 throw std::runtime_error("IR depth_route with attention relative_kv is not implemented in ember");
1450 mor_rows_.assign(n_layers_, {});
1451 }
1452 // Per-turn state tokens (paperkiln IR_SPEC_DRAFT.md 1.1, 1.3, 1.4): the extended embedding
1453 // (state_weight, one embedding in the embed subgraph, its name in full), attention "mask"
1454 // "turn_state" (window 0 and sinks 0 only), kv "evict" "turn_state" (every attention node
1455 // turn_state). Not with depth_route (MoR's compact caches) or, for eviction, recurrent state.
1456 for (const auto* sub : {&embed_, &block_, &head_})
1457 for (const auto& n : *sub) {
1458 if (n.op == "embedding" && has(n.attrs, "state_weight")) {
1459 if (sub != &embed_) throw std::runtime_error("IR embedding state_weight outside the embed subgraph");
1460 if (!state_weight_.empty()) throw std::runtime_error("IR: more than one embedding with a state_weight");
1461 state_weight_ = n.attrs.at("state_weight").get<std::string>();
1462 if (state_weight_.find("{i}") != std::string::npos)
1463 throw std::runtime_error("IR embedding state_weight names its tensor in full (no {i})");
1464 }
1465 if (n.op == "attention" && n.attrs.contains("mask")) {
1466 const std::string mask = n.attrs.at("mask").is_string() ? n.attrs.at("mask").get<std::string>() : "";
1467 if (mask != "causal" && mask != "turn_state")
1468 throw std::runtime_error("IR attention mask " + n.attrs.at("mask").dump() +
1469 " is not implemented in ember (causal or turn_state)");
1470 if (mask == "turn_state") turn_state_ = true;
1471 }
1472 }
1473 if (turn_state_) {
1474 for (const auto& n : block_) {
1475 if (n.op != "attention" || n.attrs.value("mask", "causal") != "turn_state") continue;
1476 for (int l = 0; l < n_layers_; ++l) {
1477 int w = 0, s = 0;
1478 attention_window(n, l, w, s);
1479 if (w != 0 || s != 0)
1480 throw std::runtime_error("IR attention mask turn_state needs window 0 and sinks 0 (block " + std::to_string(l) +
1481 " has window " + std::to_string(w) + ", sinks " + std::to_string(s) + ")");
1482 }
1483 }
1484 if (window_ > 0 || !window_layer_.empty() || sinks_ > 0)
1485 throw std::runtime_error("IR attention mask turn_state with a windowed kv state is not allowed");
1486 if (!mor_step_.empty() || kv_rows_active)
1487 throw std::runtime_error("IR attention mask turn_state with depth_route / kv_rows active is not implemented in ember");
1488 }
1489 {
1490 bool all_turn_state = true;
1491 for (const auto& n : block_)
1492 if (n.op == "attention" && n.attrs.value("mask", "causal") != "turn_state") all_turn_state = false;
1494 throw std::runtime_error("IR kv state evict turn_state needs every attention node to have mask turn_state");
1497 if (const char* e = std::getenv("EMBER_EVICT")) {
1498 if (std::string(e) == "0") evict_ = false;
1499 if (std::string(e) == "1" && turn_state_) {
1500 if (!all_turn_state)
1501 throw std::runtime_error("EMBER_EVICT=1: eviction needs every attention node to have mask turn_state");
1502 evict_ = true;
1503 }
1504 }
1505 if (evict_ && !rec_slots_.empty())
1506 throw std::runtime_error("IR kv state evict turn_state with recurrent state is not implemented in ember");
1507 }
1508 // EMBER_IR_CTX=N: hold at most N positions (a third-party IR declares the
1509 // model's whole trained context, e.g. 32768, which is ~0.8 GB of f32 KV for
1510 // Qwen2.5-0.5B).
1511 if (const char* c = std::getenv("EMBER_IR_CTX")) {
1512 const int cap = std::atoi(c);
1513 if (cap > 0 && max_len_ > cap) {
1514 Logger::info("[IR] EMBER_IR_CTX: the kv state holds " + std::to_string(cap) + " of the IR's " +
1515 std::to_string(max_len_) + " positions");
1516 max_len_ = cap;
1517 }
1518 }
1519 // EMBER_KV=f16|q8|q4 (kv_quant.h, R2): the kv cache's storage format; unset or f32 changes nothing.
1520 kv_fmt_ = ember_kvq::parse_format(std::getenv("EMBER_KV"));
1521 if (kv_fmt_ != ember_kvq::kF32 && head_dim_ > 0) {
1522 const std::string why = ember_kvq::shape_error(kv_fmt_, head_dim_);
1523 if (!why.empty()) throw std::runtime_error(std::string("EMBER_KV=") + ember_kvq::format_name(kv_fmt_) + ": " + why);
1524 for (const auto& n : block_) // its latent cache [c | kr] has its own write path, not quantised
1525 if (n.op == "mla_attention")
1526 throw std::runtime_error(std::string("EMBER_KV=") + ember_kvq::format_name(kv_fmt_) +
1527 ": mla_attention's latent cache is not quantised");
1528 Logger::info(std::string("[IR] EMBER_KV: the kv cache stores ") + ember_kvq::format_name(kv_fmt_));
1529 }
1530 bool has_attention = false;
1531 for (const auto& n : block_) has_attention = has_attention || n.op == "attention" || n.op == "mla_attention";
1532 if ((has_kv || has_attention) && (kv_heads_ <= 0 || head_dim_ <= 0 || max_len_ <= 0))
1533 throw std::runtime_error("IR block declares no usable kv state (kv_heads, head_dim, max_len)");
1534 if (max_len_ <= 0) throw std::runtime_error("IR model.ctx is required when no kv state bounds the sequence");
1535 for (const auto* sub : {&embed_, &head_})
1536 for (const auto& n : *sub)
1537 if (kRecurrentOps.count(n.op)) throw std::runtime_error("IR op '" + n.op + "' outside the block has no state");
1538 // Every recurrent node names a slot of its op whose tensors fit the node.
1539 for (const auto& n : block_) {
1540 if (n.op == "linear_attention") {
1541 const RecSlot& slot = rec_slot(n, "linear_attention");
1542 const int heads = n.attrs.at("heads").get<int>(), hd = n.attrs.at("head_dim").get<int>();
1543 if (n.attrs.value("feature_map", "elu_plus_one") != "elu_plus_one")
1544 throw std::runtime_error("IR linear_attention feature_map '" + n.attrs.value("feature_map", "") +
1545 "' is not implemented in ember");
1546 if (!n.attrs.value("causal", true))
1547 throw std::runtime_error("IR linear_attention with causal=false is not implemented in ember");
1548 if (n.in.size() != 3 || n.out.size() != 1) throw std::runtime_error("IR linear_attention reads [q, k, v] and writes one output");
1549 for (const char* t : {"num", "den"}) {
1550 bool found = false;
1551 for (const auto& rt : slot.tensors)
1552 if (rt.name == t) {
1553 found = true;
1554 if (rt.shape != std::vector<int64_t>{heads, hd})
1555 throw std::runtime_error("IR recurrent state '" + slot.name + "': " + t + " is not [heads, head_dim]");
1556 }
1557 if (!found) throw std::runtime_error("IR recurrent state '" + slot.name + "' has no tensor '" + t + "'");
1558 }
1559 } else if (n.op == "ssm_scan") {
1560 const RecSlot& slot = rec_slot(n, "ssm_scan");
1561 const int64_t sn = n.attrs.at("n").get<int64_t>();
1562 if (n.in.size() != 1 || n.out.size() != 1) throw std::runtime_error("IR ssm_scan reads [u] and writes one output");
1563 if (slot.tensors.size() != 1 || slot.tensors[0].name != "s" || slot.tensors[0].shape != std::vector<int64_t>{sn})
1564 throw std::runtime_error("IR recurrent state '" + slot.name + "' must hold one tensor s [n]");
1565 } else if (n.op == "conv1d_causal") {
1566 const RecSlot& slot = rec_slot(n, "conv1d_causal");
1567 const int64_t C = n.attrs.at("channels").get<int64_t>(), K = n.attrs.at("kernel").get<int64_t>();
1568 if (n.in.size() != 1 || n.out.size() != 1) throw std::runtime_error("IR conv1d_causal reads [x] and writes one output");
1569 if (K < 2 || C < 1) throw std::runtime_error("IR conv1d_causal needs channels >= 1 and kernel >= 2");
1570 if (slot.tensors.size() != 1 || slot.tensors[0].name != "x" ||
1571 slot.tensors[0].shape != std::vector<int64_t>{K - 1, C})
1572 throw std::runtime_error("IR recurrent state '" + slot.name + "' must hold one tensor x [kernel - 1, channels]");
1573 } else if (n.op == "selective_scan") {
1574 const RecSlot& slot = rec_slot(n, "selective_scan");
1575 const int64_t D = n.attrs.at("channels").get<int64_t>(), N = n.attrs.at("d_state").get<int64_t>();
1576 if (n.in.size() != 4 || n.out.size() != 1)
1577 throw std::runtime_error("IR selective_scan reads [x, dt, B, C] and writes one output");
1578 if (slot.tensors.size() != 1 || slot.tensors[0].name != "s" || slot.tensors[0].shape != std::vector<int64_t>{D, N})
1579 throw std::runtime_error("IR recurrent state '" + slot.name + "' must hold one tensor s [channels, d_state]");
1580 } else if (n.op == "recurrence") { // IR_SPEC 6.10, 10.1 rules 31-33 (graph_ir_recurrence.h)
1581 auto prog = std::make_shared<rec::Recurrence>(rec::Recurrence::compile(n.attrs));
1582 if (n.out.size() != 1) throw std::runtime_error("IR recurrence writes one output");
1583 const std::string name = has(n.attrs, "state") ? n.attrs.at("state").get<std::string>() : "";
1584 const json* decl = nullptr; // into the document (not a copy: it is read below)
1585 if (j.at("block").contains("state") && j.at("block").at("state").is_array())
1586 for (const auto& s : j.at("block").at("state"))
1587 if (s.is_object() && s.value("name", "") == name && s.value("kind", "") == "recurrent") decl = &s;
1588 if (!decl) throw std::runtime_error("IR_SPEC 6.10 (rule 33): recurrence names no recurrent slot '" + name + "'");
1589 const std::string why = prog->slot_errors(*decl);
1590 if (!why.empty()) throw std::runtime_error("IR_SPEC 6.10 (rule 33): " + why);
1591 rec_slot(n, "recurrence");
1592 for (const auto& other : block_)
1593 if (&other != &n && has(other.attrs, "state") && other.attrs.at("state") == name)
1594 throw std::runtime_error("IR_SPEC 6.10 (rule 33): the slot '" + name + "' of a recurrence is read by another node (" +
1595 other.op + ")");
1596 recs_[&n] = std::move(prog);
1597 }
1598 }
1599 if (rec_floats_ > 0) {
1600 // Every application's initial state: +0, or (op recurrence with "init" "body") the init body over the
1601 // application's weights, set after binding.
1602 rec_init_.assign(static_cast<size_t>(n_layers_) * rec_floats_, 0.0f);
1603 for (int l = 0; l < n_layers_; ++l)
1604 for (const auto& slot : rec_slots_)
1605 for (const auto& rt : slot.tensors)
1606 std::fill(rec_init_.begin() + l * rec_floats_ + rt.offset, rec_init_.begin() + l * rec_floats_ + rt.offset + rt.floats,
1607 rt.init);
1608 rec_ = rec_init_;
1609 // Snapshots are a whole state each; at most 64, and at most 256 MiB.
1610 const size_t budget = (256u << 20) / sizeof(float);
1611 rec_max_snaps_ = std::max<size_t>(2, std::min<size_t>(64, budget / rec_.size()));
1612 }
1613
1614 // Quantised weights stay in their blocks for large models on the CPU (the
1615 // memory saving pays for the per-row dequantisation); small models, like
1616 // the llama path's dequantize_all_if_small, and the CUDA path (f32 kernels)
1617 // dequantise at load. EMBER_IR_QUANT=1 / EMBER_IR_DEQUANT=1 force either.
1618 {
1619 size_t params = 0;
1620 for (const auto& kv : gguf.tensor_infos_map) params += kv.second.num_elements;
1621#ifdef HAS_CUDA
1622 const bool device = use_gpu;
1623#else
1624 const bool device = false;
1625#endif
1626 dequant_all_ = device || params <= 64u * 1000 * 1000;
1627 const char* q = std::getenv("EMBER_IR_QUANT");
1628 if (q && std::string(q) == "1" && !device) dequant_all_ = false;
1629 if (const char* dq = std::getenv("EMBER_IR_DEQUANT")) dequant_all_ = dequant_all_ || std::string(dq) == "1";
1630 }
1631 // depth_attend's source count S (IR_SPEC 5.18) is structural: the entries list and append put in its list
1632 // before the node, in run order (embed once, the block per application, the head), plus its other inputs.
1633 {
1634 std::unordered_map<std::string, int> entries; // list values: their tensor count
1635 auto walk = [&](const std::vector<Node>& nodes, int layer) {
1636 for (const auto& n : nodes) {
1637 const auto held = [&](const std::string& v) {
1638 const auto it = entries.find(v);
1639 return it == entries.end() ? 0 : it->second;
1640 };
1641 if (n.op == "list") {
1642 entries[n.out.at(0)] = static_cast<int>(n.in.size());
1643 } else if (n.op == "append") {
1644 entries[n.out.at(0)] = held(n.in.at(0)) + 1;
1645 } else {
1646 if (n.op == "depth_attend") da_sources_[{&n, layer}] = held(n.in.at(0)) + static_cast<int>(n.in.size()) - 1;
1647 for (const auto& o : n.out) entries.erase(o);
1648 }
1649 }
1650 };
1651 walk(embed_, -1);
1652 for (int l = 0; l < n_layers_; ++l) walk(block_, l);
1653 walk(head_, -1);
1654 }
1655 bind(embed_, -1, gguf);
1656 for (int l = 0; l < n_layers_; ++l) bind(block_, l, gguf);
1657 bind(head_, -1, gguf);
1658 // A recurrence's initial state (IR_SPEC 6.10.3): its init bodies over each application's weights.
1659 for (const auto& [np, prog] : recs_) {
1660 const RecSlot& slot = rec_slot(*np, "recurrence");
1661 AppIndex ai(app_index_, *np);
1662 for (int l = 0; l < n_layers_; ++l) {
1663 std::vector<const float*> w;
1664 for (const auto& wd : prog->weights()) w.push_back(weight(layer_name(wd.tensor, l)).data());
1665 std::vector<float*> st;
1666 for (const auto& s : prog->state()) {
1667 const RecTensor* rt = nullptr;
1668 for (const auto& t : slot.tensors)
1669 if (t.name == s.name) rt = &t;
1670 st.push_back(rec_init_.data() + static_cast<size_t>(l) * rec_floats_ + rt->offset);
1671 }
1672 prog->init(w, st);
1673 }
1674 }
1675 if (!recs_.empty()) rec_ = rec_init_;
1676 // The state rows: state_weight [k, d] (ggml ne0 = d, the token embedding's width); k from its
1677 // size, equal to the file's paperkiln.state_tokens.k when the file names one.
1678 {
1679 const auto meta_k = meta_number(gguf, "paperkiln.state_tokens.k");
1680 if (!state_weight_.empty()) {
1681 bind_tensor(state_weight_, gguf, true);
1682 int d = 0;
1683 for (const auto& n : embed_)
1684 if (n.op == "embedding" && has(n.attrs, "state_weight")) {
1685 std::string w = layer_name(n.attrs.at("weight").get<std::string>(), -1);
1686 d = static_cast<int>(shapes_.at(w).at(0));
1687 }
1688 const auto& sh = shapes_.at(state_weight_);
1689 const size_t numel = bound(state_weight_).numel;
1690 if (sh.empty() || sh[0] != d || d <= 0 || numel % static_cast<size_t>(d) != 0 || numel == 0)
1691 throw std::runtime_error("IR embedding state_weight '" + state_weight_ + "' is not [k, d] with d " +
1692 std::to_string(d) + " (the token embedding's width)");
1693 state_k_ = static_cast<int>(numel / static_cast<size_t>(d));
1694 if (meta_k && static_cast<int>(*meta_k) != state_k_)
1695 throw std::runtime_error("paperkiln.state_tokens.k is " + std::to_string(static_cast<int>(*meta_k)) +
1696 ", the IR's state_weight '" + state_weight_ + "' has " + std::to_string(state_k_) +
1697 " rows");
1698 Logger::info("[IR] per-turn state tokens: k " + std::to_string(state_k_) + " (ids " + std::to_string(vocab_) +
1699 " .. " + std::to_string(vocab_ + state_k_ - 1) + " read '" + state_weight_ + "')" +
1700 (turn_state_ ? std::string(", attention mask turn_state") : std::string()) +
1701 (evict_ ? std::string(", kv eviction at turn_state") : std::string()));
1702 } else if (meta_k && *meta_k > 0) {
1703 throw std::runtime_error("paperkiln.state_tokens.k is " + std::to_string(static_cast<int>(*meta_k)) +
1704 " but the IR's embedding has no state_weight");
1705 }
1706 }
1707 // paperkiln spec/IR_SPEC.md section 10.1 (graph_ir_validate.h): after ember's own checks above, so a
1708 // document they refuse keeps their message; then what ember does not implement among valid
1709 // documents (section 10.3).
1710 validate_ir(j, gguf);
1711 {
1712 int kv_slots = 0;
1713 for (const auto& s : j.at("block").value("state", json::array())) kv_slots += s.value("kind", "") == "kv";
1714 if (kv_slots > 1)
1715 throw std::runtime_error("IR block declares " + std::to_string(kv_slots) +
1716 " kv slots: more than one kv slot per block (IR_SPEC 6.1) is not implemented in ember");
1717 }
1718 size_t n_quant = 0, bytes_quant = 0, bytes_f32 = 0, n_mapped = 0;
1719 for (const auto& kv : weights_) {
1720 n_quant += kv.second.quantised();
1721 n_mapped += kv.second.quantised() && kv.second.q.empty();
1722 bytes_quant += kv.second.qbytes;
1723 bytes_f32 += kv.second.f32.size() * sizeof(float);
1724 }
1725
1726 build_rope_tables(gguf);
1727#ifdef HAS_CUDA
1728 if (use_gpu) {
1729 init_device();
1730 gpu_ = true;
1731 }
1732#else
1733 (void)use_gpu;
1734#endif
1735 if (!gpu_ && kv_heads_ > 0) {
1736 kv_.resize(n_layers_);
1737 for (auto& layer : kv_) {
1738 layer.k.assign(static_cast<size_t>(max_len_) * kv_heads_ * head_dim_, 0.0f);
1739 layer.v.assign(layer.k.size(), 0.0f);
1740 }
1741 }
1742 Logger::info("[IR] interpreter ready: family_hint=" + family_hint_ + ", " + std::to_string(n_layers_) + " blocks of " +
1743 std::to_string(block_.size()) + " ops, " + std::to_string(weights_.size()) + " weights bound (" +
1744 std::to_string(n_quant) + " kept quantised, " + std::to_string(bytes_quant >> 20) + " MiB" +
1745 (n_mapped ? " (" + std::to_string(n_mapped) + " read in place from the memory map)" : std::string()) +
1746 "; f32 " +
1747 std::to_string(bytes_f32 >> 20) + " MiB" + (dequant_all_ ? "; dequantised at load" : "") + "), device " +
1748 (gpu_ ? "cuda" : "cpu"));
1749 { // The document run, by its digest (IR_SPEC 3.10): a citation names it as sha256:<hex>.
1750 const DigestResult d = ir_digest(ir_json);
1751 Logger::info(d.ok ? "[IR] document sha256:" + d.sha256 : "[IR] the document has no digest (" + d.refused + ")");
1752 }
1753 if (rec_floats_ > 0)
1754 Logger::info("[IR] recurrent state: " + std::to_string(rec_slots_.size()) + " slot(s), " +
1755 std::to_string(rec_floats_) + " floats per block; up to " + std::to_string(rec_max_snaps_) +
1756 " snapshots for prefix reuse and speculative rewinds");
1757}
1758
1759Interpreter::~Interpreter() {
1760#ifdef HAS_CUDA
1761 free_device();
1762#endif
1763}
1764
1765const char* Interpreter::kv_format() const { return ember_kvq::format_name(kv_fmt_); }
1766
1767size_t Interpreter::kv_cache_bytes() const {
1768#ifdef HAS_CUDA
1769 if (gpu_)
1770 return kv_fmt_ != ember_kvq::kF32 ? kv_dev_bytes_
1771 : static_cast<size_t>(n_layers_) * 2 * max_len_ * kv_heads_ * head_dim_ * sizeof(float);
1772#endif
1773 return 0;
1774}
1775
1776size_t Interpreter::kv_scratch_bytes() const {
1777#ifdef HAS_CUDA
1778 if (gpu_) return kv_scratch_dev_bytes_;
1779#endif
1780 return 0;
1781}
1782
1783void Interpreter::attention_window(const Node& n, int layer, int& window, int& sinks) const {
1784 window = has(n.attrs, "window") ? at_layer(n.attrs.at("window"), layer).get<int>()
1785 : (window_layer_.empty() ? window_ : window_layer_.at(layer));
1786 sinks = has(n.attrs, "sinks") ? at_layer(n.attrs.at("sinks"), layer).get<int>() : sinks_;
1787}
1788
1789const std::vector<std::pair<float, float>>& Interpreter::rope_table(int layer) const {
1790 const std::vector<int>& idx = rope_long_ && !rope_tab_long_.empty() ? rope_tab_long_ : rope_tab_short_;
1791 const int l = layer < 0 ? 0 : layer;
1792 if (l >= static_cast<int>(idx.size()) || idx[l] < 0)
1793 throw std::runtime_error("IR rope: block " + std::to_string(l) + " has no RoPE table (theta must be positive)");
1794 return rope_tabs_[idx[l]];
1795}
1796
1797// The RoPE tables, computed as TinyLlamaModel::initialize_rope_freqs over rope_dim_ (n_rot; head_dim for full
1798// rotary), one per distinct (theta, freq_scale, attn_factor, frequency factors) among the blocks (IR_SPEC 5.14): for
1799// pair j, freq_j = 1 / powf(theta, f32(2j) / f32(n_rot)), angle = (f32(pos) * freq_scale) * freq_j, divided by f_j
1800// with frequency factors (revision 1.5), each step rounded; the table holds cosf(angle) * m and sinf(angle) * m
1801// (attn_factor m, 1 by default: the product is then exact). With two sets each block has a short and a long table.
1802void Interpreter::build_rope_tables(const GGUFData& gguf) {
1803 rope_tabs_.clear();
1804 rope_tab_short_.clear();
1805 rope_tab_long_.clear();
1806 rope_ff_ctx_ = 0;
1807 if (rope_theta_layer_.empty()) return;
1808 const int half = rope_dim_ / 2;
1809 std::string short_name, long_name; // tensor names as written ({i} allowed)
1810 std::vector<float> values; // freq_factors given by value (revision 1.6: YaRN's divisors), each as f32
1811 if (rope_ff_.is_array()) { // n_rot / 2 positive numbers (rule 41, validate_ir)
1812 for (const auto& v : rope_ff_) values.push_back(v.get<float>());
1813 if (values.size() != static_cast<size_t>(half))
1814 throw std::runtime_error("IR_SPEC 5.14 (rule 41): rope freq_factors holds " + std::to_string(values.size()) +
1815 " values, not n_rot / 2 = " + std::to_string(half));
1816 short_name = "=values"; // the table's key; no tensor is read
1817 } else if (rope_ff_.is_string()) {
1818 short_name = rope_ff_.get<std::string>();
1819 } else if (rope_ff_.is_object()) { // {short, long, original_ctx}: rule 41 holds (validate_ir)
1820 short_name = rope_ff_.at("short").get<std::string>();
1821 long_name = rope_ff_.at("long").get<std::string>();
1822 rope_ff_ctx_ = rope_ff_.at("original_ctx").get<int>();
1823 }
1824 std::map<std::tuple<float, float, float, std::string>, int> index;
1825 auto table = [&](int l, const std::string& pattern) -> int {
1826 const float theta = rope_theta_layer_[l], fs = rope_scale_layer_[l], m = rope_attn_layer_.empty() ? 1.0f : rope_attn_layer_[l];
1827 if (!(theta > 0.0f)) return -1;
1828 const std::string name = pattern.empty() || pattern == "=values" ? pattern : layer_name(pattern, l);
1829 const auto key = std::make_tuple(theta, fs, m, name);
1830 if (const auto it = index.find(key); it != index.end()) return it->second;
1831 std::vector<float> f;
1832 if (pattern == "=values") {
1833 f = values;
1834 } else if (!name.empty()) {
1835 bind_tensor(name, gguf);
1836 f = weight(name);
1837 if (f.size() != static_cast<size_t>(half))
1838 throw std::runtime_error("IR_SPEC 5.14 (rule 41): rope freq_factors '" + name + "' holds " +
1839 std::to_string(f.size()) + " values, not n_rot / 2 = " + std::to_string(half));
1840 }
1841 std::vector<std::pair<float, float>> tab(static_cast<size_t>(max_len_) * half);
1842 for (int pos = 0; pos < max_len_; ++pos)
1843 for (int i = 0; i < rope_dim_; i += 2) {
1844 const float freq = 1.0f / std::pow(theta, float(i) / rope_dim_);
1845 float val = (static_cast<float>(pos) * fs) * freq;
1846 if (!f.empty()) val = val / f[i / 2];
1847 tab[static_cast<size_t>(pos) * half + i / 2] = {std::cos(val) * m, std::sin(val) * m};
1848 }
1849 rope_tabs_.push_back(std::move(tab));
1850 index[key] = static_cast<int>(rope_tabs_.size()) - 1;
1851 return index[key];
1852 };
1853 for (int l = 0; l < static_cast<int>(rope_theta_layer_.size()); ++l) {
1854 rope_tab_short_.push_back(table(l, short_name));
1855 if (!long_name.empty()) rope_tab_long_.push_back(table(l, long_name));
1856 }
1857 if (rope_ff_ctx_ > 0)
1858 Logger::info("[IR] LongRoPE: frequency factors '" + short_name + "' up to " + std::to_string(rope_ff_ctx_) +
1859 " positions, '" + long_name + "' past them; the set follows the sequence's length (a call or a "
1860 "truncate that changes it runs the sequence again)");
1861}
1862
1863void Interpreter::set_rope_long(bool v) {
1864 if (v == rope_long_) return;
1865 rope_long_ = v;
1866#ifdef HAS_CUDA
1867 if (gpu_) ++alloc_epoch_; // the captured decode step reads the other set's tables: it is captured again
1868#endif
1869}
1870
1871std::string Interpreter::layer_name(const std::string& pattern, int layer) const {
1872 std::string s = pattern;
1873 const auto p = s.find("{i}");
1874 if (p != std::string::npos) {
1875 if (layer < 0) throw std::runtime_error("IR weight '" + pattern + "' uses {i} outside the block");
1876 // m_i under weight_index, except in a node tagged "tensor_index": "application" (IR_SPEC 7.2): i.
1877 s.replace(p, 3, std::to_string(weight_map_.empty() || app_index_ ? layer : weight_map_.at(layer)));
1878 }
1879 return s;
1880}
1881
1882void Interpreter::bind_tensor(const std::string& name, const GGUFData& gguf, bool keep_quantised) {
1883 if (auto bw = weights_.find(name); bw != weights_.end()) {
1884 Weight& w = bw->second;
1885 if (keep_quantised || !w.quantised()) return;
1886 // Bound quantised for a row-wise op, now needed whole (a norm, a bias): f32.
1887 w.f32.resize(w.numel);
1888 dequant_span_to_f32(w.type, w.qp, 0, w.numel, w.f32.data());
1889 std::vector<uint8_t>().swap(w.q);
1890 w.qp = nullptr;
1891 w.qbytes = 0;
1892 return;
1893 }
1894 auto it = gguf.tensor_infos_map.find(name);
1895 if (it == gguf.tensor_infos_map.end()) throw std::runtime_error("IR weight '" + name + "' is not in the GGUF");
1896 const GGUFTensorInfo& info = it->second;
1897 const uint8_t* base = gguf.mapped_tensor_data
1898 ? static_cast<const uint8_t*>(gguf.mapped_tensor_data) + gguf.offset_diff_for_mmap
1899 : gguf.tensor_data.data();
1900 const uint8_t* src = base + info.offset;
1901 std::vector<int64_t> shape(info.shape.begin(), info.shape.end());
1902 const GGMLType t = info.type;
1906 k_quant;
1907 const int64_t width = k_quant ? GGML_QK_K : 32; // elements per block
1909 if (keep_quantised && !dequant_all_ && !shape.empty() && (half || (block_type && shape[0] % width == 0))) {
1910 // Rows (ne0 elements) start on block boundaries, so a row is a whole
1911 // number of blocks and dequantises on its own.
1912 const size_t bytes = half ? info.num_elements * 2 : info.num_elements / width * ggml_type_size(t);
1913 if (info.size_in_bytes < bytes)
1914 throw std::runtime_error("IR weight '" + name + "' is shorter than its element count implies");
1915 Weight w;
1916 if (gguf.mapped_tensor_data) {
1917 w.qp = src; // read in place: the mapping lives as long as the GGUFData
1918 } else {
1919 w.q.assign(src, src + bytes);
1920 w.qp = w.q.data(); // a vector's buffer survives the moves below
1921 }
1922 w.qbytes = bytes;
1923 w.type = t;
1924 w.numel = info.num_elements;
1925 shapes_[name] = shape;
1926 weights_[name] = std::move(w);
1927 return;
1928 }
1929 std::vector<float> out;
1930 if (info.type == GGMLType::GGML_TYPE_F32) {
1931 out.resize(info.num_elements);
1932 std::memcpy(out.data(), src, info.num_elements * sizeof(float));
1933 } else if (info.type == GGMLType::GGML_TYPE_Q8_0) {
1934 std::vector<block_q8_0> blocks(info.size_in_bytes / sizeof(block_q8_0));
1935 std::memcpy(blocks.data(), src, blocks.size() * sizeof(block_q8_0));
1937 } else if (!dequant_legacy_to_f32(info, src, out)) {
1938 throw std::runtime_error("IR weight '" + name + "': tensor type " + ggml_type_name(info.type) +
1939 " is not supported by ember's interpreter");
1940 }
1941 shapes_[name] = shape;
1942 Weight w;
1943 w.numel = out.size();
1944 w.f32 = std::move(out);
1945 weights_[name] = std::move(w);
1946}
1947
1948void Interpreter::bind(const std::vector<Node>& nodes, int layer, const GGUFData& gguf) {
1949 for (const auto& n : nodes) {
1950 if (!applies(n, layer)) continue; // its tensors are needed only where it runs (IR_SPEC 3.8, revision 1.6)
1951 AppIndex ti(app_index_, n);
1952 // depth_attend over one source never reads its pseudo-query (IR_SPEC 5.18), so a file may leave it
1953 // out; with S >= 2 sources a missing one is refused here, at load (rule 25).
1954 if (n.op == "depth_attend" && has(n.attrs, "weight")) {
1955 const std::string w = layer_name(n.attrs.at("weight").get<std::string>(), layer);
1956 if (!gguf.tensor_infos_map.count(w)) {
1957 const auto it = da_sources_.find({&n, layer});
1958 const int S = it == da_sources_.end() ? 0 : it->second;
1959 if (S == 1) continue;
1960 throw std::runtime_error("IR_SPEC 5.18: depth_attend" +
1961 (layer >= 0 ? " at application " + std::to_string(layer) : std::string()) + " has " +
1962 std::to_string(S) + " sources, and its pseudo-query '" + w + "' is not in the GGUF");
1963 }
1964 }
1965 if (has(n.attrs, "weight")) {
1966 std::string w = layer_name(n.attrs.at("weight").get<std::string>(), layer);
1967 if (!gguf.tensor_infos_map.count(w) && has(n.attrs, "tied_to"))
1968 w = layer_name(n.attrs.at("tied_to").get<std::string>(), layer);
1969 bind_tensor(w, gguf, n.op == "linear" || n.op == "embedding");
1970 }
1971 if (has(n.attrs, "bias")) bind_tensor(layer_name(n.attrs.at("bias").get<std::string>(), layer), gguf);
1972 if (n.op == "attention" && has(n.attrs, "position_bias") && n.attrs.at("position_bias").is_object() &&
1973 n.attrs.at("position_bias").contains("weight") && n.attrs.at("position_bias").at("weight").is_string())
1974 bind_tensor(layer_name(n.attrs.at("position_bias").at("weight").get<std::string>(), layer), gguf);
1975 if (n.op == "attention" && has(n.attrs, "relative_kv") && n.attrs.at("relative_kv").is_object())
1976 for (const char* member : {"key", "value"})
1977 if (n.attrs.at("relative_kv").contains(member) && n.attrs.at("relative_kv").at(member).is_string())
1978 bind_tensor(layer_name(n.attrs.at("relative_kv").at(member).get<std::string>(), layer), gguf);
1979 // rmsnorm weight_offset c: the weight is read as w + c, rounded to f32 once at load (Gemma's (1 + w)
1980 // for a file holding HF's raw w; llama.cpp's convert already stores 1 + w).
1981 if (n.op == "rmsnorm" && has(n.attrs, "weight_offset") && n.attrs.at("weight_offset").get<float>() != 0.0f) {
1982 const std::string w = layer_name(n.attrs.at("weight").get<std::string>(), layer);
1983 if (offset_applied_.insert(w).second) {
1984 const float off = n.attrs.at("weight_offset").get<float>();
1985 for (float& x : weights_.at(w).f32) x = x + off;
1986 }
1987 }
1988 if (n.op == "moe_ffn") {
1989 const std::string norm = n.attrs.value("gate_norm", "renorm"), act = n.attrs.value("activation", "silu");
1990 if (norm != "renorm" && norm != "raw") throw std::runtime_error("IR moe_ffn gate_norm '" + norm + "' is not implemented");
1991 if (act != "silu") throw std::runtime_error("IR moe_ffn activation '" + act + "' is not implemented");
1992 bind_tensor(layer_name(n.attrs.at("router").get<std::string>(), layer), gguf);
1993 for (const char* part : {"gate", "up", "down"})
1994 bind_tensor(layer_name(n.attrs.at("experts").at(part).get<std::string>(), layer), gguf);
1995 if (has(n.attrs, "selection_bias")) { // ne {n_experts} (IR_SPEC 5.16, rule 44, revision 1.6)
1996 const std::string b = layer_name(n.attrs.at("selection_bias").get<std::string>(), layer);
1997 bind_tensor(b, gguf);
1998 const int E = at_layer(n.attrs.at("n_experts"), std::max(layer, 0)).get<int>();
1999 if (bound(b).numel != static_cast<size_t>(E) || shapes_.at(b).size() != 1)
2000 throw std::runtime_error("IR_SPEC 5.16 (rule 44): moe_ffn selection_bias '" + b + "' is not of ne {n_experts} = {" +
2001 std::to_string(E) + "}");
2002 }
2003 }
2004 if (n.op == "mla_attention") { // k_b ne {dn, r, H}, v_b ne {r, dv, H} (IR_SPEC 5.20, rule 42)
2005 const auto at = [&](const char* k) { return static_cast<int64_t>(at_layer(n.attrs.at(k), std::max(layer, 0)).get<int>()); };
2006 const int64_t H = at("heads"), r = at("kv_rank"), dn = at("qk_nope"), dv = at("v_head_dim");
2007 const std::pair<const char*, std::vector<int64_t>> parts[] = {{"k_b", {dn, r, H}}, {"v_b", {r, dv, H}}};
2008 for (const auto& [key, want] : parts) {
2009 const std::string w = layer_name(n.attrs.at(key).get<std::string>(), layer);
2010 bind_tensor(w, gguf);
2011 std::vector<int64_t> got = shapes_.at(w);
2012 while (got.size() < 3) got.push_back(1); // trailing ne of 1 (one head) may be left out
2013 if (got != want)
2014 throw std::runtime_error("IR_SPEC 5.20 (rule 42): mla_attention " + std::string(key) + " '" + w +
2015 "' is not of ne {" + std::to_string(want[0]) + ", " + std::to_string(want[1]) + ", " +
2016 std::to_string(want[2]) + "}");
2017 }
2018 }
2019 if (n.op == "ssm_scan") {
2020 const size_t sn = n.attrs.at("n").get<size_t>();
2021 const std::pair<const char*, size_t> parts[] = {{"a", sn * sn}, {"b", sn}, {"c", sn}, {"d", 1}};
2022 for (const auto& [part, numel] : parts) {
2023 const std::string w = layer_name(ssm_weight(n.attrs, part), layer);
2024 bind_tensor(w, gguf);
2025 if (bound(w).numel != numel)
2026 throw std::runtime_error("IR ssm_scan '" + w + "' has " + std::to_string(bound(w).numel) +
2027 " elements, expected " + std::to_string(numel));
2028 }
2029 }
2030 auto expect = [&](const std::string& w, size_t numel) {
2031 if (bound(w).numel != numel)
2032 throw std::runtime_error("IR " + n.op + " '" + w + "' has " + std::to_string(bound(w).numel) +
2033 " elements, expected " + std::to_string(numel));
2034 };
2035 if (n.op == "conv1d_causal") { // weight [channels, kernel], bias [channels] (bound above)
2036 const size_t C = n.attrs.at("channels").get<size_t>(), K = n.attrs.at("kernel").get<size_t>();
2037 expect(layer_name(n.attrs.at("weight").get<std::string>(), layer), C * K);
2038 if (has(n.attrs, "bias")) expect(layer_name(n.attrs.at("bias").get<std::string>(), layer), C);
2039 }
2040 if (n.op == "selective_scan") { // a [channels, d_state] as stored, d [channels] optional
2041 const size_t D = n.attrs.at("channels").get<size_t>(), N = n.attrs.at("d_state").get<size_t>();
2042 const std::string a = layer_name(n.attrs.at("a").get<std::string>(), layer);
2043 bind_tensor(a, gguf);
2044 expect(a, D * N);
2045 if (has(n.attrs, "d")) {
2046 const std::string d = layer_name(n.attrs.at("d").get<std::string>(), layer);
2047 bind_tensor(d, gguf);
2048 expect(d, D);
2049 }
2050 }
2051 if (n.op == "recurrence") { // each weight as many elements as its declaration, read row-major (IR_SPEC 6.10.1)
2052 const auto it = recs_.find(&n);
2053 if (it == recs_.end()) throw std::runtime_error("IR recurrence: the node was not compiled");
2054 for (const auto& wd : it->second->weights()) {
2055 const std::string w = layer_name(wd.tensor, layer);
2056 bind_tensor(w, gguf);
2057 if (bound(w).numel != wd.numel)
2058 throw std::runtime_error("IR_SPEC 6.10 (rule 34): recurrence weight " + wd.decl + " reads '" + w + "', which has " +
2059 std::to_string(bound(w).numel) + " elements; its declaration has " +
2060 std::to_string(wd.numel));
2061 }
2062 }
2063 }
2064}
2065
2066const Interpreter::RecSlot& Interpreter::rec_slot(const Node& n, const char* op) const {
2067 if (!has(n.attrs, "state")) throw std::runtime_error(std::string("IR ") + op + " names no state slot");
2068 const std::string name = n.attrs.at("state").get<std::string>();
2069 for (const auto& slot : rec_slots_)
2070 if (slot.name == name) {
2071 if (slot.op != op)
2072 throw std::runtime_error("IR recurrent state '" + name + "' belongs to op '" + slot.op + "', not " + op);
2073 return slot;
2074 }
2075 throw std::runtime_error(std::string("IR ") + op + ": no recurrent state named '" + name + "'");
2076}
2077
2078float* Interpreter::rec_tensor(const RecSlot& slot, const char* tensor, int layer, size_t floats) {
2079 for (const auto& rt : slot.tensors)
2080 if (rt.name == tensor) {
2081 if (rt.floats != floats)
2082 throw std::runtime_error("IR recurrent state '" + slot.name + "': " + tensor + " does not fit the op");
2083 return rec_.data() + static_cast<size_t>(layer) * rec_floats_ + rt.offset;
2084 }
2085 throw std::runtime_error("IR recurrent state '" + slot.name + "' has no tensor '" + tensor + "'");
2086}
2087
2088int Interpreter::ts_seg_after(int p) const {
2089 if (p < 0) return 0;
2090 return ts_seg_.at(p) + (state_k_ > 0 && ts_sidx_.at(p) == state_k_ - 1 ? 1 : 0);
2091}
2092
2093int Interpreter::ts_segment_start(int m) const {
2094 if (m <= 0) return 0;
2095 // The position after the last state id of block m - 1.
2096 for (int p = static_cast<int>(ts_seg_.size()) - 1; p >= 0; --p)
2097 if (ts_seg_[p] == m - 1 && ts_sidx_[p] == state_k_ - 1) return p + 1;
2098 return static_cast<int>(ts_seg_.size());
2099}
2100
2101void Interpreter::ts_evict_below(int seg) {
2102 if (!evict_ || seg <= evicted_below_) return;
2103 // Keep every state row and the text rows of segments >= seg; move them down in every block's
2104 // cache, in order (row w < row r, so the copies never overlap).
2105 const size_t row = static_cast<size_t>(kv_heads_) * head_dim_;
2106#ifdef HAS_CUDA
2107 if (gpu_) { // the same rows kept, in the same order, compacted in each block's device cache
2108 std::vector<int> idx;
2109 std::vector<int> kept;
2110 for (size_t r = 0; r < kv_pos_.size(); ++r) {
2111 const int p = kv_pos_[r];
2112 if (ts_sidx_.at(p) < 0 && ts_seg_.at(p) < seg) continue;
2113 idx.push_back(static_cast<int>(r));
2114 kept.push_back(p);
2115 }
2116 int w0 = 0;
2117 while (w0 < static_cast<int>(idx.size()) && idx[w0] == w0) ++w0;
2118 if (w0 < static_cast<int>(idx.size())) {
2119 // Pageable source: the copy has read idx when cudaMemcpyAsync returns.
2120 gpuErrchk(cudaMemcpyAsync(dkv_idx_, idx.data(), idx.size() * sizeof(int), cudaMemcpyHostToDevice, stream_));
2121 for (int l = 0; l < n_layers_; ++l)
2122 ir_kv_compact_cuda(dk_[l], dv_[l], dkv_idx_, w0, static_cast<int>(idx.size()), static_cast<int>(row), stream_);
2123 }
2124 kv_pos_ = std::move(kept);
2125 evicted_below_ = seg;
2126 return;
2127 }
2128#endif
2129 size_t w = 0;
2130 for (size_t r = 0; r < kv_pos_.size(); ++r) {
2131 const int p = kv_pos_[r];
2132 if (ts_sidx_.at(p) < 0 && ts_seg_.at(p) < seg) continue;
2133 if (w != r) {
2134 for (auto& layer : kv_) {
2135 std::memcpy(layer.k.data() + w * row, layer.k.data() + r * row, row * sizeof(float));
2136 std::memcpy(layer.v.data() + w * row, layer.v.data() + r * row, row * sizeof(float));
2137 }
2138 kv_pos_[w] = p;
2139 }
2140 ++w;
2141 }
2142 kv_pos_.resize(w);
2143 evicted_below_ = seg;
2144}
2145
2146void Interpreter::ts_begin(const std::vector<int>& tokens, int start_pos) {
2147 const int n = static_cast<int>(ts_seg_.size()), T = static_cast<int>(tokens.size());
2149 throw std::runtime_error("IR state tokens: a forward at position " + std::to_string(start_pos) +
2150 ", but the sequence holds " + std::to_string(n) + " positions");
2151 // The segment the first token lands in, and how much of a state block is open before it.
2152 const int m = ts_seg_after(start_pos - 1);
2153 int open = 0;
2154 if (start_pos > 0 && ts_sidx_[start_pos - 1] >= 0 && ts_sidx_[start_pos - 1] < state_k_ - 1)
2155 open = ts_sidx_[start_pos - 1] + 1;
2156 // Ids alone fix s(p) and c(p): a state id must be the next slot of an open block (S_0 opens
2157 // one), a text id must not interrupt one. Checked before anything changes.
2158 std::vector<int> segs(T), sidx(T);
2159 int seg = m;
2160 for (int t = 0; t < T; ++t) {
2161 const int id = tokens[t];
2162 if (id >= vocab_ && state_k_ > 0) {
2163 const int j = id - vocab_;
2164 if (j >= state_k_)
2165 throw std::runtime_error("IR state tokens: id " + std::to_string(id) + " is past the " + std::to_string(state_k_) +
2166 " state slots (vocab " + std::to_string(vocab_) + ")");
2167 if (j != open)
2168 throw std::runtime_error("IR state tokens: state id S_" + std::to_string(j) + " at position " +
2169 std::to_string(start_pos + t) + ", expected " +
2170 (open == 0 ? std::string("S_0 to open a block") : "S_" + std::to_string(open)));
2171 segs[t] = seg;
2172 sidx[t] = j;
2173 if (++open == state_k_) {
2174 open = 0;
2175 ++seg;
2176 }
2177 } else {
2178 if (open != 0)
2179 throw std::runtime_error("IR state tokens: text id " + std::to_string(id) + " at position " +
2180 std::to_string(start_pos + t) + " inside an incomplete state block (S_0 .. S_" +
2181 std::to_string(open - 1) + " of " + std::to_string(state_k_) + ")");
2182 segs[t] = seg;
2183 sidx[t] = -1;
2184 }
2185 }
2186 // A rewind into a segment whose text rows are gone can resume only at that segment's start
2187 // (resume_point); the rows of segments >= m all lie at or past it and are dropped below.
2188 if (turn_state_ && m < evicted_below_) {
2189 const int s0 = ts_segment_start(m);
2190 if (start_pos != s0)
2191 throw std::runtime_error("IR state tokens: position " + std::to_string(start_pos) + " is inside turn " +
2192 std::to_string(m) + ", whose text rows were evicted; resume from its start, position " +
2193 std::to_string(s0) + " (resume_point)");
2194 evicted_below_ = m;
2195 }
2196 ts_seg_.resize(start_pos);
2197 ts_sidx_.resize(start_pos);
2198 ts_seg_.insert(ts_seg_.end(), segs.begin(), segs.end());
2199 ts_sidx_.insert(ts_sidx_.end(), sidx.begin(), sidx.end());
2200 if (!turn_state_) return;
2201 while (!kv_pos_.empty() && kv_pos_.back() >= start_pos) kv_pos_.pop_back();
2202 // Rows no query of this call can see: earlier segments' text.
2203 if (evict_ && T > 0) ts_evict_below(segs[0]);
2204 kv_row0_ = static_cast<int>(kv_pos_.size());
2205 if (kv_row0_ + T > max_len_)
2206 throw std::runtime_error("IR attention: " + std::to_string(kv_row0_ + T) + " kv rows exceed the kv state (" +
2207 std::to_string(max_len_) + ")");
2208 // Visible rows per query (section 1.3), ascending: held rows, then this call's rows up to t. The
2209 // device path's kernel reads each row's code instead (ts_upload_codes).
2210 ts_vis_.assign(gpu_ ? 0 : T, {});
2211 for (int t = 0; t < T && !gpu_; ++t) {
2212 const int s = segs[t];
2213 auto visible = [&](int j) { return ts_seg_[j] == s || (ts_sidx_[j] >= 0 && ts_seg_[j] < s); };
2214 std::vector<int>& v = ts_vis_[t];
2215 for (int r = 0; r < kv_row0_; ++r)
2216 if (visible(kv_pos_[r])) v.push_back(r);
2217 for (int u = 0; u <= t; ++u)
2218 if (visible(start_pos + u)) v.push_back(kv_row0_ + u);
2219 }
2220}
2221
2222int Interpreter::resume_point(int want) const {
2223 if (turn_state_ && evict_ && want > 0 && want <= static_cast<int>(ts_seg_.size())) {
2224 const int m = ts_seg_after(want - 1);
2225 if (m < evicted_below_) return ts_segment_start(m);
2226 }
2227 if (rec_floats_ == 0 || want <= 0) return std::max(want, 0);
2228 if (want == rec_pos_) return want;
2229 auto below = [want](const auto& snaps) {
2230 auto it = snaps.upper_bound(want); // the first snapshot past want
2231 return it == snaps.begin() ? 0 : std::prev(it)->first;
2232 };
2233#ifdef HAS_CUDA
2234 if (gpu_) return below(dsnaps_);
2235#endif
2236 return below(rec_snaps_);
2237}
2238
2239// The device path keeps the same bookkeeping (keys, kept rows, eviction) over
2240// device buffers: drec_ is the live state, each snapshot a device copy, and
2241// every copy is device to device on the interpreter's stream.
2242void Interpreter::rec_begin(int start_pos, int T, bool all_rows) {
2243#ifdef HAS_CUDA
2244 const size_t bytes = rec_.size() * sizeof(float);
2245#endif
2246 if (start_pos == 0) { // a new sequence
2247#ifdef HAS_CUDA
2248 if (gpu_) {
2250 while (!dsnaps_.empty()) dsnap_drop(dsnaps_.begin());
2251 } else
2252#endif
2253 {
2254 rec_ = rec_init_; // every application's initial state
2255 rec_snaps_.clear();
2256 }
2257 } else if (start_pos != rec_pos_) {
2258 auto missing = [&] {
2259 return std::runtime_error("IR recurrent state holds " + std::to_string(rec_pos_) + " tokens and has no snapshot at " +
2260 std::to_string(start_pos) + " (resume_point says where a prefix can resume)");
2261 };
2262#ifdef HAS_CUDA
2263 if (gpu_) {
2264 auto it = dsnaps_.find(start_pos);
2265 if (it == dsnaps_.end()) throw missing();
2267 } else
2268#endif
2269 {
2270 auto it = rec_snaps_.find(start_pos);
2271 if (it == rec_snaps_.end()) throw missing();
2272 rec_ = it->second;
2273 }
2274 }
2275 // Snapshots past start_pos belong to the continuation being replaced.
2276#ifdef HAS_CUDA
2277 if (gpu_)
2278 while (!dsnaps_.empty() && dsnaps_.rbegin()->first > start_pos) dsnap_drop(std::prev(dsnaps_.end()));
2279 else
2280#endif
2281 rec_snaps_.erase(rec_snaps_.upper_bound(start_pos), rec_snaps_.end());
2282 rec_pos_ = -1; // until this call completes
2283 // Rows whose state is kept: every row of an all_rows call (speculative
2284 // verification), else the second-to-last row (the same prompt again).
2285 rec_snap_rows_.assign(T, -1);
2286 int kept = 0;
2287 for (int t = 0; t + 1 < T; ++t)
2288 if (all_rows || t == T - 2) rec_snap_rows_[t] = kept++;
2289 // Each kept row starts as the current state (a slot no node updates keeps it).
2290#ifdef HAS_CUDA
2291 if (gpu_) {
2292 if (kept > 0) grow(drec_rows_, static_cast<size_t>(kept) * rec_.size());
2293 for (int k = 0; k < kept; ++k)
2294 gpuErrchk(cudaMemcpyAsync(drec_rows_.alloc + k * rec_.size(), drec_, bytes, cudaMemcpyDeviceToDevice, stream_));
2295 return;
2296 }
2297#endif
2298 rec_rows_.resize(static_cast<size_t>(kept) * rec_.size());
2299 for (int k = 0; k < kept; ++k) std::copy(rec_.begin(), rec_.end(), rec_rows_.begin() + k * rec_.size());
2300}
2301
2302void Interpreter::rec_row_done(const RecSlot& slot, int layer, int t) {
2303 const int k = rec_snap_rows_[t];
2304 if (k < 0) return;
2305 // This slot only: another slot of the block may be at another row.
2306 const size_t off = static_cast<size_t>(layer) * rec_floats_ + slot.tensors.front().offset;
2307 const size_t floats = slot.tensors.back().offset + slot.tensors.back().floats - slot.tensors.front().offset;
2308#ifdef HAS_CUDA
2309 if (gpu_) {
2310 gpuErrchk(cudaMemcpyAsync(drec_rows_.alloc + k * rec_.size() + off, drec_ + off, floats * sizeof(float),
2311 cudaMemcpyDeviceToDevice, stream_));
2312 return;
2313 }
2314#endif
2315 std::copy(rec_.begin() + off, rec_.begin() + off + floats, rec_rows_.begin() + k * rec_.size() + off);
2316}
2317
2318void Interpreter::rec_end(int start_pos, int T) {
2319#ifdef HAS_CUDA
2320 if (gpu_) {
2321 const size_t bytes = rec_.size() * sizeof(float);
2322 for (int t = 0; t + 1 < T; ++t) {
2323 const int k = rec_snap_rows_[t];
2324 if (k < 0) continue;
2325 gpuErrchk(cudaMemcpyAsync(dsnap_take(start_pos + t + 1), drec_rows_.alloc + k * rec_.size(), bytes,
2326 cudaMemcpyDeviceToDevice, stream_));
2327 }
2329 while (dsnaps_.size() > rec_max_snaps_) dsnap_drop(dsnaps_.begin()); // the oldest positions go first
2330 rec_pos_ = start_pos + T;
2331 return;
2332 }
2333#endif
2334 for (int t = 0; t + 1 < T; ++t) {
2335 const int k = rec_snap_rows_[t];
2336 if (k < 0) continue;
2337 rec_snaps_[start_pos + t + 1].assign(rec_rows_.begin() + k * rec_.size(), rec_rows_.begin() + (k + 1) * rec_.size());
2338 }
2339 rec_snaps_[start_pos + T] = rec_;
2340 while (rec_snaps_.size() > rec_max_snaps_) rec_snaps_.erase(rec_snaps_.begin()); // the oldest positions go first
2341 rec_pos_ = start_pos + T;
2342 std::vector<float>().swap(rec_rows_);
2343}
2344
2345const Interpreter::Weight& Interpreter::bound(const std::string& name) const {
2346 auto it = weights_.find(name);
2347 if (it == weights_.end()) throw std::runtime_error("IR weight '" + name + "' was not bound");
2348 return it->second;
2349}
2350
2351const std::vector<float>& Interpreter::weight(const std::string& name) const {
2352 const Weight& w = bound(name);
2353 if (w.quantised()) throw std::runtime_error("IR weight '" + name + "' is held quantised but read whole");
2354 return w.f32;
2355}
2356
2357void Interpreter::run_nodes(const std::vector<Node>& nodes, int layer, Env& env, int T, int start_pos,
2358 bool /*head_last_only*/) {
2359 auto value = [&](const Node& n, size_t k, bool want_list) -> Buf& {
2360 auto it = env.find(n.in.at(k));
2361 if (it == env.end()) throw std::runtime_error("IR op '" + n.op + "': input '" + n.in.at(k) + "' is not defined");
2362 if (it->second.is_list != want_list)
2363 throw std::runtime_error("IR op '" + n.op + "': input '" + n.in.at(k) + "' is " +
2364 (want_list ? "a tensor, not a list" : "a list, not a tensor"));
2365 return it->second;
2366 };
2367 auto in = [&](const Node& n, size_t k) -> Buf& { return value(n, k, false); };
2368 auto list_in = [&](const Node& n, size_t k) -> Buf& { return value(n, k, true); };
2369 auto wname = [&](const Node& n) {
2370 std::string w = layer_name(n.attrs.at("weight").get<std::string>(), layer);
2371 if (!weights_.count(w) && has(n.attrs, "tied_to")) w = layer_name(n.attrs.at("tied_to").get<std::string>(), layer);
2372 return w;
2373 };
2374 for (const auto& n : nodes) {
2375 if (!applies(n, layer)) continue; // apply false in this application: reads, names and writes nothing
2376 AppIndex ti(app_index_, n);
2377 if (n.op == "embedding") {
2378 const auto& tokens = in(n, 0); // token ids carried as floats (exact below 2^24)
2379 const std::string wn = wname(n);
2380 const Weight& W = bound(wn);
2381 const int d = static_cast<int>(shapes_.at(wn).at(0));
2382 Buf out{std::vector<float>(static_cast<size_t>(T) * d), d};
2383 // Extended embedding (state_weight): an id in [vocab, vocab + k) reads row id - vocab of the
2384 // state rows; without one, an id past the table is refused. EXACT.
2385 const bool ext = has(n.attrs, "state_weight");
2386 for (int t = 0; t < T; ++t) {
2387 const int id = static_cast<int>(tokens.v[t]);
2388 const Weight* src = &W;
2389 int row = id;
2390 if (ext && id >= vocab_) {
2391 if (id >= vocab_ + state_k_)
2392 throw std::runtime_error("IR embedding: token id " + std::to_string(id) + " is past the state slots (vocab " +
2393 std::to_string(vocab_) + ", k " + std::to_string(state_k_) + ")");
2394 src = &bound(state_weight_);
2395 row = id - vocab_;
2396 } else if (id < 0 || id >= vocab_ || static_cast<size_t>(id + 1) * d > W.numel) { // IR_SPEC 5.1, 6.8.1
2397 throw std::runtime_error("IR embedding: token id " + std::to_string(id) + " out of range");
2398 }
2399 float* dst = out.v.data() + static_cast<size_t>(t) * d;
2400 if (src->quantised())
2401 dequant_span_to_f32(src->type, src->qp, static_cast<size_t>(row) * d, d, dst);
2402 else
2403 std::memcpy(dst, src->f32.data() + static_cast<size_t>(row) * d, d * sizeof(float));
2404 }
2405 env[n.out.at(0)] = std::move(out);
2406 } else if (n.op == "pos_embedding") { // x + position_embd[pos], pos absolute. EXACT.
2407 Buf x = in(n, 0);
2408 const std::string wn = wname(n);
2409 const auto& P = weight(wn);
2410 const int d = x.width;
2411 if (shapes_.at(wn).at(0) != d) throw std::runtime_error("IR pos_embedding '" + wn + "': width differs from x");
2412 if (static_cast<size_t>(start_pos + T) * d > P.size())
2413 throw std::runtime_error("IR pos_embedding: position " + std::to_string(start_pos + T - 1) + " exceeds '" + wn + "'");
2414 for (int t = 0; t < T; ++t)
2415 for (int i = 0; i < d; ++i) x.v[static_cast<size_t>(t) * d + i] += P[static_cast<size_t>(start_pos + t) * d + i];
2416 env[n.out.at(0)] = std::move(x);
2417 } else if (n.op == "layernorm") { // (x - mean) / sqrt(biased var + eps) * w + b. ROUNDING.
2418 const Buf& x = in(n, 0);
2419 const auto& w = weight(wname(n));
2420 const auto& b = weight(layer_name(n.attrs.at("bias").get<std::string>(), layer));
2421 const float eps = n.attrs.value("eps", 1e-5f);
2422 const int d = x.width;
2423 Buf out{std::vector<float>(x.v.size()), d};
2424 for (int t = 0; t < T; ++t) {
2425 const float* r = x.v.data() + static_cast<size_t>(t) * d;
2426 double mean = 0.0, var = 0.0;
2427 for (int i = 0; i < d; ++i) mean += r[i];
2428 mean /= d;
2429 for (int i = 0; i < d; ++i) var += (r[i] - mean) * (r[i] - mean);
2430 var /= d;
2431 const float inv = static_cast<float>(1.0 / std::sqrt(var + eps));
2432 for (int i = 0; i < d; ++i)
2433 out.v[static_cast<size_t>(t) * d + i] = (r[i] - static_cast<float>(mean)) * inv * w[i] + b[i];
2434 }
2435 env[n.out.at(0)] = std::move(out);
2436 } else if (n.op == "split") { // columns of [T, sum(sizes)] into the outputs, in order. EXACT.
2437 const Buf& x = in(n, 0);
2438 const auto sizes = n.attrs.at("sizes").get<std::vector<int>>();
2439 int total = 0;
2440 for (int s : sizes) total += s;
2441 if (sizes.size() != n.out.size()) throw std::runtime_error("IR split: sizes do not match the outputs");
2442 // groups g (revision 1.5): x is g groups of sum(sizes) columns, output k the k-th part of every group
2443 const int groups = n.attrs.contains("groups") ? n.attrs.at("groups").get<int>() : 1;
2444 if (groups < 1) throw std::runtime_error("IR_SPEC 5.5 (rule 10): split groups " + std::to_string(groups) + " is not positive");
2445 if (static_cast<int64_t>(total) * groups != x.width)
2446 throw std::runtime_error("IR_SPEC 5.5 (rule 37): split sizes sum to " + std::to_string(total) +
2447 (groups > 1 ? " times groups " + std::to_string(groups) : std::string()) +
2448 ", not the input width " + std::to_string(x.width));
2449 if (groups > 1) {
2450 std::vector<Buf> parts;
2451 std::vector<float*> ptrs;
2452 for (int s : sizes) parts.push_back(Buf{std::vector<float>(static_cast<size_t>(T) * groups * s), groups * s});
2453 for (auto& b : parts) ptrs.push_back(b.v.data());
2454 cpu::split_groups(x.v.data(), T, sizes, groups, ptrs);
2455 for (size_t o = 0; o < sizes.size(); ++o) env[n.out[o]] = std::move(parts[o]);
2456 continue;
2457 }
2458 int col = 0;
2459 for (size_t o = 0; o < sizes.size(); ++o) {
2460 Buf part{std::vector<float>(static_cast<size_t>(T) * sizes[o]), sizes[o]};
2461 for (int t = 0; t < T; ++t)
2462 std::memcpy(part.v.data() + static_cast<size_t>(t) * sizes[o], x.v.data() + static_cast<size_t>(t) * x.width + col,
2463 sizes[o] * sizeof(float));
2464 col += sizes[o];
2465 env[n.out[o]] = std::move(part);
2466 }
2467 } else if (n.op == "gelu") { // tanh: 0.5x(1 + tanh(sqrt(2/pi)(x + 0.044715x^3))); none: 0.5x(1 + erf(x/sqrt2)).
2468 const Buf& x = in(n, 0);
2469 const bool tanh_form = n.attrs.value("approx", "none") == "tanh";
2470 Buf out{std::vector<float>(x.v.size()), x.width};
2471 const float c = std::sqrt(2.0f / 3.14159265358979323846f);
2472 for (size_t i = 0; i < x.v.size(); ++i) {
2473 const float v = x.v[i];
2474 out.v[i] = tanh_form ? 0.5f * v * (1.0f + std::tanh(c * (v + 0.044715f * v * v * v)))
2475 : 0.5f * v * (1.0f + std::erf(v / std::sqrt(2.0f)));
2476 }
2477 env[n.out.at(0)] = std::move(out);
2478 } else if (n.op == "rmsnorm") {
2479 const Buf& x = in(n, 0);
2480 Buf out{{}, x.width};
2481 out.v.resize(x.v.size());
2482 // group g: each consecutive g columns normalised on their own, one
2483 // weight [g] shared (per-head q/k norm); a row of rows of width g.
2484 const int g = has(n.attrs, "group") ? n.attrs.at("group").get<int>() : x.width;
2485 const auto& w = weight(wname(n));
2486 if (g > 0 && x.width % g != 0)
2487 throw std::runtime_error("IR_SPEC 5.6 (rule 37): rmsnorm '" + wname(n) + "': group " + std::to_string(g) +
2488 " does not divide the input width " + std::to_string(x.width));
2489 if (g <= 0 || x.width % g != 0 || w.size() != static_cast<size_t>(g))
2490 throw std::runtime_error("IR rmsnorm '" + wname(n) + "': group " + std::to_string(g) + " does not fit width " +
2491 std::to_string(x.width) + " and weight [" + std::to_string(w.size()) + "]");
2492 rmsnorm_batch_cpu(x.v, w, out.v, T * (x.width / g), g, n.attrs.value("eps", 1e-5f));
2493 env[n.out.at(0)] = std::move(out);
2494 } else if (n.op == "linear") {
2495 const Buf& x = in(n, 0);
2496 const std::string wn = wname(n);
2497 const Weight& W = bound(wn);
2498 if (shapes_.at(wn).at(0) != x.width)
2499 throw std::runtime_error("IR linear '" + wn + "': input width " + std::to_string(x.width) +
2500 " does not match the weight's ne0 " + std::to_string(shapes_.at(wn).at(0)));
2501 const int in_dim = x.width, out_dim = static_cast<int>(W.numel / in_dim);
2502 Buf out{std::vector<float>(static_cast<size_t>(T) * out_dim), out_dim};
2503 if (W.quantised()) // one row of blocks at a time; equal to the f32 path (graph_ir.h)
2505 [&](int64_t o, float* row) {
2506 dequant_span_to_f32(W.type, W.qp, static_cast<size_t>(o) * in_dim, in_dim, row);
2507 },
2508 x.v.data(), out.v.data(), T, out_dim, in_dim);
2509 else
2511 if (has(n.attrs, "bias")) {
2512 const auto& b = weight(layer_name(n.attrs.at("bias").get<std::string>(), layer));
2513 for (int t = 0; t < T; ++t)
2514 for (int o = 0; o < out_dim; ++o) out.v[static_cast<size_t>(t) * out_dim + o] += b[o];
2515 }
2516 env[n.out.at(0)] = std::move(out);
2517 } else if (n.op == "rope") {
2518 Buf q = in(n, 0), k = in(n, 1);
2519 const int hd = n.attrs.at("head_dim").get<int>();
2520 rope_widths(n, layer, q.width, k.width);
2521 const auto& freqs = rope_table(layer);
2522 if (!rope_neox_ && rope_dim_ == head_dim_) { // full interleaved: the llama path's kernel
2523 apply_rope_batch_cpu(q.v, k.v, T, q.width / hd, k.width / hd, hd, start_pos, freqs, max_len_,
2524 /*use_adjacent_pairing=*/true);
2525 } else { // NEOX pairing or partial rotary
2527 throw std::runtime_error("IR rope: position " + std::to_string(start_pos + T - 1) + " exceeds the rope table (" +
2528 std::to_string(max_len_) + ")");
2529 if (q.width % hd != 0 || k.width % hd != 0) throw std::runtime_error("IR rope: q/k widths are not whole heads");
2530 rope_rows_cpu(q.v, T, q.width / hd, hd, rope_dim_, rope_neox_, start_pos, freqs);
2531 rope_rows_cpu(k.v, T, k.width / hd, hd, rope_dim_, rope_neox_, start_pos, freqs);
2532 }
2533 env[n.out.at(0)] = std::move(q);
2534 env[n.out.at(1)] = std::move(k);
2535 } else if (n.op == "attention") {
2536 const Buf &q = in(n, 0), &k = in(n, 1), &v = in(n, 2);
2537 const int hd = n.attrs.at("head_dim").get<int>(), hq = n.attrs.at("heads").get<int>();
2538 const int hkv = n.attrs.at("kv_heads").get<int>();
2539 if (hd != head_dim_ || hkv != kv_heads_) throw std::runtime_error("IR attention shape differs from its kv state");
2540 // Under a recursion step (mor_step) the rows go to the application's compact cache, at
2541 // attn_row_, and the query reads the rows before and at it; under turn_state they go to the
2542 // compact cache at kv_row0_ and each query reads its visible rows (ts_vis_); elsewhere rows
2543 // sit at their positions.
2544 const bool ts = n.attrs.value("mask", "causal") == "turn_state";
2545 const int kpos = ts ? kv_row0_ : attn_row_ >= 0 ? attn_row_ : start_pos;
2546 if (kpos + T > max_len_)
2547 throw std::runtime_error("IR attention: position " + std::to_string(kpos + T) + " exceeds the kv state (" +
2548 std::to_string(max_len_) + ")");
2549 KVCacheLayer& cache = kv_.at(layer);
2550 const size_t row = static_cast<size_t>(hkv) * hd;
2551 std::memcpy(cache.k.data() + kpos * row, k.v.data(), static_cast<size_t>(T) * row * sizeof(float));
2552 std::memcpy(cache.v.data() + kpos * row, v.v.data(), static_cast<size_t>(T) * row * sizeof(float));
2553 if (kv_fmt_ != ember_kvq::kF32) { // EMBER_KV: the rows as the quantised cache gives them back
2554 ember_kvq::roundtrip_rows(kv_fmt_, cache.k.data() + kpos * row, static_cast<size_t>(T), row);
2555 ember_kvq::roundtrip_rows(kv_fmt_, cache.v.data() + kpos * row, static_cast<size_t>(T), row);
2556 }
2557 const float scale = n.attrs.contains("scale") ? n.attrs.at("scale").get<float>() : 1.0f / std::sqrt(float(hd));
2558 Buf out{{}, hq * hd};
2559 int window = 0, sinks = 0;
2560 attention_window(n, layer, window, sinks);
2561 // Gemma 2's soft cap of the scaled scores (IR_SPEC 5.15, revision 1.5), before any position bias; 0: none.
2562 const float softcap = has(n.attrs, "softcap") ? at_layer(n.attrs.at("softcap"), std::max(layer, 0)).get<float>() : 0.0f;
2563 if (has(n.attrs, "position_bias")) { // T5's relative position bias (IR_SPEC 5.15, revision 1.3)
2564 const json& pb = n.attrs.at("position_bias");
2565 const std::vector<int> buckets = pb.at("buckets").get<std::vector<int>>();
2566 const auto& W = weight(layer_name(pb.at("weight").get<std::string>(), layer));
2567 std::vector<int> qpos(static_cast<size_t>(T));
2568 for (int t = 0; t < T; ++t) qpos[t] = start_pos + t;
2569 // under turn_state the compact cache's row r holds position kv_pos_[r]; elsewhere row r is position r
2570 const std::vector<std::vector<int>> rows = ts ? ts_vis_ : attention_visible_rows(T, start_pos, window, sinks);
2571 attention_bias_batch_cpu(q.v, cache, out.v, T, rows, ts ? &kv_pos_ : nullptr, qpos, hq, hkv, hd, scale, W.data(),
2572 buckets, softcap);
2573 } else if (has(n.attrs, "relative_kv")) { // Shaw's relative key and value vectors (IR_SPEC 5.15, revision 1.4)
2574 const json& rk = n.attrs.at("relative_kv");
2575 const int K = rk.at("max_distance").get<int>();
2576 // a table [K + 1, w], ne {w, K + 1}, w head_dim or heads * head_dim (rule 40, checked at load)
2577 auto table = [&](const char* member, int& w) -> const float* {
2578 w = 0;
2579 if (!rk.contains(member) || !rk.at(member).is_string()) return nullptr;
2580 const std::string name = layer_name(rk.at(member).get<std::string>(), layer);
2581 const auto& s = shapes_.at(name);
2582 if (s.size() != 2 || s[1] != K + 1 || (s[0] != hd && s[0] != static_cast<int64_t>(hq) * hd))
2583 throw std::runtime_error("IR_SPEC 5.15 (rule 40): relative_kv " + std::string(member) + " '" + name +
2584 "' is not of ne {head_dim or heads * head_dim, max_distance + 1}");
2585 w = static_cast<int>(s[0]);
2586 return weight(name).data();
2587 };
2588 int rk_w = 0, rv_w = 0;
2589 const float* RK = table("key", rk_w);
2590 const float* RV = table("value", rv_w);
2591 std::vector<int> qpos(static_cast<size_t>(T));
2592 for (int t = 0; t < T; ++t) qpos[t] = start_pos + t;
2593 const std::vector<std::vector<int>> rows = ts ? ts_vis_ : attention_visible_rows(T, start_pos, window, sinks);
2594 attention_relkv_batch_cpu(q.v, cache, out.v, T, rows, ts ? &kv_pos_ : nullptr, qpos, hq, hkv, hd, scale, RK, rk_w,
2595 RV, rv_w, K, softcap);
2596 } else if (ts)
2597 attention_rows_batch_cpu(q.v, cache, out.v, T, ts_vis_, hq, hkv, hd, scale, softcap);
2598 else if (window > 0 && attn_row_ >= 0) {
2599 // A window under depth_route (IR_SPEC 5.15 and 7.5, v1.4.2, Q16): the application's compact cache holds
2600 // positions mor_rows_[layer] at rows 0 .. attn_row_ - 1, and mor_step runs one token at a time, its row at
2601 // attn_row_ (position p = start_pos). A held row at position j is visible iff j < sink_hi or j >= win_lo
2602 // (swa_visible_ranges): the sinks and positions p - window + 1 .. p it holds, counted in positions, never the
2603 // last `window` rows; rows of tokens that exited are absent, not replaced.
2604 const std::vector<int>& held = mor_rows_.at(layer);
2605 if (T != 1 || static_cast<int>(held.size()) != attn_row_)
2606 throw std::runtime_error("IR depth_route: application " + std::to_string(layer) + " runs " +
2607 std::to_string(T) + " rows over " + std::to_string(held.size()) +
2608 " held positions for " + std::to_string(attn_row_) + " rows");
2609 int sink_hi = 0, win_lo = 0;
2611 std::vector<std::vector<int>> rows(1);
2612 for (int r = 0; r < attn_row_; ++r)
2613 if (held[r] < sink_hi || held[r] >= win_lo) rows[0].push_back(r);
2614 rows[0].push_back(attn_row_); // its own row, position p
2616 } else if (window > 0)
2618 else
2620 env[n.out.at(0)] = std::move(out);
2621 } else if (n.op == "mla_attention") { // multi-head latent attention (IR_SPEC 5.20). ROUNDING.
2622 const Buf &qn = in(n, 0), &qr = in(n, 1), &c = in(n, 2), &kr = in(n, 3);
2623 const int L = std::max(layer, 0);
2624 auto at = [&](const char* k) { return at_layer(n.attrs.at(k), L).get<int>(); };
2625 const int H = at("heads"), r = at("kv_rank"), dn = at("qk_nope"), dr = at("qk_rope"), dv = at("v_head_dim");
2626 if (qn.width != H * dn || qr.width != H * dr || c.width != r || kr.width != dr)
2627 throw std::runtime_error("IR_SPEC 5.20 (rule 37): mla_attention reads qn " + std::to_string(qn.width) + ", qr " +
2628 std::to_string(qr.width) + ", c " + std::to_string(c.width) + " and kr " +
2629 std::to_string(kr.width) + " wide, not heads x qk_nope " + std::to_string(H * dn) +
2630 ", heads x qk_rope " + std::to_string(H * dr) + ", kv_rank " + std::to_string(r) +
2631 " and qk_rope " + std::to_string(dr));
2632 if (start_pos + T > max_len_)
2633 throw std::runtime_error("IR mla_attention: position " + std::to_string(start_pos + T) + " exceeds the kv state (" +
2634 std::to_string(max_len_) + ")");
2635 // The slot's row of position p is [c_p | kr_p] (kv_heads 1, head_dim r + dr; rule 12 at load).
2636 KVCacheLayer& cache = kv_.at(layer);
2637 const size_t w = static_cast<size_t>(r) + dr;
2638 for (int t = 0; t < T; ++t) {
2639 float* dst = cache.k.data() + static_cast<size_t>(start_pos + t) * w;
2640 std::memcpy(dst, c.v.data() + static_cast<size_t>(t) * r, static_cast<size_t>(r) * sizeof(float));
2641 std::memcpy(dst + r, kr.v.data() + static_cast<size_t>(t) * dr, static_cast<size_t>(dr) * sizeof(float));
2642 }
2643 int window = 0, sinks = 0;
2644 attention_window(n, layer, window, sinks);
2645 const auto& Kb = weight(layer_name(n.attrs.at("k_b").get<std::string>(), layer));
2646 const auto& Vb = weight(layer_name(n.attrs.at("v_b").get<std::string>(), layer));
2647 Buf out{std::vector<float>(static_cast<size_t>(T) * H * dv), H * dv};
2648 cpu::mla_attention(qn.v.data(), qr.v.data(), cache.k.data(), out.v.data(), T,
2649 attention_visible_rows(T, start_pos, window, sinks), H, r, dn, dr, dv, Kb.data(), Vb.data(),
2650 n.attrs.at("scale").get<float>());
2651 env[n.out.at(0)] = std::move(out);
2652 } else if (n.op == "add" || n.op == "mul") {
2653 const Buf &a = in(n, 0), &b = in(n, 1);
2654 if (a.v.size() != b.v.size() || a.width != b.width) binary_shapes(n.op, a.width, b.width);
2655 Buf out{std::vector<float>(a.v.size()), a.width};
2656 if (n.op == "add")
2657 for (size_t i = 0; i < a.v.size(); ++i) out.v[i] = a.v[i] + b.v[i];
2658 else
2659 for (size_t i = 0; i < a.v.size(); ++i) out.v[i] = a.v[i] * b.v[i];
2660 env[n.out.at(0)] = std::move(out);
2661 } else if (n.op == "moe_ffn") {
2662 const Buf& x = in(n, 0);
2663 const auto& ex = n.attrs.at("experts");
2664 const std::string gn = layer_name(ex.at("gate").get<std::string>(), layer);
2665 const auto& R = weight(layer_name(n.attrs.at("router").get<std::string>(), layer));
2666 const auto& G = weight(gn);
2667 const auto& U = weight(layer_name(ex.at("up").get<std::string>(), layer));
2668 const auto& D = weight(layer_name(ex.at("down").get<std::string>(), layer));
2669 const int E = n.attrs.at("n_experts").get<int>(), k = n.attrs.at("top_k").get<int>();
2670 const auto& gs = shapes_.at(gn); // ggml order {d, d_ff, E}
2671 if (gs.size() != 3 || gs[0] != x.width || gs[2] != E)
2672 throw std::runtime_error("IR moe_ffn '" + gn + "': expert stack shape does not match d and n_experts");
2673 const int d = x.width, d_ff = static_cast<int>(gs[1]);
2674 const bool renorm = n.attrs.value("gate_norm", "renorm") == "renorm";
2675 // DeepSeek-V3's router (IR_SPEC 5.16, revision 1.6): sigmoid scores, a selection bias, group-limited routing.
2676 const bool sigmoid = has(n.attrs, "scoring") && at_layer(n.attrs.at("scoring"), std::max(layer, 0)) == "sigmoid";
2677 const float* bias =
2678 has(n.attrs, "selection_bias") ? weight(layer_name(n.attrs.at("selection_bias").get<std::string>(), layer)).data()
2679 : nullptr;
2680 int groups_n = 1, groups_top = 1;
2681 if (has(n.attrs, "groups")) {
2682 groups_n = n.attrs.at("groups").at("n").get<int>();
2683 groups_top = n.attrs.at("groups").at("top").get<int>();
2684 if (groups_n < 1 || E % groups_n != 0 || groups_top < 1 || groups_top > groups_n || groups_top * (E / groups_n) < k)
2685 throw std::runtime_error("IR_SPEC 5.16 (rule 10): moe_ffn groups {n " + std::to_string(groups_n) + ", top " +
2686 std::to_string(groups_top) + "} do not fit n_experts " + std::to_string(E) + " and top_k " +
2687 std::to_string(k));
2688 }
2689 Buf out{std::vector<float>(static_cast<size_t>(T) * d), d};
2690#pragma omp parallel for schedule(static)
2691 for (int t = 0; t < T; ++t)
2692 moe_ffn_routed_f32_cpu(x.v.data() + static_cast<size_t>(t) * d, d, d_ff, E, k, R.data(), G.data(), U.data(),
2693 D.data(), out.v.data() + static_cast<size_t>(t) * d, renorm, sigmoid, bias, groups_n, groups_top);
2694 env[n.out.at(0)] = std::move(out);
2695 } else if (n.op == "silu") {
2696 const Buf& x = in(n, 0);
2697 Buf out{std::vector<float>(x.v.size()), x.width};
2698 silu_cpu(x.v, out.v);
2699 env[n.out.at(0)] = std::move(out);
2700 } else if (n.op == "sub") { // a - b. EXACT.
2701 const Buf &a = in(n, 0), &b = in(n, 1);
2702 if (a.v.size() != b.v.size() || a.width != b.width) binary_shapes(n.op, a.width, b.width);
2703 Buf out{std::vector<float>(a.v.size()), a.width};
2704 cpu::sub(a.v.data(), b.v.data(), out.v.data(), a.v.size());
2705 env[n.out.at(0)] = std::move(out);
2706 } else if (n.op == "relu" || n.op == "sigmoid") { // relu EXACT; sigmoid ROUNDING.
2707 const Buf& x = in(n, 0);
2708 Buf out{std::vector<float>(x.v.size()), x.width};
2709 (n.op == "relu" ? cpu::relu : cpu::sigmoid)(x.v.data(), out.v.data(), x.v.size());
2710 env[n.out.at(0)] = std::move(out);
2711 } else if (n.op == "affine") { // (x * scale) + shift. EXACT.
2712 const Buf& x = in(n, 0);
2713 Buf out{std::vector<float>(x.v.size()), x.width};
2714 cpu::affine(x.v.data(), out.v.data(), x.v.size(), at_layer(n.attrs.at("scale"), layer).get<float>(),
2715 at_layer(n.attrs.at("shift"), layer).get<float>());
2716 env[n.out.at(0)] = std::move(out);
2717 } else if (n.op == "scale") { // (x * g) + b per feature, g and b [d]. EXACT.
2718 const Buf& x = in(n, 0);
2719 const std::string wn = wname(n);
2720 const auto& g = weight(wn);
2721 const int d = x.width;
2722 if (d <= 0 || g.size() != static_cast<size_t>(d))
2723 throw std::runtime_error("IR_SPEC 5.4 (rule 37): scale '" + wn + "' has " + std::to_string(g.size()) +
2724 " values for width " + std::to_string(d));
2725 const float* b = nullptr;
2726 if (has(n.attrs, "bias")) {
2727 const std::string bn = layer_name(n.attrs.at("bias").get<std::string>(), layer);
2728 const auto& B = weight(bn);
2729 if (B.size() != static_cast<size_t>(d))
2730 throw std::runtime_error("IR_SPEC 5.4 (rule 37): scale bias '" + bn + "' has " + std::to_string(B.size()) +
2731 " values for width " + std::to_string(d));
2732 b = B.data();
2733 }
2734 Buf out{std::vector<float>(x.v.size()), d};
2735 cpu::scale(x.v.data(), g.data(), b, out.v.data(), static_cast<int>(x.v.size() / d), d);
2736 env[n.out.at(0)] = std::move(out);
2737 } else if (n.op == "mul_col") { // x [T, w] * c [T, 1]. EXACT.
2738 const Buf &x = in(n, 0), &c = in(n, 1);
2739 if (c.width != 1 || c.v.size() != static_cast<size_t>(T) || x.v.size() != static_cast<size_t>(T) * x.width)
2740 throw std::runtime_error("IR_SPEC 5.12 (rule 37): mul_col: c must be [T, 1] beside x [T, w]");
2741 Buf out{std::vector<float>(x.v.size()), x.width};
2742 cpu::mul_col(x.v.data(), c.v.data(), out.v.data(), T, x.width);
2743 env[n.out.at(0)] = std::move(out);
2744 } else if (n.op == "rms_row") { // [T, w] -> [T, 1]: sqrt(ss / w + eps). ROUNDING.
2745 const Buf& x = in(n, 0);
2746 if (x.width <= 0 || x.v.size() != static_cast<size_t>(T) * x.width)
2747 throw std::runtime_error("IR rms_row: input is not [T, w]");
2748 Buf out{std::vector<float>(static_cast<size_t>(T)), 1};
2749 cpu::rms_row(x.v.data(), out.v.data(), T, x.width, n.attrs.at("eps").get<float>());
2750 env[n.out.at(0)] = std::move(out);
2751 } else if (n.op == "expr") { // runtime-defined elementwise op (graph_ir_expr.h). ROUNDING.
2752 std::vector<const float*> ptrs;
2753 const Buf& x0 = in(n, 0);
2754 for (size_t k = 0; k < n.in.size(); ++k) {
2755 const Buf& a = in(n, k);
2756 if (a.width != x0.width || a.v.size() != x0.v.size())
2757 throw std::runtime_error("IR_SPEC 5.19 (rule 37): expr inputs must share one shape [T, w]");
2758 ptrs.push_back(a.v.data());
2759 }
2760 if (x0.width <= 0 || x0.v.size() % x0.width) throw std::runtime_error("IR expr: input is not [T, w]");
2761 Buf out{std::vector<float>(x0.v.size()), x0.width};
2762 expr::eval(expr::cached(n.attrs.at("expr").get<std::string>(), n.in.size()), ptrs,
2763 static_cast<int>(x0.v.size() / x0.width), x0.width, out.v.data());
2764 env[n.out.at(0)] = std::move(out);
2765 } else if (n.op == "list") { // the inputs, in order, as one list value. EXACT.
2766 Buf out;
2767 out.is_list = true;
2768 for (size_t k = 0; k < n.in.size(); ++k) out.items.push_back(in(n, k));
2769 env[n.out.at(0)] = std::move(out);
2770 } else if (n.op == "append") { // the list with x added at the end. EXACT.
2771 const Buf& x = in(n, 1);
2772 // The usual form appends in place (out names the list): no copy.
2773 Buf out;
2774 if (n.out.at(0) == n.in.at(0))
2775 out = std::move(list_in(n, 0));
2776 else
2777 out = list_in(n, 0);
2778 out.items.push_back(x);
2779 env[n.out.at(0)] = std::move(out);
2780 } else if (n.op == "depth_attend") { // attention over depth (AttnRes block form). ROUNDING.
2781 const Buf& L = list_in(n, 0);
2782 std::vector<const float*> src;
2783 int d = 0;
2784 auto add_source = [&](const Buf& s) {
2785 if (d == 0) d = s.width;
2786 if (s.width != d || d <= 0 || s.v.size() != static_cast<size_t>(T) * d)
2787 throw std::runtime_error("IR_SPEC 5.18 (rule 37): depth_attend sources must all be one shape [T, d]");
2788 src.push_back(s.v.data());
2789 };
2790 for (const Buf& s : L.items) add_source(s);
2791 for (size_t k = 1; k < n.in.size(); ++k) add_source(in(n, k));
2792 if (src.empty()) throw std::runtime_error("IR depth_attend: no sources");
2793 const float* w = nullptr;
2794 if (src.size() > 1) { // one source: out = s_0 and the pseudo-query is not read
2795 const std::string wn = layer_name(n.attrs.at("weight").get<std::string>(), layer);
2796 if (!weights_.count(wn)) throw std::runtime_error("IR depth_attend: pseudo-query '" + wn + "' is not in the GGUF");
2797 const auto& W = weight(wn);
2798 if (W.size() != static_cast<size_t>(d))
2799 throw std::runtime_error("IR depth_attend: pseudo-query '" + wn + "' is not [d]");
2800 w = W.data();
2801 }
2802 Buf out{std::vector<float>(static_cast<size_t>(T) * d), d};
2803 cpu::depth_attend(src, w, out.v.data(), T, d, n.attrs.value("eps", 1e-5f));
2804 env[n.out.at(0)] = std::move(out);
2805 } else if (n.op == "linear_attention") { // elu+1 linear attention over the slot's num, den. ROUNDING.
2806 const Buf &q = in(n, 0), &k = in(n, 1), &v = in(n, 2);
2807 const int w = n.attrs.at("heads").get<int>() * n.attrs.at("head_dim").get<int>();
2808 if (q.width != w || k.width != w || v.width != w)
2809 throw std::runtime_error("IR linear_attention: q/k/v widths do not match heads * head_dim");
2810 const RecSlot& slot = rec_slot(n, "linear_attention");
2811 float* num = rec_tensor(slot, "num", layer, w);
2812 float* den = rec_tensor(slot, "den", layer, w);
2813 const float floor = n.attrs.value("den_floor", 1e-8f);
2814 Buf out{std::vector<float>(static_cast<size_t>(T) * w), w};
2815 for (int t = 0; t < T; ++t) { // the decode update for t = 0..T-1, in order
2816 const size_t r = static_cast<size_t>(t) * w;
2817 linear_attention_row_cpu(q.v.data() + r, k.v.data() + r, v.v.data() + r, num, den, out.v.data() + r, w, floor);
2818 rec_row_done(slot, layer, t);
2819 }
2820 env[n.out.at(0)] = std::move(out);
2821 } else if (n.op == "ssm_scan") { // the sequential state-space scan over the slot's s. ROUNDING.
2822 const Buf& u = in(n, 0);
2823 const int sn = n.attrs.at("n").get<int>();
2824 if (u.width != sn) throw std::runtime_error("IR ssm_scan: input width differs from n");
2825 const RecSlot& slot = rec_slot(n, "ssm_scan");
2826 float* s = rec_tensor(slot, "s", layer, sn);
2827 const auto& A = weight(layer_name(ssm_weight(n.attrs, "a"), layer));
2828 const auto& B = weight(layer_name(ssm_weight(n.attrs, "b"), layer));
2829 const auto& C = weight(layer_name(ssm_weight(n.attrs, "c"), layer));
2830 const float D = weight(layer_name(ssm_weight(n.attrs, "d"), layer)).at(0);
2831 std::vector<float> scratch(sn);
2832 Buf out{std::vector<float>(static_cast<size_t>(T) * sn), sn};
2833 for (int t = 0; t < T; ++t) {
2834 const size_t r = static_cast<size_t>(t) * sn;
2835 ssm_scan_row_cpu(u.v.data() + r, A.data(), B.data(), C.data(), D, s, scratch.data(), out.v.data() + r, sn);
2836 rec_row_done(slot, layer, t);
2837 }
2838 env[n.out.at(0)] = std::move(out);
2839 } else if (n.op == "conv1d_causal") { // depthwise causal conv over the slot's previous inputs. ROUNDING.
2840 const Buf& x = in(n, 0);
2841 const int C = n.attrs.at("channels").get<int>(), K = n.attrs.at("kernel").get<int>();
2842 if (x.width != C) throw std::runtime_error("IR conv1d_causal: input width differs from channels");
2843 const RecSlot& slot = rec_slot(n, "conv1d_causal");
2844 float* st = rec_tensor(slot, "x", layer, static_cast<size_t>(K - 1) * C);
2845 const auto& w = weight(layer_name(n.attrs.at("weight").get<std::string>(), layer));
2846 const float* bias =
2847 has(n.attrs, "bias") ? weight(layer_name(n.attrs.at("bias").get<std::string>(), layer)).data() : nullptr;
2848 Buf out{std::vector<float>(static_cast<size_t>(T) * C), C};
2849 for (int t = 0; t < T; ++t) {
2850 const size_t r = static_cast<size_t>(t) * C;
2851 cpu::conv1d_causal_row(x.v.data() + r, w.data(), bias, st, out.v.data() + r, C, K);
2852 rec_row_done(slot, layer, t);
2853 }
2854 env[n.out.at(0)] = std::move(out);
2855 } else if (n.op == "selective_scan") { // Mamba-1's scan over the slot's s. ROUNDING.
2856 const Buf &x = in(n, 0), &dt = in(n, 1), &bm = in(n, 2), &cm = in(n, 3);
2857 const int D = n.attrs.at("channels").get<int>(), N = n.attrs.at("d_state").get<int>();
2858 if (x.width != D || dt.width != D || bm.width != N || cm.width != N)
2859 throw std::runtime_error("IR selective_scan: x/dt must be [T, channels] and B/C [T, d_state]");
2860 const RecSlot& slot = rec_slot(n, "selective_scan");
2861 float* s = rec_tensor(slot, "s", layer, static_cast<size_t>(D) * N);
2862 const auto& A = weight(layer_name(n.attrs.at("a").get<std::string>(), layer));
2863 const float* d = has(n.attrs, "d") ? weight(layer_name(n.attrs.at("d").get<std::string>(), layer)).data() : nullptr;
2864 const bool softplus = n.attrs.value("dt_softplus", true);
2865 Buf out{std::vector<float>(static_cast<size_t>(T) * D), D};
2866 for (int t = 0; t < T; ++t) {
2867 cpu::selective_scan_row(x.v.data() + static_cast<size_t>(t) * D, dt.v.data() + static_cast<size_t>(t) * D,
2868 bm.v.data() + static_cast<size_t>(t) * N, cm.v.data() + static_cast<size_t>(t) * N,
2869 A.data(), d, s, out.v.data() + static_cast<size_t>(t) * D, D, N, softplus);
2870 rec_row_done(slot, layer, t);
2871 }
2872 env[n.out.at(0)] = std::move(out);
2873 } else if (n.op == "recurrence") { // a recurrent op the document defines (IR_SPEC 6.10). EXACT or ROUNDING.
2874 const rec::Recurrence& prog = *recs_.at(&n);
2875 const RecSlot& slot = rec_slot(n, "recurrence");
2876 std::vector<const Buf*> ins;
2877 for (size_t k = 0; k < n.in.size(); ++k) {
2878 const Buf& b = in(n, k);
2879 if (b.is_list || static_cast<size_t>(b.width) != prog.input_widths()[k] ||
2880 b.v.size() != static_cast<size_t>(T) * b.width)
2881 throw std::runtime_error("IR_SPEC 6.10 (rule 34): recurrence input '" + n.in[k] + "' is " +
2882 std::to_string(b.width) + " wide; its declaration is " +
2883 std::to_string(prog.input_widths()[k]));
2884 ins.push_back(&b);
2885 }
2886 std::vector<const float*> w;
2887 for (const auto& wd : prog.weights()) w.push_back(weight(layer_name(wd.tensor, layer)).data());
2888 std::vector<float*> st;
2889 for (const auto& s : prog.state())
2890 st.push_back(rec_tensor(slot, s.name.c_str(), layer, s.numel));
2891 const size_t ow = prog.out_width();
2892 Buf out{std::vector<float>(static_cast<size_t>(T) * ow), static_cast<int>(ow)};
2893 std::vector<const float*> row(ins.size());
2894 for (int t = 0; t < T; ++t) { // token by token, in order (prefill = decode)
2895 for (size_t k = 0; k < ins.size(); ++k) row[k] = ins[k]->v.data() + static_cast<size_t>(t) * ins[k]->width;
2896 prog.step(row, w, st, out.v.data() + static_cast<size_t>(t) * ow);
2897 rec_row_done(slot, layer, t);
2898 }
2899 env[n.out.at(0)] = std::move(out);
2900 }
2901 }
2902}
2903
2904// One application of a recursion step (ir.hpp depth_route (a)-(d)). The first application of step
2905// 0 routes every row; the first of step k keeps the step's input x_in; every application runs the
2906// block's other nodes on the tokens deeper than k, one at a time in position order (T = 1 at its
2907// absolute position, its k/v row appended to this application's compact cache); after the step's
2908// last application gate "score" blends each active row with its x_in. Inactive rows keep x_in.
2909void Interpreter::mor_step(int layer, Env& env, int T, int start_pos) {
2910 const int s = mor_step_[layer];
2911 const bool first = layer == 0 || mor_step_[layer - 1] != s;
2912 const bool last = layer == n_layers_ - 1 || mor_step_[layer + 1] != s;
2913 auto it = env.find(mor_x_);
2914 if (it == env.end() || it->second.is_list) throw std::runtime_error("IR depth_route: input '" + mor_x_ + "' is not a tensor");
2915 Buf& x = it->second;
2916 const int d = x.width;
2917 if (s == 0 && first) {
2918 const auto& W = weight(mor_router_);
2919 if (shapes_.at(mor_router_).at(0) != d || W.size() != static_cast<size_t>(mor_R_) * d)
2920 throw std::runtime_error("IR depth_route '" + mor_router_ + "' is not [n_rec, d]");
2921 std::vector<float> logits(static_cast<size_t>(T) * mor_R_);
2922 matmul_f32_f32_batch_cpu(W, x.v, logits, T, mor_R_, d); // x W^T, as linear
2923 mor_depth_.assign(T, 0);
2924 mor_g_.assign(T, 0.0f);
2925 for (int t = 0; t < T; ++t) cpu::mor_route(logits.data() + static_cast<size_t>(t) * mor_R_, mor_R_, mor_depth_[t], mor_g_[t]);
2926 env[mor_out_] = Buf{std::vector<float>(mor_depth_.begin(), mor_depth_.end()), 1};
2927 }
2928 if (first) mor_xin_ = x.v;
2929 std::vector<int> active;
2930 for (int t = 0; t < T; ++t)
2931 if (mor_depth_.at(t) > s) active.push_back(t);
2932 for (int t : active) {
2933 Env row;
2934 for (const auto& kv : env)
2935 if (!kv.second.is_list && kv.second.width > 0 && kv.second.v.size() == static_cast<size_t>(T) * kv.second.width) {
2936 const auto b = kv.second.v.begin() + static_cast<size_t>(t) * kv.second.width;
2937 row[kv.first] = Buf{std::vector<float>(b, b + kv.second.width), kv.second.width};
2938 }
2939 attn_row_ = static_cast<int>(mor_rows_[layer].size());
2940 try {
2941 run_nodes(block_, layer, row, 1, start_pos + t, false);
2942 } catch (...) {
2943 attn_row_ = -1;
2944 throw;
2945 }
2946 attn_row_ = -1;
2947 mor_rows_[layer].push_back(start_pos + t);
2948 const Buf& y = row.at(mor_x_);
2949 if (y.width != d) throw std::runtime_error("IR depth_route: the block changes the width of '" + mor_x_ + "'");
2950 std::copy(y.v.begin(), y.v.end(), x.v.begin() + static_cast<size_t>(t) * d);
2951 }
2952 if (last && mor_gate_score_)
2953 for (int t : active)
2954 cpu::mor_gate(mor_xin_.data() + static_cast<size_t>(t) * d, x.v.data() + static_cast<size_t>(t) * d, d, mor_g_[t]);
2955}
2956
2957std::vector<float> Interpreter::forward(const std::vector<int>& tokens, int start_pos, bool all_rows, int tail_rows) {
2958 const int T = static_cast<int>(tokens.size());
2959 if (T == 0) return {};
2960 if (rope_ff_ctx_ <= 0) return forward_impl(tokens, start_pos, all_rows, tail_rows);
2961 // LongRoPE with two sets (IR_SPEC 5.14, 6.9; revision 1.5): n = start_pos + T selects the long set when
2962 // n > original_ctx, for this call's rows and every row the kv slot holds.
2963 if (start_pos < 0 || start_pos > static_cast<int>(ids_.size()))
2964 throw std::runtime_error("IR forward at position " + std::to_string(start_pos) + ": the sequence holds " +
2965 std::to_string(ids_.size()) + " ids");
2966 const bool want = start_pos + T > rope_ff_ctx_;
2967 if (want == rope_long_ || start_pos == 0) {
2968 set_rope_long(want);
2969 std::vector<float> out = forward_impl(tokens, start_pos, all_rows, tail_rows);
2970 ids_.resize(start_pos);
2971 ids_.insert(ids_.end(), tokens.begin(), tokens.end());
2972 return out;
2973 }
2974 // The held rows were rotated with the other set: the whole sequence again from position 0, as Phi-3's own
2975 // generation recomputes its cache when the length first passes original_ctx; the same rows of logits come back.
2976 std::vector<int> all(ids_.begin(), ids_.begin() + start_pos);
2977 all.insert(all.end(), tokens.begin(), tokens.end());
2978 set_rope_long(want);
2979 Logger::info("[IR] LongRoPE: the sequence's length " + std::to_string(all.size()) + (want ? " passes " : " is within ") +
2980 std::to_string(rope_ff_ctx_) + ": the " + (want ? "long" : "short") +
2981 " set, every kv row recomputed from position 0");
2982 std::vector<float> out;
2983 if (all_rows) {
2984 out = forward_impl(all, 0, true, 0);
2985 const size_t width = out.size() / all.size(); // the head's row width
2986 out.erase(out.begin(), out.end() - static_cast<size_t>(T) * width);
2987 } else {
2988 out = forward_impl(all, 0, false, std::min(T, std::max(1, tail_rows)));
2989 }
2990 ids_ = std::move(all);
2991 return out;
2992}
2993
2994std::vector<float> Interpreter::forward_impl(const std::vector<int>& tokens, int start_pos, bool all_rows,
2995 int tail_rows) {
2996 const int T = static_cast<int>(tokens.size());
2997 if (T == 0) return {};
2998 if (rec_floats_ > 0 && (start_pos < 0 || start_pos + T > max_len_))
2999 throw std::runtime_error("IR recurrent state: positions " + std::to_string(start_pos) + ".." +
3000 std::to_string(start_pos + T - 1) + " exceed model.ctx (" + std::to_string(max_len_) + ")");
3001#ifdef HAS_CUDA
3002 if (gpu_) {
3003 std::vector<float> out = forward_device(tokens, start_pos, all_rows, tail_rows);
3004 seq_len_ = start_pos + T;
3005 return out;
3006 }
3007#endif
3008 if (state_k_ > 0 || turn_state_) ts_begin(tokens, start_pos);
3009 if (rec_floats_ > 0) rec_begin(start_pos, T, all_rows);
3010 Env env;
3011 env["tokens"] = Buf{std::vector<float>(tokens.begin(), tokens.end()), 1};
3012 run_nodes(embed_, -1, env, T, start_pos, false);
3013 if (!mor_step_.empty()) {
3014 // Resuming at start_pos (a new sequence, prefix reuse, a rewind): each compact cache keeps
3015 // the rows of positions below it.
3016 for (auto& rows : mor_rows_)
3017 while (!rows.empty() && rows.back() >= start_pos) rows.pop_back();
3018 mor_depth_.clear();
3019 }
3020 for (int l = 0; l < n_layers_; ++l) {
3021 if (!mor_step_.empty() && mor_step_[l] >= 0)
3022 mor_step(l, env, T, start_pos);
3023 else
3024 run_nodes(block_, l, env, T, start_pos, false);
3025 }
3026 if (turn_state_) {
3027 // Every block has written this call's rows at kv_row0_ ..: they now hold these positions.
3028 for (int t = 0; t < T; ++t) kv_pos_.push_back(start_pos + t);
3029 // The text rows of segments before the last token's are invisible to every later query.
3030 if (evict_) ts_evict_below(ts_seg_.back());
3031 }
3032 if (rec_floats_ > 0) rec_end(start_pos, T);
3033 // Head ops are row-wise: run them on the last row only, or on the last tail_rows rows.
3034 const int rows = all_rows ? T : std::min(T, std::max(1, tail_rows));
3035 if (rows < T) {
3036 auto last_rows = [T, rows](Buf& b) {
3037 if (b.width > 0 && b.v.size() == static_cast<size_t>(T) * b.width)
3038 b.v.erase(b.v.begin(), b.v.end() - static_cast<size_t>(rows) * b.width);
3039 };
3040 for (auto& kv : env) {
3041 last_rows(kv.second);
3042 for (Buf& item : kv.second.items) last_rows(item); // a list's tensors
3043 }
3044 }
3045 run_nodes(head_, -1, env, rows, start_pos, !all_rows);
3046 auto it = env.find("logits");
3047 if (it == env.end()) throw std::runtime_error("IR head produced no 'logits'");
3048 seq_len_ = start_pos + T;
3049 return std::move(it->second.v);
3050}
3051
3052// Conformance hooks (graph_ir.h). Each reads or sets the CPU state only.
3053static void cpu_only(bool gpu, const char* what) {
3054 if (gpu) throw std::runtime_error(std::string("IR ") + what + " is implemented for the CPU path only");
3055}
3056
3057int Interpreter::kv_rows_in_use(int application) const {
3058 if (kv_.empty()) return 0;
3059 if (turn_state_) return static_cast<int>(kv_pos_.size());
3060 if (!mor_step_.empty() && mor_step_.at(application) >= 0) return static_cast<int>(mor_rows_.at(application).size());
3061 return seq_len_;
3062}
3063
3064std::vector<int> Interpreter::kv_positions(int application) const {
3065 cpu_only(gpu_, "kv_positions");
3066 if (application < 0 || application >= n_layers_)
3067 throw std::runtime_error("IR kv_positions: application " + std::to_string(application) + " of " +
3068 std::to_string(n_layers_));
3069 if (kv_.empty()) return {};
3070 if (turn_state_) return kv_pos_;
3071 if (!mor_step_.empty() && mor_step_[application] >= 0) return mor_rows_[application];
3072 std::vector<int> p(static_cast<size_t>(seq_len_));
3073 for (int i = 0; i < seq_len_; ++i) p[i] = i;
3074 return p;
3075}
3076
3077std::vector<int> Interpreter::held_positions(int application) const {
3078 if (application < 0 || application >= n_layers_)
3079 throw std::runtime_error("IR held_positions: application " + std::to_string(application) + " of " +
3080 std::to_string(n_layers_));
3081 if (kv_heads_ <= 0) return {};
3082 if (turn_state_) return kv_pos_;
3083 if (!mor_step_.empty() && mor_step_[application] >= 0) return mor_rows_[application];
3084 std::vector<int> p(static_cast<size_t>(seq_len_));
3085 for (int i = 0; i < seq_len_; ++i) p[i] = i;
3086 return p;
3087}
3088
3089std::vector<Interpreter::RecurrentTensorInfo> Interpreter::recurrent_tensors() const {
3090 std::vector<RecurrentTensorInfo> out;
3091 for (const auto& slot : rec_slots_)
3092 for (const auto& rt : slot.tensors) out.push_back({slot.name, rt.name, rt.shape});
3093 return out;
3094}
3095
3096std::vector<float> Interpreter::recurrent_values(const std::string& slot, const std::string& tensor,
3097 int application) const {
3098 cpu_only(gpu_, "recurrent_values");
3099 if (application < 0 || application >= n_layers_)
3100 throw std::runtime_error("IR recurrent_values: application " + std::to_string(application) + " of " +
3101 std::to_string(n_layers_));
3102 for (const auto& s : rec_slots_)
3103 if (s.name == slot)
3104 for (const auto& rt : s.tensors)
3105 if (rt.name == tensor) {
3106 const auto b = rec_.begin() + static_cast<size_t>(application) * rec_floats_ + rt.offset;
3107 return std::vector<float>(b, b + rt.floats);
3108 }
3109 throw std::runtime_error("IR recurrent_values: no tensor '" + tensor + "' in a slot '" + slot + "'");
3110}
3111
3112void Interpreter::truncate(int n) {
3113 cpu_only(gpu_, "truncate");
3114 if (n < 0 || n > seq_len_)
3115 throw std::runtime_error("IR truncate to " + std::to_string(n) + ": the state covers " + std::to_string(seq_len_) +
3116 " positions");
3117 if (rec_floats_ > 0 && rec_pos_ < 0) throw std::runtime_error("IR truncate: the recurrent state is not usable (a call failed)");
3118 if (resume_point(n) != n)
3119 throw std::runtime_error("IR truncate to " + std::to_string(n) + ": the state can resume only at " +
3120 std::to_string(resume_point(n)) + " (re-prefill from there)");
3121 if (n == seq_len_) return;
3122 if (state_k_ > 0 || turn_state_) {
3123 // n is its segment's start when that segment's text rows are gone (resume_point): every row
3124 // of segments >= m lies at or past n, so dropping positions >= n leaves segments < m only.
3125 const int m = n > 0 ? ts_seg_after(n - 1) : 0;
3126 if (turn_state_ && m < evicted_below_) evicted_below_ = m;
3127 ts_seg_.resize(n);
3128 ts_sidx_.resize(n);
3129 while (!kv_pos_.empty() && kv_pos_.back() >= n) kv_pos_.pop_back();
3130 }
3131 for (auto& rows : mor_rows_)
3132 while (!rows.empty() && rows.back() >= n) rows.pop_back();
3133 if (rec_floats_ > 0 && n != rec_pos_) {
3134 if (n == 0) {
3135 rec_ = rec_init_; // every application's initial state
3136 rec_snaps_.clear();
3137 } else {
3138 rec_ = rec_snaps_.at(n);
3139 rec_snaps_.erase(rec_snaps_.upper_bound(n), rec_snaps_.end());
3140 }
3141 rec_pos_ = n;
3142 }
3143 seq_len_ = n;
3144 if (rope_ff_ctx_ > 0) { // LongRoPE with two sets (IR_SPEC 6.9, revision 1.5)
3145 ids_.resize(static_cast<size_t>(n));
3146 if (n > 0 && (n > rope_ff_ctx_) != rope_long_) {
3147 // The kept rows were rotated with the other set: the first n ids again, with the set n selects.
3148 const std::vector<int> prefix = ids_;
3149 set_rope_long(n > rope_ff_ctx_);
3150 Logger::info("[IR] LongRoPE: a truncate to " + std::to_string(n) + " changes the set; the rows recomputed");
3151 forward_impl(prefix, 0, false, 1);
3152 }
3153 }
3154}
3155
3156Interpreter::State Interpreter::save_state() const {
3157 cpu_only(gpu_, "save_state");
3158 State s;
3159 s.seq_len = seq_len_;
3160 const size_t row = static_cast<size_t>(kv_heads_) * head_dim_;
3161 for (int l = 0; l < static_cast<int>(kv_.size()); ++l) {
3162 const size_t n = static_cast<size_t>(kv_rows_in_use(l)) * row;
3163 s.k.emplace_back(kv_[l].k.begin(), kv_[l].k.begin() + n);
3164 s.v.emplace_back(kv_[l].v.begin(), kv_[l].v.begin() + n);
3165 }
3166 s.ts_seg = ts_seg_;
3167 s.ts_sidx = ts_sidx_;
3168 s.kv_pos = kv_pos_;
3169 s.evicted_below = evicted_below_;
3170 s.mor_rows = mor_rows_;
3171 s.mor_depth = mor_depth_;
3172 s.rec = rec_;
3173 s.rec_pos = rec_pos_;
3174 s.rec_snaps = rec_snaps_;
3175 s.ids = ids_;
3176 s.rope_long = rope_long_;
3177 return s;
3178}
3179
3180void Interpreter::load_state(const State& s) {
3181 cpu_only(gpu_, "load_state");
3182 if (s.k.size() != kv_.size() || s.v.size() != kv_.size() || s.rec.size() != rec_.size() ||
3183 s.mor_rows.size() != mor_rows_.size())
3184 throw std::runtime_error("IR load_state: the state is not this interpreter's");
3185 for (size_t l = 0; l < kv_.size(); ++l) {
3186 if (s.k[l].size() > kv_[l].k.size() || s.v[l].size() != s.k[l].size())
3187 throw std::runtime_error("IR load_state: the kv rows do not fit the cache");
3188 std::copy(s.k[l].begin(), s.k[l].end(), kv_[l].k.begin());
3189 std::copy(s.v[l].begin(), s.v[l].end(), kv_[l].v.begin());
3190 }
3191 seq_len_ = s.seq_len;
3192 ts_seg_ = s.ts_seg;
3193 ts_sidx_ = s.ts_sidx;
3194 kv_pos_ = s.kv_pos;
3195 evicted_below_ = s.evicted_below;
3196 mor_rows_ = s.mor_rows;
3197 mor_depth_ = s.mor_depth;
3198 rec_ = s.rec;
3199 rec_pos_ = s.rec_pos;
3200 rec_snaps_ = s.rec_snaps;
3201 ids_ = s.ids;
3202 rope_long_ = s.rope_long;
3203}
3204
3205#ifdef HAS_CUDA
3206// ---------------------------------------------------------------------------
3207// Device path: the CPU run_nodes above, op for op, on device buffers.
3208
3209void Interpreter::init_device() {
3211 ember::deterministic_cublas_env(); // EMBER_DETERMINISTIC=1 only (determinism.h)
3212 if (cublasCreate(&cublas_) != CUBLAS_STATUS_SUCCESS) throw std::runtime_error("IR CUDA: cublasCreate failed");
3214 cublasSetStream(cublas_, stream_);
3215 size_t floats = 0;
3216 for (auto& w : weights_) { // the host copies are not used on this path: release them
3217 if (w.second.quantised()) // the device path binds every weight as f32 (dequant_all_)
3218 throw std::runtime_error("IR CUDA: weight '" + w.first + "' is still quantised");
3219 std::vector<float>& host = w.second.f32;
3220 float* d = nullptr;
3221 gpuErrchk(cudaMalloc(&d, host.size() * sizeof(float)));
3222 gpuErrchk(cudaMemcpy(d, host.data(), host.size() * sizeof(float), cudaMemcpyHostToDevice));
3223 dweights_[w.first] = d;
3224 floats += host.size();
3225 std::vector<float>().swap(host);
3226 }
3227 const size_t kv_floats = static_cast<size_t>(max_len_) * kv_heads_ * head_dim_;
3228 dk_.assign(n_layers_, nullptr);
3229 dv_.assign(n_layers_, nullptr);
3230 if (kv_fmt_ != ember_kvq::kF32 && kv_floats > 0) { // EMBER_KV: codes and scales in place of dk_/dv_
3231 if (turn_state_ || !mor_step_.empty())
3232 throw std::runtime_error(std::string("EMBER_KV=") + ember_kvq::format_name(kv_fmt_) +
3233 ": the CUDA path quantises plain causal attention only (no turn_state, depth_route)");
3234 const size_t row = static_cast<size_t>(kv_heads_) * head_dim_;
3235 const size_t db = static_cast<size_t>(max_len_) * ember_kvq::data_bytes(kv_fmt_, row);
3236 const size_t sb = static_cast<size_t>(max_len_) * ember_kvq::scale_bytes(kv_fmt_, row);
3237 for (auto* v : {&dkq_, &dks_, &dvq_, &dvs_}) v->assign(n_layers_, nullptr);
3238 for (int l = 0; l < n_layers_; ++l) {
3239 for (void** p : {&dkq_[l], &dvq_[l]}) {
3241 gpuErrchk(cudaMemset(*p, 0, db));
3242 }
3243 for (void** p : {&dks_[l], &dvs_[l]}) {
3244 if (sb == 0) continue;
3246 gpuErrchk(cudaMemset(*p, 0, sb));
3247 }
3248 }
3249 kv_dev_bytes_ = static_cast<size_t>(n_layers_) * 2 * (db + sb);
3250 int hq_max = 0;
3251 for (const auto& n : block_)
3252 if (n.op == "attention") hq_max = std::max(hq_max, n.attrs.at("heads").get<int>());
3253 const size_t part = ember_kvq::partial_floats(std::max(hq_max, 1), head_dim_, max_len_);
3254 gpuErrchk(cudaMalloc(&dkv_f32_, 2 * kv_floats * sizeof(float)));
3255 gpuErrchk(cudaMalloc(&dkv_part_, part * sizeof(float)));
3256 kv_scratch_dev_bytes_ = (2 * kv_floats + part) * sizeof(float);
3257 Logger::info(std::string("[IR] EMBER_KV=") + ember_kvq::format_name(kv_fmt_) + ": kv cache " +
3258 std::to_string(kv_dev_bytes_ >> 20) + " MiB (f32 would be " +
3259 std::to_string((static_cast<size_t>(n_layers_) * 2 * kv_floats * sizeof(float)) >> 20) +
3260 " MiB), prefill and decode scratch " + std::to_string(kv_scratch_dev_bytes_ >> 20) + " MiB");
3261 }
3262 for (int l = 0; l < n_layers_ && kv_floats > 0 && kv_fmt_ == ember_kvq::kF32; ++l) {
3263 gpuErrchk(cudaMalloc(&dk_[l], kv_floats * sizeof(float)));
3264 gpuErrchk(cudaMalloc(&dv_[l], kv_floats * sizeof(float)));
3265 gpuErrchk(cudaMemset(dk_[l], 0, kv_floats * sizeof(float)));
3266 gpuErrchk(cudaMemset(dv_[l], 0, kv_floats * sizeof(float)));
3267 }
3268 for (const auto& tab : rope_tabs_) { // one per distinct (theta, freq_scale, attn_factor, frequency factors)
3269 std::vector<float> flat(tab.size() * 2);
3270 for (size_t i = 0; i < tab.size(); ++i) {
3271 flat[2 * i] = tab[i].first;
3272 flat[2 * i + 1] = tab[i].second;
3273 }
3274 float* d = nullptr;
3275 gpuErrchk(cudaMalloc(&d, flat.size() * sizeof(float)));
3276 gpuErrchk(cudaMemcpy(d, flat.data(), flat.size() * sizeof(float), cudaMemcpyHostToDevice));
3277 drope_tabs_.push_back(d);
3278 }
3279 if (rec_floats_ > 0) { // rec_ holds every block's initial state here
3280 const size_t bytes = rec_.size() * sizeof(float);
3285 }
3286 gpuErrchk(cudaMalloc(&dstep_, 3 * sizeof(int)));
3287 gpuErrchk(cudaHostAlloc(reinterpret_cast<void**>(&hstep_), 3 * sizeof(int), cudaHostAllocDefault));
3288 hstep_[2] = 0;
3289 if (turn_state_) { // per-row codes for the turn_state kernel, and eviction's row list
3290 gpuErrchk(cudaMalloc(&dts_code_, static_cast<size_t>(max_len_) * sizeof(int)));
3291 gpuErrchk(cudaMalloc(&dkv_idx_, static_cast<size_t>(max_len_) * sizeof(int)));
3292 }
3293 gpuErrchk(cudaHostAlloc(reinterpret_cast<void**>(&hlogits_), static_cast<size_t>(vocab_) * sizeof(float),
3295 Logger::info("[IR] CUDA device path: " + std::to_string(dweights_.size()) + " weights (" +
3296 std::to_string(floats * sizeof(float) / (1024 * 1024)) + " MiB f32), " +
3297 (kv_floats > 0 ? std::to_string(n_layers_) + " kv slots of " + std::to_string(max_len_) + " positions"
3298 : std::string("no kv slots")) +
3299 (rec_floats_ > 0 ? " and " + std::to_string(rec_floats_) + " recurrent floats per block" : "") +
3300 " on the device");
3302}
3303
3304int Interpreter::slot(const std::string& name) {
3305 auto it = slots_.find(name);
3306 if (it != slots_.end()) return it->second;
3307 const int s = static_cast<int>(slot_names_.size());
3308 slots_.emplace(name, s);
3309 slot_names_.push_back(name);
3310 return s;
3311}
3312
3313std::vector<Interpreter::DStep> Interpreter::compile_nodes(const std::vector<Node>& nodes, int layer) {
3314 static const std::unordered_map<std::string, DOp> kOps = {
3315 {"embedding", DOp::Embedding}, {"pos_embedding", DOp::PosEmbedding}, {"layernorm", DOp::LayerNorm},
3316 {"split", DOp::Split}, {"gelu", DOp::Gelu}, {"rmsnorm", DOp::RmsNorm},
3317 {"linear", DOp::Linear}, {"rope", DOp::Rope}, {"attention", DOp::Attention},
3318 {"add", DOp::Add}, {"mul", DOp::Mul}, {"moe_ffn", DOp::MoeFfn},
3319 {"silu", DOp::Silu}, {"sub", DOp::Sub}, {"relu", DOp::Relu},
3320 {"sigmoid", DOp::Sigmoid}, {"affine", DOp::Affine}, {"mul_col", DOp::MulCol},
3321 {"rms_row", DOp::RmsRow}, {"list", DOp::List}, {"append", DOp::Append},
3322 {"depth_attend", DOp::DepthAttend}, {"linear_attention", DOp::LinearAttention}, {"ssm_scan", DOp::SsmScan},
3323 {"expr", DOp::Expr}, {"conv1d_causal", DOp::Conv1dCausal}, {"selective_scan", DOp::SelectiveScan},
3324 {"scale", DOp::Scale}, {"mla_attention", DOp::MlaAttention}};
3325 auto wname = [&](const Node& n) {
3326 std::string w = layer_name(n.attrs.at("weight").get<std::string>(), layer);
3327 if (!weights_.count(w) && has(n.attrs, "tied_to")) w = layer_name(n.attrs.at("tied_to").get<std::string>(), layer);
3328 return w;
3329 };
3330 auto elems = [&](const std::string& wn) {
3331 size_t e = 1;
3332 for (int64_t s : shapes_.at(wn)) e *= static_cast<size_t>(s);
3333 return e;
3334 };
3335 std::vector<DStep> steps;
3336 for (const auto& n : nodes) {
3337 if (n.op == "depth_route") continue; // control, not a step: run_device's mor_step_device
3338 AppIndex ti(app_index_, n);
3339 DStep s;
3340 auto op = kOps.find(n.op);
3341 if (op == kOps.end()) throw std::runtime_error("IR op '" + n.op + "' has no CUDA kernel in ember's interpreter");
3342 s.op = op->second;
3343 s.node = &n;
3344 for (const auto& name : n.in) s.in.push_back(slot(name));
3345 for (const auto& name : n.out) s.out.push_back(slot(name));
3346 switch (s.op) {
3347 case DOp::Embedding:
3348 case DOp::PosEmbedding:
3349 case DOp::RmsNorm:
3350 case DOp::LayerNorm:
3351 case DOp::Linear:
3352 s.wname = wname(n);
3353 s.w = dweight(s.wname);
3354 s.w_elems = elems(s.wname);
3355 s.w_ne0 = static_cast<int>(shapes_.at(s.wname).at(0));
3356 if (has(n.attrs, "bias") && (s.op == DOp::LayerNorm || s.op == DOp::Linear))
3357 s.b = dweight(layer_name(n.attrs.at("bias").get<std::string>(), layer));
3358 if (s.op == DOp::LayerNorm && !s.b) throw std::runtime_error("IR layernorm '" + s.wname + "' has no bias");
3359 s.eps = n.attrs.value("eps", 1e-5f);
3360 if (s.op == DOp::RmsNorm && has(n.attrs, "group")) s.n = n.attrs.at("group").get<int>();
3361 if (s.op == DOp::Embedding && has(n.attrs, "state_weight")) s.state_w = dweight(state_weight_);
3362 break;
3363 case DOp::Split:
3364 s.sizes = n.attrs.at("sizes").get<std::vector<int>>();
3365 if (s.sizes.size() != n.out.size()) throw std::runtime_error("IR split: sizes do not match the outputs");
3366 s.groups = n.attrs.contains("groups") ? n.attrs.at("groups").get<int>() : 1;
3367 if (s.groups < 1) throw std::runtime_error("IR_SPEC 5.5 (rule 10): split groups is not positive");
3368 break;
3369 case DOp::MlaAttention: {
3370 const int L = layer < 0 ? 0 : layer;
3371 s.hq = at_layer(n.attrs.at("heads"), L).get<int>();
3372 s.kv_rank = at_layer(n.attrs.at("kv_rank"), L).get<int>();
3373 s.qk_nope = at_layer(n.attrs.at("qk_nope"), L).get<int>();
3374 s.qk_rope = at_layer(n.attrs.at("qk_rope"), L).get<int>();
3375 s.v_dim = at_layer(n.attrs.at("v_head_dim"), L).get<int>();
3376 s.scale = n.attrs.at("scale").get<float>();
3377 s.w = dweight(layer_name(n.attrs.at("k_b").get<std::string>(), layer));
3378 s.b = dweight(layer_name(n.attrs.at("v_b").get<std::string>(), layer));
3379 attention_window(n, L, s.window, s.sinks);
3380 if (kv_heads_ != 1 || head_dim_ != s.kv_rank + s.qk_rope)
3381 throw std::runtime_error("IR_SPEC 5.20 (rule 12): mla_attention's kv slot is not kv_heads 1, head_dim kv_rank + qk_rope");
3382 if (layer <= 0)
3383 Logger::info("[IR] CUDA multi-head latent attention (ir_mla_attention_cuda, absorbed form): heads " +
3384 std::to_string(s.hq) + ", kv_rank " + std::to_string(s.kv_rank) + ", qk_rope " +
3385 std::to_string(s.qk_rope));
3386 break;
3387 }
3388 case DOp::Gelu:
3389 s.flag = n.attrs.value("approx", "none") == "tanh";
3390 break;
3391 case DOp::Rope:
3392 s.hd = n.attrs.at("head_dim").get<int>();
3393 s.n_rot = rope_dim_;
3394 s.neox = rope_neox_;
3395 { // this block's tables (the long set's too under LongRoPE with two sets); null: no table
3396 const int l = layer < 0 ? 0 : layer;
3397 const int a = l < static_cast<int>(rope_tab_short_.size()) ? rope_tab_short_[l] : -1;
3398 const int b = l < static_cast<int>(rope_tab_long_.size()) ? rope_tab_long_[l] : -1;
3399 s.rope = a >= 0 ? drope_tabs_[a] : nullptr;
3400 s.rope_long = b >= 0 ? drope_tabs_[b] : nullptr;
3401 }
3402 break;
3403 case DOp::Attention:
3404 s.hd = n.attrs.at("head_dim").get<int>();
3405 s.hq = n.attrs.at("heads").get<int>();
3406 s.hkv = n.attrs.at("kv_heads").get<int>();
3407 if (s.hd != head_dim_ || s.hkv != kv_heads_) throw std::runtime_error("IR attention shape differs from its kv state");
3408 s.scale = n.attrs.contains("scale") ? n.attrs.at("scale").get<float>() : 1.0f / std::sqrt(float(s.hd));
3409 s.ts = n.attrs.value("mask", "causal") == "turn_state";
3410 s.softcap = has(n.attrs, "softcap") ? at_layer(n.attrs.at("softcap"), layer < 0 ? 0 : layer).get<float>() : 0.0f;
3411 attention_window(n, layer < 0 ? 0 : layer, s.window, s.sinks);
3412 if (s.window > 0 && layer <= 0)
3413 Logger::info("[IR] CUDA sliding-window attention (ir_window_attention_cuda): window " +
3414 std::to_string(s.window) + ", sinks " + std::to_string(s.sinks) +
3415 (window_layer_.empty() && !is_per_layer(n.attrs.value("window", json(0))) ? ""
3416 : " (block 0)"));
3417 break;
3418 case DOp::MoeFfn: {
3419 const auto& ex = n.attrs.at("experts");
3420 s.wname = layer_name(ex.at("gate").get<std::string>(), layer);
3421 s.w = dweight(layer_name(n.attrs.at("router").get<std::string>(), layer));
3422 s.gate = dweight(s.wname);
3423 s.up = dweight(layer_name(ex.at("up").get<std::string>(), layer));
3424 s.down = dweight(layer_name(ex.at("down").get<std::string>(), layer));
3425 s.E = n.attrs.at("n_experts").get<int>();
3426 s.top_k = n.attrs.at("top_k").get<int>();
3427 const auto& gs = shapes_.at(s.wname); // ggml order {d, d_ff, E}
3428 if (gs.size() != 3 || gs[2] != s.E)
3429 throw std::runtime_error("IR moe_ffn '" + s.wname + "': expert stack shape does not match n_experts");
3430 s.w_ne0 = static_cast<int>(gs[0]);
3431 s.d_ff = static_cast<int>(gs[1]);
3432 s.flag = n.attrs.value("gate_norm", "renorm") == "renorm";
3433 break;
3434 }
3435 case DOp::Affine:
3436 s.scale = at_layer(n.attrs.at("scale"), layer).get<float>();
3437 s.shift = at_layer(n.attrs.at("shift"), layer).get<float>();
3438 break;
3439 case DOp::RmsRow:
3440 s.eps = n.attrs.at("eps").get<float>();
3441 break;
3442 case DOp::DepthAttend:
3443 // The pseudo-query may be absent (one source never reads it; a call
3444 // with more sources then fails, as on the CPU).
3445 s.eps = n.attrs.value("eps", 1e-5f);
3446 s.wname = layer_name(n.attrs.at("weight").get<std::string>(), layer);
3447 if (dweights_.count(s.wname)) {
3448 s.w = dweight(s.wname);
3449 s.w_elems = elems(s.wname);
3450 }
3451 break;
3452 case DOp::LinearAttention: {
3453 s.rec = &rec_slot(n, "linear_attention");
3454 s.hq = n.attrs.at("heads").get<int>();
3455 s.hd = n.attrs.at("head_dim").get<int>();
3456 const size_t w = static_cast<size_t>(s.hq) * s.hd;
3457 // rec_tensor checks the slot's tensors; the device state has the host layout.
3458 s.rec_a = drec_ + (rec_tensor(*s.rec, "num", layer, w) - rec_.data());
3459 s.rec_b = drec_ + (rec_tensor(*s.rec, "den", layer, w) - rec_.data());
3460 s.eps = n.attrs.value("den_floor", 1e-8f);
3461 break;
3462 }
3463 case DOp::SsmScan:
3464 s.rec = &rec_slot(n, "ssm_scan");
3465 s.n = n.attrs.at("n").get<int>();
3466 s.rec_a = drec_ + (rec_tensor(*s.rec, "s", layer, s.n) - rec_.data());
3467 s.w = dweight(layer_name(ssm_weight(n.attrs, "a"), layer));
3468 s.b = dweight(layer_name(ssm_weight(n.attrs, "b"), layer));
3469 s.ssm_c = dweight(layer_name(ssm_weight(n.attrs, "c"), layer));
3470 s.ssm_d = dweight(layer_name(ssm_weight(n.attrs, "d"), layer));
3471 break;
3472 case DOp::Scale:
3473 s.wname = wname(n);
3474 s.w = dweight(s.wname);
3475 s.w_elems = elems(s.wname);
3476 if (has(n.attrs, "bias")) s.b = dweight(layer_name(n.attrs.at("bias").get<std::string>(), layer));
3477 break;
3478 case DOp::Expr:
3479 s.expr = expr::lower(expr::cached(n.attrs.at("expr").get<std::string>(), n.in.size()));
3480 break;
3481 case DOp::Conv1dCausal:
3482 s.rec = &rec_slot(n, "conv1d_causal");
3483 s.n = n.attrs.at("channels").get<int>();
3484 s.kernel = n.attrs.at("kernel").get<int>();
3485 s.rec_a = drec_ + (rec_tensor(*s.rec, "x", layer, static_cast<size_t>(s.kernel - 1) * s.n) - rec_.data());
3486 s.wname = layer_name(n.attrs.at("weight").get<std::string>(), layer);
3487 s.w = dweight(s.wname);
3488 if (elems(s.wname) != static_cast<size_t>(s.n) * s.kernel)
3489 throw std::runtime_error("IR conv1d_causal '" + s.wname + "' is not [channels, kernel]");
3490 if (has(n.attrs, "bias")) s.b = dweight(layer_name(n.attrs.at("bias").get<std::string>(), layer));
3491 break;
3492 case DOp::SelectiveScan:
3493 s.rec = &rec_slot(n, "selective_scan");
3494 s.n = n.attrs.at("channels").get<int>();
3495 s.d_state = n.attrs.at("d_state").get<int>();
3496 s.rec_a = drec_ + (rec_tensor(*s.rec, "s", layer, static_cast<size_t>(s.n) * s.d_state) - rec_.data());
3497 s.wname = layer_name(n.attrs.at("a").get<std::string>(), layer);
3498 s.w = dweight(s.wname);
3499 if (elems(s.wname) != static_cast<size_t>(s.n) * s.d_state)
3500 throw std::runtime_error("IR selective_scan '" + s.wname + "' is not [channels, d_state]");
3501 if (has(n.attrs, "d")) s.ssm_d = dweight(layer_name(n.attrs.at("d").get<std::string>(), layer));
3502 s.flag = n.attrs.value("dt_softplus", true);
3503 break;
3504 case DOp::Add:
3505 case DOp::Mul:
3506 case DOp::Silu:
3507 case DOp::Sub:
3508 case DOp::Relu:
3509 case DOp::Sigmoid:
3510 case DOp::MulCol:
3511 case DOp::List:
3512 case DOp::Append:
3513 break;
3514 }
3515 if (s.op == DOp::Embedding || s.op == DOp::PosEmbedding) { // the graph replay's range checks
3516 int& rows = s.op == DOp::Embedding ? embed_rows_ : pos_rows_;
3517 rows = static_cast<int>(std::min<size_t>(rows, s.w_elems / s.w_ne0));
3518 if (s.op == DOp::Embedding) rows = std::min(rows, vocab_); // an id >= vocab is refused (IR_SPEC 5.1)
3519 }
3520 steps.push_back(std::move(s));
3521 }
3522 return steps;
3523}
3524
3525void Interpreter::compile_device() {
3526 embed_rows_ = pos_rows_ = std::numeric_limits<int>::max();
3527 plan_embed_ = compile_nodes(embed_, -1);
3528 plan_block_.resize(n_layers_);
3529 for (int l = 0; l < n_layers_; ++l) plan_block_[l] = compile_nodes(block_, l);
3530 plan_head_ = compile_nodes(head_, -1);
3531 logits_slot_ = slot("logits");
3532 if (!mor_step_.empty()) {
3533 // depth_route's slots, and the block's (its other nodes'): every slot read or written, and those
3534 // read before the block writes them (its inputs from outside the block; x among them).
3535 mor_xs_ = slot(mor_x_);
3536 mor_out_slot_ = slot(mor_out_);
3537 std::set<int> touched{mor_xs_}, written;
3538 for (const auto& n : block_) {
3539 if (n.op == "depth_route") continue;
3540 for (const auto& name : n.in) {
3541 const int sl = slot(name);
3542 touched.insert(sl);
3543 if (!written.count(sl) && sl != mor_xs_ &&
3544 std::find(mor_livein_.begin(), mor_livein_.end(), sl) == mor_livein_.end())
3545 mor_livein_.push_back(sl);
3546 }
3547 for (const auto& name : n.out) {
3548 touched.insert(slot(name));
3549 written.insert(slot(name));
3550 }
3551 }
3552 mor_touch_.assign(touched.begin(), touched.end());
3553 }
3554 dbufs_.assign(slot_names_.size(), DBuf{}); // sized once: steps hold slots, dout returns references
3555 dspare_.assign(slot_names_.size(), DBuf{});
3556 ditems_.assign(slot_names_.size(), {});
3557 msave_.assign(mor_step_.empty() ? 0 : slot_names_.size(), DBuf{});
3558 Logger::info("[IR] CUDA device plan: " + std::to_string(plan_embed_.size()) + " + " + std::to_string(n_layers_) +
3559 " x " + std::to_string(block_.size()) + " + " + std::to_string(plan_head_.size()) + " steps over " +
3560 std::to_string(slot_names_.size()) + " buffer slots");
3561}
3562
3563void Interpreter::free_device() {
3564 auto release = [](std::vector<DBuf>& bufs) {
3565 for (auto& b : bufs)
3566 if (b.alloc) cudaFree(b.alloc);
3567 bufs.clear();
3568 };
3570 graph_exec_ = nullptr;
3571 release(dbufs_);
3573 release(msave_);
3574 for (float* p : {dmor_logits_.alloc, dmor_xin_.alloc, dmor_g_.alloc})
3575 if (p) cudaFree(p);
3578 dmor_idx_ = nullptr;
3579 for (auto& items : ditems_) release(items);
3580 ditems_.clear();
3581 for (auto& w : dweights_) cudaFree(w.second);
3582 dweights_.clear();
3583 for (float* p : dk_) if (p) cudaFree(p);
3584 for (float* p : dv_) if (p) cudaFree(p);
3585 dk_.clear();
3586 dv_.clear();
3587 for (auto* v : {&dkq_, &dks_, &dvq_, &dvs_}) {
3588 for (void* p : *v) if (p) cudaFree(p);
3589 v->clear();
3590 }
3591 for (float* p : {dkv_f32_, dkv_part_}) if (p) cudaFree(p);
3592 dkv_f32_ = dkv_part_ = nullptr;
3593 for (auto& kv : dsnaps_) cudaFree(kv.second);
3594 for (float* p : dsnap_pool_) cudaFree(p);
3595 dsnaps_.clear();
3596 dsnap_pool_.clear();
3597 for (float* tab : drope_tabs_) cudaFree(tab);
3598 drope_tabs_.clear();
3599 for (float* p : {drec_, drec_init_, drec_rows_.alloc, dmoe_.alloc, dexpr_.alloc})
3600 if (p) cudaFree(p);
3601 drec_ = drec_init_ = nullptr;
3602 drec_rows_ = dmoe_ = dexpr_ = DBuf{};
3606 dts_code_ = dkv_idx_ = nullptr;
3607 if (dstep_) cudaFree(dstep_);
3610 dtokens_ = dstep_ = hstep_ = nullptr;
3611 hlogits_ = nullptr;
3613 if (stream_) cudaStreamDestroy(stream_);
3614 cublas_ = nullptr;
3615 stream_ = nullptr;
3616}
3617
3618const float* Interpreter::dweight(const std::string& name) const {
3619 auto it = dweights_.find(name);
3620 if (it == dweights_.end()) throw std::runtime_error("IR weight '" + name + "' was not bound");
3621 return it->second;
3622}
3623
3624void Interpreter::grow(DBuf& b, size_t need) {
3625 if (b.cap >= need) return;
3626 // Grows only on the first calls of a shape; cudaFree waits for the device.
3627 // A captured graph may hold the old pointer: it is captured again.
3628 if (capturing_) throw std::runtime_error("IR CUDA graph: a buffer must grow during capture");
3629 if (b.alloc) gpuErrchk(cudaFree(b.alloc));
3630 gpuErrchk(cudaMalloc(&b.alloc, need * sizeof(float)));
3631 b.cap = need;
3632 ++alloc_epoch_;
3633}
3634
3635Interpreter::DBuf& Interpreter::dout(int slot, int rows, int width, std::initializer_list<const DBuf*> inputs) {
3636 return dout(slot, rows, width, std::vector<const DBuf*>(inputs));
3637}
3638
3639Interpreter::DBuf& Interpreter::dout(int slot, int rows, int width, const std::vector<const DBuf*>& inputs) {
3640 DBuf* b = &dbufs_[slot];
3641 for (const DBuf* in : inputs)
3642 if (in && b->alloc && in->alloc == b->alloc) {
3643 b = &dspare_[slot];
3644 pending_.push_back(slot);
3645 break;
3646 }
3647 grow(*b, static_cast<size_t>(rows) * width);
3648 b->p = b->alloc;
3649 b->rows = rows;
3650 b->width = width;
3651 b->is_list = false;
3652 return *b;
3653}
3654
3655void Interpreter::copy_item(DBuf& dst, const DBuf& x) {
3656 grow(dst, static_cast<size_t>(x.rows) * x.width);
3657 dst.p = dst.alloc;
3658 dst.rows = x.rows;
3659 dst.width = x.width;
3660 gpuErrchk(cudaMemcpyAsync(dst.p, x.p, static_cast<size_t>(x.rows) * x.width * sizeof(float),
3661 cudaMemcpyDeviceToDevice, stream_));
3662}
3663
3664float* Interpreter::dsnap_take(int key) {
3665 auto it = dsnaps_.find(key);
3666 if (it != dsnaps_.end()) return it->second;
3667 float* p = nullptr;
3668 if (!dsnap_pool_.empty()) {
3669 p = dsnap_pool_.back();
3670 dsnap_pool_.pop_back();
3671 } else {
3672 gpuErrchk(cudaMalloc(&p, rec_.size() * sizeof(float)));
3673 }
3674 dsnaps_.emplace(key, p);
3675 return p;
3676}
3677
3678void Interpreter::dsnap_drop(std::map<int, float*>::iterator it) {
3679 dsnap_pool_.push_back(it->second);
3680 dsnaps_.erase(it);
3681}
3682
3683void Interpreter::commit() {
3684 for (int s : pending_) std::swap(dbufs_[s], dspare_[s]);
3685 pending_.clear();
3686}
3687
3688void Interpreter::run_nodes_device(const std::vector<DStep>& steps, int layer, int T, int start_pos) {
3689 cudaStream_t st = stream_;
3690 auto value = [&](const DStep& s, size_t k, bool want_list) -> DBuf& {
3691 DBuf& b = dbufs_[s.in.at(k)];
3692 if (b.rows == 0)
3693 throw std::runtime_error("IR op '" + s.node->op + "': input '" + s.node->in.at(k) + "' is not defined");
3694 if (b.is_list != want_list)
3695 throw std::runtime_error("IR op '" + s.node->op + "': input '" + s.node->in.at(k) + "' is " +
3696 (want_list ? "a tensor, not a list" : "a list, not a tensor"));
3697 return b;
3698 };
3699 auto in = [&](const DStep& s, size_t k) -> DBuf& { return value(s, k, false); };
3700 auto list_in = [&](const DStep& s, size_t k) -> DBuf& { return value(s, k, true); };
3701 // A recurrent op over rows [0, T): one launch per run of rows up to each
3702 // kept row, whose state rec_row_done then copies (the CPU's per-row
3703 // snapshot, rec_begin). A call without kept rows is one launch.
3704 auto rec_rows = [&](const DStep& s, auto&& launch) {
3705 for (int t0 = 0, t = 0; t < T; ++t) {
3706 const bool kept = t + 1 < T && rec_snap_rows_[t] >= 0;
3707 if (!kept && t + 1 < T) continue;
3708 launch(t0, t - t0 + 1);
3709 if (kept) rec_row_done(*s.rec, layer, t);
3710 t0 = t + 1;
3711 }
3712 };
3713 for (const DStep& s : steps) {
3714 switch (s.op) {
3715 case DOp::Embedding: {
3716 const int d = s.w_ne0;
3717 const size_t rows = std::min<size_t>(s.w_elems / d, static_cast<size_t>(vocab_)); // IR_SPEC 5.1
3718 for (int t = 0; t < T; ++t) {
3719 const int id = htokens_[t];
3720 if (s.state_w && id >= vocab_) { // the extended embedding (IR_SPEC 6.8.1)
3721 if (id >= vocab_ + state_k_)
3722 throw std::runtime_error("IR embedding: token id " + std::to_string(id) + " is past the state slots (vocab " +
3723 std::to_string(vocab_) + ", k " + std::to_string(state_k_) + ")");
3724 } else if (id < 0 || static_cast<size_t>(id) >= rows) {
3725 throw std::runtime_error("IR embedding: token id " + std::to_string(id) + " out of range");
3726 }
3727 }
3728 DBuf& out = dout(s.out[0], T, d, {});
3729 if (s.state_w)
3730 ir_embedding_state_cuda(s.w, s.state_w, vocab_, capturing_ ? dstep_ : dtokens_, out.p, T, d, st);
3731 else
3732 ir_embedding_cuda(s.w, capturing_ ? dstep_ : dtokens_, out.p, T, d, st); // dstep_[0]: the token
3733 break;
3734 }
3735 case DOp::PosEmbedding: {
3736 const DBuf& x = in(s, 0);
3737 const int d = x.width;
3738 if (drowpos_) throw std::runtime_error("IR pos_embedding inside a recursion step has no CUDA form");
3739 if (s.w_ne0 != d) throw std::runtime_error("IR pos_embedding '" + s.wname + "': width differs from x");
3740 if (static_cast<size_t>(start_pos + T) * d > s.w_elems)
3741 throw std::runtime_error("IR pos_embedding: position " + std::to_string(start_pos + T - 1) + " exceeds '" +
3742 s.wname + "'");
3743 DBuf& out = dout(s.out[0], T, d, {&x});
3744 ir_pos_embedding_cuda(x.p, s.w, out.p, T, d, start_pos, st, dpos_);
3745 break;
3746 }
3747 case DOp::LayerNorm: {
3748 const DBuf& x = in(s, 0);
3749 DBuf& out = dout(s.out[0], T, x.width, {&x});
3750 ir_layernorm_cuda(x.p, s.w, s.b, out.p, T, x.width, s.eps, st);
3751 break;
3752 }
3753 case DOp::Split: {
3754 const DBuf& x = in(s, 0);
3755 int total = 0;
3756 for (int sz : s.sizes) total += sz;
3757 if (static_cast<int64_t>(total) * s.groups != x.width)
3758 throw std::runtime_error("IR_SPEC 5.5 (rule 37): split sizes sum to " + std::to_string(total) +
3759 (s.groups > 1 ? " times groups " + std::to_string(s.groups) : std::string()) +
3760 ", not the input width " + std::to_string(x.width));
3761 // groups g (revision 1.5): x [T, g * total] read as [T g, total] rows; part o is [T g, sizes[o]] rows,
3762 // which is [T, g * sizes[o]] (the o-th part of every group, in order). g = 1: the plain split.
3763 int col = 0;
3764 for (size_t o = 0; o < s.sizes.size(); ++o) {
3765 DBuf& part = dout(s.out[o], T, s.groups * s.sizes[o], {&x});
3766 gpuErrchk(cudaMemcpy2DAsync(part.p, s.sizes[o] * sizeof(float), x.p + col, total * sizeof(float),
3767 s.sizes[o] * sizeof(float), static_cast<size_t>(T) * s.groups,
3769 col += s.sizes[o];
3770 }
3771 break;
3772 }
3773 case DOp::MlaAttention: {
3774 const DBuf &qn = in(s, 0), &qr = in(s, 1), &c = in(s, 2), &kr = in(s, 3);
3775 const int H = s.hq, r = s.kv_rank, dn = s.qk_nope, dr = s.qk_rope, dv = s.v_dim;
3776 if (qn.width != H * dn || qr.width != H * dr || c.width != r || kr.width != dr)
3777 throw std::runtime_error("IR_SPEC 5.20 (rule 37): mla_attention input widths do not fit its attributes");
3778 if (attn_row_ >= 0 || s.ts) throw std::runtime_error("IR mla_attention under depth_route or turn_state");
3779 if (start_pos + T > max_len_)
3780 throw std::runtime_error("IR mla_attention: position " + std::to_string(start_pos + T) +
3781 " exceeds the kv state (" + std::to_string(max_len_) + ")");
3782 float* KV = dk_[layer];
3784 DBuf& out = dout(s.out[0], T, H * dv, {&qn, &qr, &c, &kr});
3785 ir_mla_attention_cuda(qn.p, qr.p, KV, s.w, s.b, out.p, T, start_pos, H, r, dn, dr, dv, s.scale, s.window,
3786 s.sinks, st, dpos_);
3787 break;
3788 }
3789 case DOp::Gelu: {
3790 const DBuf& x = in(s, 0);
3791 DBuf& out = dout(s.out[0], T, x.width, {&x});
3792 ir_gelu_cuda(x.p, out.p, static_cast<size_t>(T) * x.width, s.flag, st);
3793 break;
3794 }
3795 case DOp::RmsNorm: { // group g: rows of width g, one weight [g] (per-head q/k norm)
3796 const DBuf& x = in(s, 0);
3797 const int g = s.n ? s.n : x.width;
3798 if (g > 0 && x.width % g != 0)
3799 throw std::runtime_error("IR_SPEC 5.6 (rule 37): rmsnorm '" + s.wname + "': group " + std::to_string(g) +
3800 " does not divide the input width " + std::to_string(x.width));
3801 if (g <= 0 || x.width % g != 0 || s.w_elems != static_cast<size_t>(g))
3802 throw std::runtime_error("IR rmsnorm '" + s.wname + "': group " + std::to_string(g) + " does not fit width " +
3803 std::to_string(x.width) + " and weight [" + std::to_string(s.w_elems) + "]");
3804 DBuf& out = dout(s.out[0], T, x.width, {&x});
3805 rmsnorm_batch_cuda(out.p, x.p, s.w, T * (x.width / g), g, s.eps, st);
3806 break;
3807 }
3808 case DOp::Linear: {
3809 const DBuf& x = in(s, 0);
3810 if (s.w_ne0 != x.width)
3811 throw std::runtime_error("IR linear '" + s.wname + "': input width " + std::to_string(x.width) +
3812 " does not match the weight's ne0 " + std::to_string(s.w_ne0));
3813 const int out_dim = static_cast<int>(s.w_elems / x.width);
3814 DBuf& out = dout(s.out[0], T, out_dim, {&x});
3815 gemm_rm_f32_cuda(cublas_, x.p, s.w, out.p, T, out_dim, x.width, st);
3816 if (s.b) ir_bias_add_cuda(out.p, s.b, T, out_dim, st);
3817 break;
3818 }
3819 case DOp::Rope: {
3820 // LongRoPE with two sets: the set the sequence's length selects (forward's set_rope_long).
3821 const float* tab = rope_long_ && s.rope_long ? s.rope_long : s.rope;
3822 if (start_pos + T > max_len_ || !tab)
3823 throw std::runtime_error("IR rope: position " + std::to_string(start_pos + T - 1) + " exceeds the rope table");
3824 DBuf* q = &in(s, 0);
3825 DBuf* k = &in(s, 1);
3826 // Rotated in place when the outputs reuse the input names (the usual
3827 // form); otherwise copied to the outputs first.
3828 for (size_t o = 0; o < 2; ++o) {
3829 DBuf*& src = o == 0 ? q : k;
3830 if (s.out.at(o) == s.in.at(o)) continue;
3831 DBuf& dst = dout(s.out.at(o), T, src->width, {src});
3832 gpuErrchk(cudaMemcpyAsync(dst.p, src->p, static_cast<size_t>(T) * src->width * sizeof(float),
3834 src = &dst;
3835 }
3836 rope_widths(*s.node, layer, q->width, k->width);
3837 if (drowpos_) { // a recursion step's active rows, each at its own position (mor_step_device)
3838 if (!s.neox && s.n_rot == s.hd) {
3839 ir_rope_at_cuda(q->p, k->p, T, q->width / s.hd, k->width / s.hd, s.hd, drowpos_, tab, st);
3840 } else {
3841 if (q->width % s.hd != 0 || k->width % s.hd != 0)
3842 throw std::runtime_error("IR rope: q/k widths are not whole heads");
3843 ir_rope_rows_at_cuda(q->p, T, q->width / s.hd, s.hd, s.n_rot, s.neox, drowpos_, tab, st);
3844 ir_rope_rows_at_cuda(k->p, T, k->width / s.hd, s.hd, s.n_rot, s.neox, drowpos_, tab, st);
3845 }
3846 } else if (!s.neox && s.n_rot == s.hd) { // full interleaved: the llama path's form
3847 ir_rope_cuda(q->p, k->p, T, q->width / s.hd, k->width / s.hd, s.hd, start_pos, tab, st, dpos_);
3848 } else { // NEOX pairing or partial rotary (rope_rows_cpu)
3849 if (q->width % s.hd != 0 || k->width % s.hd != 0)
3850 throw std::runtime_error("IR rope: q/k widths are not whole heads");
3851 ir_rope_rows_cuda(q->p, T, q->width / s.hd, s.hd, s.n_rot, s.neox, start_pos, tab, st, dpos_);
3852 ir_rope_rows_cuda(k->p, T, k->width / s.hd, s.hd, s.n_rot, s.neox, start_pos, tab, st, dpos_);
3853 }
3854 break;
3855 }
3856 case DOp::Attention: {
3857 const DBuf &q = in(s, 0), &k = in(s, 1), &v = in(s, 2);
3858 const int hd = s.hd, hq = s.hq, hkv = s.hkv;
3859 if (k.width != hkv * hd || v.width != hkv * hd || q.width != hq * hd)
3860 throw std::runtime_error("IR attention: q/k/v widths do not match heads * head_dim");
3861 if (attn_row_ >= 0) {
3862 // A recursion step (mor_step_device): this call's rows (the active tokens, ascending) go to the
3863 // application's compact cache from attn_row_, and query t reads its rows 0 .. attn_row_ + t:
3864 // exactly the rows held at positions <= its own (kv_rows "active").
3865 if (attn_row_ + T > max_len_)
3866 throw std::runtime_error("IR attention: " + std::to_string(attn_row_ + T) + " kv rows exceed the kv state (" +
3867 std::to_string(max_len_) + ")");
3868 if (kv_fmt_ != ember_kvq::kF32)
3869 throw std::runtime_error("EMBER_KV: a recursion step's compact cache is not quantised on CUDA");
3870 float* K = dk_[layer];
3871 float* V = dv_[layer];
3872 ir_kv_write_cuda(K, V, k.p, v.p, T, hkv * hd, attn_row_, st);
3873 DBuf& out = dout(s.out[0], T, hq * hd, {&q, &k, &v});
3874 if (T == 1)
3875 attention_decode_at_cuda(q.p, K, V, out.p, hq, attn_row_ + 1, hd, s.scale, max_len_, hkv, st);
3876 else
3877 attention_batch_prefill_cuda(q.p, nullptr, nullptr, K, V, out.p, T, attn_row_, max_len_, hq, hkv, hd,
3878 s.scale, st, nullptr);
3879 break;
3880 }
3881 if (start_pos + T > max_len_)
3882 throw std::runtime_error("IR attention: position " + std::to_string(start_pos + T) + " exceeds the kv state (" +
3883 std::to_string(max_len_) + ")");
3884 const size_t row = static_cast<size_t>(hkv) * hd;
3885 if (kv_fmt_ != ember_kvq::kF32) { // EMBER_KV (kv_quant.h): codes in the cache, dequantised as read
3886 if (s.ts || s.window > 0)
3887 throw std::runtime_error("EMBER_KV: turn_state and windowed attention are not quantised on CUDA");
3888 void *Kd = dkq_[layer], *Ks = dks_[layer], *Vd = dvq_[layer], *Vs = dvs_[layer];
3889 ember_kvq::write_cuda(kv_fmt_, Kd, Ks, Vd, Vs, k.p, v.p, T, static_cast<int>(row), start_pos, st, dpos_);
3890 DBuf& out = dout(s.out[0], T, hq * hd, {&q, &k, &v});
3891 if (T == 1) {
3892 ember_kvq::decode_cuda(kv_fmt_, q.p, Kd, Ks, Vd, Vs, out.p, hq, hkv, hd, s.scale, max_len_, start_pos + 1,
3893 dkv_part_, st, dpos_);
3894 } else { // the rows [0, start_pos + T) dequantised, then the f32 prefill kernel over them
3895 float* K = dkv_f32_;
3896 float* V = dkv_f32_ + static_cast<size_t>(max_len_) * row;
3897 ember_kvq::dequant_rows_cuda(kv_fmt_, Kd, Ks, Vd, Vs, K, V, start_pos + T, static_cast<int>(row), st);
3898 attention_batch_prefill_cuda(q.p, nullptr, nullptr, K, V, out.p, T, start_pos, max_len_, hq, hkv, hd,
3899 s.scale, st, nullptr);
3900 }
3901 break;
3902 }
3903 float* K = dk_[layer];
3904 float* V = dv_[layer];
3905 if (s.ts) { // the compact cache (ts_begin): this call's rows from kv_row0_, dstep_[2] in the graph
3906 const int* drow = dpos_ ? dstep_ + 2 : nullptr;
3907 ir_kv_write_cuda(K, V, k.p, v.p, T, static_cast<int>(row), kv_row0_, st, drow);
3908 DBuf& out = dout(s.out[0], T, hq * hd, {&q, &k, &v});
3909 ir_turn_state_attention_cuda(q.p, K, V, out.p, T, kv_row0_, dts_code_, hq, hkv, hd, s.scale, st, drow);
3910 break;
3911 }
3912 ir_kv_write_cuda(K, V, k.p, v.p, T, static_cast<int>(row), start_pos, st, dpos_);
3913 DBuf& out = dout(s.out[0], T, hq * hd, {&q, &k, &v});
3914 if (s.window > 0 || s.softcap > 0.0f) // the cache keeps every position, so the window reads it in place
3915 // Gemma 2's soft cap (IR_SPEC 5.15, revision 1.5) is this kernel's: unwindowed, its window is max_len
3916 ir_window_attention_cuda(q.p, K, V, out.p, T, start_pos, hq, hkv, hd, s.scale, s.window > 0 ? s.window : max_len_,
3917 s.sinks, st, dpos_, s.softcap);
3918 else if (T == 1 && dpos_) // the same split kernel as below, its length read from dstep_[1]
3919 attention_devpos_cuda(q.p, K, V, out.p, hq, hd, s.scale, max_len_, hkv, dstep_, st);
3920 else if (T == 1)
3921 attention_decode_at_cuda(q.p, K, V, out.p, hq, start_pos + 1, hd, s.scale, max_len_, hkv, st);
3922 else
3923 attention_batch_prefill_cuda(q.p, nullptr, nullptr, K, V, out.p, T, start_pos, max_len_, hq, hkv, hd, s.scale,
3924 st, nullptr);
3925 break;
3926 }
3927 case DOp::Add:
3928 case DOp::Mul: {
3929 const DBuf &a = in(s, 0), &b = in(s, 1);
3930 if (a.rows != b.rows || a.width != b.width) binary_shapes(s.node->op, a.width, b.width);
3931 DBuf& out = dout(s.out[0], a.rows, a.width, {&a, &b});
3932 ir_binary_cuda(a.p, b.p, out.p, static_cast<size_t>(a.rows) * a.width, s.op == DOp::Add ? 0 : 1, st);
3933 break;
3934 }
3935 case DOp::MoeFfn: {
3936 const DBuf& x = in(s, 0);
3937 if (s.w_ne0 != x.width)
3938 throw std::runtime_error("IR moe_ffn '" + s.wname + "': expert stack shape does not match d and n_experts");
3939 const int d = x.width;
3940 grow(dmoe_, moe_ffn_scratch_floats(T, d, s.d_ff, s.E, s.top_k));
3941 DBuf& out = dout(s.out[0], T, d, {&x});
3942 moe_ffn_cuda(cublas_, x.p, T, d, s.d_ff, s.E, s.top_k, s.flag, s.w, s.gate, s.up, s.down, dmoe_.alloc, out.p,
3943 st);
3944 break;
3945 }
3946 case DOp::Silu: {
3947 const DBuf& x = in(s, 0);
3948 DBuf& out = dout(s.out[0], T, x.width, {&x});
3949 ir_silu_cuda(x.p, out.p, static_cast<size_t>(T) * x.width, st);
3950 break;
3951 }
3952 case DOp::Sub: {
3953 const DBuf &a = in(s, 0), &b = in(s, 1);
3954 if (a.rows != b.rows || a.width != b.width) binary_shapes(s.node->op, a.width, b.width);
3955 DBuf& out = dout(s.out[0], a.rows, a.width, {&a, &b});
3956 ir_binary_cuda(a.p, b.p, out.p, static_cast<size_t>(a.rows) * a.width, 2, st);
3957 break;
3958 }
3959 case DOp::Relu:
3960 case DOp::Sigmoid:
3961 case DOp::Affine: {
3962 const DBuf& x = in(s, 0);
3963 DBuf& out = dout(s.out[0], x.rows, x.width, {&x});
3964 const size_t n = static_cast<size_t>(x.rows) * x.width;
3965 if (s.op == DOp::Relu)
3966 ir_relu_cuda(x.p, out.p, n, st);
3967 else if (s.op == DOp::Sigmoid)
3968 ir_sigmoid_cuda(x.p, out.p, n, st);
3969 else
3970 ir_affine_cuda(x.p, out.p, n, s.scale, s.shift, st);
3971 break;
3972 }
3973 case DOp::Scale: {
3974 const DBuf& x = in(s, 0);
3975 if (s.w_elems != static_cast<size_t>(x.width))
3976 throw std::runtime_error("IR_SPEC 5.4 (rule 37): scale '" + s.wname + "' has " + std::to_string(s.w_elems) +
3977 " values for width " + std::to_string(x.width));
3978 DBuf& out = dout(s.out[0], x.rows, x.width, {&x});
3979 ir_scale_cuda(x.p, s.w, s.b, out.p, x.rows, x.width, st);
3980 break;
3981 }
3982 case DOp::MulCol: {
3983 const DBuf &x = in(s, 0), &c = in(s, 1);
3984 if (c.width != 1 || c.rows != T || x.rows != T)
3985 throw std::runtime_error("IR_SPEC 5.12 (rule 37): mul_col: c must be [T, 1] beside x [T, w]");
3986 DBuf& out = dout(s.out[0], T, x.width, {&x, &c});
3987 ir_mul_col_cuda(x.p, c.p, out.p, T, x.width, st);
3988 break;
3989 }
3990 case DOp::RmsRow: {
3991 const DBuf& x = in(s, 0);
3992 if (x.width <= 0 || x.rows != T) throw std::runtime_error("IR rms_row: input is not [T, w]");
3993 DBuf& out = dout(s.out[0], T, 1, {&x});
3994 ir_rms_row_cuda(x.p, out.p, T, x.width, s.eps, st);
3995 break;
3996 }
3997 case DOp::List: { // the inputs, copied in order, as one list value
3998 std::vector<DBuf>& items = ditems_[s.out[0]];
3999 if (items.size() < s.in.size()) items.resize(s.in.size());
4000 for (size_t k = 0; k < s.in.size(); ++k) copy_item(items[k], in(s, k));
4001 DBuf& L = dbufs_[s.out[0]];
4002 L.is_list = true;
4003 L.rows = T;
4004 L.width = items[0].width;
4005 L.items = static_cast<int>(s.in.size());
4006 break;
4007 }
4008 case DOp::Append: { // the list with a copy of x added at the end (in place when out names the list)
4009 const DBuf& x = in(s, 1);
4010 const DBuf& src = list_in(s, 0);
4011 const int o = s.out[0];
4012 if (o != s.in[0]) {
4013 std::vector<DBuf>& items = ditems_[o];
4014 if (items.size() < static_cast<size_t>(src.items)) items.resize(src.items);
4015 for (int k = 0; k < src.items; ++k) copy_item(items[k], ditems_[s.in[0]][k]);
4016 DBuf& L = dbufs_[o];
4017 L.is_list = true;
4018 L.rows = src.rows;
4019 L.width = src.width;
4020 L.items = src.items;
4021 }
4022 DBuf& L = dbufs_[o];
4023 std::vector<DBuf>& items = ditems_[o];
4024 if (items.size() < static_cast<size_t>(L.items) + 1) items.resize(L.items + 1);
4025 copy_item(items[L.items], x);
4026 ++L.items;
4027 break;
4028 }
4029 case DOp::DepthAttend: { // attention over depth (AttnRes block form)
4030 const DBuf& L = list_in(s, 0);
4031 std::vector<const float*> src;
4032 std::vector<const DBuf*> tensors; // the extra inputs (aliasing for dout)
4033 int d = 0;
4034 auto add_source = [&](const DBuf& b) {
4035 if (d == 0) d = b.width;
4036 if (b.width != d || d <= 0 || b.rows != T)
4037 throw std::runtime_error("IR_SPEC 5.18 (rule 37): depth_attend sources must all be one shape [T, d]");
4038 src.push_back(b.p);
4039 };
4040 for (int k = 0; k < L.items; ++k) add_source(ditems_[s.in[0]][k]);
4041 for (size_t k = 1; k < s.in.size(); ++k) {
4042 tensors.push_back(&in(s, k));
4043 add_source(*tensors.back());
4044 }
4045 if (src.empty()) throw std::runtime_error("IR depth_attend: no sources");
4046 DBuf& out = dout(s.out[0], T, d, tensors);
4047 if (src.size() == 1) { // softmax over one source is the identity: s_0 bit for bit, w not read
4048 gpuErrchk(cudaMemcpyAsync(out.p, src[0], static_cast<size_t>(T) * d * sizeof(float),
4050 break;
4051 }
4052 if (!s.w) throw std::runtime_error("IR depth_attend: pseudo-query '" + s.wname + "' is not in the GGUF");
4053 if (s.w_elems != static_cast<size_t>(d))
4054 throw std::runtime_error("IR depth_attend: pseudo-query '" + s.wname + "' is not [d]");
4055 ir_depth_attend_cuda(src.data(), static_cast<int>(src.size()), s.w, out.p, T, d, s.eps, st);
4056 break;
4057 }
4058 case DOp::LinearAttention: { // over the slot's num, den (device state)
4059 const DBuf &q = in(s, 0), &k = in(s, 1), &v = in(s, 2);
4060 const int w = s.hq * s.hd;
4061 if (q.width != w || k.width != w || v.width != w || q.rows != T || k.rows != T || v.rows != T)
4062 throw std::runtime_error("IR linear_attention: q/k/v widths do not match heads * head_dim");
4063 DBuf& out = dout(s.out[0], T, w, {&q, &k, &v});
4064 rec_rows(s, [&](int t0, int n) {
4065 const size_t r = static_cast<size_t>(t0) * w;
4066 ir_linear_attention_cuda(q.p + r, k.p + r, v.p + r, s.rec_a, s.rec_b, out.p + r, n, w, s.eps, st);
4067 });
4068 break;
4069 }
4070 case DOp::Expr: { // a runtime-defined elementwise op (graph_ir_expr.h)
4071 const DBuf& x0 = in(s, 0);
4072 std::vector<const float*> ins;
4073 std::vector<const DBuf*> bufs;
4074 for (size_t k = 0; k < s.in.size(); ++k) {
4075 const DBuf& a = in(s, k);
4076 if (a.width != x0.width || a.rows != x0.rows)
4077 throw std::runtime_error("IR_SPEC 5.19 (rule 37): expr inputs must share one shape [T, w]");
4078 ins.push_back(a.p);
4079 bufs.push_back(&a);
4080 }
4081 if (x0.width <= 0 || x0.rows <= 0) throw std::runtime_error("IR expr: input is not [T, w]");
4082 grow(dexpr_, ir_expr_scratch_floats(s.expr, x0.rows, x0.width));
4083 DBuf& out = dout(s.out[0], x0.rows, x0.width, bufs);
4084 ir_expr_cuda(s.expr, ins.data(), static_cast<int>(ins.size()), dexpr_.alloc, out.p, x0.rows, x0.width, st);
4085 break;
4086 }
4087 case DOp::Conv1dCausal: { // over the slot's x (device state)
4088 const DBuf& x = in(s, 0);
4089 if (x.width != s.n || x.rows != T) throw std::runtime_error("IR conv1d_causal: input width differs from channels");
4090 DBuf& out = dout(s.out[0], T, s.n, {&x});
4091 rec_rows(s, [&](int t0, int n) {
4092 const size_t r = static_cast<size_t>(t0) * s.n;
4093 ir_conv1d_causal_cuda(x.p + r, s.w, s.b, s.rec_a, out.p + r, n, s.n, s.kernel, st);
4094 });
4095 break;
4096 }
4097 case DOp::SelectiveScan: { // Mamba-1's scan over the slot's s (device state)
4098 const DBuf &x = in(s, 0), &dt = in(s, 1), &bm = in(s, 2), &cm = in(s, 3);
4099 const int D = s.n, N = s.d_state;
4100 if (x.width != D || dt.width != D || bm.width != N || cm.width != N || x.rows != T || dt.rows != T ||
4101 bm.rows != T || cm.rows != T)
4102 throw std::runtime_error("IR selective_scan: x/dt must be [T, channels] and B/C [T, d_state]");
4103 DBuf& out = dout(s.out[0], T, D, {&x, &dt, &bm, &cm});
4104 rec_rows(s, [&](int t0, int n) {
4105 const size_t rd = static_cast<size_t>(t0) * D, rn = static_cast<size_t>(t0) * N;
4106 ir_selective_scan_cuda(x.p + rd, dt.p + rd, bm.p + rn, cm.p + rn, s.w, s.ssm_d, s.rec_a, out.p + rd, n, D, N,
4107 s.flag, st);
4108 });
4109 break;
4110 }
4111 case DOp::SsmScan: { // over the slot's s (device state)
4112 const DBuf& u = in(s, 0);
4113 if (u.width != s.n || u.rows != T) throw std::runtime_error("IR ssm_scan: input width differs from n");
4114 DBuf& out = dout(s.out[0], T, s.n, {&u});
4115 rec_rows(s, [&](int t0, int n) {
4116 const size_t r = static_cast<size_t>(t0) * s.n;
4117 ir_ssm_scan_cuda(u.p + r, s.w, s.b, s.ssm_c, s.ssm_d, s.rec_a, out.p + r, n, s.n, st);
4118 });
4119 break;
4120 }
4121 }
4122 commit();
4123 }
4124}
4125
4126void Interpreter::run_device(int T, int start_pos) {
4127 for (auto& b : dbufs_) b.rows = b.items = 0; // allocations persist, definitions do not
4128 pending_.clear();
4130 for (int l = 0; l < n_layers_; ++l) {
4131 if (!mor_step_.empty() && mor_step_[l] >= 0)
4133 else
4135 }
4136}
4137
4138// One application of a recursion step on the device (graph_ir.h, mor_step_device; the CPU's mor_step,
4139// ir.hpp depth_route (a)-(d)).
4140void Interpreter::mor_step_device(int layer, int T, int start_pos) {
4141 cudaStream_t st = stream_;
4142 const int s = mor_step_[layer];
4143 const bool first = layer == 0 || mor_step_[layer - 1] != s;
4144 const bool last = layer == n_layers_ - 1 || mor_step_[layer + 1] != s;
4145 auto take = [&](DBuf& b, int rows, int width) {
4146 grow(b, static_cast<size_t>(rows) * width);
4147 b.p = b.alloc;
4148 b.rows = rows;
4149 b.width = width;
4150 b.is_list = false;
4151 b.items = 0;
4152 };
4153 if (s == 0 && first) { // (a) routing: the logits on the device, depth and gate on the host
4154 const DBuf& x = dbufs_[mor_xs_];
4155 if (x.rows != T || x.is_list) throw std::runtime_error("IR depth_route: input '" + mor_x_ + "' is not a tensor");
4156 const int d = x.width;
4157 const float* W = dweight(mor_router_);
4158 size_t numel = 1;
4159 for (int64_t e : shapes_.at(mor_router_)) numel *= static_cast<size_t>(e);
4160 if (shapes_.at(mor_router_).at(0) != d || numel != static_cast<size_t>(mor_R_) * d)
4161 throw std::runtime_error("IR depth_route '" + mor_router_ + "' is not [n_rec, d]");
4162 grow(dmor_logits_, static_cast<size_t>(T) * mor_R_);
4163 gemm_rm_f32_cuda(cublas_, x.p, W, dmor_logits_.alloc, T, mor_R_, d, st); // x W^T, as linear
4164 std::vector<float> logits(static_cast<size_t>(T) * mor_R_);
4165 gpuErrchk(cudaMemcpyAsync(logits.data(), dmor_logits_.alloc, logits.size() * sizeof(float), cudaMemcpyDeviceToHost,
4166 st));
4168 mor_depth_.assign(T, 0);
4169 mor_g_.assign(T, 0.0f);
4170 for (int t = 0; t < T; ++t) cpu::mor_route(logits.data() + static_cast<size_t>(t) * mor_R_, mor_R_, mor_depth_[t], mor_g_[t]);
4171 // The depth report (out), [T, 1], as the CPU's env holds it. Pageable source: the copy has read it
4172 // when cudaMemcpyAsync returns.
4173 const std::vector<float> depth(mor_depth_.begin(), mor_depth_.end());
4174 DBuf& o = dout(mor_out_slot_, T, 1, {});
4175 gpuErrchk(cudaMemcpyAsync(o.p, depth.data(), depth.size() * sizeof(float), cudaMemcpyHostToDevice, st));
4176 commit();
4177 }
4178 if (first) {
4179 // (b) the step's active tokens, in position order, and its input x_in; every slot the block
4180 // touches is held aside, so the step runs on compact rows and the full values return after it.
4181 mor_active_.clear();
4182 for (int t = 0; t < T; ++t)
4183 if (mor_depth_.at(t) > s) mor_active_.push_back(t);
4184 mor_A_ = static_cast<int>(mor_active_.size());
4185 if (mor_A_ == 0) return; // (d) no active token: the step runs nothing and writes no kv rows
4186 const DBuf& xf = dbufs_[mor_xs_];
4187 if (xf.rows != T || xf.is_list) throw std::runtime_error("IR depth_route: input '" + mor_x_ + "' is not a tensor");
4188 const int d = xf.width;
4189 if (mor_A_ < T) {
4190 if (dmor_idx_cap_ < T) {
4192 gpuErrchk(cudaMalloc(&dmor_idx_, static_cast<size_t>(2) * T * sizeof(int)));
4193 dmor_idx_cap_ = T;
4194 }
4195 std::vector<int> h(static_cast<size_t>(2) * mor_A_);
4196 for (int a = 0; a < mor_A_; ++a) {
4197 h[a] = mor_active_[a];
4198 h[mor_A_ + a] = start_pos + mor_active_[a];
4199 }
4200 gpuErrchk(cudaMemcpyAsync(dmor_idx_, h.data(), h.size() * sizeof(int), cudaMemcpyHostToDevice, st));
4201 }
4202 for (int sl : mor_touch_) {
4203 std::swap(dbufs_[sl], msave_[sl]);
4204 dbufs_[sl].rows = dbufs_[sl].items = 0;
4205 dbufs_[sl].is_list = false;
4206 }
4207 DBuf& xc = dbufs_[mor_xs_];
4208 const DBuf& full = msave_[mor_xs_];
4209 take(xc, mor_A_, d);
4210 if (mor_A_ < T)
4212 else
4213 gpuErrchk(cudaMemcpyAsync(xc.p, full.p, static_cast<size_t>(T) * d * sizeof(float), cudaMemcpyDeviceToDevice, st));
4214 if (mor_gate_score_) {
4215 take(dmor_xin_, mor_A_, d);
4216 gpuErrchk(cudaMemcpyAsync(dmor_xin_.p, xc.p, static_cast<size_t>(mor_A_) * d * sizeof(float),
4218 std::vector<float> g(static_cast<size_t>(mor_A_));
4219 for (int a = 0; a < mor_A_; ++a) g[a] = mor_g_[mor_active_[a]];
4220 take(dmor_g_, mor_A_, 1);
4221 gpuErrchk(cudaMemcpyAsync(dmor_g_.p, g.data(), g.size() * sizeof(float), cudaMemcpyHostToDevice, st));
4222 }
4223 } else if (mor_A_ == 0) {
4224 return;
4225 }
4226 const int A = mor_A_;
4227 // The block's other inputs: each application reads their values from before the step (the CPU
4228 // builds each token's row from the full values; only x carries from one application to the next).
4229 for (int sl : mor_livein_) {
4230 const DBuf& full = msave_[sl];
4231 DBuf& b = dbufs_[sl];
4232 b.rows = b.items = 0;
4233 b.is_list = false;
4234 if (full.is_list || full.rows != T || full.width <= 0) continue; // not a [T, w] tensor: undefined here
4235 take(b, A, full.width);
4236 if (A < T)
4237 ir_rows_gather_cuda(full.p, dmor_idx_, b.p, A, full.width, st);
4238 else
4239 gpuErrchk(cudaMemcpyAsync(b.p, full.p, static_cast<size_t>(T) * full.width * sizeof(float),
4241 }
4242 attn_row_ = static_cast<int>(mor_rows_[layer].size());
4243 drowpos_ = A < T ? dmor_idx_ + A : nullptr;
4244 try {
4246 } catch (...) {
4247 attn_row_ = -1;
4248 drowpos_ = nullptr;
4249 throw;
4250 }
4251 attn_row_ = -1;
4252 drowpos_ = nullptr;
4253 for (int t : mor_active_) mor_rows_[layer].push_back(start_pos + t);
4254 if (!last) return;
4255 // (c) the gate on the active rows, then those rows back into the full x (inactive rows keep x_in),
4256 // and every held value back in its slot.
4257 DBuf& y = dbufs_[mor_xs_];
4258 DBuf& full = msave_[mor_xs_];
4259 const int d = full.width;
4260 if (y.is_list || y.rows != A || y.width != d)
4261 throw std::runtime_error("IR depth_route: the block changes the width of '" + mor_x_ + "'");
4262 if (mor_gate_score_) ir_mor_gate_cuda(dmor_xin_.p, y.p, dmor_g_.p, A, d, st);
4263 if (A < T)
4265 else
4266 gpuErrchk(cudaMemcpyAsync(full.p, y.p, static_cast<size_t>(T) * d * sizeof(float), cudaMemcpyDeviceToDevice, st));
4267 for (int sl : mor_touch_) std::swap(dbufs_[sl], msave_[sl]);
4268}
4269
4270// Captures the T = 1 step (embed, blocks, head) on stream_ as a graph. The
4271// kernels are those of the per-launch step, with the token and position read
4272// from dstep_ (dpos_), so a replay computes exactly what the per-launch step
4273// would. Nothing runs while capturing; the caller replays the graph. False
4274// (and no graph) when the capture fails, e.g. a buffer that must still grow
4275// at this shape: the call then runs per-launch, which grows it, and the next
4276// T = 1 call captures again. Four failures in a row turn the graph off.
4277bool Interpreter::capture_decode(int start_pos) {
4279 graph_exec_ = nullptr;
4282 graph_off_ = true;
4283 Logger::warning("[IR] CUDA graph: stream capture unavailable; decode runs per-launch.");
4284 return false;
4285 }
4286 capturing_ = true;
4287 dpos_ = dstep_ + 1;
4288 std::string err;
4289 try {
4292 if (dbufs_[logits_slot_].rows != 1 || dbufs_[logits_slot_].width > vocab_)
4293 err = "the head produced no [1, vocab] 'logits'";
4294 } catch (const std::exception& e) {
4295 err = e.what();
4296 }
4297 capturing_ = false;
4298 dpos_ = nullptr;
4299 pending_.clear();
4300 cudaGraph_t graph = nullptr;
4302 if (err.empty() && e == cudaSuccess) e = cudaGraphInstantiate(&graph_exec_, graph, 0);
4304 if (!err.empty() || e != cudaSuccess) {
4307 graph_exec_ = nullptr;
4308 if (++graph_failures_ >= 4) graph_off_ = true;
4309 Logger::info("[IR] CUDA graph: capture failed (" + (err.empty() ? std::string(cudaGetErrorString(e)) : err) + ")" +
4310 (graph_off_ ? "; decode runs per-launch from now on." : "; this step runs per-launch."));
4311 return false;
4312 }
4313 graph_failures_ = 0;
4318 Logger::info("[IR] CUDA graph: captured the T = 1 step (" + std::to_string(n_layers_) + " blocks, capture " +
4319 std::to_string(graph_captures_) + "); decode replays it with the position in device memory");
4320 return true;
4321}
4322
4323bool Interpreter::decode_graph(int token, int start_pos) {
4324 static const bool enabled = [] {
4325 const char* v = std::getenv("EMBER_IR_GRAPH");
4326 return !(v && std::string(v) == "0");
4327 }();
4328 // The range checks a replay cannot make (the per-launch step throws them).
4331 return false;
4332 hstep_[0] = token;
4333 hstep_[1] = start_pos;
4334 hstep_[2] = kv_row0_; // the turn_state compact cache's row for this token (unused otherwise)
4335 gpuErrchk(cudaMemcpyAsync(dstep_, hstep_, 3 * sizeof(int), cudaMemcpyHostToDevice, stream_));
4337 htokens_.assign(1, token);
4338 if (!capture_decode(start_pos)) return false;
4339 }
4341 gpuErrchk(cudaMemcpyAsync(hlogits_, graph_logits_, static_cast<size_t>(graph_vocab_) * sizeof(float),
4342 cudaMemcpyDeviceToHost, stream_));
4343 return true;
4344}
4345
4346void Interpreter::ts_upload_codes(int start_pos, int T) {
4347 // Row r's code 2 s + c: held rows (kv_pos_) then this call's, at kv_row0_ + t. Pageable source:
4348 // the copy has read it when cudaMemcpyAsync returns.
4349 const int n = kv_row0_ + T;
4350 std::vector<int> code(static_cast<size_t>(n));
4351 for (int r = 0; r < n; ++r) {
4352 const int p = r < kv_row0_ ? kv_pos_[r] : start_pos + (r - kv_row0_);
4353 code[r] = 2 * ts_seg_[p] + (ts_sidx_[p] >= 0 ? 1 : 0);
4354 }
4355 gpuErrchk(cudaMemcpyAsync(dts_code_, code.data(), code.size() * sizeof(int), cudaMemcpyHostToDevice, stream_));
4356}
4357
4358std::vector<float> Interpreter::forward_device(const std::vector<int>& tokens, int start_pos, bool all_rows,
4359 int tail_rows) {
4360 const int T = static_cast<int>(tokens.size());
4361 // Per-turn state tokens: the CPU path's bookkeeping (codes, the rows held, eviction before the
4362 // call), then the rows' codes for the kernel; after the blocks, this call's rows are held and the
4363 // finished segments' text rows go (lazy eviction), as forward() does on the CPU.
4364 if (state_k_ > 0 || turn_state_) ts_begin(tokens, start_pos);
4365 if (turn_state_) ts_upload_codes(start_pos, T);
4366 auto ts_end = [&] {
4367 if (!turn_state_) return;
4368 for (int t = 0; t < T; ++t) kv_pos_.push_back(start_pos + t);
4369 if (evict_) ts_evict_below(ts_seg_.back());
4370 };
4371 if (rec_floats_ > 0) rec_begin(start_pos, T, all_rows);
4372 if (!mor_step_.empty()) {
4373 // As forward() on the CPU: resuming at start_pos keeps each compact cache's rows below it. The
4374 // routing reads the host, so a depth_route never takes the captured decode graph.
4375 for (auto& rows : mor_rows_)
4376 while (!rows.empty() && rows.back() >= start_pos) rows.pop_back();
4377 mor_depth_.clear();
4378 }
4379 if (T == 1 && mor_step_.empty() && decode_graph(tokens[0], start_pos)) {
4380 ts_end();
4381 if (rec_floats_ > 0) rec_end(start_pos, T);
4383 return std::vector<float>(hlogits_, hlogits_ + graph_vocab_);
4384 }
4385 htokens_ = tokens;
4386 if (dtokens_cap_ < T) {
4388 gpuErrchk(cudaMalloc(&dtokens_, static_cast<size_t>(T) * sizeof(int)));
4389 dtokens_cap_ = T;
4390 }
4391 gpuErrchk(cudaMemcpyAsync(dtokens_, tokens.data(), static_cast<size_t>(T) * sizeof(int), cudaMemcpyHostToDevice,
4392 stream_));
4394 ts_end();
4395 if (rec_floats_ > 0) rec_end(start_pos, T);
4396 // Head ops are row-wise: run them on the last row only, or on the last tail_rows rows.
4397 const int rows = all_rows ? T : std::min(T, std::max(1, tail_rows));
4398 if (rows < T) {
4399 auto last_rows = [T, rows](DBuf& b) {
4400 if (b.rows == T && b.alloc) {
4401 b.p = b.alloc + static_cast<size_t>(T - rows) * b.width;
4402 b.rows = rows;
4403 }
4404 };
4405 for (size_t sl = 0; sl < dbufs_.size(); ++sl) {
4406 DBuf& b = dbufs_[sl];
4407 if (b.is_list && b.rows == T) { // a list's tensors
4408 for (int k = 0; k < b.items; ++k) last_rows(ditems_[sl][k]);
4409 b.rows = rows;
4410 } else {
4411 last_rows(b);
4412 }
4413 }
4414 }
4416 const DBuf& logits = dbufs_[logits_slot_];
4417 if (logits.rows == 0) throw std::runtime_error("IR head produced no 'logits'");
4418 std::vector<float> out(static_cast<size_t>(logits.rows) * logits.width);
4419 gpuErrchk(cudaMemcpyAsync(out.data(), logits.p, out.size() * sizeof(float), cudaMemcpyDeviceToHost, stream_));
4421 return out;
4422}
4423#endif // HAS_CUDA
4424
4425} // namespace ember_ir
static void warning(const std::string &message)
Definition logger.cpp:139
static void info(const std::string &message)
Definition logger.cpp:135
std::vector< RecSlot > rec_slots_
Definition graph_ir.h:359
void attention_window(const Node &n, int layer, int &window, int &sinks) const
std::vector< int > mor_step_
Definition graph_ir.h:283
std::string family_hint_
Definition graph_ir.h:306
std::vector< std::vector< int > > mor_rows_
Definition graph_ir.h:284
std::string mor_x_
Definition graph_ir.h:287
std::map< const Node *, std::shared_ptr< rec::Recurrence > > recs_
Definition graph_ir.h:363
bool evict_declared() const
Definition graph_ir.h:181
std::vector< float > rope_scale_layer_
Definition graph_ir.h:322
std::string mor_router_
Definition graph_ir.h:287
std::vector< Node > head_
Definition graph_ir.h:266
std::vector< Node > block_
Definition graph_ir.h:266
std::string mor_out_
Definition graph_ir.h:287
nlohmann::json rope_ff_
Definition graph_ir.h:332
std::vector< Node > embed_
Definition graph_ir.h:266
std::vector< float > rope_theta_layer_
Definition graph_ir.h:322
std::vector< int > window_layer_
Definition graph_ir.h:321
void init(const nlohmann::json &j, const std::string &ir_json, const GGUFData &gguf, bool use_gpu)
const RecSlot & rec_slot(const Node &n, const char *op) const
std::string state_weight_
Definition graph_ir.h:301
std::unordered_map< std::string, Buf > Env
Definition graph_ir.h:230
std::vector< int > weight_map_
Definition graph_ir.h:273
Interpreter(const std::string &ir_json, const GGUFData &gguf, bool use_gpu=false)
std::string kv_slot_
Definition graph_ir.h:221
std::vector< float > rope_attn_layer_
Definition graph_ir.h:322
static Recurrence compile(const nlohmann::json &node)
void attention_window_batch_cpu(const std::vector< float > &q_batch_roped, KVCacheLayer &current_layer_kv_cache, std::vector< float > &batch_attn_output, int num_tokens_in_batch, int start_pos_in_sequence, int num_q_heads, int num_kv_heads, int head_dim, float attention_scale, int window, int sinks, float softcap)
std::vector< std::vector< int > > attention_visible_rows(int num_tokens_in_batch, int start_pos_in_sequence, int window, int sinks)
void attention_batch_cpu(const std::vector< float > &q_batch_roped, KVCacheLayer &current_layer_kv_cache, std::vector< float > &batch_attn_output, int num_tokens_in_batch, int start_pos_in_sequence, int num_q_heads, int num_kv_heads, int head_dim, float attention_scale, float softcap)
void attention_bias_batch_cpu(const std::vector< float > &q_batch_roped, const KVCacheLayer &current_layer_kv_cache, std::vector< float > &batch_attn_output, int num_tokens_in_batch, const std::vector< std::vector< int > > &rows, const std::vector< int > *key_pos, const std::vector< int > &query_pos, int num_q_heads, int num_kv_heads, int head_dim, float attention_scale, const float *bias, const std::vector< int > &buckets, float softcap)
void attention_rows_batch_cpu(const std::vector< float > &q_batch_roped, const KVCacheLayer &current_layer_kv_cache, std::vector< float > &batch_attn_output, int num_tokens_in_batch, const std::vector< std::vector< int > > &rows, int num_q_heads, int num_kv_heads, int head_dim, float attention_scale, float softcap)
void swa_visible_ranges(int pos, int window, int sinks, int &sink_hi, int &win_lo)
void attention_relkv_batch_cpu(const std::vector< float > &q_batch_roped, const KVCacheLayer &current_layer_kv_cache, std::vector< float > &batch_attn_output, int num_tokens_in_batch, const std::vector< std::vector< int > > &rows, const std::vector< int > *key_pos, const std::vector< int > &query_pos, int num_q_heads, int num_kv_heads, int head_dim, float attention_scale, const float *rk, int rk_width, const float *rv, int rv_width, int max_distance, float softcap)
void gemm_rm_f32_cuda(cublasHandle_t handle, const float *X, const float *W, float *Y, int T, int O, int I, cudaStream_t stream)
void moe_ffn_cuda(cublasHandle_t handle, const float *x, int T, int d, int d_ff, int E, int k, bool renorm, const float *router, const float *gate_exps, const float *up_exps, const float *down_exps, float *scratch, float *out, cudaStream_t stream)
size_t moe_ffn_scratch_floats(int T, int d, int d_ff, int E, int k)
void attention_devpos_cuda(const float *Q_current_dev, const float *K_layer_cache_base, const float *V_layer_cache_base, float *out_dev, int num_heads, int head_dim, float scale, int cache_max_seq_len, int cache_num_kv_heads, const int *step_dev, cudaStream_t stream)
void attention_decode_at_cuda(const float *Q_current_dev, const float *K_layer_cache_base, const float *V_layer_cache_base, float *out_dev, int num_heads, int seq_len, int head_dim, float scale, int cache_max_seq_len, int cache_num_kv_heads, cudaStream_t stream)
GGMLType
Enumeration of GGML tensor data types.
Definition ggml_types.h:21
@ GGML_TYPE_F32
Definition ggml_types.h:22
@ GGML_TYPE_BF16
Definition ggml_types.h:40
@ GGML_TYPE_Q5_0
Definition ggml_types.h:27
@ GGML_TYPE_F16
Definition ggml_types.h:23
@ GGML_TYPE_Q6_K
Definition ggml_types.h:35
@ GGML_TYPE_Q4_1
Definition ggml_types.h:25
@ GGML_TYPE_Q8_0
Definition ggml_types.h:29
@ GGML_TYPE_Q5_K
Definition ggml_types.h:34
@ GGML_TYPE_Q5_1
Definition ggml_types.h:28
@ GGML_TYPE_Q4_K
Definition ggml_types.h:33
@ GGML_TYPE_Q4_0
Definition ggml_types.h:24
constexpr size_t GGML_QK_K
Block size constants for different quantization formats.
Definition gguf_parser.h:42
Data structures for GGUF (GPT-Generated Unified Format) file format.
const json & doc_
Definition graph_ir.cpp:431
std::string slot_
Definition graph_ir.cpp:434
int H_
Definition graph_ir.cpp:435
std::map< std::string, std::string > role_
Definition graph_ir.cpp:433
const ModelConfig cfg_
Definition graph_ir.cpp:432
std::string x_
Definition graph_ir.cpp:434
int KV_
Definition graph_ir.cpp:435
std::string why
Definition graph_ir.cpp:280
int hd_
Definition graph_ir.cpp:435
int d_
Definition graph_ir.cpp:435
const float * in[expr::Expr::kMaxInputs]
const float * p[kIrDepthSourcesMax]
expr::Instr code[expr::kMaxCode]
const float * col[expr::kMaxColumns]
int n
float value
size_t arity
std::vector< float > v
const char * name
enum ember_ir::expr::@33::Value::Shape shape
Kind kind
float c
size_t numel
std::vector< int > sizes
std::string where
std::vector< E > a
bool inputs
bool weights
node layernorm
node moe_ffn
node rmsnorm
node sub
node out
node scale
node sigmoid
node linear
size_t j
std::vector< Value > items
Definition ir_digest.cpp:67
Logging utilities for the TinyLlama implementation.
ModelConfig parse_model_config_from_gguf(const GGUFData &gguf)
Constants for attention mechanism calculations.
std::vector< Stage > lower(const Expr &e)
const Expr & cached(const std::string &text, size_t n_inputs)
Definition api.h:20
std::string build_ir_from_gguf(const GGUFData &g, bool log)
Definition graph_ir.cpp:553
void ir_linear_attention_cuda(const float *q, const float *k, const float *v, float *num, float *den, float *out, int T, int width, float den_floor, cudaStream_t stream)
void ir_conv1d_causal_cuda(const float *x, const float *w, const float *bias, float *state, float *out, int T, int C, int K, cudaStream_t stream)
void ir_turn_state_attention_cuda(const float *q, const float *K, const float *V, float *out, int T, int row0, const int *codes, int hq, int hkv, int hd, float scale, cudaStream_t stream, const int *drow)
bool has_recurrent_state(const std::string &ir_json)
Definition graph_ir.cpp:237
void ir_binary_cuda(const float *a, const float *b, float *out, size_t n, int op, cudaStream_t stream)
void ir_bias_add_cuda(float *y, const float *b, int T, int O, cudaStream_t stream)
bool has_state_tokens(const std::string &ir_json)
Definition graph_ir.cpp:257
void ir_rope_rows_at_cuda(float *x, int T, int heads, int hd, int n_rot, bool neox, const int *pos, const float *freqs, cudaStream_t stream)
void ir_rows_gather_cuda(const float *src, const int *idx, float *dst, int n, int w, cudaStream_t stream)
void ir_rows_scatter_cuda(const float *src, const int *idx, float *dst, int n, int w, cudaStream_t stream)
void ir_kv_write_cuda(float *K, float *V, const float *k, const float *v, int T, int row, int start_pos, cudaStream_t stream, const int *dpos)
size_t ir_expr_scratch_floats(const std::vector< expr::Stage > &stages, int T, int w)
void linear_attention_row_cpu(const float *q, const float *k, const float *v, float *num, float *den, float *out, int width, float den_floor)
Definition graph_ir.cpp:213
bool has_windowed_attention(const std::string &ir_json)
Definition graph_ir.cpp:245
void ir_embedding_cuda(const float *table, const int *tokens, float *out, int T, int d, cudaStream_t stream)
static void cpu_only(bool gpu, const char *what)
bool tuned_llama_match(const std::string &ir_json, const GGUFData &gguf, std::string *why)
Definition graph_ir.cpp:440
void ir_gelu_cuda(const float *x, float *out, size_t n, bool tanh_form, cudaStream_t stream)
void ir_silu_cuda(const float *x, float *out, size_t n, cudaStream_t stream)
json parse_ir_document(const std::string &text)
void ir_depth_attend_cuda(const float *const *sources, int S, const float *w, float *out, int T, int d, float eps, cudaStream_t stream)
DigestResult ir_digest(const std::string &text, bool prose)
void ir_rope_rows_cuda(float *x, int T, int heads, int hd, int n_rot, bool neox, int start_pos, const float *freqs, cudaStream_t stream, const int *dpos)
bool device_supported(const std::string &ir_json)
Definition graph_ir.cpp:463
void validate_ir(const json &doc, const GGUFData &gguf)
void ir_kv_compact_cuda(float *K, float *V, const int *idx, int w0, int n, int row, cudaStream_t stream)
void ir_pos_embedding_cuda(const float *x, const float *P, float *out, int T, int d, int start_pos, cudaStream_t stream, const int *dpos)
void ir_window_attention_cuda(const float *q, const float *K, const float *V, float *out, int T, int start_pos, int hq, int hkv, int hd, float scale, int window, int sinks, cudaStream_t stream, const int *dpos, float softcap)
std::string find_ir(const GGUFData &gguf)
Definition graph_ir.cpp:525
void ir_scale_cuda(const float *x, const float *g, const float *b, float *out, int T, int w, cudaStream_t stream)
void ir_expr_cuda(const std::vector< expr::Stage > &stages, const float *const *ins, int n_ins, float *scratch, float *out, int T, int w, cudaStream_t stream)
void ir_rope_at_cuda(float *q, float *k, int T, int hq, int hk, int hd, const int *pos, const float *freqs, cudaStream_t stream)
void ir_rms_row_cuda(const float *x, float *out, int T, int w, float eps, cudaStream_t stream)
void ir_layernorm_cuda(const float *x, const float *w, const float *b, float *out, int T, int d, float eps, cudaStream_t stream)
void ssm_scan_row_cpu(const float *u, const float *A, const float *B, const float *C, float D, float *s, float *scratch, float *y, int n)
Definition graph_ir.cpp:224
void ir_relu_cuda(const float *x, float *out, size_t n, cudaStream_t stream)
void ir_affine_cuda(const float *x, float *out, size_t n, float scale, float shift, cudaStream_t stream)
void ir_selective_scan_cuda(const float *x, const float *dt, const float *B, const float *C, const float *A, const float *d, float *s, float *y, int T, int D, int N, bool softplus, cudaStream_t stream)
void ir_mla_attention_cuda(const float *qn, const float *qr, const float *KV, const float *Kb, const float *Vb, float *out, int T, int start_pos, int H, int r, int dn, int dr, int dv, float scale, int window, int sinks, cudaStream_t stream, const int *dpos)
void ir_mul_col_cuda(const float *x, const float *c, float *out, int T, int w, cudaStream_t stream)
void ir_embedding_state_cuda(const float *table, const float *state, int vocab, const int *tokens, float *out, int T, int d, cudaStream_t stream)
void ir_mor_gate_cuda(const float *x_in, float *x, const float *g, int n, int w, cudaStream_t stream)
void ir_mla_kv_write_cuda(float *KV, const float *c, const float *kr, int T, int r, int dr, int start_pos, cudaStream_t stream, const int *dpos)
void ir_rope_cuda(float *q, float *k, int T, int hq, int hk, int hd, int start_pos, const float *freqs, cudaStream_t stream, const int *dpos)
void ir_ssm_scan_cuda(const float *u, const float *A, const float *B, const float *C, const float *D, float *s, float *y, int T, int n, cudaStream_t stream)
bool has_ir_builder(const std::string &arch)
Definition graph_ir.cpp:519
void ir_sigmoid_cuda(const float *x, float *out, size_t n, cudaStream_t stream)
const char * format_name(int f)
Definition kv_quant.h:45
void write_cuda(int f, void *Kd, void *Ks, void *Vd, void *Vs, const float *k, const float *v, int T, int row, int start_pos, cudaStream_t st, const int *dpos)
Definition kv_quant.cu:260
size_t partial_floats(int hq, int hd, int max_len)
Definition kv_quant.cu:284
void decode_cuda(int f, const float *q, const void *Kd, const void *Ks, const void *Vd, const void *Vs, float *out, int hq, int hkv, int hd, float scale, int max_len, int seq_len, float *part, cudaStream_t st, const int *dpos)
Definition kv_quant.cu:288
size_t data_bytes(int f, size_t row)
Definition kv_quant.h:48
void dequant_rows_cuda(int f, const void *Kd, const void *Ks, const void *Vd, const void *Vs, float *K, float *V, int n, int row, cudaStream_t st)
Definition kv_quant.cu:303
std::string shape_error(int f, int head_dim)
Definition kv_quant.h:55
size_t scale_bytes(int f, size_t row)
Definition kv_quant.h:51
Format parse_format(const char *s)
Definition kv_quant.h:37
void roundtrip_rows(int f, float *x, size_t rows, size_t row)
Definition kv_quant.h:138
bool deterministic()
Definition determinism.h:22
void deterministic_cublas_env()
Definition determinism.h:32
Template get(const std::string &id)
Constants for Rotary Position Embedding (RoPE)
size_t ggml_type_size(GGMLType type)
Gets the size in bytes of a GGML type.
const char * ggml_type_name(GGMLType type)
Gets the string name of a GGML type.
void dequantize_vector_q8_0_to_f32(const std::vector< block_q8_0 > &q_weights, std::vector< float > &f32_weights, size_t total_num_elements, int log_first_n_blocks)
Dequantizes a vector of Q8_0 blocks to a vector of float32.
Weight quantization structures and functions for model compression.
nlohmann::json json
Definition server.cpp:68
Complete representation of a GGUF file's contents.
std::vector< std::string > tokenizer_tokens
std::map< std::string, GGUFMetadataValue > metadata
std::map< std::string, GGUFTensorInfo > tensor_infos_map
Information about a tensor stored in a GGUF file.
size_t size_in_bytes
size_t num_elements
uint64_t offset
std::vector< uint64_t > shape
Key-Value cache for a single transformer layer.
Definition model.h:136
std::vector< float > k
Definition model.h:137
Model configuration structure holding architecture and hyperparameters.
Definition model.h:82
int hidden_size
Definition model.h:83
float rms_norm_eps
Definition model.h:90
int num_attention_heads
Definition model.h:85
float rope_theta
Definition model.h:91
int num_experts_used
Definition model.h:108
int num_hidden_layers
Definition model.h:87
int num_key_value_heads
Definition model.h:86
int num_experts
Definition model.h:107
std::string hidden_act
Definition model.h:92
Simple 8-bit quantized block structure.
std::vector< float > v
Definition graph_ir.h:225
std::vector< RecTensor > tensors
Definition graph_ir.h:352
std::vector< float > f32
Definition graph_ir.h:235
void moe_ffn_routed_f32_cpu(const float *x, int d, int d_ff, int E, int k, const float *router, const float *gate_exps, const float *up_exps, const float *down_exps, float *out, bool renorm, bool sigmoid, const float *bias, int groups_n, int groups_top)
Definition utils.cpp:763
void matmul_rows_f32_batch_cpu(const std::function< void(int64_t, float *)> &row_fn, const float *batch_input_activations, float *batch_output_activations, int num_tokens, int output_dim, int input_dim)
Definition utils.cpp:916
void apply_rope_batch_cpu(std::vector< float > &q_batch, std::vector< float > &k_batch, int num_tokens, int num_q_heads, int num_kv_heads, int head_dim, int start_pos_in_sequence, const std::vector< std::pair< float, float > > &all_freqs_cis, int max_pos_embeddings, bool use_adjacent_pairing)
Definition utils.cpp:491
void matmul_f32_f32_batch_cpu(const std::vector< float > &mat_weights, const std::vector< float > &batch_input_activations, std::vector< float > &batch_output_activations, int num_tokens, int output_dim, int input_dim)
Definition utils.cpp:872
void silu_cpu(const std::vector< float > &x, std::vector< float > &out)
Definition utils.cpp:842
void rmsnorm_batch_cpu(const std::vector< float > &x_batch, const std::vector< float > &weight, std::vector< float > &out_batch, int num_tokens, int hidden_size, float eps)
Definition utils.cpp:613
bool dequant_legacy_to_f32(const GGUFTensorInfo &info, const uint8_t *src, std::vector< float > &out)
bool dequant_span_to_f32(GGMLType t, const uint8_t *src, size_t first, size_t n, float *out)