Skip to main content

sokuto_runtime/engine/
session.rs

1//! # ONNX Runtime 推論セッション管理モジュール
2//!
3//! エクスポート済み ONNX モデルグラフのライフサイクル、入力テンソル整合性検証、
4//! ゼロコピーテンソルバインド、およびセッションプールによる高並列フォワードパスを提供する。
5
6use 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
35/// スレッドセーフな ONNX Runtime 推論エンジン。
36///
37/// 内部にセッションプールを保持し、複数スレッドからの並行推論要求に対して
38/// アロケーションフリーかつ競合を最小化したフォワードパスを実行する。
39/// `Arc<InferenceEngine>` として複数ワーカー間で安全に共有可能である。
40pub struct InferenceEngine {
41    /// セッションプール。並行度に応じた複数の ORT セッションを保持する。
42    sessions: Vec<Mutex<Session>>,
43    /// ラウンドロビン選択用インデックスカウンター。
44    next_session_idx: AtomicUsize,
45    /// 実際に有効化された Execution Provider。
46    active_provider: ExecutionProvider,
47    /// モデルが要求する入力テンソル名一覧。
48    input_names: Vec<String>,
49    /// モデルが出力するテンソル名一覧。
50    output_names: Vec<String>,
51}
52
53impl InferenceEngine {
54    /// ファイルパスからモデルを読み込み、推論エンジンを初期化する。
55    ///
56    /// # 引数
57    /// - `model_path`: ONNX モデルファイルのパス。
58    /// - `config`: セッション構築設定。
59    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    /// メモリ上のバイト列からモデルを読み込み、推論エンジンを初期化する。
94    ///
95    /// # 引数
96    /// - `model_bytes`: ONNX モデルバイナリデータ。
97    /// - `config`: セッション構築設定。
98    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    /// 構築済みセッション群から契約を検証し、エンジンインスタンスを完成させる。
125    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        // 必須入力テンソル契約の検証
145        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        // 必須出力テンソル契約の検証
162        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    /// 有効化された Execution Provider を取得する。
185    pub fn active_provider(&self) -> ExecutionProvider {
186        self.active_provider
187    }
188
189    /// モデルの入力テンソル名一覧を取得する。
190    pub fn input_names(&self) -> &[String] {
191        &self.input_names
192    }
193
194    /// モデルの出力テンソル名一覧を取得する。
195    pub fn output_names(&self) -> &[String] {
196        &self.output_names
197    }
198
199    /// セッションプールサイズを取得する。
200    pub fn pool_size(&self) -> usize {
201        self.sessions.len()
202    }
203
204    /// 平坦化された生テンソルスライスを受け取り、推論を実行してロジットを返却する。
205    ///
206    /// # 引数
207    /// - `input_ids`: トークン ID 列 (`[batch_size * seq_len]`)。
208    /// - `attention_mask`: アテンションマスク列 (`[batch_size * seq_len]`)。
209    /// - `op_indices`: オプションマーカー位置インデックス (`[batch_size * num_options]`)。
210    /// - `dims`: 入力テンソルの次元情報 (`batch_size`, `sequence_length`, `num_options`)。
211    ///
212    /// # 戻り値
213    /// 各バッチサンプルのロジット配列 (`Vec<Vec<f64>>`)。
214    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    /// 平坦化された生テンソルスライスを受け取り、推論結果を指定されたスライスに書き込む。
241    ///
242    /// 入力テンソルは借用ビュー (`TensorRef`) によりゼロコピーで ORT に渡され、
243    /// 出力も事前確保済みスライスに直接展開されるため、ヒープ確保が発生しない。
244    ///
245    /// # 引数
246    /// - `input_ids`: トークン ID 列 (`[batch_size * seq_len]`)。
247    /// - `attention_mask`: アテンションマスク列 (`[batch_size * seq_len]`)。
248    /// - `op_indices`: オプションマーカー位置インデックス (`[batch_size * num_options]`)。
249    /// - `dims`: 入力テンソルの次元情報 (`batch_size`, `sequence_length`, `num_options`)。
250    /// - `out_logits`: 結果ロジットの書き込み先バッファ (`[batch_size * num_options]`)。
251    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        // 1. 次元整合性検証
264        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        // 2. op_indices の範囲外インデックスチェック (Gather 層クラッシュ・セグフォ防止)
302        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        // 3. 入力 ORT TensorRef をゼロコピー借用ビューで構築 (.to_vec() 排除)
311        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        // 4. セッションプールから空きセッションを獲得 (非ブロッキング探索 -> ラウンドロビン待機)
320        let pool_len = self.sessions.len();
321        let mut acquired_guard = None;
322
323        // まずロックされていない空きセッションを優先探索
324        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        // 全セッションがビジーの場合は指定セッションをブロック待機
334        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        // 5. 推論実行
342        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        // 6. 出力抽出と検証
351        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        // 出力形状の契約検証 (スタック配列によるゼロアロケーション化)
362        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        // 7. 出力値の有限性 (NaN/Inf) 検証および f32 -> f64 キャスト
371        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    /// 単一のトークナイズ済み質問を受け取り、ロジット配列を算出する。
384    ///
385    /// # 引数
386    /// - `tokenized`: 高速トークナイザーから得られた `TokenizedQuestion`。
387    ///
388    /// # 戻り値
389    /// 各選択肢に対応する決定ロジット (`Vec<f64>`)。
390    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    /// 任意の類似度スコアラーを指定して、粗密 2 段階探索 (Coarse-to-Fine) で単一質問を評価する。
408    ///
409    /// # 処理フロー
410    /// 1. `question.question_type == QuestionType::Choice` かつ `criteria.len() > coarse_config.threshold` の場合にのみ 2 段階探索を発動。
411    /// 2. ネガティブ候補保護 (Pinning) およびスコアラーによる Top-M スクリーニングを実施。
412    /// 3. 絞り込み後の候補マップを用いて部分質問 `sub_question` を構築。
413    /// 4. `tokenizer.encode_question` および `self.forward_question` を実行。
414    /// 5. 較正設定 `calib_config` (Fine 候補数 $M$ に対応するバケット) を適用して `sub_answer` を導出。
415    /// 6. `reconstruct_probabilities` により除外候補を確率 0.0 として全候補確率マップを再構成して返却。
416    ///
417    /// 発動条件を満たさない質問 (候補数 $K \le \text{threshold}$、または Score/Noul 型) は、
418    /// 直接通常推論 (`encode_question` -> `forward_question` -> `evaluate_question`) へバイパスする。
419    ///
420    /// # 引数
421    /// 任意の類似度スコアラーを指定して、粗密 2 段階探索 (Coarse-to-Fine) で単一質問を評価する。
422    ///
423    /// # 処理フロー
424    /// 1. `question.question_type == QuestionType::Choice` かつ `criteria.len() > coarse_config.threshold` の場合にのみ 2 段階探索を発動。
425    /// 2. ネガティブ候補保護 (Pinning) およびスコアラーによる Top-M スクリーニングを実施。
426    /// 3. 絞り込み後の候補マップを用いて部分質問 `sub_question` を構築。
427    /// 4. `tokenizer.encode_question` および `self.forward_question` を実行。
428    /// 5. 較正設定 `calib_config` (Fine 候補数 $M$ に対応するバケット) を適用して `sub_answer` を導出。
429    /// 6. `reconstruct_probabilities` により除外候補を確率 0.0 として全候補確率マップを再構成。
430    /// 7. 較正設定の閾値またはリクエスト指定設定に基づき、透過的に確信度ゲーティングを適用して返却。
431    ///
432    /// 発動条件を満たさない質問 (候補数 $K \le \text{threshold}$、または Score/Noul 型) は、
433    /// 直接通常推論 (`encode_question` -> `forward_question` -> `evaluate_question`) へバイパスする。
434    ///
435    /// # 引数
436    /// - `tokenizer`: Jev 高速トークナイザー。
437    /// - `state`: 文脈テキスト。
438    /// - `question`: 質問定義。
439    /// - `calib_config`: 較正温度設定。
440    /// - `coarse_config`: 粗密探索設定。
441    /// - `scorer`: 類似度スコアラー実装。
442    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    /// 任意の類似度スコアラーとゲーティング設定を指定して、粗密 2 段階探索で単一質問を評価する。
463    ///
464    /// # 引数
465    /// - `tokenizer`: Jev 高速トークナイザー。
466    /// - `state`: 文脈テキスト。
467    /// - `question`: 質問定義。
468    /// - `calib_config`: 較正温度設定。
469    /// - `coarse_config`: 粗密探索設定。
470    /// - `gating_config`: 明示的なゲーティング設定 (任意、未指定時は較正設定から導出)。
471    /// - `scorer`: 類似度スコアラー実装。
472    #[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        // 発動条件の検査: Choice 型 かつ Criteria が Map 形式 かつ 候補数 > threshold
486        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            // バイパス: 通常の単一推論を実行
496            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        // 1. Coarse 粗スクリーニング
523        let filtered = filter_top_candidates(&query, map, coarse_config, scorer)?;
524
525        // 2. 縮小質問の構築
526        let sub_question = Question::new_choice(question.instructions.clone(), filtered.selected);
527
528        // 3. Fine 精密推論
529        let sub_tokenized = tokenizer.encode_question(state, &sub_question)?;
530        let sub_logits = self.forward_question(&sub_tokenized)?;
531
532        // 4. 縮小候補空間での決定数理解決 (M 候補の温度バケットが適用される)
533        let sub_answer = evaluate_question(&sub_question, &sub_logits, calib_config)
534            .map_err(RuntimeError::Core)?;
535
536        // 5. 全候補空間に対する確率マップの再構成
537        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        // 6. ゲーティング判定の適用 (縮小空間での確信度・正規化自由エネルギーを尊重しつつ監査メタデータを付与)
552        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    /// 既定の語彙スコアラー (`LexicalCoarseScorer`) を用いて、粗密 2 段階探索で単一質問を評価する。
567    ///
568    /// # 引数
569    /// - `tokenizer`: Jev 高速トークナイザー。
570    /// - `state`: 文脈テキスト。
571    /// - `question`: 質問定義。
572    /// - `calib_config`: 較正温度設定。
573    /// - `coarse_config`: 粗密探索設定。
574    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    /// 既定の語彙スコアラーとゲーティング設定を指定して、粗密 2 段階探索で単一質問を評価する。
594    ///
595    /// # 引数
596    /// - `tokenizer`: Jev 高速トークナイザー。
597    /// - `state`: 文脈テキスト。
598    /// - `question`: 質問定義。
599    /// - `calib_config`: 較正温度設定。
600    /// - `coarse_config`: 粗密探索設定。
601    /// - `gating_config`: 明示的なゲーティング設定 (任意)。
602    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    /// 粗密二段階階層ルーティング (Coarse-to-Fine Hierarchical Routing) により単一質問を評価する。
624    ///
625    /// 大分類オントロジーと Soft-Beam、大分類エントロピー連動温度調整を活用し、
626    /// 高基数分類 ($K > 20 \sim 30$) においても高精度かつ低遅延に決定を出力する。
627    ///
628    /// # 引数
629    /// - `tokenizer`: Jev 高速トークナイザー。
630    /// - `state`: 文脈テキスト。
631    /// - `instructions`: 質問指示文。
632    /// - `mapping`: 大分類と細分類のオントロジー階層マッピング。
633    /// - `calib_config`: 較正温度設定。
634    /// - `config`: 階層ルーター設定 (Soft-Beam, Early-Exit 閾値など)。
635    /// - `gating_config`: ゲーティング閾値設定 (任意)。
636    ///
637    /// # 戻り値
638    /// 階層推論結果 (`HierarchicalResult`)。
639    #[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    /// 単一の質問をトークナイズして生ロジット配列 (Softmax 正規化前) を算出する。
655    ///
656    /// 粗密推論における第 2 パス動的温度適用や、独自の後処理を行うために使用する。
657    ///
658    /// # 引数
659    /// - `tokenizer`: Jev 高速トークナイザー。
660    /// - `state`: 文脈テキスト。
661    /// - `question`: 質問仕様。
662    ///
663    /// # 戻り値
664    /// 各選択肢の未正規化生ロジット列 (`Vec<f64>`)。
665    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    /// 単一の質問をトークナイズから決定数理評価まで一括して実行し、決定結果を返却する。
676    ///
677    /// # 引数
678    /// - `tokenizer`: Jev 高速トークナイザー。
679    /// - `state`: 文脈テキスト。
680    /// - `question`: 質問仕様。
681    /// - `calib_config`: 較正温度設定。
682    ///
683    /// # 戻り値
684    /// 確率較正済みの `Answer`。
685    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
712/// 指定された設定とプールサイズに基づいて `SessionBuilder` を構成する内部共通ヘルパー関数。
713///
714/// グラフ最適化レベル、メモリアリーナ、並列実行モード、物理コア数に応じた
715/// `intra_threads` 自動設定、および最適化済みモデルのキャッシュ設定を適用する。
716pub(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    // スレッド数の決定: 未指定時は物理コア数とセッションプール数から自動算出
735    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}