ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
graph_ir.h
Go to the documentation of this file.
1#pragma once
2// Interpreter for paperkiln's block-structured IR (LOCAL_LLM_STACK.md section 4).
3//
4// The IR (GGUF key `paperkiln.ir`, or a side file named by EMBER_IR) is an
5// `embed` subgraph, a `block` subgraph repeated L times (weights `blk.{i}.*`)
6// and a `head` subgraph. Tensors flow as [T, width] row matrices, T being the
7// tokens in this call; prefill and decode run the same graph (decode is
8// T = 1). Weights are GGUF tensors by name, [out, in] row-major. The weights
9// of `linear` and `embedding` stay in their GGUF blocks when quantised (Q8_0,
10// Q4_0, Q4_1, Q5_0, Q5_1, F16): each call dequantises one row at a time into
11// f32 with the load-time routines and dots it in the f32 kernel's order, so
12// results equal dequantising at load bit for bit. Every other weight (norms,
13// biases, MoE stacks) is f32 at load. Kept quantised only for models over
14// 64M parameters on the CPU; small models and the CUDA path dequantise every
15// weight at load (EMBER_IR_QUANT=1 / EMBER_IR_DEQUANT=1 force either way). State is owned here: per block, at most
16// one kv slot and any number of recurrent slots.
17//
18// Recurrent state (block.state kind "recurrent": linear_attention's num and
19// den, ssm_scan's s). The interpreter tracks how many tokens it has absorbed
20// (one sequence). forward() at start_pos 0 starts a sequence from the init
21// state; at the absorbed count it continues; anywhere else it restores a
22// snapshot of the state taken after start_pos tokens, or throws when it has
23// none (a recurrence cannot drop tokens). Snapshots are taken at the end of
24// every call, after the second-to-last row of a multi-row call (the same
25// prompt again resumes there), and after every row of an all_rows call
26// (speculative verification resumes after the accepted rows). A later call
27// from position p drops the snapshots past p. resume_point() tells the caller
28// where a prefix can actually be resumed.
29//
30// CPU kernels, built from the same primitives as ember's llama CPU path so
31// the arithmetic matches it: embedding, pos_embedding, rmsnorm (optionally
32// per group of `group` columns, one weight [group] shared: per-head q/k
33// norm), layernorm, linear (+ bias; tied_to names the weight a missing one
34// reads), split, rope (pairing "interleaved" (x[2j], x[2j+1]) or "neox"
35// (x[j], x[j + n_rot/2]); n_rot <= head_dim rotates each head's first n_rot
36// dims, angle pos * theta^(-2j/n_rot); k has kv_heads heads), attention
37// (causal, grouped-query: query head h reads kv head h / (heads / kv_heads);
38// KV state, optional sliding window + sinks as paperkiln's swa_attention), add, mul, silu, gelu (tanh or erf), moe_ffn (llama.cpp's expert
39// layout), linear_attention (elu+1, ops::kimi_attention) and ssm_scan
40// (ops::ssm_scan) over recurrent state, and recurrence (a recurrent op the
41// document defines, IR_SPEC 6.10, graph_ir_recurrence.h; CPU only: a CUDA
42// build serves such a document on the CPU); and for the gated and
43// attention-residual families (flex highway / plain, attnres, srd's router)
44// sub, relu, sigmoid, affine, mul_col, rms_row, list, append and depth_attend
45// (graph_ir_ops.h): a list is a value like a tensor (named by "out", read by
46// "in"), an ordered set of [T, d] tensors. Any other op fails at load,
47// naming the op and the backend. Weights may be F32, F16, BF16, Q8_0 or the
48// legacy 4/5-bit blocks.
49//
50// Third-party GGUFs (LOCAL_LLM_STACK.md section 7): a file with no
51// `paperkiln.ir` whose general.architecture is qwen2 or qwen3 gets the IR
52// that build_ir_from_gguf makes from its llama.cpp metadata, node for node
53// what paperkiln's tools/gguf_ir.py builds. llama and mistral files keep the
54// llama path (EMBER_IR_BUILD=1 builds their IR too); any other architecture
55// fails at load, naming it. EMBER_IR_CTX=N caps the positions the KV state
56// holds (a third-party IR declares the model's full trained context).
57//
58// CUDA builds add a device path, chosen at construction (use_gpu), for every
59// op above (device_supported): weights are uploaded once, the KV state, the
60// recurrent state with its snapshots and the named row buffers live on the
61// device, and the op sequence runs on one stream with no host sync until the
62// logits are copied back. The ops are compiled at load into a per-block plan
63// (enum ops, buffer slots, device weight pointers, numeric attributes), so a
64// call does no JSON or string work. GEMMs are FP32 cuBLAS (default math); the
65// other kernels follow the CPU forms (graph_ir_cuda.h), sliding-window
66// attention included, so the two paths agree to rounding. A T = 1 call
67// (decode) replays a CUDA graph of the whole step, captured once, with the
68// token and position in device memory (EMBER_IR_GRAPH=0: per-launch).
69// Mixture-of-Recursions (depth_route, kv_rows "active") runs on the device
70// too: routing on the host from the device's router logits, each recursion
71// step batched over its active rows at their absolute positions into the
72// application's compact kv cache (mor_step_device); its decode runs
73// per-launch.
74#include <initializer_list>
75#include <map>
76#include <memory>
77#include <string>
78#include <unordered_map>
79#include <utility>
80#include <set>
81#include <vector>
82
83#include <nlohmann/json.hpp>
84
85#include "ggml_types.h"
86#include "graph_ir_expr.h"
87#include "model.h"
88
89struct GGUFData;
90
91namespace ember_ir {
92namespace rec {
93class Recurrence;
94}
95
96struct Node {
97 std::string op;
98 std::vector<std::string> in, out;
99 nlohmann::json attrs; // the node's JSON object
100};
101
103 public:
104 // Parses the IR JSON and binds every weight it names from the GGUF.
105 // Throws std::runtime_error naming the op, tensor or field at fault, or the
106 // IR_SPEC rule a document breaks (graph_ir_validate.h). A
107 // weight kept quantised may point into the GGUF's memory map, so `gguf`
108 // must outlive the interpreter.
109 // use_gpu selects the device path (CUDA builds; ignored otherwise).
110 Interpreter(const std::string& ir_json, const GGUFData& gguf, bool use_gpu = false);
111 ~Interpreter();
112 Interpreter(const Interpreter&) = delete;
114
115 // Runs tokens at absolute positions start_pos .. start_pos + T - 1 (the KV
116 // state must already hold positions < start_pos). Returns logits for the
117 // last token ([vocab]) or, with all_rows, for every token ([T][vocab]).
118 // tail_rows > 0 (all_rows false): the last min(tail_rows, T) tokens' logits, each row the one
119 // all_rows gives (the head ops are row-wise); POST /score's continuation rows.
120 // Under LongRoPE with two frequency-factor sets (rope freq_factors {short, long, original_ctx}, IR_SPEC 5.14 and
121 // 6.9, revision 1.5) a call that changes the set the sequence's length selects (start_pos + T > original_ctx
122 // against the set the held rows were rotated with) runs the whole sequence again from position 0 (the ids
123 // held plus tokens), so every kv row carries the new set; the result is the same rows of logits.
124 std::vector<float> forward(const std::vector<int>& tokens, int start_pos, bool all_rows = false, int tail_rows = 0);
125
126 // True when a block declares recurrent state (see the header comment).
127 bool has_recurrent() const { return rec_floats_ > 0; }
128 // The largest position <= want from which forward() can resume the current
129 // sequence: want itself without recurrent state; else want when the state
130 // or a snapshot is there, otherwise the nearest snapshot below it (0 at
131 // worst: a fresh start). Under turn_state eviction: want, unless want lies
132 // inside a segment m whose text rows were evicted; then the start of m.
133 int resume_point(int want) const;
134
135 int vocab() const { return vocab_; }
136 int max_len() const { return max_len_; }
137 int n_layers() const { return n_layers_; }
138 const std::string& family_hint() const { return family_hint_; }
139 // Mixture-of-Recursions (depth_route): the recursion depth of each row of the last forward
140 // (1 .. n_rec), its routing trace; empty when the block has no depth_route.
141 const std::vector<int>& last_depths() const { return mor_depth_; }
142 // The absolute positions of the kv rows application `application` holds, ascending, on either path
143 // (the bookkeeping is host-side on both): a recursion step's compact cache under depth_route
144 // (kv_rows "active"), the compact cache under turn_state, else 0 .. seq_len() - 1; empty without a
145 // kv slot. kv_positions() below is the CPU-only conformance hook.
146 std::vector<int> held_positions(int application) const;
147 bool on_gpu() const { return gpu_; }
148 // EMBER_KV (kv_quant.h): the kv cache's storage format, "f32" (the default), "f16", "q8" or "q4".
149 // On the CPU a quantised format keeps the f32 cache and stores each row's dequantised value.
150 const char* kv_format() const;
151 // Device bytes of the kv cache (every block's codes and scales) and of a quantised format's prefill
152 // scratch (the dequantised rows of one block); 0 on the CPU.
153 size_t kv_cache_bytes() const;
154 size_t kv_scratch_bytes() const;
155
156 // Per-turn state tokens (paperkiln IR_SPEC_DRAFT.md section 1, tiny_chat_long_2026-10).
157 // state_k(): the k state slots of the extended embedding ("state_weight", [k, d]); ids vocab ..
158 // vocab + k - 1 read its rows; 0 without one. A state block is exactly those k ids in ascending
159 // order; forward() refuses a state id out of order and a text id inside an open block.
160 // Attention "mask": "turn_state": a key j is visible from the query at p iff s(j) == s(p), or j
161 // is a state position with s(j) < s(p), s(p) being the number of complete state blocks before p.
162 // evicts(): the kv slot's "evict": "turn_state" is in force (EMBER_EVICT=0 keeps every row;
163 // EMBER_EVICT=1 evicts for any turn_state IR): once a later segment starts, the text rows of
164 // the finished segments are dropped and their state rows kept, positions absolute. A forward
165 // from a position inside a segment whose text rows are gone throws; resume_point() then names
166 // that segment's start, where the caller re-prefills from.
167 int state_k() const { return state_k_; }
168 bool turn_state() const { return turn_state_; }
169 bool evicts() const { return evict_; }
170 // kv rows held per block (turn_state IRs; else the positions written so far).
171 int kv_rows_held() const { return turn_state_ ? static_cast<int>(kv_pos_.size()) : static_cast<int>(ts_seg_.size()); }
172
173 // Conformance hooks (tools/ir_adapter.cpp; paperkiln spec/IR_SPEC.md section 11.3). CPU path
174 // only (each throws on the device path). No other caller uses them, and none changes what
175 // forward() computes: the accessors read the state, truncate() and load_state() set it to what
176 // a fresh run over the same ids would leave.
177 // kv_slot(): the kv slot's name (empty without one). evict_declared(): the document's kv slot
178 // says "evict": "turn_state" (evicts() is what is in force after EMBER_EVICT). seq_len(): the
179 // positions the state covers (the end of the last forward, truncate or load_state).
180 const std::string& kv_slot() const { return kv_slot_; }
181 bool evict_declared() const { return evict_declared_; }
182 int seq_len() const { return seq_len_; }
183 bool has_depth_route() const { return !mor_step_.empty(); }
184 // The absolute positions of the k/v rows application `application`'s kv cache holds, ascending:
185 // under turn_state the compact cache's (after any eviction), under depth_route a recursion
186 // step's compact cache, else 0 .. seq_len() - 1. Empty without a kv slot.
187 std::vector<int> kv_positions(int application) const;
188 // The recurrent slots' tensors in declaration order, and one tensor's values at an application.
190 std::string slot, tensor;
191 std::vector<int64_t> shape;
192 };
193 std::vector<RecurrentTensorInfo> recurrent_tensors() const;
194 std::vector<float> recurrent_values(const std::string& slot, const std::string& tensor, int application) const;
195 // Drops the state of positions >= n (IR_SPEC.md section 6.9), as a forward from n would before
196 // running. n must be a point the state can resume from (resume_point(n) == n): the caller
197 // re-prefills from resume_point(n) otherwise. Under LongRoPE with two sets, a cut whose length selects
198 // the other set runs the first n ids again (IR_SPEC 6.9, revision 1.5).
199 void truncate(int n);
200 // The whole sequence state (kv rows in use, state-token bookkeeping, MoR row sets, recurrent
201 // tensors and their snapshots), copied out and back (the adapter's snapshot / restore).
202 struct State {
203 int seq_len = 0;
204 std::vector<std::vector<float>> k, v; // per application: its rows in use
205 std::vector<int> ts_seg, ts_sidx, kv_pos;
207 std::vector<std::vector<int>> mor_rows;
208 std::vector<int> mor_depth;
209 std::vector<float> rec;
210 int rec_pos = 0;
211 std::map<int, std::vector<float>> rec_snaps;
212 std::vector<int> ids; // LongRoPE with two sets: the ids the rows hold
213 bool rope_long = false; // and the set they were rotated with
214 };
215 State save_state() const;
216 void load_state(const State& s);
217
218 private:
219 void init(const nlohmann::json& j, const std::string& ir_json, const GGUFData& gguf, bool use_gpu);
220 int kv_rows_in_use(int application) const; // rows 0 .. n-1 of the application's cache are live
221 std::string kv_slot_;
222 bool evict_declared_ = false;
223 int seq_len_ = 0;
224 struct Buf {
225 std::vector<float> v;
226 int width = 0;
227 bool is_list = false; // a list value (op list / append): its tensors are `items`, v is empty
228 std::vector<Buf> items; // in order; each a tensor
229 };
230 using Env = std::unordered_map<std::string, Buf>;
231 // A bound weight: f32 values, or (quantised) the tensor's raw GGUF bytes at qp: inside the
232 // GGUF's memory map when it is mapped (no copy; the GGUFData outlives the interpreter), else
233 // in q.
234 struct Weight {
235 std::vector<float> f32;
236 std::vector<uint8_t> q;
237 const uint8_t* qp = nullptr;
238 size_t qbytes = 0;
240 size_t numel = 0;
241 bool quantised() const { return qp != nullptr; }
242 };
243
244 void run_nodes(const std::vector<Node>& nodes, int layer, Env& env, int T, int start_pos, bool head_last_only);
245 // The attention node's own window/sinks when it names them, else its kv state's.
246 // Per-layer values ({"per_layer": [...]}) are read for `layer`.
247 void attention_window(const Node& n, int layer, int& window, int& sinks) const;
248 // The RoPE table of block `layer` (its theta; one table per distinct theta).
249 const std::vector<std::pair<float, float>>& rope_table(int layer) const;
250 const std::vector<float>& weight(const std::string& name) const; // f32 weights only
251 const Weight& bound(const std::string& name) const;
252 std::string layer_name(const std::string& pattern, int layer) const;
253 // The node being bound, run or compiled carries "tensor_index": "application" (IR_SPEC 7.2): its tensor
254 // names' {i} is the application index, not the stored layer m_i (layer_name). Set per node by AppIndex.
255 bool app_index_ = false;
256 struct AppIndex {
257 bool& flag;
258 bool saved;
259 AppIndex(bool& f, const Node& n) : flag(f), saved(f) { flag = n.attrs.contains("tensor_index"); }
261 };
262 void bind(const std::vector<Node>& nodes, int layer, const GGUFData& gguf);
263 // keep_quantised: the tensor is only read row-wise (linear, embedding).
264 void bind_tensor(const std::string& name, const GGUFData& gguf, bool keep_quantised = false);
265
266 std::vector<Node> embed_, block_, head_;
267 // depth_attend's source count S per node and application (-1 outside the block), fixed at load by the
268 // lists' construction (IR_SPEC 5.18).
269 std::map<std::pair<const Node*, int>, int> da_sources_;
270 int n_layers_ = 0, vocab_ = 0, max_len_ = 0;
271 // block.weight_index (paperkiln ir.hpp; tied-block recursion): application i
272 // reads stored layer weight_map_[i]'s tensors. Empty: the identity.
273 std::vector<int> weight_map_;
274 // Mixture-of-Recursions (paperkiln ir.hpp depth_route, causal token choice). mor_step_[i]: the
275 // recursion step of application i, -1 outside it (empty: no depth_route). A step's applications
276 // run on its active tokens only (depth > step), one at a time in position order; each such
277 // application keeps a compact kv cache holding just the rows written there, mor_rows_[i] their
278 // absolute positions (kv_rows "active"), so attention's causal kernel reads exactly those rows
279 // and RoPE keeps the absolute position; a window and sinks keep the rows whose positions they
280 // cover (IR_SPEC 5.15, v1.4.2), never the last `window` rows. attn_row_ >= 0 places the next kv
281 // row (and the query's causal limit) in that compact cache.
282 void mor_step(int layer, Env& env, int T, int start_pos);
283 std::vector<int> mor_step_;
284 std::vector<std::vector<int>> mor_rows_;
285 int mor_R_ = 0;
286 bool mor_gate_score_ = false;
288 std::vector<int> mor_depth_;
289 std::vector<float> mor_g_, mor_xin_;
290 int attn_row_ = -1;
291 // Per-turn state tokens (see state_k()). ts_seg_[p] = s(p), ts_sidx_[p] = j for the state id
292 // vocab + j, -1 for text, over the positions of the current sequence. Under turn_state the kv
293 // cache is compact: row r holds position kv_pos_[r] (ascending; the identity without eviction),
294 // a call's rows start at kv_row0_, and ts_vis_[t] lists the rows its token t sees, ascending.
295 // Text rows of segments below evicted_below_ are gone.
296 void ts_begin(const std::vector<int>& tokens, int start_pos);
297 void ts_evict_below(int seg);
298 int ts_seg_after(int p) const; // s of a token placed at position p + 1
299 int ts_segment_start(int m) const;
300 int state_k_ = 0;
301 std::string state_weight_;
302 bool turn_state_ = false, evict_ = false;
303 std::vector<int> ts_seg_, ts_sidx_, kv_pos_;
304 std::vector<std::vector<int>> ts_vis_;
306 std::string family_hint_;
307 bool dequant_all_ = false; // every weight f32 at load (see the header comment)
308 std::unordered_map<std::string, Weight> weights_;
309 std::unordered_map<std::string, std::vector<int64_t>> shapes_; // ggml order (ne0 first)
310 std::vector<KVCacheLayer> kv_; // per block: [max_len][kv_heads][head_dim]
311 int kv_heads_ = 0, head_dim_ = 0;
312 int kv_fmt_ = 0; // ember_kvq::Format from EMBER_KV (kv_quant.h); 0 = f32, every path as before
313 int window_ = 0, sinks_ = 0; // kv state's sliding window (0 = unbounded) and sinks
314 float rope_theta_ = 0.0f;
315 int rope_dim_ = 0; // n_rot (head_dim_ when the rope node names none)
316 bool rope_neox_ = false; // pairing "neox"
317 float rope_freq_scale_ = 1.0f; // linear RoPE scaling: the angle is (pos * freq_scale) * theta^(-2j/n_rot)
318 // Per-layer attributes (paperkiln ir.hpp, {"per_layer": [v_0, ..., v_{L-1}]}): the kv state's window
319 // per block (empty when one value serves every block), each block's RoPE theta, freq_scale and
320 // attn_factor, a table per distinct (theta, freq_scale, attn_factor, frequency factors).
321 std::vector<int> window_layer_;
323 // The RoPE tables ([max_len][rope_dim_ / 2] (cos, sin) each, deduplicated) and each block's: rope_tab_short_ its
324 // table without frequency factors or with the one set or the short set (IR_SPEC 5.14, revision 1.5), rope_tab_long_
325 // its long set's (empty without two sets).
326 std::vector<std::vector<std::pair<float, float>>> rope_tabs_;
328 // LongRoPE with two sets (freq_factors {short, long, original_ctx}): rope_ff_ctx_ = original_ctx N (0 otherwise).
329 // The set follows the sequence's length n after a call: long when n > N. rope_long_ is the set every row of the kv
330 // slot holds; ids_ the ids at positions 0 .. seq_len_ - 1, so a call that changes the set runs the whole sequence
331 // again (forward, truncate), on either path.
332 nlohmann::json rope_ff_; // the rope node's freq_factors as written (null: none)
334 bool rope_long_ = false;
335 std::vector<int> ids_;
336 void build_rope_tables(const GGUFData& gguf);
337 void set_rope_long(bool v);
338 std::vector<float> forward_impl(const std::vector<int>& tokens, int start_pos, bool all_rows, int tail_rows);
339 std::set<std::string> offset_applied_; // rmsnorm weights already read as w + weight_offset
340 bool gpu_ = false;
341
342 // Recurrent slots: each block's state is rec_floats_ floats, slot after
343 // slot, each slot's tensors in declaration order.
344 struct RecTensor {
345 std::string name;
346 std::vector<int64_t> shape;
347 size_t offset = 0, floats = 0; // within the block's state
348 float init = 0.0f;
349 };
350 struct RecSlot {
351 std::string name, op;
352 std::vector<RecTensor> tensors;
353 };
354 const RecSlot& rec_slot(const Node& n, const char* op) const;
355 float* rec_tensor(const RecSlot& slot, const char* tensor, int layer, size_t floats);
356 void rec_begin(int start_pos, int T, bool all_rows);
357 void rec_row_done(const RecSlot& slot, int layer, int t); // the slot's state after row t (kept rows only)
358 void rec_end(int start_pos, int T);
359 std::vector<RecSlot> rec_slots_;
360 size_t rec_floats_ = 0;
361 std::vector<float> rec_init_; // every application's initial state, [n_layers][rec_floats_]
362 // The recurrence nodes of the block (IR_SPEC 6.10; graph_ir_recurrence.h), compiled at load.
363 std::map<const Node*, std::shared_ptr<rec::Recurrence>> recs_;
364 std::vector<float> rec_; // [n_layers][rec_floats_]
365 int rec_pos_ = 0; // tokens absorbed; -1 after a failed call
366 std::map<int, std::vector<float>> rec_snaps_; // state after `key` tokens
367 size_t rec_max_snaps_ = 0;
368 std::vector<int> rec_snap_rows_; // this call: rows whose state is kept (-1 = not kept), indices into rec_rows_
369 std::vector<float> rec_rows_; // this call: [kept row][n_layers][rec_floats_]
370
371
372#ifdef HAS_CUDA
373 // Device path. A DBuf is a [rows, width] buffer, one per tensor name of the
374 // IR (its slot); `p` points into the owned allocation (the head runs on the
375 // last row of a prefill in place). Buffers persist across calls so
376 // steady-state decode allocates nothing; an op whose output name is also
377 // one of its inputs writes the slot's spare and swaps.
378 // A list value (op list / append) is a DBuf with is_list set and `items`
379 // tensors, held as copies in the slot's ditems_ (a list is a value: a later
380 // write of a tensor it was built from does not change it).
381 struct DBuf {
382 float* alloc = nullptr;
383 float* p = nullptr;
384 size_t cap = 0; // floats
385 int width = 0, rows = 0; // rows == 0: not defined in this call
386 bool is_list = false;
387 int items = 0;
388 };
389 // One op of the device plan, resolved once at load (compile_device): the op
390 // as an enum, its tensors as slots, its weights as device pointers and its
391 // attributes as numbers, so a call does no JSON, string or map work. Only
392 // the checks that depend on the call (widths, positions) remain per call.
393 enum class DOp {
395 Sub, Relu, Sigmoid, Affine, MulCol, RmsRow, List, Append, DepthAttend, LinearAttention, SsmScan, Expr,
397 };
398 struct DStep {
399 DOp op = DOp::Add;
400 const Node* node = nullptr; // names for error messages
401 std::vector<int> in, out; // slots
402 std::string wname; // the resolved weight (error messages)
403 const float* w = nullptr; // weight: table, norm scale, linear [out, in], moe router, depth_attend query, ssm A
404 const float* b = nullptr; // bias (layernorm, linear); ssm B
405 const float *gate = nullptr, *up = nullptr, *down = nullptr; // moe_ffn expert stacks
406 const float *ssm_c = nullptr, *ssm_d = nullptr; // ssm_scan C [n], D (a device scalar)
407 float *rec_a = nullptr, *rec_b = nullptr; // device state: linear_attention num, den; ssm_scan s
408 const RecSlot* rec = nullptr; // the recurrent op's state slot
409 size_t w_elems = 0;
410 int w_ne0 = 0; // the weight's ne0 (rows of width ne0)
411 int hd = 0, hq = 0, hkv = 0, window = 0, sinks = 0; // rope, attention, linear_attention (hq heads)
412 int E = 0, top_k = 0, d_ff = 0; // moe_ffn
413 int n = 0; // ssm_scan state size; rmsnorm group (0: the row); conv1d_causal, selective_scan channels
414 int n_rot = 0, kernel = 0, d_state = 0; // rope n_rot; conv1d_causal kernel; selective_scan d_state
415 bool neox = false; // rope pairing
416 const float* rope = nullptr; // rope: this block's (cos, sin) table on the device
417 const float* rope_long = nullptr; // rope: its long set's table (LongRoPE, two sets), else null
418 float softcap = 0.0f; // attention: softcap c (IR_SPEC 5.15, revision 1.5), 0 without
419 std::vector<expr::Stage> expr; // expr: the device program (graph_ir_expr.h lower)
420 float eps = 1e-5f, scale = 0.0f, shift = 0.0f; // eps: also linear_attention's den_floor
421 bool flag = false; // gelu: tanh form; moe_ffn: renorm gates
422 std::vector<int> sizes; // split
423 int groups = 1; // split: groups (IR_SPEC 5.5, revision 1.5)
424 const float* state_w = nullptr; // embedding: the state rows (state_weight), ids vocab .. vocab + k - 1
425 bool ts = false; // attention: mask turn_state
426 // mla_attention (IR_SPEC 5.20): heads in hq, k_b in w, v_b in b, window and sinks as attention's
427 int kv_rank = 0, qk_nope = 0, qk_rope = 0, v_dim = 0;
428 };
429 void init_device();
430 void compile_device();
431 std::vector<DStep> compile_nodes(const std::vector<Node>& nodes, int layer);
432 int slot(const std::string& name);
433 void free_device();
434 const float* dweight(const std::string& name) const;
435 // (Re)allocates b to hold `need` floats when it is smaller. A growth during
436 // graph capture throws (the capture is abandoned; see capture_decode).
437 void grow(DBuf& b, size_t need);
438 DBuf& dout(int slot, int rows, int width, std::initializer_list<const DBuf*> inputs);
439 DBuf& dout(int slot, int rows, int width, const std::vector<const DBuf*>& inputs);
440 void copy_item(DBuf& dst, const DBuf& x);
441 void commit();
442 void run_nodes_device(const std::vector<DStep>& steps, int layer, int T, int start_pos);
443 std::vector<float> forward_device(const std::vector<int>& tokens, int start_pos, bool all_rows, int tail_rows);
444 void run_device(int T, int start_pos); // embed, blocks; not the head
445 // The captured T = 1 step: true when it ran (logits in hlogits_).
446 bool decode_graph(int token, int start_pos);
447 bool capture_decode(int start_pos);
448 float* dsnap_take(int key); // the device snapshot buffer for `key`, created or reused
449 void dsnap_drop(std::map<int, float*>::iterator it);
450
451 cublasHandle_t cublas_ = nullptr;
452 cudaStream_t stream_ = nullptr;
453 std::unordered_map<std::string, float*> dweights_;
454 std::unordered_map<std::string, int> slots_; // tensor name -> slot (load time only)
455 std::vector<std::string> slot_names_;
456 std::vector<DBuf> dbufs_, dspare_; // by slot
457 std::vector<std::vector<DBuf>> ditems_; // by slot: a list value's tensors
458 std::vector<int> pending_; // slots written to their spare by dout, swapped by commit
459 std::vector<DStep> plan_embed_, plan_head_;
460 std::vector<std::vector<DStep>> plan_block_; // per block
461 int logits_slot_ = -1;
462 std::vector<float*> dk_, dv_; // per block: [max_len][kv_heads][head_dim]
463 // kv_fmt_ != f32: per block the codes and scales (kv_quant.h) in place of dk_/dv_ (left null), the
464 // prefill's dequantised rows [2][max_len][row] (one block at a time) and the decode kernel's scratch.
465 std::vector<void*> dkq_, dks_, dvq_, dvs_;
466 float* dkv_f32_ = nullptr;
467 float* dkv_part_ = nullptr;
468 size_t kv_dev_bytes_ = 0, kv_scratch_dev_bytes_ = 0;
469 std::vector<float*> drope_tabs_; // rope_tabs_ on the device
470 DBuf dexpr_; // expr scratch (a reduction's argument and its columns)
471 int* dtokens_ = nullptr;
472 int dtokens_cap_ = 0;
473 std::vector<int> htokens_; // this call's tokens (range checks on the host)
474 DBuf dmoe_; // moe_ffn scratch
475 // Per-turn state tokens on the device: the host bookkeeping is the CPU path's (ts_begin,
476 // ts_evict_below, kv_pos_); dts_code_ [max_len_] holds each kv row's code 2 s + c for the
477 // turn_state kernel, uploaded before every call (ts_upload_codes), and eviction compacts the
478 // device caches in place through the row list dkv_idx_ [max_len_]. A call's first kv row is
479 // kv_row0_ (dstep_[2] in the captured decode step).
480 void ts_upload_codes(int start_pos, int T);
481 int* dts_code_ = nullptr;
482 int* dkv_idx_ = nullptr;
483
484 // Mixture-of-Recursions on the device (mor_step_device, the CPU's mor_step). Routing: the router
485 // logits x W^T by cuBLAS, then each token's depth and gate on the host by cpu::mor_route (the CPU's
486 // code; one sync per forward), so the depth is EXACT given the logits. A step holds the full value of
487 // every slot the block reads or writes aside (mor_touch_, swapped into msave_), gathers the active
488 // rows of x and of the block's other inputs (mor_livein_, re-gathered at each application, as the
489 // CPU rebuilds each token's row from the full values) and runs the block's plan with T = the active
490 // count: rope at each row's absolute position (drowpos_, absent when every row is active), attention
491 // over the application's compact cache from row attn_row_ (the rows it holds, mor_rows_). After the
492 // step's last application the gate blends the compact x with its input, the rows go back into the
493 // full x (inactive rows keep x_in) and the held values return, as the CPU's env keeps them. A forward
494 // through a depth_route never takes the captured decode graph (the routing reads the host).
495 void mor_step_device(int layer, int T, int start_pos);
496 int mor_xs_ = -1, mor_out_slot_ = -1; // slots of depth_route's input and of its depth report
497 std::vector<int> mor_touch_, mor_livein_;
498 std::vector<DBuf> msave_; // by slot: the full value held aside during a step
500 int* dmor_idx_ = nullptr; // [2 cap]: the step's active rows, then their absolute positions
501 int dmor_idx_cap_ = 0;
502 const int* drowpos_ = nullptr; // non-null inside a step whose rows are not all active
503 std::vector<int> mor_active_; // this step's active rows (host)
504 int mor_A_ = 0;
505
506 // Device recurrent state: drec_ [n_layers][rec_floats_] (fixed: a restore
507 // copies into it, so a captured graph keeps reading the live state);
508 // drec_init_ the initial state of every block; snapshots and this call's
509 // kept rows as on the CPU path (rec_begin / rec_row_done / rec_end), by
510 // device-to-device copies.
511 float* drec_ = nullptr;
512 float* drec_init_ = nullptr;
513 std::map<int, float*> dsnaps_; // state after `key` tokens
514 std::vector<float*> dsnap_pool_; // released snapshot buffers
515 DBuf drec_rows_; // this call: [kept row][n_layers][rec_floats_]
516
517 // CUDA graph of the T = 1 step (EMBER_IR_GRAPH=0 disables): captured once,
518 // replayed with the token and position in device memory (dstep_ = [token,
519 // pos, kv row]; dpos_ points at the position while capturing). Any buffer growth
520 // (alloc_epoch_) invalidates it; it is captured again at the next T = 1.
522 int* dstep_ = nullptr;
523 int* hstep_ = nullptr; // pinned
524 float* hlogits_ = nullptr; // pinned, [vocab]
525 const int* dpos_ = nullptr; // non-null only while capturing
526 bool capturing_ = false, graph_off_ = false;
527 int graph_failures_ = 0; // consecutive failed captures
528 int graph_captures_ = 0;
529 unsigned long long alloc_epoch_ = 0, graph_epoch_ = 0;
530 const float* graph_logits_ = nullptr;
531 int graph_vocab_ = 0;
532 int embed_rows_ = 0, pos_rows_ = 0; // replay range checks: token ids, positions
533#endif
534};
535
536// The IR for a GGUF: the EMBER_IR side file if set, else the `paperkiln.ir`
537// key, else the IR build_ir_from_gguf makes for a qwen2 / qwen3 / gpt2 /
538// gptneox / gemma3 / mamba file (and llama / mistral when the file names a
539// sliding window, or with EMBER_IR_BUILD=1), else empty (the llama path).
540// Throws for a file with no IR whose architecture ember cannot run, naming it.
541std::string find_ir(const GGUFData& gguf);
542
543// The IR of a third-party GGUF from its metadata and tensor names, node for
544// node as paperkiln's tools/gguf_ir.py builds it:
545// llama, mistral, qwen2, qwen3: pre-RMSNorm blocks, q/k/v biases when the
546// file has blk.0.attn_q.bias, per-head q/k RMSNorm (group head_dim) when it
547// has blk.0.attn_q_norm.weight, RoPE pairing "neox" for qwen2/qwen3 and
548// "interleaved" for llama/mistral, grouped-query attention (llama/mistral's
549// attention.sliding_window as the window), SwiGLU;
550// gpt2, gptneox: LayerNorm blocks over a fused biased qkv, gelu (tanh for
551// gpt2, exact for gptneox); gpt2 learned positions, gptneox partial NEOX
552// RoPE and (use_parallel_residual) llama.cpp's parallel residual;
553// gemma3: sqrt(d)-scaled embeddings, sandwich RMSNorms, per-head q/k
554// RMSNorm, NEOX RoPE and GeGLU; window and theta 10000 on layers with
555// i % 6 < 5, the file's theta (and linear scaling) on the rest;
556// mamba: Mamba-1 blocks (conv1d_causal, selective_scan) with no positions.
557// A llama file with llama.expert_count and blk.0.ffn_gate_inp.weight (Mixtral's
558// layout, IR_SPEC 9.3 revision 1.3) has a moe_ffn (router ffn_gate_inp, experts
559// ffn_{gate,up,down}_exps, top_k expert_used_count, renorm, silu) in place of
560// the SwiGLU. The head is tied to token_embd.weight when output.weight is absent
561// (gptneox always has its own). Throws naming the architecture or the missing
562// key. log false: no log line (rule 38's check, graph_ir_validate.h).
563std::string build_ir_from_gguf(const GGUFData& gguf, bool log = true);
564
565// The architectures build_ir_from_gguf builds (IR_SPEC 9.3).
566bool has_ir_builder(const std::string& arch);
567
568// ember's tuned llama path (model.cpp, api.cpp) computes one graph, and a document may take it only when it
569// is that graph (paperkiln spec/IR_SPEC.md 9.1: family_hint is never semantics; it is a pre-filter here):
570// a valid document (graph_ir_validate.h) with family_hint "llama" whose nodes are, modulo value names and the
571// operand order of add and mul,
572// embed embedding(tokens; token_embd.weight)
573// block rmsnorm(attn_norm) -> linear attn_q, attn_k, attn_v (no bias) -> rope (interleaved, every head,
574// n_rot = head_dim, freq_scale 1, the q / k heads) -> attention (the one kv slot: window 0, sinks 0,
575// causal mask, scale 1/sqrt(head_dim), no evict, kv_rows or weight_index) -> linear attn_output -> add
576// -> rmsnorm(ffn_norm) -> SwiGLU (ffn_gate, ffn_up, silu, mul, ffn_down) or moe_ffn (ffn_gate_inp and
577// the *_exps stacks, gate_norm renorm, silu) -> add
578// head rmsnorm(output_norm) -> linear output.weight (tied to token_embd.weight or not)
579// with the numbers the tuned path reads from the GGUF's metadata instead (parse_model_config_from_gguf: d,
580// layers, heads, kv heads, head_dim = d / heads, every eps, theta, experts and experts used, hidden_act silu).
581// Otherwise the interpreter runs the document. why (optional) gets the first difference.
582bool tuned_llama_match(const std::string& ir_json, const GGUFData& gguf, std::string* why = nullptr);
583
584// True when the IR declares sliding-window attention (a kv state or an
585// attention node with window != 0). Only the interpreter implements it, so
586// such a GGUF never runs on the llama path, whatever its family_hint.
587bool has_windowed_attention(const std::string& ir_json);
588
589// True when the IR declares recurrent state (a block.state of kind
590// "recurrent").
591bool has_recurrent_state(const std::string& ir_json);
592
593// True when the CUDA path has a kernel for every op of the IR (recurrent
594// state, per-turn state tokens: state_weight, mask "turn_state", evict, and
595// Mixture-of-Recursions: depth_route, kv_rows "active", with no pos_embedding
596// or windowed attention in its block) within its limits (depth_attend sources, ssm_scan n);
597// otherwise the interpreter must run on the CPU.
598bool device_supported(const std::string& ir_json);
599
600// True when the IR uses per-turn state tokens: an embedding with
601// "state_weight" or an attention node with "mask": "turn_state". Only the
602// interpreter implements them, so such a GGUF never runs on the llama path.
603bool has_state_tokens(const std::string& ir_json);
604
605// The recurrent ops for one token (row), in paperkiln's order of operations
606// (graph_ir.cpp); the state is updated in place. Exposed for the op tests.
607// linear_attention: q, k, v, num, den, out each `width` = heads * head_dim.
608void linear_attention_row_cpu(const float* q, const float* k, const float* v, float* num, float* den, float* out,
609 int width, float den_floor);
610// ssm_scan: u, s, y [n]; A [n, n] row-major; B, C [n]; scratch [n].
611void ssm_scan_row_cpu(const float* u, const float* A, const float* B, const float* C, float D, float* s,
612 float* scratch, float* y, int n);
613
614} // namespace ember_ir
std::vector< RecSlot > rec_slots_
Definition graph_ir.h:359
std::vector< float > mor_xin_
Definition graph_ir.h:289
void attention_window(const Node &n, int layer, int &window, int &sinks) const
void ts_evict_below(int seg)
int max_len() const
Definition graph_ir.h:136
const std::string & kv_slot() const
Definition graph_ir.h:180
std::vector< int > ids_
Definition graph_ir.h:335
std::vector< int > mor_step_
Definition graph_ir.h:283
void load_state(const State &s)
std::set< std::string > offset_applied_
Definition graph_ir.h:339
void rec_row_done(const RecSlot &slot, int layer, int t)
const char * kv_format() const
void set_rope_long(bool v)
const std::vector< int > & last_depths() const
Definition graph_ir.h:141
std::string family_hint_
Definition graph_ir.h:306
std::vector< float > rec_init_
Definition graph_ir.h:361
int n_layers() const
Definition graph_ir.h:137
int ts_seg_after(int p) const
bool has_depth_route() const
Definition graph_ir.h:183
Interpreter(const Interpreter &)=delete
void run_nodes(const std::vector< Node > &nodes, int layer, Env &env, int T, int start_pos, bool head_last_only)
std::vector< std::vector< int > > mor_rows_
Definition graph_ir.h:284
std::vector< float > rec_rows_
Definition graph_ir.h:369
std::vector< int > rope_tab_short_
Definition graph_ir.h:327
std::vector< int > kv_pos_
Definition graph_ir.h:303
std::string mor_x_
Definition graph_ir.h:287
std::vector< float > forward(const std::vector< int > &tokens, int start_pos, bool all_rows=false, int tail_rows=0)
std::map< const Node *, std::shared_ptr< rec::Recurrence > > recs_
Definition graph_ir.h:363
std::vector< std::vector< std::pair< float, float > > > rope_tabs_
Definition graph_ir.h:326
void rec_end(int start_pos, int T)
bool evict_declared() const
Definition graph_ir.h:181
int kv_rows_in_use(int application) const
const std::string & family_hint() const
Definition graph_ir.h:138
std::vector< RecurrentTensorInfo > recurrent_tensors() const
std::vector< float > rope_scale_layer_
Definition graph_ir.h:322
void ts_begin(const std::vector< int > &tokens, int start_pos)
std::vector< int > ts_seg_
Definition graph_ir.h:303
std::string mor_router_
Definition graph_ir.h:287
int resume_point(int want) const
std::vector< Node > head_
Definition graph_ir.h:266
std::vector< float > recurrent_values(const std::string &slot, const std::string &tensor, int application) const
int kv_rows_held() const
Definition graph_ir.h:171
State save_state() const
Interpreter & operator=(const Interpreter &)=delete
bool turn_state() const
Definition graph_ir.h:168
std::vector< int > rope_tab_long_
Definition graph_ir.h:327
std::vector< KVCacheLayer > kv_
Definition graph_ir.h:310
std::vector< int > kv_positions(int application) const
std::vector< float > rec_
Definition graph_ir.h:364
bool on_gpu() const
Definition graph_ir.h:147
std::vector< Node > block_
Definition graph_ir.h:266
std::string mor_out_
Definition graph_ir.h:287
int vocab() const
Definition graph_ir.h:135
nlohmann::json rope_ff_
Definition graph_ir.h:332
bool evicts() const
Definition graph_ir.h:169
bool has_recurrent() const
Definition graph_ir.h:127
std::vector< Node > embed_
Definition graph_ir.h:266
std::vector< float > rope_theta_layer_
Definition graph_ir.h:322
int seq_len() const
Definition graph_ir.h:182
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
void bind_tensor(const std::string &name, const GGUFData &gguf, bool keep_quantised=false)
size_t kv_cache_bytes() const
std::vector< int > mor_depth_
Definition graph_ir.h:288
std::vector< float > mor_g_
Definition graph_ir.h:289
std::map< std::pair< const Node *, int >, int > da_sources_
Definition graph_ir.h:269
void mor_step(int layer, Env &env, int T, int start_pos)
void build_rope_tables(const GGUFData &gguf)
const Weight & bound(const std::string &name) const
std::unordered_map< std::string, Buf > Env
Definition graph_ir.h:230
std::map< int, std::vector< float > > rec_snaps_
Definition graph_ir.h:366
std::vector< int > ts_sidx_
Definition graph_ir.h:303
const std::vector< std::pair< float, float > > & rope_table(int layer) const
std::vector< int > rec_snap_rows_
Definition graph_ir.h:368
size_t kv_scratch_bytes() const
std::unordered_map< std::string, Weight > weights_
Definition graph_ir.h:308
std::vector< int > weight_map_
Definition graph_ir.h:273
std::vector< float > forward_impl(const std::vector< int > &tokens, int start_pos, bool all_rows, int tail_rows)
std::vector< int > held_positions(int application) const
std::vector< std::vector< int > > ts_vis_
Definition graph_ir.h:304
int ts_segment_start(int m) const
void rec_begin(int start_pos, int T, bool all_rows)
int state_k() const
Definition graph_ir.h:167
void bind(const std::vector< Node > &nodes, int layer, const GGUFData &gguf)
std::string layer_name(const std::string &pattern, int layer) const
const std::vector< float > & weight(const std::string &name) const
float * rec_tensor(const RecSlot &slot, const char *tensor, int layer, size_t floats)
std::unordered_map< std::string, std::vector< int64_t > > shapes_
Definition graph_ir.h:309
std::string kv_slot_
Definition graph_ir.h:221
std::vector< float > rope_attn_layer_
Definition graph_ir.h:322
Type definitions for GGML (Georgi Gerganov Machine Learning) library.
GGMLType
Enumeration of GGML tensor data types.
Definition ggml_types.h:21
@ GGML_TYPE_F32
Definition ggml_types.h:22
std::string why
Definition graph_ir.cpp:280
const float * in[expr::Expr::kMaxInputs]
const float * p[kIrDepthSourcesMax]
int n
std::vector< float > v
const char * name
std::vector< int > sizes
bool inputs
node expr
node out
node scale
size_t j
std::vector< Value > items
Definition ir_digest.cpp:67
Definition api.h:20
std::string build_ir_from_gguf(const GGUFData &g, bool log)
Definition graph_ir.cpp:553
bool has_recurrent_state(const std::string &ir_json)
Definition graph_ir.cpp:237
bool has_state_tokens(const std::string &ir_json)
Definition graph_ir.cpp:257
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
bool tuned_llama_match(const std::string &ir_json, const GGUFData &gguf, std::string *why)
Definition graph_ir.cpp:440
bool device_supported(const std::string &ir_json)
Definition graph_ir.cpp:463
std::string find_ir(const GGUFData &gguf)
Definition graph_ir.cpp:525
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
bool has_ir_builder(const std::string &arch)
Definition graph_ir.cpp:519
Constants for Rotary Position Embedding (RoPE)
Complete representation of a GGUF file's contents.
AppIndex(bool &f, const Node &n)
Definition graph_ir.h:259
std::vector< Buf > items
Definition graph_ir.h:228
std::vector< float > v
Definition graph_ir.h:225
std::vector< RecTensor > tensors
Definition graph_ir.h:352
std::vector< int64_t > shape
Definition graph_ir.h:346
std::vector< std::vector< int > > mor_rows
Definition graph_ir.h:207
std::vector< int > ids
Definition graph_ir.h:212
std::vector< float > rec
Definition graph_ir.h:209
std::map< int, std::vector< float > > rec_snaps
Definition graph_ir.h:211
std::vector< int > ts_seg
Definition graph_ir.h:205
std::vector< int > ts_sidx
Definition graph_ir.h:205
std::vector< int > kv_pos
Definition graph_ir.h:205
std::vector< std::vector< float > > v
Definition graph_ir.h:204
std::vector< std::vector< float > > k
Definition graph_ir.h:204
std::vector< int > mor_depth
Definition graph_ir.h:208
std::vector< float > f32
Definition graph_ir.h:235
std::vector< uint8_t > q
Definition graph_ir.h:236
std::vector< std::string > out
Definition graph_ir.h:98
nlohmann::json attrs
Definition graph_ir.h:99
std::vector< std::string > in
Definition graph_ir.h:98
std::string op
Definition graph_ir.h:97