use ferrotensor_loader::Tokenizer;
use serde::Deserialize;
use std::fs;
use std::path::Path;
use std::sync::Arc;
pub trait Dataset: Send + Sync {
fn len(&self) -> usize;
fn get(&self, idx: usize) -> (Vec<u32>, Vec<u32>);
fn get_loss_mask(&self, idx: usize) -> Option<Vec<u8>> {
let (_, targets) = self.get(idx);
Some(vec![1u8; targets.len()])
}
fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Clone, Debug)]
pub struct TrainingExample {
pub input_ids: Vec<u32>,
pub target_ids: Vec<u32>,
pub loss_mask: Vec<u8>,
}
impl TrainingExample {
pub fn from_next_token(sequence: Vec<u32>) -> Self {
if sequence.len() < 2 {
let len = sequence.len();
return Self {
input_ids: sequence.clone(),
target_ids: sequence,
loss_mask: vec![1u8; len],
};
}
let input = sequence[..sequence.len() - 1].to_vec();
let target = sequence[1..].to_vec();
let loss_mask = vec![1u8; target.len()];
Self {
input_ids: input,
target_ids: target,
loss_mask,
}
}
pub fn from_sft_prompt_response(prompt_tokens: Vec<u32>, response_tokens: Vec<u32>) -> Self {
let mut full = prompt_tokens.clone();
full.extend(response_tokens.iter().copied());
if full.len() < 2 {
return Self::from_next_token(full);
}
let input = full[..full.len() - 1].to_vec();
let target = full[1..].to_vec();
let mut loss_mask = vec![0u8; target.len()];
if !prompt_tokens.is_empty() && !response_tokens.is_empty() {
let first_response_target = prompt_tokens.len().saturating_sub(1);
loss_mask[first_response_target..].fill(1);
} else {
loss_mask.fill(1);
}
Self {
input_ids: input,
target_ids: target,
loss_mask,
}
}
}
#[derive(Clone)]
pub struct TextDataset {
samples: Vec<TrainingExample>,
}
impl TextDataset {
pub fn new() -> Self {
Self {
samples: Vec::new(),
}
}
pub fn add_sample(&mut self, input_ids: Vec<u32>, target_ids: Vec<u32>) {
let loss_mask = vec![1u8; target_ids.len()];
self.samples.push(TrainingExample {
input_ids,
target_ids,
loss_mask,
});
}
pub fn add_example(&mut self, example: TrainingExample) {
self.samples.push(example);
}
pub fn from_sequences(sequences: Vec<Vec<u32>>) -> Self {
let samples = sequences
.into_iter()
.map(TrainingExample::from_next_token)
.collect();
Self { samples }
}
pub fn from_jsonl_sft<P: AsRef<Path>>(
path: P,
tokenizer: &Tokenizer,
use_chat_template: bool,
max_len: usize,
) -> Result<Self, std::io::Error> {
#[derive(Deserialize)]
struct JsonlSample {
prompt: String,
response: String,
#[serde(default)]
system: Option<String>,
}
let content = fs::read_to_string(path)?;
let mut samples = Vec::new();
for (line_no, line) in content.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let sample = serde_json::from_str::<JsonlSample>(line).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"line {}: expected {{\"prompt\": ..., \"response\": ...}}: {e}",
line_no + 1
),
)
})?;
if let Some(example) = build_sft_example(
tokenizer,
&sample.prompt,
&sample.response,
sample.system.as_deref(),
use_chat_template,
max_len,
) {
samples.push(example);
}
}
Ok(Self { samples })
}
pub fn from_jsonl<P: AsRef<Path>>(
path: P,
tokenizer: &Tokenizer,
) -> Result<Self, std::io::Error> {
Self::from_jsonl_sft(path, tokenizer, true, usize::MAX)
}
pub fn from_text_file<P: AsRef<Path>>(
path: P,
tokenizer: &Tokenizer,
) -> Result<Self, std::io::Error> {
let content = fs::read_to_string(path)?;
let sequences = content
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| tokenizer.encode(line))
.collect();
Ok(Self::from_sequences(sequences))
}
pub fn from_token_bin<P: AsRef<Path>>(
path: P,
sequence_length: usize,
) -> Result<Self, std::io::Error> {
let bytes = fs::read(path)?;
let mut tokens = Vec::with_capacity(bytes.len() / 4);
for chunk in bytes.as_chunks::<4>().0 {
tokens.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
}
Ok(Self::from_packed_tokens(tokens, sequence_length))
}
pub fn from_packed_tokens(tokens: Vec<u32>, sequence_length: usize) -> Self {
if sequence_length < 2 || tokens.len() < sequence_length {
return Self::new();
}
let mut samples = Vec::new();
let stride = sequence_length.saturating_sub(1).max(1);
let mut start = 0usize;
while start + sequence_length <= tokens.len() {
let seq = tokens[start..start + sequence_length].to_vec();
samples.push(TrainingExample::from_next_token(seq));
start += stride;
}
Self { samples }
}
pub fn pack_sequences(sequences: Vec<Vec<u32>>, max_len: usize) -> Self {
if max_len < 2 {
return Self::new();
}
let mut packed = Vec::new();
let mut current = Vec::new();
for seq in sequences {
if seq.is_empty() {
continue;
}
if current.len() + seq.len() > max_len && current.len() >= 2 {
packed.push(current);
current = Vec::new();
}
current.extend(seq);
}
if current.len() >= 2 {
packed.push(current);
}
Self::from_sequences(packed)
}
pub fn examples(&self) -> &[TrainingExample] {
&self.samples
}
}
pub fn build_sft_example(
tokenizer: &Tokenizer,
prompt: &str,
response: &str,
system: Option<&str>,
use_chat_template: bool,
max_len: usize,
) -> Option<TrainingExample> {
let (prompt_tokens, response_tokens) = if use_chat_template {
let mut response_part = tokenizer.encode_continuation(response);
response_part.push(tokenizer.eos_token_id);
(tokenizer.encode_chat(system, prompt), response_part)
} else {
(
tokenizer.encode(prompt),
tokenizer.encode_continuation(response),
)
};
if prompt_tokens.is_empty() || response_tokens.is_empty() {
return None;
}
if max_len < 2 || prompt_tokens.len() > max_len {
return None;
}
let mut example = TrainingExample::from_sft_prompt_response(prompt_tokens, response_tokens);
if example.input_ids.len() > max_len {
example.input_ids.truncate(max_len);
example.target_ids.truncate(max_len);
example.loss_mask.truncate(max_len);
}
if example.input_ids.len() < 2 || !example.loss_mask.contains(&1) {
return None;
}
Some(example)
}
impl Default for TextDataset {
fn default() -> Self {
Self::new()
}
}
impl Dataset for TextDataset {
fn len(&self) -> usize {
self.samples.len()
}
fn get(&self, idx: usize) -> (Vec<u32>, Vec<u32>) {
let sample = &self.samples[idx];
(sample.input_ids.clone(), sample.target_ids.clone())
}
fn get_loss_mask(&self, idx: usize) -> Option<Vec<u8>> {
Some(self.samples[idx].loss_mask.clone())
}
}
pub struct Batch {
pub input_ids: Vec<Vec<u32>>,
pub target_ids: Vec<Vec<u32>>,
pub padded_input_ids: Vec<Vec<u32>>,
pub padded_target_ids: Vec<Vec<u32>>,
pub attention_mask: Vec<Vec<u8>>,
pub loss_masks: Vec<Vec<u8>>,
pub batch_size: usize,
pub max_seq_len: usize,
}
pub struct DataLoader<D: Dataset> {
dataset: Arc<D>,
batch_size: usize,
pad_token_id: u32,
shuffle: bool,
base_indices: Vec<usize>,
indices: Vec<usize>,
current_idx: usize,
seed: Option<u64>,
epoch: u64,
}
impl<D: Dataset> DataLoader<D> {
pub fn new(dataset: D, batch_size: usize, shuffle: bool) -> Self {
Self::new_with_pad_token(dataset, batch_size, shuffle, 0)
}
pub fn new_with_indices(
dataset: D,
batch_size: usize,
shuffle: bool,
pad_token_id: u32,
indices: Vec<usize>,
) -> Self {
Self {
dataset: Arc::new(dataset),
batch_size: batch_size.max(1),
pad_token_id,
shuffle,
base_indices: indices.clone(),
indices,
current_idx: 0,
seed: None,
epoch: 0,
}
}
pub fn new_with_pad_token(
dataset: D,
batch_size: usize,
shuffle: bool,
pad_token_id: u32,
) -> Self {
let indices: Vec<usize> = (0..dataset.len()).collect();
Self::new_with_indices(dataset, batch_size, shuffle, pad_token_id, indices)
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn set_epoch(&mut self, epoch: u64) {
self.epoch = epoch;
}
pub fn reset(&mut self) {
self.current_idx = 0;
self.indices.clone_from(&self.base_indices);
if self.shuffle {
let seed = match self.seed {
Some(seed) => seed ^ self.epoch.wrapping_mul(0x9E37_79B9_7F4A_7C15),
None => {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
};
let mut rng = SimpleRng::new(seed);
for i in (1..self.indices.len()).rev() {
let j = (rng.next() >> 33) as usize % (i + 1);
self.indices.swap(i, j);
}
}
}
pub fn num_batches(&self) -> usize {
self.indices.len().div_ceil(self.batch_size)
}
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
}
impl<D: Dataset> Iterator for DataLoader<D> {
type Item = Batch;
fn next(&mut self) -> Option<Self::Item> {
if self.current_idx >= self.indices.len() {
return None;
}
let end = std::cmp::min(self.current_idx + self.batch_size, self.indices.len());
let batch_indices = &self.indices[self.current_idx..end];
let mut input_ids = Vec::with_capacity(batch_indices.len());
let mut target_ids = Vec::with_capacity(batch_indices.len());
let mut loss_masks_raw = Vec::with_capacity(batch_indices.len());
let mut max_seq_len = 0usize;
for &idx in batch_indices {
let (inp, tgt) = self.dataset.get(idx);
let mask = self
.dataset
.get_loss_mask(idx)
.unwrap_or_else(|| vec![1u8; tgt.len()]);
max_seq_len = max_seq_len.max(inp.len()).max(tgt.len());
input_ids.push(inp);
target_ids.push(tgt);
loss_masks_raw.push(mask);
}
let mut padded_input_ids = Vec::with_capacity(batch_indices.len());
let mut padded_target_ids = Vec::with_capacity(batch_indices.len());
let mut attention_mask = Vec::with_capacity(batch_indices.len());
let mut loss_masks = Vec::with_capacity(batch_indices.len());
for ((inp, tgt), loss_mask) in input_ids.iter().zip(&target_ids).zip(&loss_masks_raw) {
let valid_len = inp.len().min(tgt.len());
let mut padded_inp = inp.clone();
padded_inp.resize(max_seq_len, self.pad_token_id);
let mut padded_tgt = tgt.clone();
padded_tgt.resize(max_seq_len, self.pad_token_id);
let mut attn = vec![0u8; max_seq_len];
attn.iter_mut().take(valid_len).for_each(|m| *m = 1);
let mut padded_loss = vec![0u8; max_seq_len];
for (dst, src) in padded_loss.iter_mut().zip(loss_mask.iter()).take(valid_len) {
*dst = *src;
}
padded_input_ids.push(padded_inp);
padded_target_ids.push(padded_tgt);
attention_mask.push(attn);
loss_masks.push(padded_loss);
}
self.current_idx = end;
Some(Batch {
batch_size: batch_indices.len(),
input_ids,
target_ids,
padded_input_ids,
padded_target_ids,
attention_mask,
loss_masks,
max_seq_len,
})
}
}
struct SimpleRng {
state: u64,
}
impl SimpleRng {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next(&mut self) -> u64 {
self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_dataset() {
let dataset = TextDataset::from_sequences(vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]]);
assert_eq!(dataset.len(), 2);
let (inp, tgt) = dataset.get(0);
assert_eq!(inp, vec![1, 2, 3]);
assert_eq!(tgt, vec![2, 3, 4]);
assert_eq!(dataset.get_loss_mask(0).unwrap(), vec![1, 1, 1]);
}
#[test]
fn sft_example_masks_only_response_tokens() {
let prompt = vec![10, 11, 12];
let response = vec![20, 21];
let example = TrainingExample::from_sft_prompt_response(prompt, response);
assert_eq!(example.input_ids, vec![10, 11, 12, 20]);
assert_eq!(example.target_ids, vec![11, 12, 20, 21]);
assert_eq!(example.loss_mask, vec![0, 0, 1, 1]);
}
#[test]
fn test_dataloader_batching() {
let dataset = TextDataset::from_sequences(vec![
vec![1, 2],
vec![3, 4],
vec![5, 6],
vec![7, 8],
vec![9, 10],
]);
let loader = DataLoader::new(dataset, 2, false);
assert_eq!(loader.num_batches(), 3);
}
#[test]
fn test_dataloader_pads_and_masks_batches() {
let dataset = TextDataset::from_sequences(vec![vec![1, 2, 3, 4], vec![5, 6, 7]]);
let mut loader = DataLoader::new_with_pad_token(dataset, 2, false, 99);
let batch = loader.next().expect("expected batch");
assert_eq!(batch.max_seq_len, 3);
assert_eq!(batch.padded_input_ids[0], vec![1, 2, 3]);
assert_eq!(batch.padded_input_ids[1], vec![5, 6, 99]);
assert_eq!(batch.padded_target_ids[1], vec![6, 7, 99]);
assert_eq!(batch.attention_mask[0], vec![1, 1, 1]);
assert_eq!(batch.attention_mask[1], vec![1, 1, 0]);
assert_eq!(batch.loss_masks[1], vec![1, 1, 0]);
}
#[test]
fn packed_bin_and_txt_share_next_token_mask_semantics() {
let packed = TextDataset::from_packed_tokens(vec![1, 2, 3, 4, 5, 6], 4);
assert!(!packed.is_empty());
for idx in 0..packed.len() {
let mask = packed.get_loss_mask(idx).unwrap();
assert!(mask.iter().all(|&m| m == 1));
let (inp, tgt) = packed.get(idx);
assert_eq!(inp.len(), tgt.len());
assert_eq!(mask.len(), tgt.len());
}
let lines = TextDataset::from_sequences(vec![vec![9, 8, 7, 6]]);
assert_eq!(lines.get_loss_mask(0).unwrap(), vec![1, 1, 1]);
}
#[test]
fn sft_dataloader_preserves_response_mask_under_padding() {
let mut dataset = TextDataset::new();
dataset.add_example(TrainingExample::from_sft_prompt_response(
vec![1, 2, 3],
vec![4, 5],
));
dataset.add_example(TrainingExample::from_sft_prompt_response(
vec![10, 11],
vec![12],
));
let mut loader = DataLoader::new_with_pad_token(dataset, 2, false, 0);
let batch = loader.next().expect("batch");
assert_eq!(batch.loss_masks[0].iter().filter(|&&m| m == 1).count(), 2);
assert!(batch.loss_masks[0].iter().take(2).all(|&m| m == 0));
assert!(batch.loss_masks[1].contains(&1));
assert!(batch.loss_masks[1].contains(&0));
}
}