1use candle_core::{DType, Device, Tensor};
4use candle_nn::VarBuilder;
5use laya_core::calibrate::{calibrated_softmax, confidence_from_probs};
6use laya_core::prompt::{build_prefix, extend_with_state, serialize_state, Prefix, Sequence};
7use laya_core::pycompat::round4;
8use laya_core::question::{Question, QuestionKind};
9use laya_core::Tokenizer;
10use rayon::prelude::*;
11use serde_json::{Map, Value};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Mutex;
15
16use crate::config::{AgentConfig, EncoderConfig};
17use crate::model::DecisionModel;
18use crate::weights::SanitizedBackend;
19use crate::{Error, Result};
20
21const REQUIRED_FILES: [&str; 3] = [
23 "model.safetensors",
24 "rl_agent_config.json",
25 "encoder/config.json",
26];
27
28const DEFAULT_BATCH_SIZE: usize = 16;
30const PREFIX_CACHE_CAPACITY: usize = 128;
32const PARALLEL_THRESHOLD: usize = 4;
34
35pub fn parse_dtype(name: &str) -> Result<DType> {
37 match name {
38 "float32" => Ok(DType::F32),
39 "float16" => Ok(DType::F16),
40 "bfloat16" => Ok(DType::BF16),
41 other => Err(Error::Config(format!(
42 "dtype must be one of [\"float32\", \"float16\", \"bfloat16\"], got {other:?}"
43 ))),
44 }
45}
46
47#[derive(Clone, Debug)]
49struct Item {
50 ids: Vec<u32>,
51 markers: Vec<usize>,
52 qtype: QuestionKind,
53 truncated: bool,
54}
55
56#[derive(Debug)]
58struct Batch {
59 input_ids: Tensor,
60 attention_mask: Tensor,
61 marker_pos: Tensor,
62 marker_mask: Tensor,
63 qtype: Tensor,
64}
65
66#[derive(Clone, Debug, PartialEq)]
71pub struct Answer {
72 pub kind: QuestionKind,
73 pub confidence: f64,
74 pub act_probability: f64,
75 pub labels: Vec<String>,
77 pub probabilities: Vec<f64>,
79 pub choice: Option<String>,
81 pub score: Option<f64>,
83 pub legend: Option<Map<String, Value>>,
85 pub noul: Option<f64>,
87}
88
89impl Answer {
90 pub fn to_value(&self) -> Value {
92 let mut out = Map::new();
93 out.insert("type".into(), Value::String(self.kind.name().into()));
94 out.insert("confidence".into(), number(self.confidence));
95 let mut action = Map::new();
96 action.insert("act_probability".into(), number(self.act_probability));
97 out.insert("action".into(), Value::Object(action));
98
99 match self.kind {
100 QuestionKind::Choice => {
101 out.insert(
102 "choice".into(),
103 Value::String(self.choice.clone().unwrap_or_default()),
104 );
105 out.insert("probabilities".into(), self.probability_map());
106 }
107 QuestionKind::Score => {
108 out.insert("score".into(), number(self.score.unwrap_or(0.0)));
109 out.insert(
110 "legend".into(),
111 Value::Object(self.legend.clone().unwrap_or_default()),
112 );
113 out.insert("probabilities".into(), self.probability_map());
114 }
115 QuestionKind::Noul => {
116 out.insert("noul".into(), number(self.noul.unwrap_or(0.0)));
117 }
118 }
119 Value::Object(out)
120 }
121
122 fn probability_map(&self) -> Value {
123 let mut map = Map::new();
124 for (label, probability) in self.labels.iter().zip(&self.probabilities) {
125 map.insert(label.clone(), number(*probability));
126 }
127 Value::Object(map)
128 }
129}
130
131fn number(value: f64) -> Value {
133 serde_json::Number::from_f64(value).map_or(Value::Null, Value::Number)
134}
135
136#[derive(Clone, Debug)]
138pub struct Prediction {
139 pub model: String,
140 pub answers: Map<String, Value>,
142 pub typed: Vec<(String, Answer)>,
144 pub input_tokens: usize,
146 pub output_tokens: usize,
147 pub sequence_lengths: Vec<usize>,
152 pub truncated: bool,
154 pub routing: Option<Value>,
156}
157
158impl Prediction {
159 pub fn to_value(&self) -> Value {
161 let mut out = Map::new();
162 out.insert("model".into(), Value::String(self.model.clone()));
163 out.insert("answers".into(), Value::Object(self.answers.clone()));
164 let mut usage = Map::new();
165 usage.insert("input_tokens".into(), Value::from(self.input_tokens));
166 usage.insert("output_tokens".into(), Value::from(self.output_tokens));
167 out.insert("usage".into(), Value::Object(usage));
168 if let Some(routing) = &self.routing {
169 out.insert("routing".into(), routing.clone());
170 }
171 Value::Object(out)
172 }
173
174 pub fn to_value_detailed(&self) -> Value {
179 let mut out = self.to_value();
180 if let Some(usage) = out.get_mut("usage").and_then(|usage| usage.as_object_mut()) {
181 usage.insert(
182 "sequence_lengths".into(),
183 Value::from(self.sequence_lengths.clone()),
184 );
185 usage.insert("truncated".into(), Value::Bool(self.truncated));
186 }
187 out
188 }
189
190 pub fn answer(&self, question_id: &str) -> Option<&Answer> {
192 self.typed
193 .iter()
194 .find(|(id, _)| id == question_id)
195 .map(|(_, answer)| answer)
196 }
197}
198
199#[derive(Debug, Default)]
206struct PrefixCache {
207 entries: HashMap<String, Prefix>,
208 order: Vec<String>,
209}
210
211impl PrefixCache {
212 fn key(question: &Question, head_max_len: usize) -> String {
213 let mut key = String::with_capacity(64);
214 key.push_str(question.kind.name());
215 key.push('\u{1}');
216 key.push_str(&head_max_len.to_string());
217 key.push('\u{1}');
218 key.push_str(&question.instructions);
219 for option in question.render_options() {
220 key.push('\u{1}');
221 key.push_str(&option);
222 }
223 key
224 }
225
226 fn get_or_insert(
227 &mut self,
228 question: &Question,
229 head_max_len: usize,
230 build: impl FnOnce() -> Result<Prefix>,
231 ) -> Result<Prefix> {
232 let key = Self::key(question, head_max_len);
233 if let Some(prefix) = self.entries.get(&key) {
234 let prefix = prefix.clone();
235 self.touch(&key);
236 return Ok(prefix);
237 }
238 let prefix = build()?;
239 self.entries.insert(key.clone(), prefix.clone());
240 self.order.push(key);
241 while self.order.len() > PREFIX_CACHE_CAPACITY {
242 let evicted = self.order.remove(0);
243 self.entries.remove(&evicted);
244 }
245 Ok(prefix)
246 }
247
248 fn touch(&mut self, key: &str) {
249 if let Some(index) = self.order.iter().position(|entry| entry == key) {
250 let key = self.order.remove(index);
251 self.order.push(key);
252 }
253 }
254}
255
256pub struct AgentBuilder {
258 dtype: DType,
259 device: Device,
260 batch_size: usize,
261 pad_to_multiple: Option<usize>,
262 cache_prompts: bool,
263}
264
265impl Default for AgentBuilder {
266 fn default() -> Self {
267 Self {
268 dtype: DType::F16,
269 device: Device::Cpu,
270 batch_size: DEFAULT_BATCH_SIZE,
271 pad_to_multiple: None,
272 cache_prompts: false,
273 }
274 }
275}
276
277impl AgentBuilder {
278 pub fn new() -> Self {
279 Self::default()
280 }
281
282 pub fn dtype(mut self, dtype: DType) -> Self {
284 self.dtype = dtype;
285 self
286 }
287
288 pub fn device(mut self, device: Device) -> Self {
289 self.device = device;
290 self
291 }
292
293 pub fn batch_size(mut self, batch_size: usize) -> Self {
295 self.batch_size = batch_size.max(1);
296 self
297 }
298
299 pub fn pad_to_multiple(mut self, pad_to_multiple: Option<usize>) -> Self {
301 self.pad_to_multiple = pad_to_multiple.filter(|value| *value >= 1);
302 self
303 }
304
305 pub fn cache_prompts(mut self, cache_prompts: bool) -> Self {
307 self.cache_prompts = cache_prompts;
308 self
309 }
310
311 pub fn build(self, model_dir: impl AsRef<Path>) -> Result<Agent> {
313 Agent::load(model_dir.as_ref(), self)
314 }
315}
316
317pub struct Agent {
319 model_dir: PathBuf,
320 encoder_cfg: EncoderConfig,
321 agent_cfg: AgentConfig,
322 tokenizer: Tokenizer,
323 model: DecisionModel,
324 device: Device,
325 dtype: DType,
326 batch_size: usize,
327 pad_to_multiple: Option<usize>,
328 prefix_cache: Option<Mutex<PrefixCache>>,
329}
330
331impl std::fmt::Debug for Agent {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 f.debug_struct("Agent")
334 .field("model_dir", &self.model_dir)
335 .field("dtype", &self.dtype)
336 .field("batch_size", &self.batch_size)
337 .finish_non_exhaustive()
338 }
339}
340
341impl Agent {
342 pub fn builder() -> AgentBuilder {
344 AgentBuilder::new()
345 }
346
347 pub fn from_dir(model_dir: impl AsRef<Path>) -> Result<Self> {
349 AgentBuilder::new().build(model_dir)
350 }
351
352 fn load(model_dir: &Path, options: AgentBuilder) -> Result<Self> {
353 for name in REQUIRED_FILES {
354 let path = model_dir.join(name);
355 if !path.is_file() {
356 return Err(Error::IncompleteCheckpoint(path));
357 }
358 }
359
360 let agent_raw: Value = serde_json::from_str(&std::fs::read_to_string(
361 model_dir.join("rl_agent_config.json"),
362 )?)?;
363 let encoder_raw: Value = serde_json::from_str(&std::fs::read_to_string(
364 model_dir.join("encoder/config.json"),
365 )?)?;
366
367 let encoder_cfg = EncoderConfig::from_value(&encoder_raw)?;
368 let agent_cfg = AgentConfig::from_value(&agent_raw, encoder_cfg.max_position_embeddings)?;
369 let tokenizer = Tokenizer::from_dir(model_dir.join("tokenizer"))?;
370
371 let weights_path = model_dir.join("model.safetensors");
374 let mmaped = unsafe {
375 candle_core::safetensors::MmapedSafetensors::new(&weights_path)
376 .map_err(Error::Candle)?
377 };
378 let stored_names: Vec<String> =
379 mmaped.tensors().into_iter().map(|(name, _)| name).collect();
380 let backend = SanitizedBackend::new(Box::new(mmaped), stored_names);
381 let vb = VarBuilder::from_backend(Box::new(backend), options.dtype, options.device.clone());
382
383 let model =
384 DecisionModel::load(vb, &encoder_cfg, &agent_cfg, options.dtype, &options.device)?;
385
386 Ok(Self {
387 model_dir: model_dir.to_path_buf(),
388 encoder_cfg,
389 agent_cfg,
390 tokenizer,
391 model,
392 device: options.device,
393 dtype: options.dtype,
394 batch_size: options.batch_size,
395 pad_to_multiple: options.pad_to_multiple,
396 prefix_cache: options
397 .cache_prompts
398 .then(|| Mutex::new(PrefixCache::default())),
399 })
400 }
401
402 pub fn model_dir(&self) -> &Path {
403 &self.model_dir
404 }
405
406 pub fn config(&self) -> &AgentConfig {
407 &self.agent_cfg
408 }
409
410 pub fn encoder_config(&self) -> &EncoderConfig {
411 &self.encoder_cfg
412 }
413
414 pub fn tokenizer(&self) -> &Tokenizer {
415 &self.tokenizer
416 }
417
418 pub fn device(&self) -> &Device {
419 &self.device
420 }
421
422 pub fn dtype(&self) -> DType {
423 self.dtype
424 }
425
426 fn prepare(
428 &self,
429 state: &Value,
430 questions: &Map<String, Value>,
431 ) -> Result<(Vec<Item>, Vec<Question>)> {
432 if questions.is_empty() {
433 return Ok((Vec::new(), Vec::new()));
434 }
435 let max_len = self.agent_cfg.max_len;
436 let head_max_len = self.agent_cfg.head_max_len;
437
438 let state_text = serialize_state(state).replace(self.tokenizer.mask_token.as_str(), " ");
441 let state_ids = self.tokenizer.encode(&state_text)?;
442
443 let entries: Vec<(&String, &Value)> = questions.iter().collect();
444 let build_one = |(qid, definition): &(&String, &Value)| -> Result<(Item, Question)> {
445 let question = Question::from_value(definition)?;
446 let option_count = question.render_options().len();
447
448 let prefix = match &self.prefix_cache {
449 Some(cache) => {
450 let mut cache = cache.lock().expect("prefix cache is not poisoned");
451 cache.get_or_insert(&question, head_max_len, || {
452 build_prefix(&self.tokenizer, &question, head_max_len).map_err(Error::Core)
453 })?
454 }
455 None => build_prefix(&self.tokenizer, &question, head_max_len)?,
456 };
457
458 let Sequence {
459 ids,
460 markers,
461 truncated,
462 } = extend_with_state(&self.tokenizer, &prefix, &state_ids, max_len, false);
463 if markers.len() != option_count {
464 return Err(Error::Core(laya_core::Error::Question(format!(
465 "Question {qid:?} has too many options for the token budget"
466 ))));
467 }
468 let qtype = question.kind;
469 Ok((
470 Item {
471 ids,
472 markers,
473 qtype,
474 truncated,
475 },
476 question,
477 ))
478 };
479
480 let prepared: Vec<(Item, Question)> =
483 if entries.len() >= PARALLEL_THRESHOLD && self.prefix_cache.is_none() {
484 entries.par_iter().map(build_one).collect::<Result<_>>()?
485 } else {
486 entries.iter().map(build_one).collect::<Result<_>>()?
487 };
488
489 Ok(prepared.into_iter().unzip())
490 }
491
492 fn collate(&self, items: &[Item]) -> Result<Batch> {
494 let rows = items.len();
495 let mut length = items.iter().map(|item| item.ids.len()).max().unwrap_or(0);
496 if let Some(multiple) = self.pad_to_multiple {
497 length = length.div_ceil(multiple) * multiple;
498 length = std::cmp::min(length, self.agent_cfg.max_len);
499 }
500 let slots = std::cmp::max(
502 2,
503 items
504 .iter()
505 .map(|item| item.markers.len())
506 .max()
507 .unwrap_or(0),
508 );
509
510 let pad_id = self.tokenizer.pad_token_id;
511 let mut input_ids = vec![pad_id; rows * length];
512 let mut attention_mask = vec![0u8; rows * length];
513 let mut marker_pos = vec![0i64; rows * slots];
514 let mut marker_mask = vec![0u8; rows * slots];
515 let mut qtype = Vec::with_capacity(rows);
516
517 for (row, item) in items.iter().enumerate() {
518 let used = std::cmp::min(item.ids.len(), length);
519 input_ids[row * length..row * length + used].copy_from_slice(&item.ids[..used]);
520 attention_mask[row * length..row * length + used].fill(1);
521 for (slot, marker) in item.markers.iter().enumerate() {
522 marker_pos[row * slots + slot] = *marker as i64;
523 marker_mask[row * slots + slot] = 1;
524 }
525 qtype.push(item.qtype.index() as u32);
526 }
527
528 Ok(Batch {
529 input_ids: Tensor::from_vec(input_ids, (rows, length), &self.device)?,
530 attention_mask: Tensor::from_vec(attention_mask, (rows, length), &self.device)?,
531 marker_pos: Tensor::from_vec(marker_pos, (rows, slots), &self.device)?,
532 marker_mask: Tensor::from_vec(marker_mask, (rows, slots), &self.device)?,
533 qtype: Tensor::from_vec(qtype, rows, &self.device)?,
534 })
535 }
536
537 pub fn predict(&self, state: &Value, questions: &Value) -> Result<Prediction> {
539 let questions = questions.as_object().ok_or_else(|| {
540 Error::Core(laya_core::Error::Question(
541 "questions must be a dictionary keyed by question id".into(),
542 ))
543 })?;
544 self.predict_map(state, questions)
545 }
546
547 pub fn system_one(&self, state: &Value, questions: &Value) -> Result<Prediction> {
549 self.predict(state, questions)
550 }
551
552 pub fn predict_map(&self, state: &Value, questions: &Map<String, Value>) -> Result<Prediction> {
554 let (items, internal) = self.prepare(state, questions)?;
555 let question_ids: Vec<&String> = questions.keys().collect();
556
557 let mut answers = Map::new();
558 let mut typed = Vec::with_capacity(items.len());
559
560 for (chunk_index, chunk) in items.chunks(self.batch_size).enumerate() {
561 let batch = self.collate(chunk)?;
562 let output = self.model.forward(
563 &batch.input_ids,
564 &batch.attention_mask,
565 &batch.marker_pos,
566 &batch.marker_mask,
567 &batch.qtype,
568 )?;
569
570 let logits: Vec<f32> = output.logits.flatten_all()?.to_vec1()?;
571 let action: Vec<f32> = output.action.flatten_all()?.to_vec1()?;
572 if logits.iter().chain(action.iter()).any(|v| !v.is_finite()) {
573 return Err(Error::NonFinite);
574 }
575
576 let slots = output.logits.dim(1)?;
577 let act_classes = output.action.dim(1)?;
578 let offset = chunk_index * self.batch_size;
579
580 for (row, item) in chunk.iter().enumerate() {
581 let index = offset + row;
582 let qid = question_ids[index];
583 let question = &internal[index];
584 let k = item.markers.len();
585
586 let temperature = self.agent_cfg.temperature_for(item.qtype, k);
587 let row_logits = &logits[row * slots..row * slots + k];
588 let probabilities = calibrated_softmax(row_logits, temperature);
589
590 let act_row = &action[row * act_classes..(row + 1) * act_classes];
592 let act_probabilities = laya_core::calibrate::softmax(act_row);
593 let act_probability = round4(act_probabilities.first().copied().unwrap_or(0.0));
594
595 let answer = build_answer(question, &probabilities, k, act_probability);
596 answers.insert(qid.to_string(), answer.to_value());
597 typed.push((qid.to_string(), answer));
598 }
599 }
600
601 Ok(Prediction {
602 model: "laya-rl-agent".into(),
603 answers,
604 typed,
605 input_tokens: items.iter().map(|item| item.ids.len()).sum(),
606 output_tokens: 0,
607 sequence_lengths: items.iter().map(|item| item.ids.len()).collect(),
608 truncated: items.iter().any(|item| item.truncated),
609 routing: None,
610 })
611 }
612}
613
614fn build_answer(
616 question: &Question,
617 probabilities: &[f64],
618 k: usize,
619 act_probability: f64,
620) -> Answer {
621 let rounded: Vec<f64> = probabilities.iter().copied().map(round4).collect();
622 let confidence = round4(confidence_from_probs(probabilities, k));
623
624 match question.kind {
625 QuestionKind::Choice => {
626 let labels = question.choice_labels().unwrap_or_default();
627 let best = probabilities
628 .iter()
629 .enumerate()
630 .fold((0usize, f64::NEG_INFINITY), |acc, (index, value)| {
631 if *value > acc.1 {
633 (index, *value)
634 } else {
635 acc
636 }
637 })
638 .0;
639 Answer {
640 kind: QuestionKind::Choice,
641 confidence,
642 act_probability,
643 choice: labels.get(best).cloned(),
644 labels,
645 probabilities: rounded,
646 score: None,
647 legend: None,
648 noul: None,
649 }
650 }
651 QuestionKind::Score => {
652 let expected: f64 = probabilities
653 .iter()
654 .enumerate()
655 .map(|(index, value)| index as f64 * value)
656 .sum();
657 Answer {
658 kind: QuestionKind::Score,
659 confidence,
660 act_probability,
661 labels: (0..probabilities.len()).map(|i| i.to_string()).collect(),
662 probabilities: rounded,
663 choice: None,
664 score: Some(round4(expected)),
665 legend: question.score_legend(),
666 noul: None,
667 }
668 }
669 QuestionKind::Noul => {
670 let p_true = probabilities.get(1).copied().unwrap_or(0.0);
671 Answer {
672 kind: QuestionKind::Noul,
673 confidence: round4(p_true.max(1.0 - p_true)),
675 act_probability,
676 labels: vec!["false".into(), "true".into()],
677 probabilities: rounded,
678 choice: None,
679 score: None,
680 legend: None,
681 noul: Some(round4(p_true)),
682 }
683 }
684 }
685}