ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
graph_ir_validate.cpp
Go to the documentation of this file.
1// IR_SPEC section 10.1 at load (graph_ir_validate.h): a port of paperkiln's tools/ir_validate.py.
2#include "graph_ir_validate.h"
3
4#include <algorithm>
5#include <functional>
6#include <map>
7#include <optional>
8#include <regex>
9#include <set>
10#include <stdexcept>
11#include <type_traits>
12#include <utility>
13#include <variant>
14
15#include "gguf_structs.h"
16#include "graph_ir.h"
17#include "graph_ir_expr.h"
18#include "graph_ir_recurrence.h"
19#include "graph_ir_schema.h"
20#include "ir_digest.h"
21
22using nlohmann::json;
23
24namespace ember_ir {
25namespace {
26
27std::string E(const std::string& section, const std::string& msg) { return "IR_SPEC " + section + ": " + msg; }
28
29std::string short_dump(const json& v) {
30 std::string s = v.dump(-1, ' ', false, json::error_handler_t::replace);
31 return s.size() > 60 ? s.substr(0, 60) : s;
32}
33
34// ---------------------------------------------------------------- the schema (rule 1)
35
36const json& schema() {
37 static const json s = [] {
38 std::string text;
39 for (const char* p : kIrSchemaPieces) text += p;
40 return json::parse(text);
41 }();
42 return s;
43}
44
45// JSON equality where a boolean equals only a boolean (the validator's _json_eq).
46bool json_eq(const json& a, const json& b) {
47 if (a.is_boolean() || b.is_boolean()) return a.is_boolean() && b.is_boolean() && a == b;
48 return a == b;
49}
50
51bool type_ok(const json& v, const json& t) {
52 if (t.is_array()) {
53 for (const auto& x : t)
54 if (type_ok(v, x)) return true;
55 return false;
56 }
57 const std::string n = t.get<std::string>();
58 if (n == "object") return v.is_object();
59 if (n == "array") return v.is_array();
60 if (n == "string") return v.is_string();
61 if (n == "boolean") return v.is_boolean();
62 if (n == "null") return v.is_null();
63 if (n == "integer") return v.is_number_integer();
64 if (n == "number") return v.is_number();
65 throw std::logic_error("IR schema: unknown type " + n);
66}
67
68bool pattern_match(const std::string& pattern, const std::string& key) {
69 static std::map<std::string, std::regex> cache;
70 auto it = cache.find(pattern);
71 if (it == cache.end()) it = cache.emplace(pattern, std::regex(pattern, std::regex::ECMAScript)).first;
72 return std::regex_search(key, it->second);
73}
74
75using Failures = std::vector<std::pair<std::string, std::string>>;
76
77void eval(const json& s, const json& v, const std::string& path, Failures& out);
78
79Failures collect(const json& s, const json& v, const std::string& path) {
81 eval(s, v, path, out);
82 return out;
83}
84
85// The keywords spec/ir.schema.json uses, as tools/ir_validate.py evaluates them.
86void eval(const json& s, const json& v, const std::string& path, Failures& out) {
87 if (s.is_boolean()) {
88 if (!s.get<bool>()) out.emplace_back(path, "is not allowed");
89 return;
90 }
91 if (s.empty()) return;
92 if (s.contains("$ref")) {
93 const std::string ref = s.at("$ref").get<std::string>();
94 const std::string pre = "#/$defs/";
95 if (ref.compare(0, pre.size(), pre) != 0) throw std::logic_error("IR schema: $ref " + ref);
96 eval(schema().at("$defs").at(ref.substr(pre.size())), v, path, out);
97 }
98 if (s.contains("type") && !type_ok(v, s.at("type"))) {
99 out.emplace_back(path, "must be of type " + s.at("type").dump() + ", not " + short_dump(v));
100 return;
101 }
102 if (s.contains("enum")) {
103 bool any = false;
104 for (const auto& e : s.at("enum")) any = any || json_eq(v, e);
105 if (!any) out.emplace_back(path, short_dump(v) + " is not one of " + s.at("enum").dump());
106 }
107 if (s.contains("const") && !json_eq(v, s.at("const"))) out.emplace_back(path, "must be " + s.at("const").dump());
108 if (v.is_number() && s.contains("minimum") && v.get<double>() < s.at("minimum").get<double>())
109 out.emplace_back(path, v.dump() + " is below the minimum " + s.at("minimum").dump());
110 if (v.is_string() && s.contains("minLength") && v.get<std::string>().size() < s.at("minLength").get<size_t>())
111 out.emplace_back(path, "must not be empty");
112 if (v.is_array()) {
113 if (s.contains("minItems") && v.size() < s.at("minItems").get<size_t>())
114 out.emplace_back(path, "needs at least " + s.at("minItems").dump() + " item(s)");
115 if (s.contains("items"))
116 for (size_t i = 0; i < v.size(); ++i) eval(s.at("items"), v[i], path + "[" + std::to_string(i) + "]", out);
117 }
118 if (v.is_object()) {
119 if (s.contains("required"))
120 for (const auto& k : s.at("required"))
121 if (!v.contains(k.get<std::string>())) out.emplace_back(path, "lacks the required key '" + k.get<std::string>() + "'");
122 static const json kEmpty = json::object();
123 const json& props = s.contains("properties") ? s.at("properties") : kEmpty;
124 const json& pats = s.contains("patternProperties") ? s.at("patternProperties") : kEmpty;
125 for (const auto& kv : v.items()) {
126 bool matched = false;
127 if (props.contains(kv.key())) {
128 matched = true;
129 eval(props.at(kv.key()), kv.value(), path + "." + kv.key(), out);
130 }
131 for (const auto& p : pats.items())
132 if (pattern_match(p.key(), kv.key())) {
133 matched = true;
134 eval(p.value(), kv.value(), path + "." + kv.key(), out);
135 }
136 if (!matched && s.contains("additionalProperties")) {
137 const json& ap = s.at("additionalProperties");
138 if (ap.is_boolean() && !ap.get<bool>()) {
139 // A node names its op, so the message says which op does not define the key (rule 4).
140 const std::string op = v.contains("op") && v.at("op").is_string() ? " (" + v.at("op").get<std::string>() + ")" : "";
141 out.emplace_back(path + op, "has the key '" + kv.key() + "', which its specification does not define");
142 } else {
143 eval(ap, kv.value(), path + "." + kv.key(), out);
144 }
145 }
146 }
147 }
148 if (s.contains("allOf"))
149 for (const auto& sub : s.at("allOf")) eval(sub, v, path, out);
150 if (s.contains("anyOf")) {
151 bool any = false;
152 for (const auto& sub : s.at("anyOf")) any = any || collect(sub, v, path).empty();
153 if (!any) {
154 // The closest alternative's failures (the fewest), as the validator reports them.
156 bool first = true;
157 for (const auto& sub : s.at("anyOf")) {
158 Failures f = collect(sub, v, path);
159 if (first || f.size() < best.size()) best = std::move(f);
160 first = false;
161 }
162 if (best.empty()) best.emplace_back(path, "matches no alternative");
163 out.insert(out.end(), best.begin(), best.end());
164 }
165 }
166 if (s.contains("if")) {
167 if (collect(s.at("if"), v, path).empty()) {
168 if (s.contains("then")) eval(s.at("then"), v, path, out);
169 } else if (s.contains("else")) {
170 eval(s.at("else"), v, path, out);
171 }
172 }
173}
174
175void add_unique(std::vector<std::string>& out, std::set<std::string>& seen, const std::string& m) {
176 if (seen.insert(m).second) out.push_back(m);
177}
178
179// ---------------------------------------------------------------- the document rules (3, 5-23)
180
181using Errs = std::vector<std::string>;
182
183const json& kNull() {
184 static const json n;
185 return n;
186}
187const json& kObject() {
188 static const json o = json::object();
189 return o;
190}
191const json& get(const json& o, const std::string& k) { return o.is_object() && o.contains(k) ? o.at(k) : kNull(); }
192bool is_int(const json& v) { return v.is_number_integer(); }
193bool is_pl(const json& v) { return v.is_object() && v.contains("per_layer"); }
194// Python's truthiness, for the rules the validator states with any().
195bool truthy(const json& v) {
196 if (v.is_null()) return false;
197 if (v.is_boolean()) return v.get<bool>();
198 if (v.is_number()) return v.get<double>() != 0.0;
199 if (v.is_string()) return !v.get<std::string>().empty();
200 return !v.empty();
201}
202// A value as a message shows it: a string bare, anything else as JSON (null as None, as the validator prints).
203std::string show(const json& v) {
204 if (v.is_string()) return v.get<std::string>();
205 if (v.is_null()) return "None";
206 return v.dump();
207}
208std::string q(const std::string& s) { return "'" + s + "'"; }
209
210// The value per application: the per_layer list (nullopt when its length is not L) or the scalar L times.
211std::optional<std::vector<json>> layers(const json& v, int L) {
212 if (is_pl(v)) {
213 const json& pl = v.at("per_layer");
214 if (!pl.is_array() || static_cast<int>(pl.size()) != L) return std::nullopt;
215 return std::vector<json>(pl.begin(), pl.end());
216 }
217 return std::vector<json>(static_cast<size_t>(std::max(L, 0)), v);
218}
219
220struct Sect {
221 std::string sect;
222 size_t j;
223 const json* n;
224};
225
226std::vector<Sect> sections(const json& doc) {
227 std::vector<Sect> out;
228 for (const char* sect : {"embed", "block", "head"}) {
229 const json& nodes = get(get(doc, sect), "nodes");
230 if (!nodes.is_array()) continue;
231 for (size_t j = 0; j < nodes.size(); ++j)
232 if (nodes[j].is_object()) out.push_back({sect, j, &nodes[j]});
233 }
234 return out;
235}
236
237const std::set<std::string> kNotPerLayer = {"op", "in", "out", "state", "tensor_index", "sizes", "expr", "weight", "bias",
238 "position_bias", // attention's (IR_SPEC 5.15, revision 1.3)
239 "relative_kv", // attention's (IR_SPEC 5.15, revision 1.4)
240 "freq_factors", // rope's (IR_SPEC 5.14, revision 1.5)
241 "selection_bias", "groups", // moe_ffn's (IR_SPEC 5.16, revision 1.6)
242 "router", "experts", "state_weight", "tied_to", "a", "b", "c", "d",
243 // mla_attention's tensors and split's groups (revision 1.5)
244 "k_b", "v_b", "groups",
245 // recurrence (IR_SPEC 6.10.1: every key structural)
246 "index", "inputs", "weights", "init", "let", "update", "readout"};
247const std::set<std::string> kSlotNotPerLayer = {"name", "kind", "op", "dtype", "tensors", "evict", "kv_rows"};
248const std::map<std::string, size_t> kArity = {
249 {"embedding", 1}, {"pos_embedding", 2}, {"linear", 1}, {"scale", 1}, {"split", 1}, {"rmsnorm", 1},
250 {"layernorm", 1}, {"gelu", 1}, {"silu", 1}, {"sigmoid", 1}, {"relu", 1}, {"add", 2}, {"mul", 2}, {"sub", 2},
251 {"affine", 1}, {"mul_col", 2}, {"rms_row", 1}, {"rope", 3}, {"attention", 3}, {"moe_ffn", 1}, {"append", 2},
252 {"linear_attention", 3}, {"ssm_scan", 1}, {"conv1d_causal", 1}, {"selective_scan", 4}, {"depth_route", 1},
253 {"mla_attention", 4}};
254const std::set<std::string> kAtLeastOne = {"list", "depth_attend", "expr", "recurrence"};
255const std::map<std::string, std::string> kStateful = {{"attention", "kv"},
256 {"mla_attention", "kv"},
257 {"linear_attention", "recurrent"},
258 {"ssm_scan", "recurrent"},
259 {"conv1d_causal", "recurrent"},
260 {"selective_scan", "recurrent"},
261 {"recurrence", "recurrent"}};
262const std::set<std::string> kRecurrent = {"linear_attention", "ssm_scan", "conv1d_causal", "selective_scan",
263 "recurrence"};
264
265std::string op_of(const json& n) { return get(n, "op").is_string() ? get(n, "op").get<std::string>() : ""; }
266
267// (key, name) for every tensor a node names.
268std::vector<std::pair<std::string, std::string>> tensor_names(const json& n) {
269 std::vector<std::pair<std::string, std::string>> out;
270 for (const char* k : {"weight", "bias", "router", "state_weight", "tied_to"})
271 if (get(n, k).is_string()) out.emplace_back(k, get(n, k).get<std::string>());
272 const std::string op = op_of(n);
273 if (op == "mla_attention")
274 for (const char* k : {"k_b", "v_b"})
275 if (get(n, k).is_string()) out.emplace_back(k, get(n, k).get<std::string>());
276 if (op == "ssm_scan" || op == "selective_scan")
277 for (const char* k : {"a", "b", "c", "d"})
278 if (get(n, k).is_string()) out.emplace_back(k, get(n, k).get<std::string>());
279 if (get(n, "experts").is_object())
280 for (const auto& kv : n.at("experts").items())
281 if (kv.value().is_string()) out.emplace_back("experts." + kv.key(), kv.value().get<std::string>());
282 if (op == "recurrence" && get(n, "weights").is_object())
283 for (const auto& kv : n.at("weights").items())
284 if (kv.value().is_string()) out.emplace_back("weights." + kv.key(), kv.value().get<std::string>());
285 return out;
286}
287
288size_t count_i(const std::string& s) {
289 size_t c = 0;
290 for (size_t p = s.find("{i}"); p != std::string::npos; p = s.find("{i}", p + 3)) ++c;
291 return c;
292}
293
294// Rules 4-10 and 14 over every node, and the names and graph inputs (3.5, 3.7, 7.2).
295void node_errors(const json& doc, Errs& errs) {
296 const json& blk = get(doc, "block").is_object() ? doc.at("block") : kObject();
297 const int L = is_int(get(blk, "repeat")) ? blk.at("repeat").get<int>() : 0;
298 for (const auto& [sect, j, np] : sections(doc)) {
299 const json& n = *np;
300 const std::string op = op_of(n);
301 const std::string where = sect + "[" + std::to_string(j) + "] " + show(get(n, "op"));
302 std::vector<json> ins;
303 if (get(n, "in").is_array()) ins.assign(n.at("in").begin(), n.at("in").end());
304 const json& outs = get(n, "out");
305 // Rule 6: per-layer values.
306 for (const auto& kv : n.items()) {
307 if (!is_pl(kv.value())) continue;
308 const json& pl = kv.value().at("per_layer");
309 if (sect != "block")
310 errs.push_back(E("3.8", where + ": per-layer " + q(kv.key()) + " outside the block"));
311 else if (kNotPerLayer.count(kv.key()))
312 errs.push_back(E("3.8", where + ": " + q(kv.key()) + " is structural and cannot be per-layer"));
313 else if (!pl.is_array() || static_cast<int>(pl.size()) != L)
314 errs.push_back(E("3.8", where + ": per-layer " + q(kv.key()) + " has " +
315 (pl.is_array() ? std::to_string(pl.size()) : std::string("no list")) +
316 " entries for block.repeat " + std::to_string(L)));
317 }
318 // Rule 7: arity.
319 if (auto it = kArity.find(op); it != kArity.end() && ins.size() != it->second)
320 errs.push_back(E("10.1 rule 7", where + " reads " + std::to_string(ins.size()) + " value(s), not " +
321 std::to_string(it->second)));
322 if (kAtLeastOne.count(op) && ins.empty()) errs.push_back(E("10.1 rule 7", where + " reads no value"));
323 if (op == "expr" && ins.size() > 23)
324 errs.push_back(E("5.19", where + " reads " + std::to_string(ins.size()) + " values (at most 23)"));
325 if (op == "split" || op == "rope") {
326 if (!outs.is_array())
327 errs.push_back(E("10.1 rule 7", where + " writes a list of names"));
328 else if (op == "rope" && outs.size() != 2)
329 errs.push_back(E("5.14", where + " writes " + std::to_string(outs.size()) + " values, not [q, k]"));
330 } else if (!get(n, "op").is_null() && !outs.is_string()) {
331 errs.push_back(E("10.1 rule 7", where + " writes one value"));
332 }
333 // Rule 9: expr parses.
334 if (op == "expr" && get(n, "expr").is_string()) {
335 try {
336 expr::Expr::parse(n.at("expr").get<std::string>(), std::max<size_t>(1, ins.size()));
337 } catch (const std::exception& e) {
338 errs.push_back(E("5.19", where + ": " + e.what()));
339 }
340 }
341 // Rules 10 and 14: values.
342 if (op == "split" && get(n, "sizes").is_array() && outs.is_array() && outs.size() != n.at("sizes").size())
343 errs.push_back(E("5.5", where + ": " + std::to_string(n.at("sizes").size()) + " sizes for " +
344 std::to_string(outs.size()) + " outputs"));
345 if (op == "rope" && L) {
346 const auto hd = layers(get(n, "head_dim"), L), nr = layers(get(n, "n_rot"), L), fs = layers(get(n, "freq_scale"), L);
347 for (int i = 0; i < L; ++i) {
348 if (nr && hd && !(*nr)[i].is_null() && is_int((*hd)[i]) && is_int((*nr)[i])) {
349 const int64_t a = (*nr)[i].get<int64_t>(), h = (*hd)[i].get<int64_t>();
350 if (!(0 < a && a <= h && a % 2 == 0)) {
351 errs.push_back(E("5.14", where + ": n_rot " + std::to_string(a) + " is not even and in 1 .. head_dim " +
352 std::to_string(h)));
353 break;
354 }
355 }
356 if (fs && !(*fs)[i].is_null() && (*fs)[i].is_number() && !((*fs)[i].get<double>() > 0)) {
357 errs.push_back(E("5.14", where + ": freq_scale " + show((*fs)[i]) + " is not positive"));
358 break;
359 }
360 }
361 // Rule 10 (revision 1.5): attn_factor m > 0, the table's scaling (LongRoPE, YaRN).
362 if (const auto af = layers(get(n, "attn_factor"), L))
363 for (int i = 0; i < L; ++i)
364 if ((*af)[i].is_number() && !((*af)[i].get<double>() > 0)) {
365 errs.push_back(E("5.14 (rule 10)", where + ": attn_factor " + show((*af)[i]) + " is not positive"));
366 break;
367 }
368 }
369 // Rule 41 (revision 1.5): freq_factors is a tensor name or {short, long, original_ctx} with two names and an
370 // integer original_ctx >= 1 (`_` members allowed, no other); the schema states the same form, its failures
371 // cite this rule (schema_rule).
372 if (op == "rope" && n.contains("freq_factors") && !is_pl(n.at("freq_factors"))) {
373 const json& ff = n.at("freq_factors");
374 bool ok = ff.is_string() && !ff.get<std::string>().empty();
375 if (ff.is_array()) { // revision 1.6: the n_rot / 2 values themselves, each positive, in every application
376 ok = !ff.empty();
377 for (const auto& v : ff) ok = ok && v.is_number() && v.get<double>() > 0;
378 const auto hd = layers(get(n, "head_dim"), std::max(L, 1)), nr = layers(get(n, "n_rot"), std::max(L, 1));
379 for (int i = 0; ok && hd && nr && i < std::max(L, 1); ++i) {
380 const json& r = (*nr)[i].is_null() ? (*hd)[i] : (*nr)[i];
381 if (is_int(r) && static_cast<int64_t>(ff.size()) != r.get<int64_t>() / 2) ok = false;
382 }
383 }
384 if (ff.is_object()) {
385 ok = get(ff, "short").is_string() && get(ff, "long").is_string() && is_int(get(ff, "original_ctx")) &&
386 ff.at("original_ctx").get<int64_t>() >= 1;
387 for (const auto& kv : ff.items())
388 if (kv.key() != "short" && kv.key() != "long" && kv.key() != "original_ctx" && kv.key().rfind('_', 0) != 0)
389 ok = false;
390 }
391 if (!ok)
392 errs.push_back(E("5.14 (rule 41)", where + ": freq_factors " + short_dump(ff) +
393 " is neither a tensor name, {short, long, original_ctx} with two "
394 "names and an integer original_ctx >= 1, nor a list of n_rot / 2 "
395 "positive numbers"));
396 }
397 // Rule 10 (revision 1.6): moe_ffn groups {n, top}: n divides n_experts, top in 1 .. n, top * (E / n) >= top_k.
398 if (op == "moe_ffn" && get(n, "groups").is_object() && is_int(get(n.at("groups"), "n")) &&
399 is_int(get(n.at("groups"), "top")) && is_int(get(n, "n_experts")) && is_int(get(n, "top_k"))) {
400 const int64_t G = n.at("groups").at("n").get<int64_t>(), g = n.at("groups").at("top").get<int64_t>();
401 const int64_t nE = n.at("n_experts").get<int64_t>(), k = n.at("top_k").get<int64_t>();
402 if (G < 1 || nE % G != 0 || g < 1 || g > G || g * (nE / G) < k)
403 errs.push_back(E("5.16 (rule 10)", where + ": groups {n " + std::to_string(G) + ", top " + std::to_string(g) +
404 "} do not divide n_experts " + std::to_string(nE) +
405 " into groups keeping at least top_k " + std::to_string(k) + " experts"));
406 }
407 // Rule 43 (revision 1.6): apply only in the block, entries true or false, never beside depth_route.
408 if (n.contains("apply")) {
409 const json& a = n.at("apply");
410 bool route = false;
411 if (get(blk, "nodes").is_array())
412 for (const auto& b : blk.at("nodes")) route = route || op_of(b) == "depth_route";
413 if (sect != "block")
414 errs.push_back(E("3.8 (rule 43)", where + ": apply outside the block"));
415 else if (route)
416 errs.push_back(E("3.8 (rule 43)", where + ": apply in a block with depth_route"));
417 if (is_pl(a) && a.at("per_layer").is_array()) {
418 for (const auto& v : a.at("per_layer"))
419 if (!v.is_boolean()) {
420 errs.push_back(E("3.8 (rule 43)", where + ": apply entry " + show(v) + " is not true or false"));
421 break;
422 }
423 }
424 }
425 // Rule 10 (revision 1.5): softcap c > 0 (Gemma 2's attention soft cap).
426 if (op == "attention" && L)
427 if (const auto sc = layers(get(n, "softcap"), L))
428 for (int i = 0; i < L; ++i)
429 if ((*sc)[i].is_number() && !((*sc)[i].get<double>() > 0)) {
430 errs.push_back(E("5.15 (rule 10)", where + ": softcap " + show((*sc)[i]) + " is not positive"));
431 break;
432 }
433 if (op == "attention" && L) {
434 const auto h = layers(get(n, "heads"), L), kvh = layers(get(n, "kv_heads"), L), w = layers(get(n, "window"), L),
435 s = layers(get(n, "sinks"), L), c = layers(get(n, "causal"), L);
436 for (int i = 0; h && kvh && i < L; ++i)
437 if (is_int((*h)[i]) && is_int((*kvh)[i]) && (*kvh)[i].get<int64_t>() > 0 &&
438 (*h)[i].get<int64_t>() % (*kvh)[i].get<int64_t>()) {
439 errs.push_back(E("5.15", where + ": heads " + show((*h)[i]) + " is not a multiple of kv_heads " + show((*kvh)[i])));
440 break;
441 }
442 for (int i = 0; i < L; ++i)
443 if ((w && is_int((*w)[i]) && (*w)[i].get<int64_t>() < 0) || (s && is_int((*s)[i]) && (*s)[i].get<int64_t>() < 0)) {
444 errs.push_back(E("5.15", where + ": window and sinks must be >= 0"));
445 break;
446 }
447 bool not_true = false;
448 for (int i = 0; c && i < L; ++i) not_true = not_true || !((*c)[i].is_boolean() && (*c)[i].get<bool>());
449 if (not_true) errs.push_back(E("5.15", where + ": causal must be true (Q6)"));
450 }
451 // Rule 40 (revision 1.4): relative_kv and position_bias are each defined alone on a node. The members of
452 // relative_kv (key a name, value a name or null, max_distance an integer >= 0, nothing else) are the schema's.
453 if (op == "attention" && n.contains("relative_kv") && n.contains("position_bias"))
454 errs.push_back(E("5.15 (rule 40)", where + ": relative_kv and position_bias on one node (revision 1.4 defines "
455 "each alone)"));
456 if (op == "linear_attention" && !(get(n, "causal").is_boolean() && n.at("causal").get<bool>()))
457 errs.push_back(E("6.4", where + ": causal must be true"));
458 if (op == "mla_attention" && L) { // revision 1.5 (5.20): causal true (rule 14), window and sinks >= 0 (rule 10)
459 const auto w = layers(get(n, "window"), L), s = layers(get(n, "sinks"), L), c = layers(get(n, "causal"), L);
460 for (int i = 0; i < L; ++i)
461 if ((w && is_int((*w)[i]) && (*w)[i].get<int64_t>() < 0) || (s && is_int((*s)[i]) && (*s)[i].get<int64_t>() < 0)) {
462 errs.push_back(E("5.20 (rule 10)", where + ": window and sinks must be >= 0"));
463 break;
464 }
465 bool not_true = false;
466 for (int i = 0; c && i < L; ++i) not_true = not_true || !((*c)[i].is_boolean() && (*c)[i].get<bool>());
467 if (not_true) errs.push_back(E("5.20 (rule 14)", where + ": causal must be true"));
468 }
469 if (op == "moe_ffn" && is_int(get(n, "top_k")) && is_int(get(n, "n_experts"))) {
470 const int64_t k = n.at("top_k").get<int64_t>(), e = n.at("n_experts").get<int64_t>();
471 if (!(1 <= k && k <= e))
472 errs.push_back(E("5.16", where + ": top_k " + std::to_string(k) + " is not in 1 .. n_experts " + std::to_string(e)));
473 }
474 if (op == "conv1d_causal" && ((is_int(get(n, "channels")) && n.at("channels").get<int64_t>() < 1) ||
475 (is_int(get(n, "kernel")) && n.at("kernel").get<int64_t>() < 2)))
476 errs.push_back(E("6.6", where + ": channels must be >= 1 and kernel >= 2"));
477 // Names: {i} outside the block or twice (3.7).
478 for (const auto& [key, name] : tensor_names(n)) {
479 if (name.find("{i}") != std::string::npos && sect != "block")
480 errs.push_back(E("3.7", where + ": " + key + " " + q(name) + " uses {i} outside the block"));
481 if (count_i(name) > 1) errs.push_back(E("3.7", where + ": " + key + " " + q(name) + " holds {i} more than once"));
482 }
483 // The graph inputs (3.5): tokens for embeddings, pos as rope's third and pos_embedding's second input.
484 for (size_t k = 0; k < ins.size(); ++k) {
485 if (!ins[k].is_string()) continue;
486 const std::string nm = ins[k].get<std::string>();
487 const bool ok = nm == "tokens" ? (op == "embedding" && k == 0)
488 : nm == "pos" ? ((op == "rope" && k == 2) || (op == "pos_embedding" && k == 1))
489 : true;
490 if (!ok) errs.push_back(E("3.5", where + " reads the graph input " + q(nm) + " as its input " + std::to_string(k)));
491 }
492 if ((op == "rope" && ins.size() == 3 && ins[2] != "pos") || (op == "pos_embedding" && ins.size() == 2 && ins[1] != "pos"))
493 errs.push_back(E("3.5", where + ": the position input must be the graph input 'pos'"));
494 if (op == "embedding" && !ins.empty() && ins[0] != "tokens")
495 errs.push_back(E("3.5", where + ": an embedding reads the graph input 'tokens'"));
496 const std::vector<json> out_list = outs.is_array() ? std::vector<json>(outs.begin(), outs.end()) : std::vector<json>{outs};
497 for (const auto& o : out_list)
498 if (o == "tokens" || o == "pos") errs.push_back(E("3.5", where + " writes the graph input " + q(o.get<std::string>())));
499 if (n.contains("tensor_index") && (sect != "block" || !blk.contains("weight_index")))
500 errs.push_back(E("7.2", where + ": tensor_index needs a block with weight_index"));
501 }
502}
503
504// Rule 12 (attention against its kv slot) and rule 13 (a recurrent slot's tensors fit its op).
505void slot_fit(const json& node, const json& slot, int L, Errs& errs) {
506 const std::string op = op_of(node);
507 if (op == "attention") {
508 for (const char* key : {"window", "sinks", "kv_heads", "head_dim"}) {
509 const auto a = layers(get(node, key), L), b = layers(get(slot, key), L);
510 if (!a || !b) continue;
511 for (int i = 0; i < L; ++i)
512 if ((*a)[i] != (*b)[i]) {
513 const bool q7 = std::string(key) == "kv_heads" || std::string(key) == "head_dim";
514 errs.push_back(E("5.15", "attention " + std::string(key) + " " + show((*a)[i]) + " (application " +
515 std::to_string(i) + ") differs from its kv state " + show(get(slot, "name")) +
516 "'s " + show((*b)[i]) + (q7 ? " (Q7)" : "")));
517 break;
518 }
519 }
520 return;
521 }
522 if (op == "mla_attention") { // revision 1.5 (5.20): kv_heads 1, head_dim kv_rank + qk_rope, the node's window and sinks
523 const auto kr = layers(get(node, "kv_rank"), L), qr = layers(get(node, "qk_rope"), L);
524 const auto kvh = layers(get(slot, "kv_heads"), L), hd = layers(get(slot, "head_dim"), L);
525 for (int i = 0; kvh && i < L; ++i)
526 if ((*kvh)[i] != json(1)) {
527 errs.push_back(E("5.20 (rule 12)", "mla_attention reads kv state " + show(get(slot, "name")) + " of kv_heads " +
528 show((*kvh)[i]) + " (application " + std::to_string(i) + "), not 1"));
529 break;
530 }
531 for (int i = 0; kr && qr && hd && i < L; ++i)
532 if (is_int((*kr)[i]) && is_int((*qr)[i]) && (*hd)[i] != json((*kr)[i].get<int64_t>() + (*qr)[i].get<int64_t>())) {
533 errs.push_back(E("5.20 (rule 12)", "mla_attention reads kv state " + show(get(slot, "name")) + " of head_dim " +
534 show((*hd)[i]) + " (application " + std::to_string(i) +
535 "), not kv_rank + qk_rope = " +
536 std::to_string((*kr)[i].get<int64_t>() + (*qr)[i].get<int64_t>())));
537 break;
538 }
539 for (const char* key : {"window", "sinks"}) {
540 const auto a = layers(get(node, key), L), b = layers(get(slot, key), L);
541 if (!a || !b) continue;
542 for (int i = 0; i < L; ++i)
543 if ((*a)[i] != (*b)[i]) {
544 errs.push_back(E("5.20 (rule 12)", "mla_attention " + std::string(key) + " " + show((*a)[i]) + " (application " +
545 std::to_string(i) + ") differs from its kv state " +
546 show(get(slot, "name")) + "'s " + show((*b)[i])));
547 break;
548 }
549 }
550 return;
551 }
552 if (op == "recurrence") { // rules 31-33 (graph_ir_recurrence.h)
553 // A slot whose tensors rule 13 refuses (no name, shape or init 0 / "body") is not compared with the node: the
554 // fault is rule 13's alone.
555 if (get(slot, "tensors").is_array())
556 for (const auto& t : slot.at("tensors"))
557 if (!t.is_object() || !(get(t, "init") == json(0) || get(t, "init") == "body") || !get(t, "shape").is_array())
558 return;
559 try {
560 const std::string why = rec::Recurrence::compile(node).slot_errors(slot);
561 if (!why.empty()) errs.push_back(E("6.10", "(rule 33) " + why));
562 } catch (const std::exception& e) {
563 std::string m = e.what();
564 const std::string tag = "IR_SPEC 6.10 ";
565 errs.push_back(E("6.10", m.rfind(tag, 0) == 0 ? m.substr(tag.size()) : m));
566 }
567 return;
568 }
569 json shapes = json::object();
570 if (get(slot, "tensors").is_array())
571 for (const auto& t : slot.at("tensors"))
572 if (t.is_object()) shapes[show(get(t, "name"))] = get(t, "shape");
573 json want;
574 if (op == "linear_attention") {
575 want = json::object();
576 want["num"] = json::array({get(node, "heads"), get(node, "head_dim")});
577 want["den"] = want["num"];
578 } else if (op == "ssm_scan") {
579 want = json::object();
580 want["s"] = json::array({get(node, "n")});
581 } else if (op == "conv1d_causal") {
582 const json& k = get(node, "kernel");
583 want = json::object();
584 want["x"] = json::array({is_int(k) ? json(k.get<int64_t>() - 1) : k, get(node, "channels")});
585 } else {
586 want = json::object();
587 want["s"] = json::array({get(node, "channels"), get(node, "d_state")});
588 }
589 if (shapes != want)
590 errs.push_back(E("6.3", "recurrent state " + show(get(slot, "name")) + ": tensors " + shapes.dump() + " do not fit " +
591 op + " (want " + want.dump() + ")"));
592}
593
594// Rules 3, 11-13 and 18: the model fields, the slots, the stateful nodes, weight_index.
595void state_errors(const json& doc, Errs& errs) {
596 const json& model = get(doc, "model").is_object() ? doc.at("model") : kObject();
597 const json& blk = get(doc, "block").is_object() ? doc.at("block") : kObject();
598 for (const char* k : {"d", "vocab", "ctx", "n_layers"})
599 if (!is_int(get(model, k)) || model.at(k).get<int64_t>() <= 0)
600 errs.push_back(E("3.3", std::string("model.") + k + " missing or not a positive integer"));
601 if (get(blk, "repeat") != get(model, "n_layers"))
602 errs.push_back(E("3.3", "block.repeat " + show(get(blk, "repeat")) + " != model.n_layers " + show(get(model, "n_layers"))));
603 const int L = is_int(get(blk, "repeat")) ? blk.at("repeat").get<int>() : 0;
604 if (blk.contains("weight_index")) {
605 const json& m = get(blk.at("weight_index"), "per_layer");
606 bool ok = m.is_array() && static_cast<int>(m.size()) == L;
607 for (size_t i = 0; ok && i < m.size(); ++i) ok = is_int(m[i]) && m[i].get<int64_t>() >= 0;
608 if (!ok) {
609 errs.push_back(E("7.1", "block.weight_index must be {per_layer: [block.repeat integers >= 0]}"));
610 } else if (!m.empty()) {
611 std::set<int64_t> used;
612 for (const auto& x : m) used.insert(x.get<int64_t>());
613 const int64_t mx = *used.rbegin();
614 if (static_cast<int64_t>(used.size()) != mx + 1) {
615 std::string s;
616 for (int64_t x : used) s += (s.empty() ? "" : ", ") + std::to_string(x);
617 errs.push_back(E("7.1", "block.weight_index uses stored layers [" + s + "], not every one of 0.." + std::to_string(mx)));
618 }
619 }
620 }
621 std::vector<std::string> order;
622 std::map<std::string, const json*> slots;
623 if (get(blk, "state").is_array())
624 for (const auto& s : blk.at("state")) {
625 if (!s.is_object()) continue;
626 const std::string name = show(get(s, "name"));
627 if (slots.count(name))
628 errs.push_back(E("6.1", "state slot " + name + " declared twice"));
629 else
630 order.push_back(name);
631 slots[name] = &s;
632 for (const auto& kv : s.items()) {
633 if (!is_pl(kv.value())) continue;
634 const json& pl = kv.value().at("per_layer");
635 if (kSlotNotPerLayer.count(kv.key()))
636 errs.push_back(E("3.8", "state slot " + name + ": " + q(kv.key()) + " is structural and cannot be per-layer"));
637 else if (!pl.is_array() || static_cast<int>(pl.size()) != L)
638 errs.push_back(E("3.8", "state slot " + name + ": per-layer " + q(kv.key()) + " needs block.repeat " +
639 std::to_string(L) + " entries"));
640 }
641 const json& kind = get(s, "kind");
642 if (kind == "kv") {
643 for (const char* k : {"heads", "kv_heads", "head_dim", "max_len", "window", "sinks"})
644 if (!(is_int(get(s, k)) || is_pl(get(s, k)))) errs.push_back(E("6.2", "kv state " + name + ": " + k + " missing"));
645 if (s.contains("kv_from")) errs.push_back(E("7.6", "kv state " + name + ": kv_from is reserved and not implemented"));
646 } else if (kind == "recurrent") {
647 const json& ts = get(s, "tensors");
648 if (!get(s, "op").is_string() || !kRecurrent.count(s.at("op").get<std::string>()) || get(s, "dtype") != "f32" ||
649 !truthy(ts))
650 errs.push_back(E("6.3", "recurrent state " + name + ": op (a recurrent op), dtype f32 and tensors required"));
651 if (ts.is_array())
652 for (const auto& t : ts) {
653 bool ok = t.is_object() && truthy(get(t, "name")) && truthy(get(t, "shape")) && get(t, "shape").is_array() &&
654 (get(t, "init") == json(0) || (get(t, "init") == "body" && get(s, "op") == "recurrence"));
655 if (ok)
656 for (const auto& x : t.at("shape")) ok = ok && is_int(x) && x.get<int64_t>() > 0;
657 if (!ok)
658 errs.push_back(E("6.3", "recurrent state " + name + ": tensor " + t.dump() + " needs name, shape and init 0"));
659 }
660 } else {
661 errs.push_back(E("6.1", "state slot " + name + ": unknown kind " + show(kind)));
662 }
663 }
664 std::set<std::string> used;
665 std::vector<std::string> reader_order;
666 std::map<std::string, int> readers;
667 for (const auto& [sect, j, np] : sections(doc)) {
668 const json& node = *np;
669 const std::string op = op_of(node);
670 const auto want = kStateful.find(op);
671 if (want == kStateful.end()) {
672 if (node.contains("state"))
673 errs.push_back(E("6.1", sect + "[" + std::to_string(j) + "] " + show(get(node, "op")) + " names state " +
674 q(show(node.at("state"))) + " but has no state"));
675 continue;
676 }
677 if (sect != "block") {
678 errs.push_back(E("6.1", sect + ": " + op + " outside the block has no state"));
679 continue;
680 }
681 const std::string st = show(get(node, "state"));
682 const auto it = get(node, "state").is_string() ? slots.find(st) : slots.end();
683 if (it == slots.end() || get(*it->second, "kind") != want->second ||
684 (want->second == "recurrent" && get(*it->second, "op") != op)) {
685 errs.push_back(E("6.1", sect + ": " + op + " reads state " + q(st) + ", not a declared " + want->second + " slot" +
686 (want->second == "recurrent" ? " of op " + op : std::string())));
687 continue;
688 }
689 used.insert(st);
690 if (op == "attention" || op == "mla_attention" || op == "recurrence") {
691 if (!readers.count(st)) reader_order.push_back(st);
692 ++readers[st];
693 }
694 slot_fit(node, *it->second, L, errs);
695 }
696 for (const auto& name : reader_order)
697 if (readers[name] > 1) {
698 if (get(*slots.at(name), "kind") == "kv")
699 errs.push_back(E("6.1", "kv state " + name + " is read by " + std::to_string(readers[name]) +
700 " attention nodes (exactly one is defined)"));
701 else
702 errs.push_back(E("6.10", "(rule 33) the slot " + name + " of a recurrence is read by " +
703 std::to_string(readers[name]) + " nodes (the node alone reads it)"));
704 }
705 for (const auto& name : order)
706 if (!used.count(name)) errs.push_back(E("6.1", "state slot " + name + " is read by no node"));
707}
708
709// Rules 20, 21 and 23: depth_route first and once, its steps, the router name, kv_rows active.
710void depth_route_errors(const json& doc, Errs& errs) {
711 const json& blk = get(doc, "block").is_object() ? doc.at("block") : kObject();
712 const json& nodes = get(blk, "nodes").is_array() ? blk.at("nodes") : json::array();
713 for (const auto& [sect, j, np] : sections(doc))
714 if (op_of(*np) == "depth_route" && sect != "block") errs.push_back(E("7.4", "depth_route outside the block (" + sect + ")"));
715 std::vector<size_t> at;
716 for (size_t j = 0; j < nodes.size(); ++j)
717 if (nodes[j].is_object() && op_of(nodes[j]) == "depth_route") at.push_back(j);
718 std::vector<const json*> kvs;
719 if (get(blk, "state").is_array())
720 for (const auto& s : blk.at("state"))
721 if (s.is_object() && get(s, "kind") == "kv") kvs.push_back(&s);
722 for (const json* s : kvs)
723 if (s->contains("kv_rows") && s->at("kv_rows") != "active")
724 errs.push_back(E("7.5", "kv state " + show(get(*s, "name")) + ": kv_rows " + q(show(s->at("kv_rows"))) +
725 " (only \"active\" is defined)"));
726 if (at.empty()) return;
727 if (at != std::vector<size_t>{0}) {
728 errs.push_back(E("7.4", "depth_route must be the block's first node, once"));
729 return;
730 }
731 const json& r = nodes[0];
732 const json& Rj = get(r, "n_rec");
733 if (!is_int(Rj) || Rj.get<int64_t>() < 1) {
734 errs.push_back(E("7.4", "depth_route: n_rec must be an integer >= 1"));
735 return;
736 }
737 const int64_t R = Rj.get<int64_t>();
738 if (get(r, "route") != "token_choice")
739 errs.push_back(E("7.7", "depth_route: route " + q(show(get(r, "route"))) + " (only token_choice, causal, is defined)"));
740 if (get(r, "gate") != "score" && get(r, "gate") != "none")
741 errs.push_back(E("7.4", "depth_route: gate " + q(show(get(r, "gate"))) + " is not score or none"));
742 const json& w = get(r, "weight");
743 if (!w.is_string() || w.get<std::string>().find('{') != std::string::npos)
744 errs.push_back(E("7.7", "depth_route: weight must be the router's full tensor name (constraint 4: no substitution)"));
745 if (!get(r, "out").is_string()) errs.push_back(E("7.4", "depth_route: out must name the depth trace"));
746 const json& st = get(get(r, "step"), "per_layer");
747 const json& rep = get(blk, "repeat");
748 bool ints = st.is_array() && is_int(rep) && static_cast<int64_t>(st.size()) == rep.get<int64_t>();
749 for (size_t i = 0; ints && i < st.size(); ++i) ints = is_int(st[i]);
750 if (!ints) {
751 errs.push_back(E("7.4", "depth_route: step must be {per_layer: [block.repeat integers]}"));
752 return;
753 }
754 std::vector<int64_t> s;
755 for (const auto& x : st) s.push_back(x.get<int64_t>());
756 std::vector<std::pair<int64_t, int64_t>> runs;
757 for (size_t i = 0; i < s.size(); ++i) {
758 if (s[i] == -1) continue;
759 if (i == 0 || s[i - 1] != s[i]) runs.emplace_back(s[i], 0);
760 ++runs.back().second;
761 }
762 std::vector<size_t> inside;
763 for (size_t i = 0; i < s.size(); ++i)
764 if (s[i] != -1) inside.push_back(i);
765 const bool contiguous = !inside.empty() && inside.back() - inside.front() + 1 == inside.size();
766 bool range_bad = false, keys_bad = static_cast<int64_t>(runs.size()) != R;
767 for (int64_t x : s) range_bad = range_bad || x < -1 || x >= R;
768 for (size_t k = 0; !keys_bad && k < runs.size(); ++k) keys_bad = runs[k].first != static_cast<int64_t>(k);
769 if (range_bad || keys_bad || !contiguous) {
770 errs.push_back(E("7.4", "depth_route: steps " + st.dump() + " are not -1 or the consecutive steps 0 .. " +
771 std::to_string(R - 1) + " in order"));
772 } else {
773 std::set<int64_t> lens;
774 for (const auto& run : runs) lens.insert(run.second);
775 if (lens.size() != 1) errs.push_back(E("7.4", "depth_route: steps " + st.dump() + " differ in length"));
776 }
777 bool all_active = !kvs.empty();
778 for (const json* k : kvs) all_active = all_active && get(*k, "kv_rows") == "active";
779 if (!all_active) errs.push_back(E("7.5", "depth_route needs kv_rows \"active\" on every kv slot"));
780 for (const auto& n : nodes)
781 if (n.is_object() && op_of(n) == "attention" && get(n, "mask") == "turn_state") {
782 errs.push_back(E("9.2", "depth_route with mask turn_state is not defined (llama_state)"));
783 break;
784 }
785 for (const auto& [sect, j, np] : sections(doc)) {
786 const std::string op = op_of(*np);
787 if (op == "list" || op == "append" || op == "depth_attend") {
788 errs.push_back(E("7.4", "depth_route with list values (list, append, depth_attend) is not defined"));
789 break;
790 }
791 }
792 const json& out = get(r, "out");
793 if (out.is_string())
794 for (const auto& [sect, j, np] : sections(doc)) {
795 const json& ins = get(*np, "in");
796 if (ins.is_array() && std::find(ins.begin(), ins.end(), out) != ins.end())
797 errs.push_back(E("7.4", sect + "[" + std::to_string(j) + "] " + show(get(*np, "op")) + " reads depth_route's out " +
798 q(out.get<std::string>()) + " (the depth report)"));
799 }
800}
801
802// Rules 15-17: the turn_state mask, kv evict and the state_weight's placement.
803void state_token_doc_errors(const json& doc, Errs& errs) {
804 const json& blk = get(doc, "block").is_object() ? doc.at("block") : kObject();
805 const int L = is_int(get(blk, "repeat")) ? blk.at("repeat").get<int>() : 0;
806 size_t sws = 0;
807 for (const auto& [sect, j, np] : sections(doc)) {
808 const json& n = *np;
809 if (op_of(n) != "embedding" || !n.contains("state_weight")) continue;
810 ++sws;
811 if (sect != "embed")
812 errs.push_back(E("6.8.1", sect + "[" + std::to_string(j) + "]: an embedding state_weight outside the embed section (Q8)"));
813 if (n.at("state_weight").is_string() && n.at("state_weight").get<std::string>().find('{') != std::string::npos)
814 errs.push_back(E("6.8.1", "state_weight names its tensor in full (Q8)"));
815 }
816 if (sws > 1) errs.push_back(E("6.8.1", "more than one embedding with a state_weight (Q8)"));
817 std::map<std::string, const json*> slots;
818 if (get(blk, "state").is_array())
819 for (const auto& s : blk.at("state"))
820 if (s.is_object()) slots[show(get(s, "name"))] = &s;
821 std::map<std::string, std::set<std::string>> masks;
822 auto any_of = [](const std::optional<std::vector<json>>& v) {
823 if (!v) return false;
824 for (const auto& x : *v)
825 if (truthy(x)) return true;
826 return false;
827 };
828 if (get(blk, "nodes").is_array())
829 for (const auto& n : blk.at("nodes")) {
830 if (!n.is_object() || op_of(n) != "attention") continue;
831 const json m = n.contains("mask") ? n.at("mask") : json("causal");
832 if (m != "causal" && m != "turn_state")
833 errs.push_back(E("6.8.2", "attention mask " + q(show(m)) + " (causal or turn_state)"));
834 if (m == "turn_state") {
835 const json& slot = slots.count(show(get(n, "state"))) ? *slots.at(show(get(n, "state"))) : kObject();
836 auto field = [&](const json& o, const char* k) { return layers(o.contains(k) ? o.at(k) : json(0), L); };
837 if (any_of(field(n, "window")) || any_of(field(n, "sinks")) || any_of(field(slot, "window")) ||
838 any_of(field(slot, "sinks")) || sws == 0)
839 errs.push_back(E("6.8.2", "attention mask turn_state needs window 0, sinks 0 (every application, and its kv slot) "
840 "and an embedding state_weight"));
841 }
842 masks[show(get(n, "state"))].insert(show(m));
843 }
844 if (get(blk, "state").is_array())
845 for (const auto& st : blk.at("state")) {
846 if (!st.is_object() || !st.contains("evict")) continue;
847 const std::string name = show(get(st, "name"));
848 if (st.at("evict") != "turn_state")
849 errs.push_back(E("6.8.3", "kv state " + name + ": evict " + q(show(st.at("evict"))) + " (only turn_state is defined)"));
850 else if (masks[name] != std::set<std::string>{"turn_state"})
851 errs.push_back(E("6.8.3", "kv state " + name + ": evict turn_state needs every attention node reading it to mask "
852 "turn_state"));
853 }
854}
855
856// Rule 36 (revision 1.2): every value a node reads is written before it, in the order of section 3.5 (embed, the
857// block as application 0, head; tokens and pos excepted); the embed section writes x and the head logits; under
858// depth_route the block's nodes after it read only x, pos and values written before them in the same
859// application, and the head only x and the embed section's values (section 7.4(b)).
860void read_errors(const json& doc, Errs& errs) {
861 auto names = [](const json& v) {
862 std::vector<std::string> out;
863 if (v.is_string()) out.push_back(v.get<std::string>());
864 if (v.is_array())
865 for (const auto& x : v)
866 if (x.is_string()) out.push_back(x.get<std::string>());
867 return out;
868 };
869 auto nodes_of = [](const json& doc, const char* sect) -> const json& {
870 const json& n = get(get(doc, sect), "nodes");
871 return n.is_array() ? n : kNull();
872 };
873 std::set<std::string> written, embed_written;
874 auto check = [&](const std::string& sect, size_t j, const json& n, const std::set<std::string>& scope,
875 bool inputs_free, const char* section, const std::string& why) {
876 for (const auto& v : names(get(n, "in")))
877 if (!scope.count(v) && !(inputs_free && (v == "tokens" || v == "pos")))
878 errs.push_back(E(std::string(section) + " (rule 36)", sect + "[" + std::to_string(j) + "] " +
879 show(get(n, "op")) + " reads " + q(v) + ", " + why));
880 };
881 for (const char* sect : {"embed", "block", "head"})
882 if (!get(get(doc, sect), "nodes").is_array()) return; // rule 1 names the section
883 const json& embed = nodes_of(doc, "embed");
884 for (size_t j = 0; embed.is_array() && j < embed.size(); ++j) {
885 check("embed", j, embed[j], written, true, "3.5", "which no node before it writes");
886 for (const auto& o : names(get(embed[j], "out"))) written.insert(o);
887 }
888 if (!written.count("x")) errs.push_back(E("3.5 (rule 36)", "the embed section writes no 'x'"));
890 const json& block = nodes_of(doc, "block");
891 size_t route = block.is_array() ? block.size() : 0;
892 if (block.is_array() && !block.empty() && op_of(block[0]) == "depth_route") route = 0; // else rule 20
893 std::set<std::string> step = {"x", "pos"}; // what a recursion step sees (7.4(b))
894 // apply (revision 1.6, 3.8): without depth_route, every application is checked with the nodes it runs, a value
895 // written in an earlier application staying readable.
896 bool any_apply = false;
897 for (size_t j = 0; block.is_array() && j < block.size(); ++j) any_apply = any_apply || block[j].contains("apply");
898 const int L = is_int(get(get(doc, "block"), "repeat")) ? get(doc, "block").at("repeat").get<int>() : 0;
899 if (any_apply && route == block.size() && L > 0) {
900 for (int i = 0; i < L; ++i)
901 for (size_t j = 0; j < block.size(); ++j) {
902 const json& a = get(block[j], "apply");
903 const json& v = is_pl(a) && a.at("per_layer").is_array() && i < static_cast<int>(a.at("per_layer").size())
904 ? a.at("per_layer")[i]
905 : a;
906 if (v.is_boolean() && !v.get<bool>()) continue;
907 check("block", j, block[j], written, true, "3.5",
908 "which no node before it writes (application " + std::to_string(i) + ")");
909 for (const auto& o : names(get(block[j], "out"))) written.insert(o);
910 }
911 }
912 for (size_t j = 0; block.is_array() && j < block.size() && !(any_apply && route == block.size() && L > 0); ++j) {
913 if (j <= route) {
914 check("block", j, block[j], written, true, "3.5", "which no node before it writes (application 0)");
915 if (j == route) // depth_route's out is written before the step's nodes (reading it is rule 17a's)
916 for (const auto& o : names(get(block[j], "out"))) step.insert(o);
917 } else {
918 check("block", j, block[j], step, false, "7.4",
919 "outside what a depth_route step sees (x, pos and the values written before it in the application)");
920 for (const auto& o : names(get(block[j], "out"))) step.insert(o);
921 }
922 for (const auto& o : names(get(block[j], "out"))) written.insert(o);
923 }
924 const bool mor = block.is_array() && route < block.size();
925 std::set<std::string> head_scope = mor ? embed_written : written;
926 if (mor) head_scope.insert("x");
927 const json& head = nodes_of(doc, "head");
928 bool logits = false;
929 for (size_t j = 0; head.is_array() && j < head.size(); ++j) {
930 check("head", j, head[j], head_scope, true, mor ? "7.4" : "3.5",
931 mor ? "a block value other than x (under depth_route the head reads x and the embed section's values)"
932 : "which no node before it writes");
933 for (const auto& o : names(get(head[j], "out"))) {
934 head_scope.insert(o);
935 logits = logits || o == "logits";
936 }
937 }
938 if (!logits) errs.push_back(E("3.5 (rule 36)", "the head writes no 'logits'"));
939}
940
941// ---------------------------------------------------------------- the container rules (24, 26-28)
942
943std::optional<double> meta_num(const GGUFData& g, const std::string& key) {
944 auto it = g.metadata.find(key);
945 if (it == g.metadata.end()) return std::nullopt;
946 return std::visit(
947 [](const auto& v) -> std::optional<double> {
948 using T = std::decay_t<decltype(v)>;
949 if constexpr (std::is_arithmetic_v<T> && !std::is_same_v<T, bool>)
950 return static_cast<double>(v);
951 else
952 return std::nullopt;
953 },
954 it->second);
955}
956
957std::optional<std::string> meta_str(const GGUFData& g, const std::string& key) {
958 auto it = g.metadata.find(key);
959 if (it == g.metadata.end()) return std::nullopt;
960 if (const auto* s = std::get_if<std::string>(&it->second)) return *s;
961 return std::nullopt;
962}
963
964std::string num(double v) {
965 if (v == static_cast<double>(static_cast<int64_t>(v))) return std::to_string(static_cast<int64_t>(v));
966 return json(v).dump();
967}
968
969std::string ne_str(const std::vector<uint64_t>& s) {
970 std::string out;
971 for (uint64_t x : s) out += (out.empty() ? "" : ", ") + std::to_string(x);
972 return "[" + out + "]";
973}
974
975void container_errors(const json& doc, const GGUFData& g, Errs& errs) {
976 // Rule 24: the file's paperkiln.ir_version, a uint32 equal to its embedded document's ir_version.
977 if (const auto emb = meta_str(g, "paperkiln.ir")) {
978 json e;
979 // Rule 35 governs the document read (the Interpreter's parse_ir_document): a file whose own document holds a
980 // duplicate name loads with another document in its place, as the reference validator accepts it.
981 try {
982 e = json::parse(*emb);
983 } catch (const std::exception&) {
984 errs.push_back(E("3.1", "paperkiln.ir is not a JSON document"));
985 }
986 const auto it = g.metadata.find("paperkiln.ir_version");
987 const json& v = get(e, "ir_version");
988 if (it == g.metadata.end() || !std::holds_alternative<uint32_t>(it->second) || !is_int(v) ||
989 static_cast<int64_t>(std::get<uint32_t>(it->second)) != v.get<int64_t>())
990 errs.push_back(E("3.1", "paperkiln.ir_version " +
991 (it == g.metadata.end() ? std::string("None")
993 ? std::to_string(std::get<uint32_t>(it->second))
994 : std::string("(not a uint32)")) +
995 " does not match the document's ir_version " + show(v)));
996 }
997 // Rule 30: paperkiln.ir_sha256, when present, is a string equal to the digest of the document embedded in
998 // the file (section 3.10), whatever document is run in its place (EMBER_IR, the adapter's "ir").
999 if (const auto key = g.metadata.find("paperkiln.ir_sha256"); key != g.metadata.end()) {
1000 const auto* want = std::get_if<std::string>(&key->second);
1001 const auto emb = meta_str(g, "paperkiln.ir");
1002 if (!want) {
1003 errs.push_back(E("3.10", "paperkiln.ir_sha256 is not a string"));
1004 } else if (!emb) {
1005 errs.push_back(E("3.10", "paperkiln.ir_sha256 is present but the file embeds no paperkiln.ir"));
1006 } else {
1007 const DigestResult d = ir_digest(*emb);
1008 if (!d.ok)
1009 errs.push_back(E("3.10", "paperkiln.ir_sha256 is present but the embedded document has no digest (" + d.refused + ")"));
1010 else if (d.sha256 != *want)
1011 errs.push_back(E("3.10", "paperkiln.ir_sha256 " + *want + " is not the embedded document's digest " + d.sha256));
1012 }
1013 }
1014 auto ne =[&](const std::string& name) -> const std::vector<uint64_t>* {
1015 auto it = g.tensor_infos_map.find(name);
1016 return it == g.tensor_infos_map.end() ? nullptr : &it->second.shape;
1017 };
1018 const std::string arch = meta_str(g, "general.architecture").value_or("");
1019 const json& model = get(doc, "model");
1020 const json& blk = get(doc, "block");
1021 // Rule 26: the depth_route router's ne is {d, n_rec}.
1022 const json& nodes = get(blk, "nodes");
1023 if (nodes.is_array() && !nodes.empty() && op_of(nodes[0]) == "depth_route" && get(nodes[0], "weight").is_string()) {
1024 const json& r = nodes[0];
1025 if (const auto* got = ne(r.at("weight").get<std::string>())) {
1026 const bool ok = got->size() == 2 && is_int(get(model, "d")) && is_int(get(r, "n_rec")) &&
1027 static_cast<int64_t>((*got)[0]) == model.at("d").get<int64_t>() &&
1028 static_cast<int64_t>((*got)[1]) == r.at("n_rec").get<int64_t>();
1029 if (!ok)
1030 errs.push_back(E("7.4", "depth_route: " + r.at("weight").get<std::string>() + " has ne " + ne_str(*got) +
1031 ", not [d, n_rec] = [" + show(get(model, "d")) + ", " + show(get(r, "n_rec")) + "]"));
1032 }
1033 }
1034 // Rule 27: under weight_index, architecture paperkiln and <arch>.block_count = the stored layers.
1035 const json& wmap = get(get(blk, "weight_index"), "per_layer");
1036 bool wints = wmap.is_array() && !wmap.empty();
1037 for (size_t i = 0; wints && i < wmap.size(); ++i) wints = is_int(wmap[i]);
1038 if (wints) {
1039 int64_t mx = 0;
1040 for (const auto& x : wmap) mx = std::max(mx, x.get<int64_t>());
1041 if (arch != "paperkiln") errs.push_back(E("7.1", "block.weight_index needs architecture paperkiln, not " + (arch.empty() ? std::string("None") : arch)));
1042 const auto bc = meta_num(g, arch + ".block_count");
1043 if (!bc || *bc != static_cast<double>(mx + 1))
1044 errs.push_back(E("7.1", arch + ".block_count " + (bc ? num(*bc) : std::string("None")) + " != the " +
1045 std::to_string(mx + 1) + " stored layers weight_index uses"));
1046 }
1047 // Rule 28: state tokens.
1048 const auto k = meta_num(g, "paperkiln.state_tokens.k");
1049 std::vector<std::string> sws;
1050 const json& enodes = get(get(doc, "embed"), "nodes");
1051 if (enodes.is_array())
1052 for (const auto& n : enodes)
1053 if (op_of(n) == "embedding" && n.contains("state_weight")) sws.push_back(show(n.at("state_weight")));
1054 if ((k && *k > 0) != !sws.empty())
1055 errs.push_back(E("6.8.1", "paperkiln.state_tokens.k " + (k ? num(*k) : std::string("None")) +
1056 " without an embedding state_weight, or the reverse"));
1057 for (const auto& sw : sws) {
1058 const auto* got = ne(sw);
1059 if (!got) {
1060 errs.push_back(E("6.8.1", "embedding state_weight " + sw + " is not a tensor of the file"));
1061 } else if (!(got->size() == 2 && is_int(get(model, "d")) && k &&
1062 static_cast<int64_t>((*got)[0]) == model.at("d").get<int64_t>() &&
1063 static_cast<double>((*got)[1]) == *k)) {
1064 errs.push_back(E("6.8.1", "embedding state_weight " + sw + " has ne " + ne_str(*got) +
1065 ", not [d, paperkiln.state_tokens.k] = [" + show(get(model, "d")) + ", " +
1066 (k ? num(*k) : std::string("None")) + "]"));
1067 }
1068 }
1069 if (!sws.empty()) {
1070 if (arch != "paperkiln") errs.push_back(E("8.2", "state tokens need architecture paperkiln, not " + (arch.empty() ? std::string("None") : arch)));
1071 static const std::map<std::string, std::string> kMarkers = {
1072 {"eot", "<|endoftext|>"}, {"user", "<|user|>"}, {"assistant", "<|assistant|>"}}; // paperkiln/1
1073 const json& sp = get(get(doc, "tokenizer"), "specials");
1074 for (const char* role : {"eot", "user", "assistant"}) {
1075 const json& id = get(sp, role);
1076 if (!is_int(id)) {
1077 errs.push_back(E("8.2", std::string("state tokens: the tokenizer names no ") + role + " id (the template needs it)"));
1078 continue;
1079 }
1080 const int64_t i = id.get<int64_t>();
1081 const auto& toks = g.tokenizer_tokens;
1082 if (!toks.empty() && !(0 <= i && i < static_cast<int64_t>(toks.size()) && toks[i] == kMarkers.at(role)))
1083 errs.push_back(E("8.2", std::string("state tokens: specials.") + role + " = " + std::to_string(i) + " is the token " +
1084 (0 <= i && i < static_cast<int64_t>(toks.size()) ? q(toks[i]) : std::string("None")) +
1085 ", not " + q(kMarkers.at(role))));
1086 }
1087 if (const auto tmpl = meta_str(g, "paperkiln.chat_template")) {
1088 std::string name = "None";
1089 try {
1090 const json rec = json::parse(*tmpl);
1091 const std::string nm = show(get(rec, "name"));
1092 name = nm.find('/') == std::string::npos ? nm + "/" + show(get(rec, "version")) : nm;
1093 } catch (const std::exception&) {
1094 }
1095 if (name != "paperkiln/1") errs.push_back(E("8.2", "state tokens are defined over paperkiln/1 only, not " + name));
1096 }
1097 }
1098 // Rule 39 (container): each application's position_bias table is ne {heads, B}, B greater than every bucket.
1099 const int L = is_int(get(blk, "repeat")) ? blk.at("repeat").get<int>() : 0;
1100 const json& wmi = get(get(blk, "weight_index"), "per_layer");
1101 for (const auto& [sect, j, np] : sections(doc)) {
1102 const json& n = *np;
1103 const json& pb = get(n, "position_bias");
1104 if (op_of(n) != "attention" || !pb.is_object() || !get(pb, "weight").is_string() || !get(pb, "buckets").is_array())
1105 continue;
1106 int64_t top = -1;
1107 for (const auto& b : pb.at("buckets"))
1108 if (is_int(b)) top = std::max(top, b.get<int64_t>());
1109 const auto heads = layers(get(n, "heads"), std::max(L, 1));
1110 for (int i = 0; i < (sect == "block" ? L : 1); ++i) {
1111 std::string w = pb.at("weight").get<std::string>();
1112 const size_t at = w.find("{i}");
1113 if (at != std::string::npos) {
1114 const bool mapped = wmi.is_array() && i < static_cast<int>(wmi.size()) && is_int(wmi[i]) && !n.contains("tensor_index");
1115 w.replace(at, 3, std::to_string(mapped ? wmi[i].get<int64_t>() : i));
1116 }
1117 const auto* got = ne(w);
1118 if (!got || !heads || !is_int((*heads)[i])) continue; // rule 25 names a missing tensor
1119 const int64_t H = (*heads)[i].get<int64_t>();
1120 if (!(got->size() == 2 && static_cast<int64_t>((*got)[0]) == H && static_cast<int64_t>((*got)[1]) > top)) {
1121 errs.push_back(E("5.15 (rule 39)", sect + "[" + std::to_string(j) + "] attention position_bias " + q(w) +
1122 (sect == "block" ? " (application " + std::to_string(i) + ")" : "") +
1123 " has ne " + ne_str(*got) + ", not {heads, B} = {" + std::to_string(H) +
1124 ", B} with B greater than every bucket (" + std::to_string(top) + ")"));
1125 break;
1126 }
1127 }
1128 }
1129 // Rule 40 (container, revision 1.4): each application's relative_kv key and value tensors are ne
1130 // {head_dim or heads * head_dim, max_distance + 1}.
1131 for (const auto& [sect, j, np] : sections(doc)) {
1132 const json& n = *np;
1133 const json& rk = get(n, "relative_kv");
1134 if (op_of(n) != "attention" || !rk.is_object() || !is_int(get(rk, "max_distance"))) continue;
1135 const int64_t K = rk.at("max_distance").get<int64_t>();
1136 if (K < 0) continue; // the schema's (rule 40)
1137 const auto heads = layers(get(n, "heads"), std::max(L, 1)), hdim = layers(get(n, "head_dim"), std::max(L, 1));
1138 for (const char* member : {"key", "value"}) {
1139 if (!get(rk, member).is_string()) continue;
1140 for (int i = 0; i < (sect == "block" ? L : 1); ++i) {
1141 std::string w = rk.at(member).get<std::string>();
1142 const size_t at = w.find("{i}");
1143 if (at != std::string::npos) {
1144 const bool mapped = wmi.is_array() && i < static_cast<int>(wmi.size()) && is_int(wmi[i]) && !n.contains("tensor_index");
1145 w.replace(at, 3, std::to_string(mapped ? wmi[i].get<int64_t>() : i));
1146 }
1147 const auto* got = ne(w);
1148 if (!got || !heads || !hdim || !is_int((*heads)[i]) || !is_int((*hdim)[i])) continue; // rule 25 names a missing tensor
1149 const int64_t H = (*heads)[i].get<int64_t>(), hd = (*hdim)[i].get<int64_t>();
1150 const bool ok = got->size() == 2 && static_cast<int64_t>((*got)[1]) == K + 1 &&
1151 (static_cast<int64_t>((*got)[0]) == hd || static_cast<int64_t>((*got)[0]) == H * hd);
1152 if (!ok) {
1153 errs.push_back(E("5.15 (rule 40)", sect + "[" + std::to_string(j) + "] attention relative_kv " + member + " " +
1154 q(w) + (sect == "block" ? " (application " + std::to_string(i) + ")" : "") +
1155 " has ne " + ne_str(*got) + ", not {head_dim or heads * head_dim, " +
1156 "max_distance + 1} = {" + std::to_string(hd) + " or " +
1157 std::to_string(H * hd) + ", " + std::to_string(K + 1) + "}"));
1158 break;
1159 }
1160 }
1161 }
1162 }
1163 // Rule 41 (container, revision 1.5): each frequency-factor tensor a rope application reads is ne {n_rot / 2}.
1164 for (const auto& [sect, j, np] : sections(doc)) {
1165 const json& n = *np;
1166 const json& ff = get(n, "freq_factors");
1167 if (op_of(n) != "rope" || is_pl(ff)) continue;
1168 std::vector<std::string> names;
1169 if (ff.is_string()) names.push_back(ff.get<std::string>());
1170 for (const char* member : {"short", "long"})
1171 if (ff.is_object() && get(ff, member).is_string()) names.push_back(ff.at(member).get<std::string>());
1172 const auto hdim = layers(get(n, "head_dim"), std::max(L, 1)), nrot = layers(get(n, "n_rot"), std::max(L, 1));
1173 bool reported = false;
1174 for (const auto& pattern : names)
1175 for (int i = 0; i < (sect == "block" ? L : 1) && !reported; ++i) {
1176 std::string w = pattern;
1177 const size_t at = w.find("{i}");
1178 if (at != std::string::npos) {
1179 const bool mapped = wmi.is_array() && i < static_cast<int>(wmi.size()) && is_int(wmi[i]) && !n.contains("tensor_index");
1180 w.replace(at, 3, std::to_string(mapped ? wmi[i].get<int64_t>() : i));
1181 }
1182 const auto* got = ne(w);
1183 if (!got || !hdim) continue; // a missing tensor is rule 25's
1184 const json& r = nrot && !(*nrot)[i].is_null() ? (*nrot)[i] : (*hdim)[i];
1185 if (!is_int(r)) continue;
1186 const int64_t half = r.get<int64_t>() / 2;
1187 if (!(got->size() == 1 && static_cast<int64_t>((*got)[0]) == half)) {
1188 errs.push_back(E("5.14 (rule 41)", sect + "[" + std::to_string(j) + "] rope freq_factors " + q(w) +
1189 (sect == "block" ? " (application " + std::to_string(i) + ")" : "") +
1190 " has ne " + ne_str(*got) + ", not {n_rot / 2} = {" +
1191 std::to_string(half) + "}"));
1192 reported = true;
1193 }
1194 }
1195 }
1196 // Rule 44 (container, revision 1.6): a moe_ffn's selection_bias is ne {n_experts} in each application that runs it.
1197 for (const auto& [sect, j, np] : sections(doc)) {
1198 const json& n = *np;
1199 if (op_of(n) != "moe_ffn" || !get(n, "selection_bias").is_string()) continue;
1200 const auto nE = layers(get(n, "n_experts"), std::max(L, 1));
1201 const auto ap = layers(get(n, "apply"), std::max(L, 1));
1202 for (int i = 0; i < (sect == "block" ? L : 1); ++i) {
1203 if (ap && (*ap)[i].is_boolean() && !(*ap)[i].get<bool>()) continue; // not run there (3.8)
1204 std::string w = n.at("selection_bias").get<std::string>();
1205 const size_t at = w.find("{i}");
1206 if (at != std::string::npos) {
1207 const bool mapped = wmi.is_array() && i < static_cast<int>(wmi.size()) && is_int(wmi[i]) && !n.contains("tensor_index");
1208 w.replace(at, 3, std::to_string(mapped ? wmi[i].get<int64_t>() : i));
1209 }
1210 const auto* got = ne(w);
1211 if (!got || !nE || !is_int((*nE)[i])) continue; // a missing tensor is rule 25's
1212 if (!(got->size() == 1 && static_cast<int64_t>((*got)[0]) == (*nE)[i].get<int64_t>())) {
1213 errs.push_back(E("5.16 (rule 44)", sect + "[" + std::to_string(j) + "] moe_ffn selection_bias " + q(w) +
1214 (sect == "block" ? " (application " + std::to_string(i) + ")" : "") +
1215 " has ne " + ne_str(*got) + ", not {n_experts} = {" + show((*nE)[i]) + "}"));
1216 break;
1217 }
1218 }
1219 }
1220}
1221
1222// Rule 42 (container, revision 1.5): each application's mla_attention k_b is ne {qk_nope, kv_rank, heads} and its v_b
1223// ne {kv_rank, v_head_dim, heads} (a trailing ne of 1 may be left out, as GGUF stores a one-head stack).
1224void mla_container_errors(const json& doc, const GGUFData& g, Errs& errs) {
1225 const json& blk = get(doc, "block");
1226 const int L = is_int(get(blk, "repeat")) ? blk.at("repeat").get<int>() : 0;
1227 const json& wmi = get(get(blk, "weight_index"), "per_layer");
1228 for (const auto& [sect, j, np] : sections(doc)) {
1229 const json& n = *np;
1230 if (op_of(n) != "mla_attention" || sect != "block") continue;
1231 const auto H = layers(get(n, "heads"), L), r = layers(get(n, "kv_rank"), L), dn = layers(get(n, "qk_nope"), L),
1232 dv = layers(get(n, "v_head_dim"), L);
1233 if (!H || !r || !dn || !dv) continue;
1234 for (const char* key : {"k_b", "v_b"}) {
1235 if (!get(n, key).is_string()) continue;
1236 for (int i = 0; i < L; ++i) {
1237 if (!is_int((*H)[i]) || !is_int((*r)[i]) || !is_int((*dn)[i]) || !is_int((*dv)[i])) break;
1238 std::string w = n.at(key).get<std::string>();
1239 const size_t at = w.find("{i}");
1240 if (at != std::string::npos) {
1241 const bool mapped = wmi.is_array() && i < static_cast<int>(wmi.size()) && is_int(wmi[i]) && !n.contains("tensor_index");
1242 w.replace(at, 3, std::to_string(mapped ? wmi[i].get<int64_t>() : i));
1243 }
1244 const auto it = g.tensor_infos_map.find(w);
1245 if (it == g.tensor_infos_map.end()) continue; // rule 25 names a missing tensor
1246 std::vector<uint64_t> got = it->second.shape;
1247 while (got.size() < 3) got.push_back(1);
1248 const uint64_t a = (*H)[i].get<uint64_t>(), b = (*r)[i].get<uint64_t>(), c = (*dn)[i].get<uint64_t>(),
1249 e = (*dv)[i].get<uint64_t>();
1250 const std::vector<uint64_t> want = std::string(key) == "k_b" ? std::vector<uint64_t>{c, b, a}
1251 : std::vector<uint64_t>{b, e, a};
1252 if (got != want) {
1253 errs.push_back(E("5.20 (rule 42)", "block[" + std::to_string(j) + "] mla_attention " + key + " " + q(w) +
1254 " (application " + std::to_string(i) + ") has ne " +
1255 ne_str(it->second.shape) + ", not " + ne_str(want) +
1256 (std::string(key) == "k_b" ? " {qk_nope, kv_rank, heads}"
1257 : " {kv_rank, v_head_dim, heads}")));
1258 break;
1259 }
1260 }
1261 }
1262 }
1263}
1264
1265// ---------------------------------------------------------------- rule 38 (revision 1.3): the architecture
1266
1267// A document in Appendix D.7's normal form (keramos IR_SPEC v1.4.2), the comparison rule 38 makes: "_" members
1268// gone at every depth; the top-level keys model, embed, block and head only; attention.softmax and
1269// moe_ffn.routing gone; the optional attributes D.7 step 2 lists at their defaults (written here as absent, the
1270// same equality); a linear's weight the tied_to name when the file has no tensor named by its weight as written
1271// ({i} included), tied_to then null; block.state's slots named by their index (slot0, slot1, ...: every state
1272// attribute follows its slot) and compared in order; values named by their writes in the order embed, the
1273// block's nodes once, head; add's and mul's operands an unordered pair; every number the f32 it rounds to.
1274json rule38_form(const json& doc, const GGUFData& g) {
1275 std::function<json(const json&)> strip = [&](const json& v) -> json {
1276 if (v.is_object()) {
1277 json o = json::object();
1278 for (const auto& kv : v.items())
1279 if (kv.key().empty() || kv.key()[0] != '_') o[kv.key()] = strip(kv.value());
1280 return o;
1281 }
1282 if (v.is_array()) {
1283 json a = json::array();
1284 for (const auto& x : v) a.push_back(strip(x));
1285 return a;
1286 }
1287 if (v.is_number()) return json(static_cast<double>(static_cast<float>(v.get<double>())));
1288 return v;
1289 };
1290 const json d = strip(doc);
1291 std::map<std::string, std::string> cur, slot;
1292 int next = 0;
1293 auto read = [&](const std::string& nm) {
1294 if (nm == "tokens" || nm == "pos") return nm;
1295 auto it = cur.find(nm);
1296 return it == cur.end() ? "?" + nm : it->second;
1297 };
1298 auto has_tensor = [&](const std::string& nm) { return g.tensor_infos_map.count(nm) > 0; }; // as written (D.7 step 3)
1299 auto num_is = [](const json& v, double x) { return v.is_number() && v.get<double>() == static_cast<double>(static_cast<float>(x)); };
1300 auto node_form = [&](json n, bool in_block, std::vector<std::string>* carried, std::set<std::string>* written) {
1301 const std::string op = n.value("op", "");
1302 std::vector<std::string> ins;
1303 if (n.contains("in") && n.at("in").is_array())
1304 for (const auto& x : n.at("in"))
1305 if (x.is_string()) {
1306 const std::string nm = x.get<std::string>();
1307 if (in_block && carried && written && !written->count(nm) && nm != "tokens" && nm != "pos" &&
1308 std::find(carried->begin(), carried->end(), nm) == carried->end())
1309 carried->push_back(nm);
1310 ins.push_back(read(nm));
1311 }
1312 if ((op == "add" || op == "mul") && ins.size() == 2 && ins[1] < ins[0]) std::swap(ins[0], ins[1]);
1313 n["in"] = ins;
1314 if (n.contains("state") && n.at("state").is_string()) {
1315 const std::string st = n.at("state").get<std::string>();
1316 n["state"] = slot.count(st) ? slot[st] : "?" + st;
1317 }
1318 // informative attributes and optional ones at their defaults (IR_SPEC 3.5)
1319 if (op == "attention") {
1320 n.erase("softmax");
1321 if (n.value("mask", json("causal")) == "causal") n.erase("mask");
1322 }
1323 if (op == "moe_ffn") n.erase("routing");
1324 if ((op == "linear" || op == "scale") && n.contains("bias") && n.at("bias").is_null()) n.erase("bias");
1325 if (op == "linear") {
1326 if (n.contains("tied_to") && n.at("tied_to").is_string() &&
1327 !(n.contains("weight") && n.at("weight").is_string() && has_tensor(n.at("weight").get<std::string>())))
1328 n["weight"] = n.at("tied_to");
1329 n.erase("tied_to");
1330 }
1331 if (op == "rmsnorm" && n.contains("weight_offset") && num_is(n.at("weight_offset"), 0.0)) n.erase("weight_offset");
1332 if (op == "rope") {
1333 if (n.contains("kv_heads") && n.at("kv_heads") == get(n, "heads")) n.erase("kv_heads");
1334 if (n.contains("n_rot") && n.at("n_rot") == get(n, "head_dim")) n.erase("n_rot");
1335 if (n.contains("freq_scale") && num_is(n.at("freq_scale"), 1.0)) n.erase("freq_scale");
1336 if (n.contains("attn_factor") && num_is(n.at("attn_factor"), 1.0)) n.erase("attn_factor"); // revision 1.5
1337 }
1338 if (n.contains("apply") && n.at("apply") == json(true)) n.erase("apply"); // revision 1.6: absent is true
1339 if (op == "moe_ffn" && n.contains("scoring") && n.at("scoring") == "softmax") n.erase("scoring");
1340 if (op == "depth_attend" && n.contains("eps") && num_is(n.at("eps"), 1e-5)) n.erase("eps");
1341 if (op == "split" && n.contains("groups") && num_is(n.at("groups"), 1.0)) n.erase("groups"); // revision 1.5
1342 if (op == "embedding" && n.contains("state_weight") && n.at("state_weight").is_null()) n.erase("state_weight");
1343 if (op == "conv1d_causal" && n.contains("bias") && n.at("bias").is_null()) n.erase("bias");
1344 if (op == "selective_scan") {
1345 if (n.contains("d") && n.at("d").is_null()) n.erase("d");
1346 if (n.value("dt_softplus", json(true)) == json(true)) n.erase("dt_softplus");
1347 }
1348 json outs = json::array();
1349 const json& o = get(n, "out");
1350 std::vector<std::string> on;
1351 if (o.is_string()) on.push_back(o.get<std::string>());
1352 if (o.is_array())
1353 for (const auto& x : o)
1354 if (x.is_string()) on.push_back(x.get<std::string>());
1355 for (const auto& nm : on) {
1356 const std::string v = "v" + std::to_string(next++);
1357 cur[nm] = v;
1358 if (written) written->insert(nm);
1359 outs.push_back(v);
1360 }
1361 n["out"] = o.is_string() && outs.size() == 1 ? outs[0] : outs;
1362 return n;
1363 };
1364 // D.7 step 4: the k-th slot of block.state is slotk (the first of two slots with one name names it).
1365 const json& bl = get(d, "block");
1366 if (get(bl, "state").is_array())
1367 for (size_t k = 0; k < bl.at("state").size(); ++k) {
1368 const json& nm = get(bl.at("state")[k], "name");
1369 if (nm.is_string() && !slot.count(nm.get<std::string>())) slot[nm.get<std::string>()] = "slot" + std::to_string(k);
1370 }
1371 // A section as the document writes it (every member compared), its node list in normal form.
1372 auto section = [&](const char* key, bool in_block) {
1373 json sec = get(d, key);
1374 if (!sec.is_object()) return sec;
1375 if (sec.contains("nodes") && sec.at("nodes").is_array()) {
1376 json nodes = json::array();
1377 for (const auto& n : sec.at("nodes")) nodes.push_back(node_form(n, in_block, nullptr, nullptr));
1378 sec["nodes"] = nodes;
1379 }
1380 return sec;
1381 };
1382 json out = json::object();
1383 if (d.is_object() && d.contains("model")) out["model"] = d.at("model");
1384 if (d.is_object() && d.contains("embed")) out["embed"] = section("embed", false);
1385 if (d.is_object() && d.contains("block")) {
1386 json block = section("block", true);
1387 if (block.is_object() && block.contains("state") && block.at("state").is_array()) {
1388 json slots = json::array();
1389 for (size_t k = 0; k < block.at("state").size(); ++k) {
1390 json s = block.at("state")[k];
1391 if (s.is_object()) {
1392 s["name"] = "slot" + std::to_string(k);
1393 if (get(s, "kind") == "kv" && s.value("dtype", json("f32")) == "f32") s.erase("dtype");
1394 }
1395 slots.push_back(s);
1396 }
1397 block["state"] = slots;
1398 }
1399 out["block"] = block;
1400 }
1401 if (d.is_object() && d.contains("head")) out["head"] = section("head", false);
1402 return out;
1403}
1404
1405// The first place two rule-38 forms differ ("" when equal), as path: a against b.
1406std::string first_difference(const json& a, const json& b, const std::string& path) {
1407 if (a.type() != b.type() && !(a.is_number() && b.is_number()))
1408 return path + ": " + short_dump(a) + " against " + short_dump(b);
1409 if (a.is_object()) {
1410 std::set<std::string> keys;
1411 for (const auto& kv : a.items()) keys.insert(kv.key());
1412 for (const auto& kv : b.items()) keys.insert(kv.key());
1413 for (const auto& k : keys) {
1414 if (!a.contains(k)) return path + "." + k + ": absent against " + short_dump(b.at(k));
1415 if (!b.contains(k)) return path + "." + k + ": " + short_dump(a.at(k)) + " against absent";
1416 const std::string d = first_difference(a.at(k), b.at(k), path + "." + k);
1417 if (!d.empty()) return d;
1418 }
1419 return "";
1420 }
1421 if (a.is_array()) {
1422 for (size_t i = 0; i < std::min(a.size(), b.size()); ++i) {
1423 const std::string d = first_difference(a[i], b[i], path + "[" + std::to_string(i) + "]");
1424 if (!d.empty()) return d;
1425 }
1426 if (a.size() != b.size())
1427 return path + ": " + std::to_string(a.size()) + " entries against " + std::to_string(b.size());
1428 return "";
1429 }
1430 if (a.is_number() && b.is_number()) return a.get<double>() == b.get<double>() ? "" : path + ": " + a.dump() + " against " + b.dump();
1431 return a == b ? "" : path + ": " + short_dump(a) + " against " + short_dump(b);
1432}
1433
1434void architecture_errors(const GGUFData& g, Errs& errs) {
1435 const auto emb = meta_str(g, "paperkiln.ir");
1436 if (!emb) return; // a third-party file: no document to compare
1437 const std::string arch = meta_str(g, "general.architecture").value_or("");
1438 if (arch == "paperkiln") return;
1439 const std::string tail = "; a file whose model is not that architecture's graph has general.architecture "
1440 "\"paperkiln\", which no llama.cpp release builds a graph for";
1441 if (!meta_str(g, "general.architecture")) { // absent, or not a string
1442 errs.push_back(E("3.1 (rule 38)", "a file with an embedded document has no general.architecture string (\"paperkiln\" "
1443 "or one of llama, mistral, qwen2, qwen3, gpt2, gptneox, gemma3 and mamba)"));
1444 return;
1445 }
1446 if (!has_ir_builder(arch)) {
1447 errs.push_back(E("3.1 (rule 38)", "general.architecture " + q(arch) +
1448 " is neither \"paperkiln\" nor an architecture of section 9.3" + tail));
1449 return;
1450 }
1451 json doc;
1452 try {
1453 doc = json::parse(*emb);
1454 } catch (const std::exception&) {
1455 return; // rule 1 / 3.1 name it
1456 }
1457 json built;
1458 try {
1459 built = json::parse(build_ir_from_gguf(g, false));
1460 } catch (const std::exception& e) {
1461 errs.push_back(E("3.1 (rule 38)", "general.architecture " + q(arch) + ": its metadata builds no graph (" + e.what() +
1462 ")" + tail));
1463 return;
1464 }
1465 const std::string d = first_difference(rule38_form(doc, g), rule38_form(built, g), "document");
1466 if (!d.empty())
1467 errs.push_back(E("3.1 (rule 38)", "general.architecture " + q(arch) + ": the embedded document is not the " + arch +
1468 " graph its metadata describes (section 9.3; first difference, document "
1469 "against graph: " + d + ")" + tail));
1470}
1471
1472} // namespace
1473
1474json parse_ir_document(const std::string& text) {
1475 // nlohmann's parse keeps the last of two members with one name; its callback sees every key as it is read,
1476 // so each open object keeps the names it has seen.
1477 std::vector<std::set<std::string>> open;
1478 std::string dup;
1479 int dup_depth = -1;
1480 const json::parser_callback_t cb = [&](int depth, json::parse_event_t ev, json& parsed) {
1481 if (ev == json::parse_event_t::object_start) {
1482 open.emplace_back();
1483 } else if (ev == json::parse_event_t::object_end) {
1484 if (!open.empty()) open.pop_back();
1485 } else if (ev == json::parse_event_t::key && dup_depth < 0 && !open.empty() && parsed.is_string() &&
1486 !open.back().insert(parsed.get<std::string>()).second) {
1487 dup = parsed.get<std::string>();
1488 dup_depth = depth;
1489 }
1490 return true;
1491 };
1492 json j = json::parse(text, cb);
1493 if (dup_depth >= 0)
1494 throw std::runtime_error(E("3.2 (rule 35)", "an object holds the member name " +
1495 json(dup).dump(-1, ' ', false, json::error_handler_t::replace) +
1496 " twice (nesting depth " + std::to_string(dup_depth) +
1497 "): a document names each member of an object once"));
1498 return j;
1499}
1500
1501namespace {
1502
1503// The section and rule a schema failure breaks (IR_SPEC 10.1, revision 1.2): rule 1 only for what no other rule
1504// names (the types of values, the closed key sets of the document, its sections and its slots, the required
1505// sections); the version is rule 2, the model's fields rule 3, an op or an attribute its op does not define rule 4,
1506// an attribute value outside its closed set rule 5, a required attribute rule 8, causal not true rule 14, a slot's
1507// kind rule 11 and its fields rules 12 and 13, kv_from rule 22, weight_index rule 18 and a recurrence's
1508// declarations rule 31.
1509std::string schema_rule(const json& doc, const std::string& path, const std::string& msg) {
1510 static const std::regex kNode(R"(^doc\.(embed|block|head)\.nodes\[(\d+)\](?: \‍([A-Za-z0-9_]+\))?(.*)$)");
1511 static const std::regex kSlot(R"(^doc\.block\.state\[(\d+)\](.*)$)");
1512 static const std::set<std::string> kClosed = {"gelu.approx", "rope.pairing", "rope.rope_heads", "attention.mask",
1513 "moe_ffn.gate_norm", "moe_ffn.activation", "moe_ffn.scoring",
1514 "linear_attention.feature_map",
1515 "depth_route.route", "depth_route.gate"};
1516 static const std::set<std::string> kRecDecl = {"index", "inputs", "weights", "init", "let", "update", "readout"};
1517 const bool unknown_key = msg.rfind("has the key '", 0) == 0;
1518 const bool required = msg.rfind("lacks the required key '", 0) == 0;
1519 auto key_of = [&](const std::string& rest) { // ".mask" or ".mask.per_layer[0]" -> "mask"
1520 if (rest.size() < 2 || rest[0] != '.') return std::string();
1521 const size_t e = rest.find_first_of(".[", 1);
1522 return rest.substr(1, e == std::string::npos ? std::string::npos : e - 1);
1523 };
1524 std::smatch m;
1525 if (std::regex_match(path, m, kNode)) {
1526 const json& nodes = get(get(doc, m[1].str()), "nodes");
1527 const size_t j = std::stoul(m[2].str());
1528 const std::string op = nodes.is_array() && j < nodes.size() ? op_of(nodes[j]) : "";
1529 const std::string rest = m[3].str(), key = key_of(rest);
1530 if (key == "position_bias") // attention's (5.15, revision 1.3); a per-layer one breaks rule 6
1531 return nodes.is_array() && j < nodes.size() && is_pl(get(nodes[j], "position_bias")) ? "3.8 (rule 6)"
1532 : "5.15 (rule 39)";
1533 if (key == "relative_kv") // attention's (5.15, revision 1.4); a per-layer one breaks rule 6
1534 return nodes.is_array() && j < nodes.size() && is_pl(get(nodes[j], "relative_kv")) ? "3.8 (rule 6)"
1535 : "5.15 (rule 40)";
1536 if (key == "apply") // revision 1.6 (3.8): not true, false or a per_layer object; on a stateful op rule 4 (unknown_key)
1537 return unknown_key ? "3.5 (rule 4)" : "3.8 (rule 43)";
1538 if (key == "freq_factors") // rope's (5.14, revision 1.5); a per-layer one breaks rule 6
1539 return nodes.is_array() && j < nodes.size() && is_pl(get(nodes[j], "freq_factors")) ? "3.8 (rule 6)"
1540 : "5.14 (rule 41)";
1541 const bool io = key == "in" || key == "out" || (required && (msg.find("'in'") != std::string::npos ||
1542 msg.find("'out'") != std::string::npos));
1543 if (io) return "3.5 (rule 7)"; // the arity: what a node reads and writes
1544 if (unknown_key || key == "op") return "3.5 (rule 4)";
1545 if (required && rest.empty()) return "3.5 (rule 8)";
1546 if (key == "tensor_index" || kClosed.count(op + "." + key)) return "3.5 (rule 5)";
1547 if (key == "causal" && (op == "attention" || op == "linear_attention" || op == "mla_attention"))
1548 return op == "attention" ? "5.15 (rule 14)" : op == "mla_attention" ? "5.20 (rule 14)" : "6.4 (rule 14)";
1549 if (op == "split" && key == "groups") // revision 1.5: a positive integer (rule 10), structural (rule 6)
1550 return nodes.is_array() && j < nodes.size() && is_pl(get(nodes[j], "groups")) ? "3.8 (rule 6)" : "5.5 (rule 10)";
1551 if (op == "recurrence" && kRecDecl.count(key)) return "6.10 (rule 31)";
1552 return "3.5 (rule 1)";
1553 }
1554 if (std::regex_match(path, m, kSlot)) {
1555 const json& slots = get(get(doc, "block"), "state");
1556 const size_t j = std::stoul(m[1].str());
1557 const json& slot = slots.is_array() && j < slots.size() ? slots[j] : kNull();
1558 const std::string rest = m[2].str(), key = key_of(rest);
1559 if (unknown_key && get(slot, "kind") == "recurrent") return "6.3 (rule 13)";
1560 if (unknown_key) return msg.find("'kv_from'") != std::string::npos ? "7.6 (rule 22)" : "6.1 (rule 1)";
1561 if (key == "kind" || (required && msg.find("'kind'") != std::string::npos)) return "6.1 (rule 11)";
1562 if (get(slot, "kind") == "kv") {
1563 if (key == "dtype" || key == "evict" || key == "kv_rows") return "6.2 (rule 5)";
1564 return "6.2 (rule 12)";
1565 }
1566 if (get(slot, "kind") == "recurrent") return "6.3 (rule 13)";
1567 return "6.1 (rule 1)";
1568 }
1569 if (path.rfind("doc.ir_version", 0) == 0) return "2.3 (rule 2)";
1570 if (path.rfind("doc.model", 0) == 0) { // the model's fields d, vocab, ctx and n_layers are rule 3
1571 bool field = false;
1572 for (const char* k : {"vocab", "d", "n_layers", "ctx"})
1573 field = field || key_of(path.substr(9)) == k || (required && msg.find(std::string("'") + k + "'") != std::string::npos);
1574 return field ? "3.3 (rule 3)" : "3.3 (rule 1)";
1575 }
1576 if (path.rfind("doc.block.weight_index", 0) == 0) return "7.1 (rule 18)";
1577 if (path.rfind("doc.block.repeat", 0) == 0) return "3.3 (rule 3)";
1578 return "3.2 (rule 1)";
1579}
1580
1581} // namespace
1582
1583std::vector<std::string> ir_schema_errors(const json& doc) {
1584 Failures f;
1585 eval(schema(), doc, "doc", f);
1586 std::vector<std::string> out;
1587 std::set<std::string> seen;
1588 for (const auto& [p, m] : f) add_unique(out, seen, E(schema_rule(doc, p, m), p + " " + m));
1589 return out;
1590}
1591
1592std::vector<std::string> ir_document_errors(const json& doc) {
1593 if (!doc.is_object()) return {E("10.1 rule 1", "the document is not a JSON object")};
1595 const json& v = get(doc, "ir_version");
1596 if (is_int(v) && v.get<int64_t>() != 0 && v.get<int64_t>() != 1)
1597 errs.push_back(E("2.3", "ir_version " + v.dump() + " is not supported (ember reads 0, 1)"));
1603 std::vector<std::string> out;
1604 std::set<std::string> seen;
1605 for (const auto& m : errs) add_unique(out, seen, m);
1606 return out;
1607}
1608
1609std::vector<std::string> ir_container_errors(const json& doc, const GGUFData& gguf) {
1610 Errs errs;
1614 std::vector<std::string> out;
1615 std::set<std::string> seen;
1616 for (const auto& m : errs) add_unique(out, seen, m);
1617 return out;
1618}
1619
1620void validate_ir(const json& doc, const GGUFData& gguf) {
1621 std::vector<std::string> errs = ir_document_errors(doc);
1622 if (doc.is_object())
1623 for (const auto& m : ir_container_errors(doc, gguf)) errs.push_back(m);
1624 if (errs.empty()) return;
1625 std::string msg;
1626 for (const auto& e : errs) msg += (msg.empty() ? "" : "; ") + e;
1627 throw std::runtime_error(msg);
1628}
1629
1630} // namespace ember_ir
static Expr parse(const std::string &text, size_t n_inputs=1)
Data structures for GGUF (GPT-Generated Unified Format) file format.
std::string why
Definition graph_ir.cpp:280
const float * p[kIrDepthSourcesMax]
int n
float value
std::string text
std::vector< float > v
const char * name
Kind kind
float c
std::vector< Token > toks
std::string where
std::vector< E > a
node sub
node out
node required
const json * n
std::string sect
size_t j
std::vector< Value > items
Definition ir_digest.cpp:67
std::string str
Definition ir_digest.cpp:66
void eval(const Expr &e, const std::vector< const float * > &ins, int T, int w, float *out)
Definition api.h:20
std::string build_ir_from_gguf(const GGUFData &g, bool log)
Definition graph_ir.cpp:553
const char *const kIrSchemaPieces[]
json parse_ir_document(const std::string &text)
DigestResult ir_digest(const std::string &text, bool prose)
void validate_ir(const json &doc, const GGUFData &gguf)
std::vector< std::string > ir_document_errors(const json &doc)
std::vector< std::string > ir_container_errors(const json &doc, const GGUFData &gguf)
std::vector< std::string > ir_schema_errors(const json &doc)
bool has_ir_builder(const std::string &arch)
Definition graph_ir.cpp:519
Template get(const std::string &id)
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