1use serde_json::Value;
26
27use kime_tensor::plan::{Epilogue, Graph, Op, Rows};
28
29use crate::error::{Error, Result};
30use crate::tensors::Tensors;
31
32pub const HEAD_DIM: usize = 64;
34
35pub const TORCH_EPS: f64 = 1e-5;
37
38pub const ACT_HIDDEN: usize = 256;
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct EncoderConfig {
44 pub d: usize,
46 pub inter: usize,
48 pub heads: usize,
50 pub layers: usize,
52 pub vocab: usize,
54 pub global: Vec<bool>,
56 pub window: usize,
58 pub rope_global: f64,
60 pub rope_local: f64,
62 pub norm_eps: f64,
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub struct AgentConfig {
69 pub encoder: String,
71 pub head_layers: usize,
73 pub max_len: usize,
75 pub head_max_len: usize,
77 pub temperature: [f64; 3],
79 pub temperature_by_options: Vec<(String, f64)>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct LayaSpec {
86 pub id: String,
88 pub encoder: EncoderConfig,
90 pub agent: AgentConfig,
92}
93
94fn get<'a>(v: &'a Value, key: &str, file: &str) -> Result<&'a Value> {
95 v.get(key).ok_or_else(|| Error::format(format!("{file}: missing {key:?}")))
96}
97
98fn usize_of(v: &Value, key: &str, file: &str) -> Result<usize> {
99 get(v, key, file)?
100 .as_u64()
101 .and_then(|n| usize::try_from(n).ok())
102 .ok_or_else(|| Error::format(format!("{file}: {key:?} is not a non negative integer")))
103}
104
105fn f64_of(v: &Value, key: &str, file: &str) -> Result<f64> {
106 get(v, key, file)?
107 .as_f64()
108 .ok_or_else(|| Error::format(format!("{file}: {key:?} is not a number")))
109}
110
111impl EncoderConfig {
112 pub fn from_json(v: &Value) -> Result<Self> {
120 const F: &str = "encoder/config.json";
121 let d = usize_of(v, "hidden_size", F)?;
122 let heads = usize_of(v, "num_attention_heads", F)?;
123 let layers = usize_of(v, "num_hidden_layers", F)?;
124 if heads == 0 || d != heads * HEAD_DIM {
125 return Err(Error::format(format!(
126 "{F}: hidden_size {d} is not {heads} heads of {HEAD_DIM}"
127 )));
128 }
129 let global = match v.get("layer_types").and_then(Value::as_array) {
130 Some(types) => types
131 .iter()
132 .map(|t| match t.as_str() {
133 Some("full_attention") => Ok(true),
134 Some("sliding_attention") => Ok(false),
135 _ => Err(Error::format(format!("{F}: unknown layer type {t}"))),
136 })
137 .collect::<Result<Vec<_>>>()?,
138 None => {
139 let every = usize_of(v, "global_attn_every_n_layers", F)?.max(1);
140 (0..layers).map(|i| i % every == 0).collect()
141 }
142 };
143 if global.len() != layers {
144 return Err(Error::format(format!(
145 "{F}: {} layer types for {layers} layers",
146 global.len()
147 )));
148 }
149 let (rope_global, rope_local) = match v.get("rope_parameters") {
150 Some(p) => (
151 f64_of(get(p, "full_attention", F)?, "rope_theta", F)?,
152 f64_of(get(p, "sliding_attention", F)?, "rope_theta", F)?,
153 ),
154 None => (f64_of(v, "global_rope_theta", F)?, f64_of(v, "local_rope_theta", F)?),
155 };
156 Ok(Self {
157 d,
158 inter: usize_of(v, "intermediate_size", F)?,
159 heads,
160 layers,
161 vocab: usize_of(v, "vocab_size", F)?,
162 global,
163 window: usize_of(v, "local_attention", F)?,
164 rope_global,
165 rope_local,
166 norm_eps: v.get("norm_eps").and_then(Value::as_f64).unwrap_or(1e-5),
167 })
168 }
169}
170
171impl AgentConfig {
172 pub fn from_json(v: &Value) -> Result<Self> {
178 const F: &str = "rl_agent_config.json";
179 let t = get(v, "temperature", F)?
180 .as_array()
181 .filter(|a| a.len() == 3)
182 .and_then(|a| Some([a[0].as_f64()?, a[1].as_f64()?, a[2].as_f64()?]))
183 .ok_or_else(|| Error::format(format!("{F}: temperature must be three numbers")))?;
184 let by_options = match v.get("temperature_by_options") {
185 None | Some(Value::Null) => Vec::new(),
186 Some(Value::Object(m)) => m
187 .iter()
188 .map(|(k, t)| {
189 t.as_f64().map(|t| (k.clone(), t)).ok_or_else(|| {
190 Error::format(format!("{F}: temperature_by_options[{k:?}] is not a number"))
191 })
192 })
193 .collect::<Result<_>>()?,
194 Some(_) => {
195 return Err(Error::format(format!("{F}: temperature_by_options is not an object")));
196 }
197 };
198 Ok(Self {
199 encoder: get(v, "encoder", F)?.as_str().unwrap_or_default().to_string(),
200 head_layers: usize_of(v, "head_layers", F)?,
201 max_len: usize_of(v, "max_len", F)?,
202 head_max_len: usize_of(v, "head_max_len", F)?,
203 temperature: t,
204 temperature_by_options: by_options,
205 })
206 }
207}
208
209impl LayaSpec {
210 pub fn from_json(agent: &Value, encoder: &Value) -> Result<Self> {
217 let agent = AgentConfig::from_json(agent)?;
218 let encoder = EncoderConfig::from_json(encoder)?;
219 let id = match agent.encoder.as_str() {
220 "answerdotai/ModernBERT-large" => "laya",
221 "jhu-clsp/mmBERT-base" => "laya-multilingual",
222 _ => "laya-custom",
223 };
224 Ok(Self { id: id.to_string(), encoder, agent })
225 }
226
227 #[must_use]
229 pub fn expected(&self) -> Vec<(String, Vec<usize>)> {
230 let e = &self.encoder;
231 let d = e.d;
232 let mut out: Vec<(String, Vec<usize>)> = Vec::new();
233 let mut put = |name: String, shape: &[usize]| out.push((name, shape.to_vec()));
234 put("encoder.embeddings.tok_embeddings.weight".into(), &[e.vocab, d]);
235 put("encoder.embeddings.norm.weight".into(), &[d]);
236 for i in 0..e.layers {
237 let p = format!("encoder.layers.{i}");
238 if i > 0 {
239 put(format!("{p}.attn_norm.weight"), &[d]);
240 }
241 put(format!("{p}.attn.Wqkv.weight"), &[3 * d, d]);
242 put(format!("{p}.attn.Wo.weight"), &[d, d]);
243 put(format!("{p}.mlp_norm.weight"), &[d]);
244 put(format!("{p}.mlp.Wi.weight"), &[2 * e.inter, d]);
245 put(format!("{p}.mlp.Wo.weight"), &[d, e.inter]);
246 }
247 put("encoder.final_norm.weight".into(), &[d]);
248 put("type_emb.weight".into(), &[3, d]);
249 for l in 0..self.agent.head_layers {
250 let p = format!("head.layers.{l}");
251 put(format!("{p}.self_attn.in_proj_weight"), &[3 * d, d]);
252 put(format!("{p}.self_attn.in_proj_bias"), &[3 * d]);
253 put(format!("{p}.self_attn.out_proj.weight"), &[d, d]);
254 put(format!("{p}.self_attn.out_proj.bias"), &[d]);
255 put(format!("{p}.linear1.weight"), &[4 * d, d]);
256 put(format!("{p}.linear1.bias"), &[4 * d]);
257 put(format!("{p}.linear2.weight"), &[d, 4 * d]);
258 put(format!("{p}.linear2.bias"), &[d]);
259 for n in ["norm1", "norm2"] {
260 put(format!("{p}.{n}.weight"), &[d]);
261 put(format!("{p}.{n}.bias"), &[d]);
262 }
263 }
264 put("scorer.0.weight".into(), &[d]);
265 put("scorer.0.bias".into(), &[d]);
266 put("scorer.1.weight".into(), &[d, d]);
267 put("scorer.1.bias".into(), &[d]);
268 put("scorer.3.weight".into(), &[1, d]);
269 put("scorer.3.bias".into(), &[1]);
270 put("act_head.0.weight".into(), &[256, d + 4]);
271 put("act_head.0.bias".into(), &[256]);
272 put("act_head.2.weight".into(), &[2, 256]);
273 put("act_head.2.bias".into(), &[2]);
274 put("temperature".into(), &[3]);
275 out
276 }
277}
278
279pub type W = usize;
281
282#[derive(Debug, Clone, Copy, PartialEq)]
284pub struct EncoderLayer {
285 pub attn_norm: Option<W>,
287 pub wqkv: W,
289 pub wo: W,
291 pub mlp_norm: W,
293 pub wi: W,
295 pub mlp_wo: W,
297 pub global: bool,
299 pub rope_theta: f64,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305#[allow(missing_docs)]
306pub struct HeadLayer {
307 pub in_proj_w: W,
308 pub in_proj_b: W,
309 pub out_proj_w: W,
310 pub out_proj_b: W,
311 pub norm1_w: W,
312 pub norm1_b: W,
313 pub norm2_w: W,
314 pub norm2_b: W,
315 pub linear1_w: W,
316 pub linear1_b: W,
317 pub linear2_w: W,
318 pub linear2_b: W,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct Affine {
324 pub w: W,
326 pub b: W,
328}
329
330#[derive(Debug, Clone, PartialEq)]
332pub struct LayaGraph {
333 pub tok_embeddings: W,
335 pub embed_norm: W,
337 pub layers: Vec<EncoderLayer>,
339 pub final_norm: W,
341 pub type_emb: W,
343 pub head: Vec<HeadLayer>,
345 pub scorer_norm: Affine,
347 pub scorer_in: Affine,
349 pub scorer_out: Affine,
351 pub act_in: Affine,
353 pub act_out: Affine,
355 pub temperature: W,
357}
358
359impl LayaGraph {
360 pub fn bind(spec: &LayaSpec, tensors: &Tensors) -> Result<Self> {
371 let expected = spec.expected();
372 let mut problems = Vec::new();
373 for (name, shape) in &expected {
374 match tensors.get(name) {
375 None => problems.push(format!("missing {name} {shape:?}")),
376 Some(v) if v.shape != shape.as_slice() => {
377 problems.push(format!("{name} has shape {:?}, expected {shape:?}", v.shape));
378 }
379 Some(v) if !v.dtype.is_float() => {
380 problems.push(format!("{name} is {}, expected a float type", v.dtype));
381 }
382 Some(_) => {}
383 }
384 }
385 let known: std::collections::HashSet<&str> =
386 expected.iter().map(|(n, _)| n.as_str()).collect();
387 for e in tensors.entries() {
388 if !known.contains(e.name.as_str()) {
389 problems.push(format!("unexpected {} {:?}", e.name, e.shape));
390 }
391 }
392 if !problems.is_empty() {
393 return Err(Error::Mismatch(problems));
394 }
395 let w = |name: &str| tensors.index(name).expect("checked above");
396 let aff = |p: &str| Affine { w: w(&format!("{p}.weight")), b: w(&format!("{p}.bias")) };
397 let enc = &spec.encoder;
398 let layers = (0..enc.layers)
399 .map(|i| {
400 let p = format!("encoder.layers.{i}");
401 EncoderLayer {
402 attn_norm: (i > 0).then(|| w(&format!("{p}.attn_norm.weight"))),
403 wqkv: w(&format!("{p}.attn.Wqkv.weight")),
404 wo: w(&format!("{p}.attn.Wo.weight")),
405 mlp_norm: w(&format!("{p}.mlp_norm.weight")),
406 wi: w(&format!("{p}.mlp.Wi.weight")),
407 mlp_wo: w(&format!("{p}.mlp.Wo.weight")),
408 global: enc.global[i],
409 rope_theta: if enc.global[i] { enc.rope_global } else { enc.rope_local },
410 }
411 })
412 .collect();
413 let head = (0..spec.agent.head_layers)
414 .map(|l| {
415 let p = format!("head.layers.{l}");
416 HeadLayer {
417 in_proj_w: w(&format!("{p}.self_attn.in_proj_weight")),
418 in_proj_b: w(&format!("{p}.self_attn.in_proj_bias")),
419 out_proj_w: w(&format!("{p}.self_attn.out_proj.weight")),
420 out_proj_b: w(&format!("{p}.self_attn.out_proj.bias")),
421 norm1_w: w(&format!("{p}.norm1.weight")),
422 norm1_b: w(&format!("{p}.norm1.bias")),
423 norm2_w: w(&format!("{p}.norm2.weight")),
424 norm2_b: w(&format!("{p}.norm2.bias")),
425 linear1_w: w(&format!("{p}.linear1.weight")),
426 linear1_b: w(&format!("{p}.linear1.bias")),
427 linear2_w: w(&format!("{p}.linear2.weight")),
428 linear2_b: w(&format!("{p}.linear2.bias")),
429 }
430 })
431 .collect();
432 Ok(Self {
433 tok_embeddings: w("encoder.embeddings.tok_embeddings.weight"),
434 embed_norm: w("encoder.embeddings.norm.weight"),
435 layers,
436 final_norm: w("encoder.final_norm.weight"),
437 type_emb: w("type_emb.weight"),
438 head,
439 scorer_norm: aff("scorer.0"),
440 scorer_in: aff("scorer.1"),
441 scorer_out: aff("scorer.3"),
442 act_in: aff("act_head.0"),
443 act_out: aff("act_head.2"),
444 temperature: w("temperature"),
445 })
446 }
447
448 #[must_use]
452 pub fn plan(&self, spec: &LayaSpec) -> Graph {
453 let e = &spec.encoder;
454 let d = e.d;
455 let mut g = Graph::default();
456 let tok = |g: &mut Graph, w| g.val(Rows::Tokens, w);
457 let emb = tok(&mut g, d);
458 let h = tok(&mut g, d);
459 g.push(Op::Embed { table: self.tok_embeddings, out: emb });
460 g.push(Op::LayerNorm { x: emb, w: self.embed_norm, b: None, eps: e.norm_eps, out: h });
461 let gemm = |g: &mut Graph, a, w, b, epilogue, out| {
462 g.push(Op::Gemm { a, w, b, epilogue, out });
463 };
464 for l in &self.layers {
465 let x = match l.attn_norm {
466 Some(n) => {
467 let x = tok(&mut g, d);
468 g.push(Op::LayerNorm { x: h, w: n, b: None, eps: e.norm_eps, out: x });
469 x
470 }
471 None => h,
472 };
473 let qkv = tok(&mut g, 3 * d);
474 gemm(&mut g, x, l.wqkv, None, Epilogue::None, qkv);
475 g.push(Op::Rope { qkv, theta: l.rope_theta });
476 let att = tok(&mut g, d);
477 let window = (!l.global).then_some(e.window / 2);
478 g.push(Op::Attention { qkv, window, out: att });
479 gemm(&mut g, att, l.wo, None, Epilogue::Accumulate, h);
480 let x = tok(&mut g, d);
481 g.push(Op::LayerNorm { x: h, w: l.mlp_norm, b: None, eps: e.norm_eps, out: x });
482 let u = tok(&mut g, 2 * e.inter);
483 gemm(&mut g, x, l.wi, None, Epilogue::None, u);
484 let a = tok(&mut g, e.inter);
485 g.push(Op::GeGlu { x: u, out: a });
486 gemm(&mut g, a, l.mlp_wo, None, Epilogue::Accumulate, h);
487 }
488 let h2 = tok(&mut g, d);
489 g.push(Op::LayerNorm { x: h, w: self.final_norm, b: None, eps: e.norm_eps, out: h2 });
490 let h = h2;
491 g.push(Op::AddType { h, table: self.type_emb });
492 for l in &self.head {
493 let x = tok(&mut g, d);
494 g.push(Op::LayerNorm {
495 x: h,
496 w: l.norm1_w,
497 b: Some(l.norm1_b),
498 eps: TORCH_EPS,
499 out: x,
500 });
501 let qkv = tok(&mut g, 3 * d);
502 gemm(&mut g, x, l.in_proj_w, Some(l.in_proj_b), Epilogue::None, qkv);
503 let att = tok(&mut g, d);
504 g.push(Op::Attention { qkv, window: None, out: att });
505 gemm(&mut g, att, l.out_proj_w, Some(l.out_proj_b), Epilogue::Accumulate, h);
506 let x = tok(&mut g, d);
507 g.push(Op::LayerNorm {
508 x: h,
509 w: l.norm2_w,
510 b: Some(l.norm2_b),
511 eps: TORCH_EPS,
512 out: x,
513 });
514 let f = tok(&mut g, 4 * d);
515 gemm(&mut g, x, l.linear1_w, Some(l.linear1_b), Epilogue::Relu, f);
516 gemm(&mut g, f, l.linear2_w, Some(l.linear2_b), Epilogue::Accumulate, h);
517 }
518 let m = g.val(Rows::Markers, d);
519 g.push(Op::GatherMarkers { h, out: m });
520 let mn = g.val(Rows::Markers, d);
521 let sn = self.scorer_norm;
522 g.push(Op::LayerNorm { x: m, w: sn.w, b: Some(sn.b), eps: TORCH_EPS, out: mn });
523 let z = g.val(Rows::Markers, d);
524 gemm(&mut g, mn, self.scorer_in.w, Some(self.scorer_in.b), Epilogue::Gelu, z);
525 let logits = g.val(Rows::Markers, 1);
526 gemm(&mut g, z, self.scorer_out.w, Some(self.scorer_out.b), Epilogue::None, logits);
527 let f = g.val(Rows::Seqs, d + 4);
528 g.push(Op::ActFeatures { h, logits, out: f });
529 let a = g.val(Rows::Seqs, ACT_HIDDEN);
530 gemm(&mut g, f, self.act_in.w, Some(self.act_in.b), Epilogue::Gelu, a);
531 let act = g.val(Rows::Seqs, 2);
532 gemm(&mut g, a, self.act_out.w, Some(self.act_out.b), Epilogue::None, act);
533 g.logits = Some(logits);
534 g.act = Some(act);
535 g
536 }
537}