sokuto_cli/commands/
serve.rs1use std::fs;
7use std::net::SocketAddr;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use sokuto_core::contract::calibration::CalibrationConfig;
12use sokuto_runtime::engine::{
13 CacheCompressionMode, DualSessionEngine, ExecutionProvider, InferenceEngine, SessionConfig,
14 StateCacheManager,
15};
16use sokuto_runtime::tokenizer::JevTokenizer;
17use sokuto_server::metrics::setup_metrics_recorder;
18use sokuto_server::state::AppState;
19
20#[derive(Debug, Clone)]
22pub struct ServeArgs {
23 pub host: String,
25 pub port: u16,
27 pub model_dir: PathBuf,
29 pub pool_size: usize,
31 pub provider: Option<String>,
33 pub intra_threads: Option<usize>,
35 pub inter_threads: Option<usize>,
37}
38
39pub fn parse_provider(name: &str) -> Result<ExecutionProvider, String> {
41 match name.to_lowercase().as_str() {
42 "auto" => Ok(ExecutionProvider::Auto),
43 "coreml" => Ok(ExecutionProvider::CoreML),
44 "cuda" => Ok(ExecutionProvider::CUDA),
45 "tensorrt" => Ok(ExecutionProvider::TensorRT),
46 "cpu" => Ok(ExecutionProvider::CPU),
47 other => Err(format!(
48 "未知の Execution Provider です: '{other}'。利用可能: auto, coreml, cuda, tensorrt, cpu。"
49 )),
50 }
51}
52
53pub fn validate_model_dir(dir: &Path) -> Result<(), String> {
55 if !dir.exists() {
56 return Err(format!(
57 "モデルディレクトリが存在しません: {}。",
58 dir.display()
59 ));
60 }
61
62 let onnx_file = dir.join("model.onnx");
63 if !onnx_file.exists() {
64 return Err(format!(
65 "必須モデルファイルが見つかりません: {}。",
66 onnx_file.display()
67 ));
68 }
69
70 let tok_file = dir.join("tokenizer.json");
71 if !tok_file.exists() {
72 return Err(format!(
73 "必須トークナイザー定義ファイルが見つかりません: {}。",
74 tok_file.display()
75 ));
76 }
77
78 Ok(())
79}
80
81pub async fn run_serve(args: ServeArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
86 tracing_subscriber::fmt()
87 .with_env_filter(
88 tracing_subscriber::EnvFilter::try_from_default_env()
89 .unwrap_or_else(|_| "sokuto_server=info,tower_http=info".into()),
90 )
91 .init();
92
93 if let Err(err) = validate_model_dir(&args.model_dir) {
95 tracing::error!("{err}");
96 return Err(err.into());
97 }
98
99 let mut session_config = SessionConfig {
101 pool_size: args.pool_size.max(1),
102 intra_threads: args.intra_threads,
103 inter_threads: args.inter_threads.or(Some(1)),
104 ..Default::default()
105 };
106
107 if let Some(ref provider_str) = args.provider {
108 let provider = parse_provider(provider_str)?;
109 session_config.preferred_providers = match provider {
110 ExecutionProvider::Auto => SessionConfig::default().preferred_providers,
111 other => vec![other, ExecutionProvider::CPU],
112 };
113 }
114
115 let onnx_path = args.model_dir.join("model.onnx");
116 let tok_path = args.model_dir.join("tokenizer.json");
117 let calib_path = args.model_dir.join("calibration.json");
118
119 tracing::info!(
120 "sokuto サーバーを起動します (モデル: {}, アドレス: {}:{}, セッションプール数: {})...",
121 args.model_dir.display(),
122 args.host,
123 args.port,
124 args.pool_size
125 );
126
127 let prometheus_handle = match setup_metrics_recorder() {
129 Ok(handle) => Some(handle),
130 Err(err) => {
131 tracing::warn!("Prometheus メトリクス初期化をスキップしました: {err}");
132 None
133 }
134 };
135
136 let engine = Arc::new(InferenceEngine::new(&onnx_path, session_config.clone())?);
138 let tokenizer = Arc::new(JevTokenizer::from_file(&tok_path)?);
139
140 let calib_config = if calib_path.exists() {
141 let calib_json = fs::read_to_string(&calib_path)?;
142 tracing::info!("較正温度設定をロードしました: {}", calib_path.display());
143 Arc::new(CalibrationConfig::from_json_str(&calib_json)?)
144 } else {
145 tracing::info!("較正設定ファイルが存在しないため、デフォルト温度設定を使用します。");
146 Arc::new(CalibrationConfig::default())
147 };
148
149 let mut state = AppState::new(engine, tokenizer, calib_config)
150 .load_hierarchical_mapping_from_dir(&args.model_dir);
151
152 let state_model_path = args.model_dir.join("state_encoder.onnx");
154 let query_model_path = args.model_dir.join("query_evaluator.onnx");
155 if state_model_path.exists() && query_model_path.exists() {
156 tracing::info!(
157 "2 セッション分割型モデルを検出しました: State={}, Query={}",
158 state_model_path.display(),
159 query_model_path.display()
160 );
161
162 let cache_max_bytes = std::env::var("SOKUTO_STATE_CACHE_MAX_BYTES")
163 .ok()
164 .and_then(|s| s.parse::<u64>().ok())
165 .unwrap_or(512 * 1024 * 1024);
166
167 let cache_ttl_secs = std::env::var("SOKUTO_STATE_CACHE_TTL_SECS")
168 .ok()
169 .and_then(|s| s.parse::<u64>().ok())
170 .unwrap_or(600);
171
172 let compression_mode = match std::env::var("SOKUTO_STATE_CACHE_COMPRESSION")
173 .unwrap_or_else(|_| "fp16".to_string())
174 .to_lowercase()
175 .as_str()
176 {
177 "none" | "fp32" => CacheCompressionMode::None,
178 "int8" => CacheCompressionMode::Int8Symmetric,
179 _ => CacheCompressionMode::Fp16,
180 };
181
182 let cache_manager = Arc::new(StateCacheManager::new(
183 cache_max_bytes,
184 cache_ttl_secs,
185 compression_mode,
186 ));
187
188 match DualSessionEngine::new(
189 &state_model_path,
190 &query_model_path,
191 session_config,
192 Arc::clone(&cache_manager),
193 ) {
194 Ok(dual_engine) => {
195 tracing::info!(
196 "DualSessionEngine を初期化しました (キャッシュ上限: {}MB, 圧縮: {:?})。",
197 cache_max_bytes / (1024 * 1024),
198 compression_mode
199 );
200 state = state.with_dual_engine(Arc::new(dual_engine), cache_manager);
201 }
202 Err(err) => {
203 tracing::warn!(
204 "DualSessionEngine の構築に失敗したため、単一モデルモードで運用します: {err}"
205 );
206 }
207 }
208 }
209
210 let state = Arc::new(state);
211 let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse()?;
212
213 sokuto_server::run_server(addr, state, prometheus_handle).await?;
214
215 Ok(())
216}