1use ferrotensor_loader::Tokenizer;
7use serde::Deserialize;
8use std::fs;
9use std::path::Path;
10use std::sync::Arc;
11
12pub trait Dataset: Send + Sync {
14 fn len(&self) -> usize;
16
17 fn get(&self, idx: usize) -> (Vec<u32>, Vec<u32>);
20
21 fn get_loss_mask(&self, idx: usize) -> Option<Vec<u8>> {
23 let (_, targets) = self.get(idx);
24 Some(vec![1u8; targets.len()])
25 }
26
27 fn is_empty(&self) -> bool {
29 self.len() == 0
30 }
31}
32
33#[derive(Clone, Debug)]
35pub struct TrainingExample {
36 pub input_ids: Vec<u32>,
38 pub target_ids: Vec<u32>,
40 pub loss_mask: Vec<u8>,
42}
43
44impl TrainingExample {
45 pub fn from_next_token(sequence: Vec<u32>) -> Self {
48 if sequence.len() < 2 {
49 let len = sequence.len();
50 return Self {
51 input_ids: sequence.clone(),
52 target_ids: sequence,
53 loss_mask: vec![1u8; len],
54 };
55 }
56 let input = sequence[..sequence.len() - 1].to_vec();
57 let target = sequence[1..].to_vec();
58 let loss_mask = vec![1u8; target.len()];
59 Self {
60 input_ids: input,
61 target_ids: target,
62 loss_mask,
63 }
64 }
65
66 pub fn from_sft_prompt_response(prompt_tokens: Vec<u32>, response_tokens: Vec<u32>) -> Self {
71 let mut full = prompt_tokens.clone();
72 full.extend(response_tokens.iter().copied());
73 if full.len() < 2 {
74 return Self::from_next_token(full);
75 }
76
77 let input = full[..full.len() - 1].to_vec();
78 let target = full[1..].to_vec();
79
80 let mut loss_mask = vec![0u8; target.len()];
84 if !prompt_tokens.is_empty() && !response_tokens.is_empty() {
85 let first_response_target = prompt_tokens.len().saturating_sub(1);
86 loss_mask[first_response_target..].fill(1);
87 } else {
88 loss_mask.fill(1);
89 }
90
91 Self {
92 input_ids: input,
93 target_ids: target,
94 loss_mask,
95 }
96 }
97}
98
99#[derive(Clone)]
101pub struct TextDataset {
102 samples: Vec<TrainingExample>,
103}
104
105impl TextDataset {
106 pub fn new() -> Self {
108 Self {
109 samples: Vec::new(),
110 }
111 }
112
113 pub fn add_sample(&mut self, input_ids: Vec<u32>, target_ids: Vec<u32>) {
115 let loss_mask = vec![1u8; target_ids.len()];
116 self.samples.push(TrainingExample {
117 input_ids,
118 target_ids,
119 loss_mask,
120 });
121 }
122
123 pub fn add_example(&mut self, example: TrainingExample) {
125 self.samples.push(example);
126 }
127
128 pub fn from_sequences(sequences: Vec<Vec<u32>>) -> Self {
130 let samples = sequences
131 .into_iter()
132 .map(TrainingExample::from_next_token)
133 .collect();
134 Self { samples }
135 }
136
137 pub fn from_jsonl_sft<P: AsRef<Path>>(
140 path: P,
141 tokenizer: &Tokenizer,
142 use_chat_template: bool,
143 max_len: usize,
144 ) -> Result<Self, std::io::Error> {
145 #[derive(Deserialize)]
146 struct JsonlSample {
147 prompt: String,
148 response: String,
149 #[serde(default)]
150 system: Option<String>,
151 }
152
153 let content = fs::read_to_string(path)?;
154 let mut samples = Vec::new();
155 for (line_no, line) in content.lines().enumerate() {
156 if line.trim().is_empty() {
157 continue;
158 }
159 let sample = serde_json::from_str::<JsonlSample>(line).map_err(|e| {
160 std::io::Error::new(
161 std::io::ErrorKind::InvalidData,
162 format!(
163 "line {}: expected {{\"prompt\": ..., \"response\": ...}}: {e}",
164 line_no + 1
165 ),
166 )
167 })?;
168 if let Some(example) = build_sft_example(
169 tokenizer,
170 &sample.prompt,
171 &sample.response,
172 sample.system.as_deref(),
173 use_chat_template,
174 max_len,
175 ) {
176 samples.push(example);
177 }
178 }
179 Ok(Self { samples })
180 }
181
182 pub fn from_jsonl<P: AsRef<Path>>(
184 path: P,
185 tokenizer: &Tokenizer,
186 ) -> Result<Self, std::io::Error> {
187 Self::from_jsonl_sft(path, tokenizer, true, usize::MAX)
188 }
189
190 pub fn from_text_file<P: AsRef<Path>>(
192 path: P,
193 tokenizer: &Tokenizer,
194 ) -> Result<Self, std::io::Error> {
195 let content = fs::read_to_string(path)?;
196 let sequences = content
197 .lines()
198 .filter(|line| !line.trim().is_empty())
199 .map(|line| tokenizer.encode(line))
200 .collect();
201 Ok(Self::from_sequences(sequences))
202 }
203
204 pub fn from_token_bin<P: AsRef<Path>>(
206 path: P,
207 sequence_length: usize,
208 ) -> Result<Self, std::io::Error> {
209 let bytes = fs::read(path)?;
210 let mut tokens = Vec::with_capacity(bytes.len() / 4);
211 for chunk in bytes.as_chunks::<4>().0 {
212 tokens.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
213 }
214 Ok(Self::from_packed_tokens(tokens, sequence_length))
215 }
216
217 pub fn from_packed_tokens(tokens: Vec<u32>, sequence_length: usize) -> Self {
219 if sequence_length < 2 || tokens.len() < sequence_length {
220 return Self::new();
221 }
222
223 let mut samples = Vec::new();
224 let stride = sequence_length.saturating_sub(1).max(1);
225 let mut start = 0usize;
226 while start + sequence_length <= tokens.len() {
227 let seq = tokens[start..start + sequence_length].to_vec();
228 samples.push(TrainingExample::from_next_token(seq));
229 start += stride;
230 }
231
232 Self { samples }
233 }
234
235 pub fn pack_sequences(sequences: Vec<Vec<u32>>, max_len: usize) -> Self {
237 if max_len < 2 {
238 return Self::new();
239 }
240
241 let mut packed = Vec::new();
242 let mut current = Vec::new();
243 for seq in sequences {
244 if seq.is_empty() {
245 continue;
246 }
247 if current.len() + seq.len() > max_len && current.len() >= 2 {
248 packed.push(current);
249 current = Vec::new();
250 }
251 current.extend(seq);
252 }
253 if current.len() >= 2 {
254 packed.push(current);
255 }
256
257 Self::from_sequences(packed)
258 }
259
260 pub fn examples(&self) -> &[TrainingExample] {
262 &self.samples
263 }
264}
265
266pub fn build_sft_example(
275 tokenizer: &Tokenizer,
276 prompt: &str,
277 response: &str,
278 system: Option<&str>,
279 use_chat_template: bool,
280 max_len: usize,
281) -> Option<TrainingExample> {
282 let (prompt_tokens, response_tokens) = if use_chat_template {
283 let mut response_part = tokenizer.encode_continuation(response);
284 response_part.push(tokenizer.eos_token_id);
285 (tokenizer.encode_chat(system, prompt), response_part)
286 } else {
287 (
288 tokenizer.encode(prompt),
289 tokenizer.encode_continuation(response),
290 )
291 };
292
293 if prompt_tokens.is_empty() || response_tokens.is_empty() {
294 return None;
295 }
296 if max_len < 2 || prompt_tokens.len() > max_len {
298 return None;
299 }
300
301 let mut example = TrainingExample::from_sft_prompt_response(prompt_tokens, response_tokens);
302 if example.input_ids.len() > max_len {
303 example.input_ids.truncate(max_len);
304 example.target_ids.truncate(max_len);
305 example.loss_mask.truncate(max_len);
306 }
307 if example.input_ids.len() < 2 || !example.loss_mask.contains(&1) {
308 return None;
309 }
310 Some(example)
311}
312
313impl Default for TextDataset {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319impl Dataset for TextDataset {
320 fn len(&self) -> usize {
321 self.samples.len()
322 }
323
324 fn get(&self, idx: usize) -> (Vec<u32>, Vec<u32>) {
325 let sample = &self.samples[idx];
326 (sample.input_ids.clone(), sample.target_ids.clone())
327 }
328
329 fn get_loss_mask(&self, idx: usize) -> Option<Vec<u8>> {
330 Some(self.samples[idx].loss_mask.clone())
331 }
332}
333
334pub struct Batch {
336 pub input_ids: Vec<Vec<u32>>,
338 pub target_ids: Vec<Vec<u32>>,
340 pub padded_input_ids: Vec<Vec<u32>>,
342 pub padded_target_ids: Vec<Vec<u32>>,
344 pub attention_mask: Vec<Vec<u8>>,
346 pub loss_masks: Vec<Vec<u8>>,
348 pub batch_size: usize,
350 pub max_seq_len: usize,
352}
353
354pub struct DataLoader<D: Dataset> {
356 dataset: Arc<D>,
357 batch_size: usize,
358 pad_token_id: u32,
359 shuffle: bool,
360 base_indices: Vec<usize>,
361 indices: Vec<usize>,
362 current_idx: usize,
363 seed: Option<u64>,
364 epoch: u64,
365}
366
367impl<D: Dataset> DataLoader<D> {
368 pub fn new(dataset: D, batch_size: usize, shuffle: bool) -> Self {
370 Self::new_with_pad_token(dataset, batch_size, shuffle, 0)
371 }
372
373 pub fn new_with_indices(
375 dataset: D,
376 batch_size: usize,
377 shuffle: bool,
378 pad_token_id: u32,
379 indices: Vec<usize>,
380 ) -> Self {
381 Self {
382 dataset: Arc::new(dataset),
383 batch_size: batch_size.max(1),
384 pad_token_id,
385 shuffle,
386 base_indices: indices.clone(),
387 indices,
388 current_idx: 0,
389 seed: None,
390 epoch: 0,
391 }
392 }
393
394 pub fn new_with_pad_token(
396 dataset: D,
397 batch_size: usize,
398 shuffle: bool,
399 pad_token_id: u32,
400 ) -> Self {
401 let indices: Vec<usize> = (0..dataset.len()).collect();
402 Self::new_with_indices(dataset, batch_size, shuffle, pad_token_id, indices)
403 }
404
405 pub fn with_seed(mut self, seed: u64) -> Self {
408 self.seed = Some(seed);
409 self
410 }
411
412 pub fn set_epoch(&mut self, epoch: u64) {
414 self.epoch = epoch;
415 }
416
417 pub fn reset(&mut self) {
421 self.current_idx = 0;
422 self.indices.clone_from(&self.base_indices);
423 if self.shuffle {
424 let seed = match self.seed {
425 Some(seed) => seed ^ self.epoch.wrapping_mul(0x9E37_79B9_7F4A_7C15),
426 None => {
427 use std::time::{SystemTime, UNIX_EPOCH};
428 SystemTime::now()
429 .duration_since(UNIX_EPOCH)
430 .map(|d| d.as_nanos() as u64)
431 .unwrap_or(0)
432 }
433 };
434 let mut rng = SimpleRng::new(seed);
435 for i in (1..self.indices.len()).rev() {
437 let j = (rng.next() >> 33) as usize % (i + 1);
438 self.indices.swap(i, j);
439 }
440 }
441 }
442
443 pub fn num_batches(&self) -> usize {
445 self.indices.len().div_ceil(self.batch_size)
446 }
447
448 pub fn len(&self) -> usize {
450 self.indices.len()
451 }
452
453 pub fn is_empty(&self) -> bool {
455 self.indices.is_empty()
456 }
457}
458
459impl<D: Dataset> Iterator for DataLoader<D> {
460 type Item = Batch;
461
462 fn next(&mut self) -> Option<Self::Item> {
463 if self.current_idx >= self.indices.len() {
464 return None;
465 }
466
467 let end = std::cmp::min(self.current_idx + self.batch_size, self.indices.len());
468 let batch_indices = &self.indices[self.current_idx..end];
469
470 let mut input_ids = Vec::with_capacity(batch_indices.len());
471 let mut target_ids = Vec::with_capacity(batch_indices.len());
472 let mut loss_masks_raw = Vec::with_capacity(batch_indices.len());
473 let mut max_seq_len = 0usize;
474
475 for &idx in batch_indices {
476 let (inp, tgt) = self.dataset.get(idx);
477 let mask = self
478 .dataset
479 .get_loss_mask(idx)
480 .unwrap_or_else(|| vec![1u8; tgt.len()]);
481 max_seq_len = max_seq_len.max(inp.len()).max(tgt.len());
482 input_ids.push(inp);
483 target_ids.push(tgt);
484 loss_masks_raw.push(mask);
485 }
486
487 let mut padded_input_ids = Vec::with_capacity(batch_indices.len());
488 let mut padded_target_ids = Vec::with_capacity(batch_indices.len());
489 let mut attention_mask = Vec::with_capacity(batch_indices.len());
490 let mut loss_masks = Vec::with_capacity(batch_indices.len());
491 for ((inp, tgt), loss_mask) in input_ids.iter().zip(&target_ids).zip(&loss_masks_raw) {
492 let valid_len = inp.len().min(tgt.len());
493 let mut padded_inp = inp.clone();
494 padded_inp.resize(max_seq_len, self.pad_token_id);
495 let mut padded_tgt = tgt.clone();
496 padded_tgt.resize(max_seq_len, self.pad_token_id);
497 let mut attn = vec![0u8; max_seq_len];
498 attn.iter_mut().take(valid_len).for_each(|m| *m = 1);
499 let mut padded_loss = vec![0u8; max_seq_len];
500 for (dst, src) in padded_loss.iter_mut().zip(loss_mask.iter()).take(valid_len) {
501 *dst = *src;
502 }
503 padded_input_ids.push(padded_inp);
504 padded_target_ids.push(padded_tgt);
505 attention_mask.push(attn);
506 loss_masks.push(padded_loss);
507 }
508
509 self.current_idx = end;
510
511 Some(Batch {
512 batch_size: batch_indices.len(),
513 input_ids,
514 target_ids,
515 padded_input_ids,
516 padded_target_ids,
517 attention_mask,
518 loss_masks,
519 max_seq_len,
520 })
521 }
522}
523
524struct SimpleRng {
526 state: u64,
527}
528
529impl SimpleRng {
530 fn new(seed: u64) -> Self {
531 Self {
532 state: seed.wrapping_add(1),
533 }
534 }
535
536 fn next(&mut self) -> u64 {
537 self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
539 self.state
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_text_dataset() {
549 let dataset = TextDataset::from_sequences(vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]]);
550 assert_eq!(dataset.len(), 2);
551
552 let (inp, tgt) = dataset.get(0);
553 assert_eq!(inp, vec![1, 2, 3]);
554 assert_eq!(tgt, vec![2, 3, 4]);
555 assert_eq!(dataset.get_loss_mask(0).unwrap(), vec![1, 1, 1]);
556 }
557
558 #[test]
559 fn sft_example_masks_only_response_tokens() {
560 let prompt = vec![10, 11, 12];
561 let response = vec![20, 21];
562 let example = TrainingExample::from_sft_prompt_response(prompt, response);
563 assert_eq!(example.input_ids, vec![10, 11, 12, 20]);
567 assert_eq!(example.target_ids, vec![11, 12, 20, 21]);
568 assert_eq!(example.loss_mask, vec![0, 0, 1, 1]);
569 }
570
571 #[test]
572 fn test_dataloader_batching() {
573 let dataset = TextDataset::from_sequences(vec![
574 vec![1, 2],
575 vec![3, 4],
576 vec![5, 6],
577 vec![7, 8],
578 vec![9, 10],
579 ]);
580 let loader = DataLoader::new(dataset, 2, false);
581 assert_eq!(loader.num_batches(), 3);
582 }
583
584 #[test]
585 fn test_dataloader_pads_and_masks_batches() {
586 let dataset = TextDataset::from_sequences(vec![vec![1, 2, 3, 4], vec![5, 6, 7]]);
587 let mut loader = DataLoader::new_with_pad_token(dataset, 2, false, 99);
588 let batch = loader.next().expect("expected batch");
589 assert_eq!(batch.max_seq_len, 3);
590 assert_eq!(batch.padded_input_ids[0], vec![1, 2, 3]);
591 assert_eq!(batch.padded_input_ids[1], vec![5, 6, 99]);
592 assert_eq!(batch.padded_target_ids[1], vec![6, 7, 99]);
593 assert_eq!(batch.attention_mask[0], vec![1, 1, 1]);
594 assert_eq!(batch.attention_mask[1], vec![1, 1, 0]);
595 assert_eq!(batch.loss_masks[1], vec![1, 1, 0]);
596 }
597
598 #[test]
599 fn packed_bin_and_txt_share_next_token_mask_semantics() {
600 let packed = TextDataset::from_packed_tokens(vec![1, 2, 3, 4, 5, 6], 4);
602 assert!(!packed.is_empty());
603 for idx in 0..packed.len() {
604 let mask = packed.get_loss_mask(idx).unwrap();
605 assert!(mask.iter().all(|&m| m == 1));
606 let (inp, tgt) = packed.get(idx);
607 assert_eq!(inp.len(), tgt.len());
608 assert_eq!(mask.len(), tgt.len());
609 }
610
611 let lines = TextDataset::from_sequences(vec![vec![9, 8, 7, 6]]);
613 assert_eq!(lines.get_loss_mask(0).unwrap(), vec![1, 1, 1]);
614 }
615
616 #[test]
617 fn sft_dataloader_preserves_response_mask_under_padding() {
618 let mut dataset = TextDataset::new();
619 dataset.add_example(TrainingExample::from_sft_prompt_response(
620 vec![1, 2, 3],
621 vec![4, 5],
622 ));
623 dataset.add_example(TrainingExample::from_sft_prompt_response(
624 vec![10, 11],
625 vec![12],
626 ));
627 let mut loader = DataLoader::new_with_pad_token(dataset, 2, false, 0);
628 let batch = loader.next().expect("batch");
629 assert_eq!(batch.loss_masks[0].iter().filter(|&&m| m == 1).count(), 2);
631 assert!(batch.loss_masks[0].iter().take(2).all(|&m| m == 0));
632 assert!(batch.loss_masks[1].contains(&1));
634 assert!(batch.loss_masks[1].contains(&0));
635 }
636}