1use std::path::Path;
7use std::sync::Mutex;
8use std::sync::atomic::{AtomicUsize, Ordering};
9
10use sokuto_core::contract::calibration::CalibrationConfig;
11use sokuto_core::contract::model_spec::{
12 ModelInputDimensions, TENSOR_ATTENTION_MASK, TENSOR_INPUT_IDS, TENSOR_LOGITS, TENSOR_OP_INDICES,
13};
14use sokuto_core::decision::evaluate_question;
15use sokuto_core::gating::GatingConfig;
16use sokuto_core::schema::{Answer, Question, QuestionType};
17
18use ort::session::Session;
19use ort::session::builder::SessionBuilder;
20use ort::value::TensorRef;
21
22use crate::engine::coarse::{
23 CoarseScorer, CoarseToFineConfig, LexicalCoarseScorer, filter_top_candidates,
24 reconstruct_probabilities,
25};
26use crate::engine::config::{ExecutionProvider, SessionConfig, auto_intra_threads};
27use crate::engine::gating::{apply_gating_to_answer_with_energy, calculate_choice_energy};
28use crate::engine::provider::register_execution_providers;
29use crate::error::{Result, RuntimeError};
30use crate::hierarchical::{
31 CoarseToFineRouter, HierarchicalMapping, HierarchicalResult, HierarchicalRouterConfig,
32};
33use crate::tokenizer::{JevTokenizer, TokenizedQuestion};
34
35pub struct InferenceEngine {
41 sessions: Vec<Mutex<Session>>,
43 next_session_idx: AtomicUsize,
45 active_provider: ExecutionProvider,
47 input_names: Vec<String>,
49 output_names: Vec<String>,
51}
52
53impl InferenceEngine {
54 pub fn new(model_path: impl AsRef<Path>, config: SessionConfig) -> Result<Self> {
60 let model_path_ref = model_path.as_ref();
61 if !model_path_ref.exists() {
62 return Err(RuntimeError::ModelContractViolation(format!(
63 "モデルファイルが存在しません: {}。",
64 model_path_ref.display()
65 )));
66 }
67
68 let pool_size = config.pool_size.max(1);
69 let mut sessions = Vec::with_capacity(pool_size);
70 let mut active_provider = ExecutionProvider::CPU;
71
72 tracing::info!(
73 "推論セッションの初期化を開始します (パス: {}, プール数: {})...",
74 model_path_ref.display(),
75 pool_size
76 );
77
78 for i in 0..pool_size {
79 let (mut builder, provider) = create_session_builder(&config, pool_size)?;
80 if i == 0 {
81 active_provider = provider;
82 }
83
84 let session = builder
85 .commit_from_file(model_path_ref)
86 .map_err(RuntimeError::Ort)?;
87 sessions.push(Mutex::new(session));
88 }
89
90 Self::init_with_sessions(sessions, active_provider)
91 }
92
93 pub fn from_bytes(model_bytes: &[u8], config: SessionConfig) -> Result<Self> {
99 let pool_size = config.pool_size.max(1);
100 let mut sessions = Vec::with_capacity(pool_size);
101 let mut active_provider = ExecutionProvider::CPU;
102
103 tracing::info!(
104 "バイト列からの推論セッション初期化を開始します (サイズ: {} bytes, プール数: {})...",
105 model_bytes.len(),
106 pool_size
107 );
108
109 for i in 0..pool_size {
110 let (mut builder, provider) = create_session_builder(&config, pool_size)?;
111 if i == 0 {
112 active_provider = provider;
113 }
114
115 let session = builder
116 .commit_from_memory(model_bytes)
117 .map_err(RuntimeError::Ort)?;
118 sessions.push(Mutex::new(session));
119 }
120
121 Self::init_with_sessions(sessions, active_provider)
122 }
123
124 fn init_with_sessions(
126 sessions: Vec<Mutex<Session>>,
127 active_provider: ExecutionProvider,
128 ) -> Result<Self> {
129 let first_session = sessions[0].lock().map_err(|e| {
130 RuntimeError::ModelContractViolation(format!("セッションロック獲得失敗: {e}。"))
131 })?;
132
133 let input_names: Vec<String> = first_session
134 .inputs()
135 .iter()
136 .map(|o| o.name().to_string())
137 .collect();
138 let output_names: Vec<String> = first_session
139 .outputs()
140 .iter()
141 .map(|o| o.name().to_string())
142 .collect();
143
144 if !input_names.iter().any(|name| name == TENSOR_INPUT_IDS) {
146 return Err(RuntimeError::ModelContractViolation(format!(
147 "モデルに必須入力テンソル '{TENSOR_INPUT_IDS}' が存在しません: 検出={input_names:?}。"
148 )));
149 }
150 if !input_names.iter().any(|name| name == TENSOR_ATTENTION_MASK) {
151 return Err(RuntimeError::ModelContractViolation(format!(
152 "モデルに必須入力テンソル '{TENSOR_ATTENTION_MASK}' が存在しません: 検出={input_names:?}。"
153 )));
154 }
155 if !input_names.iter().any(|name| name == TENSOR_OP_INDICES) {
156 return Err(RuntimeError::ModelContractViolation(format!(
157 "モデルに必須入力テンソル '{TENSOR_OP_INDICES}' が存在しません: 検出={input_names:?}。"
158 )));
159 }
160
161 if !output_names.iter().any(|name| name == TENSOR_LOGITS) {
163 return Err(RuntimeError::ModelContractViolation(format!(
164 "モデルに必須出力テンソル '{TENSOR_LOGITS}' が存在しません: 検出={output_names:?}。"
165 )));
166 }
167
168 drop(first_session);
169
170 tracing::info!(
171 "モデル契約検証が完了しました: 入力={input_names:?}, 出力={output_names:?}, アクティブEP={}",
172 active_provider.name()
173 );
174
175 Ok(Self {
176 sessions,
177 next_session_idx: AtomicUsize::new(0),
178 active_provider,
179 input_names,
180 output_names,
181 })
182 }
183
184 pub fn active_provider(&self) -> ExecutionProvider {
186 self.active_provider
187 }
188
189 pub fn input_names(&self) -> &[String] {
191 &self.input_names
192 }
193
194 pub fn output_names(&self) -> &[String] {
196 &self.output_names
197 }
198
199 pub fn pool_size(&self) -> usize {
201 self.sessions.len()
202 }
203
204 pub fn forward_raw(
215 &self,
216 input_ids: &[i64],
217 attention_mask: &[i64],
218 op_indices: &[i64],
219 dims: ModelInputDimensions,
220 ) -> Result<Vec<Vec<f64>>> {
221 let total_logits = dims.batch_size * dims.num_options;
222 let mut flat_logits = vec![0.0f64; total_logits];
223
224 self.forward_raw_into(
225 input_ids,
226 attention_mask,
227 op_indices,
228 dims,
229 &mut flat_logits,
230 )?;
231
232 let mut batched_logits = Vec::with_capacity(dims.batch_size);
233 for chunk in flat_logits.chunks_exact(dims.num_options) {
234 batched_logits.push(chunk.to_vec());
235 }
236
237 Ok(batched_logits)
238 }
239
240 pub fn forward_raw_into(
252 &self,
253 input_ids: &[i64],
254 attention_mask: &[i64],
255 op_indices: &[i64],
256 dims: ModelInputDimensions,
257 out_logits: &mut [f64],
258 ) -> Result<()> {
259 let batch_size = dims.batch_size;
260 let seq_len = dims.sequence_length;
261 let num_options = dims.num_options;
262
263 if input_ids.len() != batch_size * seq_len {
265 return Err(RuntimeError::InvalidTensorData(format!(
266 "input_ids 長さ ({}) が期待値 ({} * {} = {}) と一致しません。",
267 input_ids.len(),
268 batch_size,
269 seq_len,
270 batch_size * seq_len
271 )));
272 }
273 if attention_mask.len() != batch_size * seq_len {
274 return Err(RuntimeError::InvalidTensorData(format!(
275 "attention_mask 長さ ({}) が期待値 ({} * {} = {}) と一致しません。",
276 attention_mask.len(),
277 batch_size,
278 seq_len,
279 batch_size * seq_len
280 )));
281 }
282 if op_indices.len() != batch_size * num_options {
283 return Err(RuntimeError::InvalidTensorData(format!(
284 "op_indices 長さ ({}) が期待値 ({} * {} = {}) と一致しません。",
285 op_indices.len(),
286 batch_size,
287 num_options,
288 batch_size * num_options
289 )));
290 }
291 if out_logits.len() != batch_size * num_options {
292 return Err(RuntimeError::InvalidTensorData(format!(
293 "出力バッファ長 ({}) が期待値 ({} * {} = {}) と一致しません。",
294 out_logits.len(),
295 batch_size,
296 num_options,
297 batch_size * num_options
298 )));
299 }
300
301 for (i, &idx) in op_indices.iter().enumerate() {
303 if idx < 0 || idx >= seq_len as i64 {
304 return Err(RuntimeError::InvalidTensorData(format!(
305 "op_indices[{i}] = {idx} が系列長 (0..{seq_len}) の範囲外です。"
306 )));
307 }
308 }
309
310 let input_ids_tensor = TensorRef::from_array_view(([batch_size, seq_len], input_ids))
312 .map_err(RuntimeError::Ort)?;
313 let attention_mask_tensor =
314 TensorRef::from_array_view(([batch_size, seq_len], attention_mask))
315 .map_err(RuntimeError::Ort)?;
316 let op_indices_tensor = TensorRef::from_array_view(([batch_size, num_options], op_indices))
317 .map_err(RuntimeError::Ort)?;
318
319 let pool_len = self.sessions.len();
321 let mut acquired_guard = None;
322
323 let start_idx = self.next_session_idx.fetch_add(1, Ordering::Relaxed) % pool_len;
325 for offset in 0..pool_len {
326 let idx = (start_idx + offset) % pool_len;
327 if let Ok(guard) = self.sessions[idx].try_lock() {
328 acquired_guard = Some(guard);
329 break;
330 }
331 }
332
333 let mut session = match acquired_guard {
335 Some(g) => g,
336 None => self.sessions[start_idx].lock().map_err(|e| {
337 RuntimeError::ModelContractViolation(format!("セッションロック獲得失敗: {e}。"))
338 })?,
339 };
340
341 let outputs = session
343 .run(ort::inputs![
344 TENSOR_INPUT_IDS => input_ids_tensor,
345 TENSOR_ATTENTION_MASK => attention_mask_tensor,
346 TENSOR_OP_INDICES => op_indices_tensor,
347 ])
348 .map_err(RuntimeError::Ort)?;
349
350 let logits_output = outputs.get(TENSOR_LOGITS).ok_or_else(|| {
352 RuntimeError::ModelContractViolation(format!(
353 "出力テンソル '{TENSOR_LOGITS}' が存在しません。"
354 ))
355 })?;
356
357 let (shape, logits_f32) = logits_output
358 .try_extract_tensor::<f32>()
359 .map_err(RuntimeError::Ort)?;
360
361 if shape.len() != 2 {
363 return Err(RuntimeError::ModelContractViolation(format!(
364 "出力テンソル '{TENSOR_LOGITS}' は 2 次元テンソル [batch, num_options] である必要があります: {shape:?}。"
365 )));
366 }
367 let shape_stack = [shape[0] as usize, shape[1] as usize];
368 dims.validate_output_shape(&shape_stack)?;
369
370 for (i, &val) in logits_f32.iter().enumerate() {
372 if !val.is_finite() {
373 return Err(RuntimeError::InvalidTensorData(format!(
374 "logits[{i}] に非有限値 (NaN または Inf) が検出されました: {val}。"
375 )));
376 }
377 out_logits[i] = val as f64;
378 }
379
380 Ok(())
381 }
382
383 pub fn forward_question(&self, tokenized: &TokenizedQuestion) -> Result<Vec<f64>> {
391 let dims =
392 ModelInputDimensions::new(1, tokenized.input_ids.len(), tokenized.op_indices.len())?;
393
394 let logits = self.forward_raw(
395 &tokenized.input_ids,
396 &tokenized.attention_mask,
397 &tokenized.op_indices,
398 dims,
399 )?;
400
401 logits
402 .into_iter()
403 .next()
404 .ok_or_else(|| RuntimeError::InvalidTensorData("推論出力が空です。".to_string()))
405 }
406
407 pub fn evaluate_question_coarse_to_fine_with_scorer<S: CoarseScorer>(
443 &self,
444 tokenizer: &JevTokenizer,
445 state: &str,
446 question: &Question,
447 calib_config: &CalibrationConfig,
448 coarse_config: &CoarseToFineConfig,
449 scorer: &S,
450 ) -> Result<Answer> {
451 self.evaluate_question_coarse_to_fine_with_gating_and_scorer(
452 tokenizer,
453 state,
454 question,
455 calib_config,
456 coarse_config,
457 None,
458 scorer,
459 )
460 }
461
462 #[allow(clippy::too_many_arguments)]
473 pub fn evaluate_question_coarse_to_fine_with_gating_and_scorer<S: CoarseScorer>(
474 &self,
475 tokenizer: &JevTokenizer,
476 state: &str,
477 question: &Question,
478 calib_config: &CalibrationConfig,
479 coarse_config: &CoarseToFineConfig,
480 gating_config: Option<&GatingConfig>,
481 scorer: &S,
482 ) -> Result<Answer> {
483 coarse_config.validate()?;
484
485 let should_trigger = question.question_type == QuestionType::Choice
487 && question
488 .criteria
489 .as_ref()
490 .and_then(|c| c.as_map())
491 .map(|map| coarse_config.should_trigger(map.len()))
492 .unwrap_or(false);
493
494 if !should_trigger {
495 let tokenized = tokenizer.encode_question(state, question)?;
497 let logits = self.forward_question(&tokenized)?;
498 let mut answer =
499 evaluate_question(question, &logits, calib_config).map_err(RuntimeError::Core)?;
500 let energy = calculate_choice_energy(question, &logits, calib_config);
501 apply_gating_to_answer_with_energy(
502 &mut answer,
503 energy,
504 Some(state),
505 None,
506 Some(question),
507 gating_config,
508 calib_config,
509 );
510 return Ok(answer);
511 }
512
513 let map = question
514 .criteria
515 .as_ref()
516 .and_then(|c| c.as_map())
517 .expect("should_trigger により Map の存在が保証されている。");
518
519 let original_keys: Vec<String> = map.keys().cloned().collect();
520 let query = format!("State: {}\nInstructions: {}", state, question.instructions);
521
522 let filtered = filter_top_candidates(&query, map, coarse_config, scorer)?;
524
525 let sub_question = Question::new_choice(question.instructions.clone(), filtered.selected);
527
528 let sub_tokenized = tokenizer.encode_question(state, &sub_question)?;
530 let sub_logits = self.forward_question(&sub_tokenized)?;
531
532 let sub_answer = evaluate_question(&sub_question, &sub_logits, calib_config)
534 .map_err(RuntimeError::Core)?;
535
536 let full_probabilities = sub_answer
538 .probabilities
539 .as_ref()
540 .map(|sub_probs| reconstruct_probabilities(&original_keys, sub_probs));
541
542 let mut answer = Answer {
543 choice: sub_answer.choice,
544 confidence: sub_answer.confidence,
545 probabilities: full_probabilities,
546 score: None,
547 noul: None,
548 gating: None,
549 };
550
551 let energy = calculate_choice_energy(&sub_question, &sub_logits, calib_config);
553 apply_gating_to_answer_with_energy(
554 &mut answer,
555 energy,
556 Some(state),
557 None,
558 Some(question),
559 gating_config,
560 calib_config,
561 );
562
563 Ok(answer)
564 }
565
566 pub fn evaluate_question_coarse_to_fine(
575 &self,
576 tokenizer: &JevTokenizer,
577 state: &str,
578 question: &Question,
579 calib_config: &CalibrationConfig,
580 coarse_config: &CoarseToFineConfig,
581 ) -> Result<Answer> {
582 let scorer = LexicalCoarseScorer::new(tokenizer);
583 self.evaluate_question_coarse_to_fine_with_scorer(
584 tokenizer,
585 state,
586 question,
587 calib_config,
588 coarse_config,
589 &scorer,
590 )
591 }
592
593 pub fn evaluate_question_coarse_to_fine_with_gating(
603 &self,
604 tokenizer: &JevTokenizer,
605 state: &str,
606 question: &Question,
607 calib_config: &CalibrationConfig,
608 coarse_config: &CoarseToFineConfig,
609 gating_config: Option<&GatingConfig>,
610 ) -> Result<Answer> {
611 let scorer = LexicalCoarseScorer::new(tokenizer);
612 self.evaluate_question_coarse_to_fine_with_gating_and_scorer(
613 tokenizer,
614 state,
615 question,
616 calib_config,
617 coarse_config,
618 gating_config,
619 &scorer,
620 )
621 }
622
623 #[allow(clippy::too_many_arguments)]
640 pub fn evaluate_question_hierarchical(
641 &self,
642 tokenizer: &JevTokenizer,
643 state: &str,
644 instructions: &str,
645 mapping: &HierarchicalMapping,
646 calib_config: &CalibrationConfig,
647 config: HierarchicalRouterConfig,
648 gating_config: Option<&GatingConfig>,
649 ) -> Result<HierarchicalResult> {
650 let router = CoarseToFineRouter::new(self, tokenizer, mapping, calib_config, config)?;
651 router.route(state, instructions, gating_config)
652 }
653
654 pub fn forward_question_raw(
666 &self,
667 tokenizer: &JevTokenizer,
668 state: &str,
669 question: &Question,
670 ) -> Result<Vec<f64>> {
671 let tokenized = tokenizer.encode_question(state, question)?;
672 self.forward_question(&tokenized)
673 }
674
675 pub fn predict_single_question(
686 &self,
687 tokenizer: &JevTokenizer,
688 state: &str,
689 question: &Question,
690 calib_config: &CalibrationConfig,
691 ) -> Result<Answer> {
692 let tokenized = tokenizer.encode_question(state, question)?;
693 let logits = self.forward_question(&tokenized)?;
694 let mut answer =
695 evaluate_question(question, &logits, calib_config).map_err(RuntimeError::Core)?;
696
697 let energy = calculate_choice_energy(question, &logits, calib_config);
698 apply_gating_to_answer_with_energy(
699 &mut answer,
700 energy,
701 Some(state),
702 None,
703 Some(question),
704 None,
705 calib_config,
706 );
707
708 Ok(answer)
709 }
710}
711
712pub(crate) fn create_session_builder(
717 config: &SessionConfig,
718 pool_size: usize,
719) -> Result<(SessionBuilder, ExecutionProvider)> {
720 let mut builder = Session::builder().map_err(RuntimeError::Ort)?;
721
722 builder = builder
723 .with_optimization_level(config.optimization_level.to_ort_level())
724 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
725
726 builder = builder
727 .with_memory_pattern(config.enable_mem_arena)
728 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
729
730 builder = builder
731 .with_parallel_execution(config.parallel_execution)
732 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
733
734 let intra = config
736 .intra_threads
737 .unwrap_or_else(|| auto_intra_threads(pool_size, 8));
738 builder = builder
739 .with_intra_threads(intra)
740 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
741
742 if let Some(inter) = config.inter_threads {
743 builder = builder
744 .with_inter_threads(inter)
745 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
746 }
747
748 if let Some(ref opt_path) = config.optimized_model_path {
749 builder = builder
750 .with_optimized_model_path(opt_path)
751 .map_err(|e| RuntimeError::OrtConfig(e.to_string()))?;
752 }
753
754 let provider = register_execution_providers(
755 &mut builder,
756 &config.preferred_providers,
757 config.enable_mem_arena,
758 )?;
759
760 Ok((builder, provider))
761}