sokuto_runtime/tokenizer/
batch.rs1use indexmap::IndexMap;
8use sokuto_core::contract::model_spec::ModelInputDimensions;
9use sokuto_core::schema::Question;
10
11use crate::error::{Result, RuntimeError};
12use crate::tokenizer::JevTokenizer;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BatchTokenizedQuestions {
17 pub input_ids: Vec<i64>,
19 pub attention_mask: Vec<i64>,
21 pub op_indices: Vec<i64>,
24 pub dims: ModelInputDimensions,
26 pub question_keys: Vec<String>,
28 pub candidate_counts: Vec<usize>,
30 pub option_keys_per_q: Vec<Vec<String>>,
32}
33
34impl JevTokenizer {
35 pub fn encode_batch_questions(
50 &self,
51 state: &str,
52 questions: &IndexMap<String, Question>,
53 ) -> Result<BatchTokenizedQuestions> {
54 self.encode_batch_questions_with_max_len(state, questions, self.max_sequence_length)
55 }
56
57 pub fn encode_batch_questions_with_max_len(
74 &self,
75 state: &str,
76 questions: &IndexMap<String, Question>,
77 max_length: usize,
78 ) -> Result<BatchTokenizedQuestions> {
79 let state_ids = self.encode_state(state)?;
80 self.encode_batch_with_pretokenized_state(&state_ids, questions, max_length)
81 }
82
83 pub fn encode_batch_with_pretokenized_state(
92 &self,
93 state_ids: &[u32],
94 questions: &IndexMap<String, Question>,
95 max_length: usize,
96 ) -> Result<BatchTokenizedQuestions> {
97 let batch_size = questions.len();
98 if batch_size == 0 {
99 return Err(RuntimeError::InvalidQuestion(
100 "質問群が空のためバッチを構築できません。".to_string(),
101 ));
102 }
103
104 let mut question_keys = Vec::with_capacity(batch_size);
105 let mut tokenized_list = Vec::with_capacity(batch_size);
106 let mut candidate_counts = Vec::with_capacity(batch_size);
107 let mut option_keys_per_q = Vec::with_capacity(batch_size);
108
109 let mut max_seq_len = 0usize;
110 let mut max_num_options = 0usize;
111
112 for (q_key, question) in questions {
113 let tokenized = self.encode_with_pretokenized_state(state_ids, question, max_length)?;
114 let seq_len = tokenized.input_ids.len();
115 let num_options = tokenized.op_indices.len();
116
117 if seq_len > max_seq_len {
118 max_seq_len = seq_len;
119 }
120 if num_options > max_num_options {
121 max_num_options = num_options;
122 }
123
124 question_keys.push(q_key.clone());
125 candidate_counts.push(num_options);
126 option_keys_per_q.push(tokenized.option_keys.clone());
127 tokenized_list.push(tokenized);
128 }
129
130 let dims = ModelInputDimensions::new(batch_size, max_seq_len, max_num_options)
131 .map_err(|e| RuntimeError::ModelContractViolation(e.to_string()))?;
132
133 let pad_id = self.pad_token_id.unwrap_or(0) as i64;
134 let total_tokens = batch_size * max_seq_len;
135 let total_options = batch_size * max_num_options;
136
137 let mut input_ids = vec![pad_id; total_tokens];
138 let mut attention_mask = vec![0i64; total_tokens];
139 let mut op_indices = vec![0i64; total_options];
141
142 for (i, tokenized) in tokenized_list.into_iter().enumerate() {
143 let token_offset = i * max_seq_len;
144 let seq_len = tokenized.input_ids.len();
145 input_ids[token_offset..token_offset + seq_len].copy_from_slice(&tokenized.input_ids);
146 attention_mask[token_offset..token_offset + seq_len]
147 .copy_from_slice(&tokenized.attention_mask);
148
149 let op_offset = i * max_num_options;
150 let num_ops = tokenized.op_indices.len();
151 op_indices[op_offset..op_offset + num_ops].copy_from_slice(&tokenized.op_indices);
152 }
153
154 Ok(BatchTokenizedQuestions {
155 input_ids,
156 attention_mask,
157 op_indices,
158 dims,
159 question_keys,
160 candidate_counts,
161 option_keys_per_q,
162 })
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use std::path::PathBuf;
170
171 fn default_tokenizer_path() -> PathBuf {
172 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
173 .parent()
174 .and_then(|p| p.parent())
175 .unwrap()
176 .join("models")
177 .join("default")
178 .join("tokenizer.json")
179 }
180
181 #[test]
182 fn test_encode_batch_questions_empty_error() {
183 let tokenizer_path = default_tokenizer_path();
184 if !tokenizer_path.exists() {
185 return;
186 }
187 let tokenizer = JevTokenizer::from_file(&tokenizer_path).unwrap();
188 let empty_questions = IndexMap::new();
189
190 let result = tokenizer.encode_batch_questions("Context", &empty_questions);
191 assert!(result.is_err());
192 assert!(matches!(
193 result.unwrap_err(),
194 RuntimeError::InvalidQuestion(_)
195 ));
196 }
197
198 #[test]
199 fn test_encode_batch_questions_padding_and_alignment() {
200 let tokenizer_path = default_tokenizer_path();
201 if !tokenizer_path.exists() {
202 return;
203 }
204 let tokenizer = JevTokenizer::from_file(&tokenizer_path).unwrap();
205
206 let mut questions = IndexMap::new();
207 let mut map_a = IndexMap::new();
209 map_a.insert("opt1".to_string(), "選択肢 1".to_string());
210 map_a.insert("opt2".to_string(), "選択肢 2".to_string());
211 questions.insert(
212 "q1".to_string(),
213 Question::new_choice("質問 1 の指示文。", map_a),
214 );
215
216 let mut map_b = IndexMap::new();
218 map_b.insert("a".to_string(), "候補 A".to_string());
219 map_b.insert("b".to_string(), "候補 B".to_string());
220 map_b.insert("c".to_string(), "候補 C".to_string());
221 map_b.insert("d".to_string(), "候補 D".to_string());
222 questions.insert(
223 "q2".to_string(),
224 Question::new_choice(
225 "質問 2 の詳細かつ非常に長い指示文です。文脈に合わせて最適な項目を選択してください。",
226 map_b,
227 ),
228 );
229
230 let batch = tokenizer
231 .encode_batch_questions("共通のコンテキスト情報です。", &questions)
232 .expect("バッチエンコードに成功すること。");
233
234 assert_eq!(batch.dims.batch_size, 2);
235 assert_eq!(batch.dims.num_options, 4);
236 assert_eq!(batch.candidate_counts, vec![2, 4]);
237 assert_eq!(batch.question_keys, vec!["q1", "q2"]);
238
239 let l_max = batch.dims.sequence_length;
240 let k_max = batch.dims.num_options;
241
242 assert_ne!(batch.op_indices[0], 0);
244 assert_ne!(batch.op_indices[1], 0);
245 assert_eq!(batch.op_indices[2], 0);
246 assert_eq!(batch.op_indices[3], 0);
247
248 let q1_tokens = &batch.input_ids[0..l_max];
250 let q1_mask = &batch.attention_mask[0..l_max];
251 let valid_q1_len = q1_mask.iter().filter(|&&m| m == 1).count();
252 assert!(valid_q1_len < l_max);
253 let pad_id = tokenizer.pad_token_id().unwrap_or(0) as i64;
255 for &m in &q1_mask[valid_q1_len..] {
256 assert_eq!(m, 0);
257 }
258 for &token in &q1_tokens[valid_q1_len..] {
259 assert_eq!(token, pad_id);
260 }
261
262 for k in 0..4 {
264 assert_ne!(batch.op_indices[k_max + k], 0);
265 }
266 }
267}