Skip to main content

o41_training_data/
engine.rs

1use crate::{config::Config, readers};
2use anyhow::{Result, ensure};
3use arrow::record_batch::RecordBatch;
4use crossbeam_channel::{Receiver, bounded};
5use serde::{Deserialize, Serialize};
6use std::{
7    collections::{HashMap, HashSet},
8    sync::Arc,
9};
10
11#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12pub struct Position {
13    pub file: usize,
14    pub row: usize,
15    pub epoch: u64,
16    pub done: bool,
17}
18#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Coordinate {
20    pub stage: usize,
21    pub source: usize,
22    pub file: usize,
23    pub row: usize,
24    pub epoch: u64,
25}
26#[derive(Clone, Debug, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct State {
29    pub version: u32,
30    pub stage: usize,
31    pub batch_index: u64,
32    pub batches_yielded: u64,
33    pub rows_yielded: u64,
34    pub positions: Vec<Position>,
35    pub mix_counter: u64,
36    pub shuffle_counter: u64,
37    pub buffer: Vec<Coordinate>,
38    pub partial: Vec<Coordinate>,
39}
40impl State {
41    pub fn new(config: &Config, stage: usize) -> Self {
42        Self {
43            version: 3,
44            stage,
45            batch_index: 0,
46            batches_yielded: 0,
47            rows_yielded: 0,
48            positions: vec![Position::default(); config.stages[stage].sources.len()],
49            mix_counter: 0,
50            shuffle_counter: 0,
51            buffer: Vec::new(),
52            partial: Vec::new(),
53        }
54    }
55    pub fn validate(&self, config: &Config) -> Result<()> {
56        ensure!(
57            self.version == 3 && self.stage < config.stages.len(),
58            "unsupported cursor version or stage"
59        );
60        let stage = &config.stages[self.stage];
61        ensure!(
62            self.positions.len() == stage.sources.len(),
63            "cursor source count mismatch"
64        );
65        for (p, g) in self.positions.iter().zip(&stage.sources) {
66            ensure!(p.file <= g.files.len(), "cursor file out of bounds");
67        }
68        ensure!(
69            self.partial.len() <= stage.batch_size,
70            "cursor partial batch too large"
71        );
72        for c in self.buffer.iter().chain(&self.partial) {
73            ensure!(
74                c.stage < config.stages.len(),
75                "cursor coordinate stage out of bounds"
76            );
77            let stage = &config.stages[c.stage];
78            ensure!(
79                c.source < stage.sources.len() && c.file < stage.sources[c.source].files.len(),
80                "cursor coordinate out of bounds"
81            );
82        }
83        Ok(())
84    }
85}
86// UTF-8 names "mix" and "shuffle". All operations wrap at 64 bits.
87// Stage keys are avalanched before combining, avoiding adjacent-counter shifts.
88fn mix64(mut x: u64) -> u64 {
89    x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
90    x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
91    x ^ (x >> 31)
92}
93pub fn random(seed: u64, stage: usize, name: &str, source: u64, counter: u64) -> u64 {
94    let mut key = 14695981039346656037u64;
95    for b in name.bytes() {
96        key = (key ^ b as u64).wrapping_mul(1099511628211);
97    }
98    let golden = 0x9e3779b97f4a7c15u64;
99    key = key.wrapping_add(seed)
100        ^ mix64((stage as u64).wrapping_add(1).wrapping_mul(golden))
101        ^ mix64(source.wrapping_mul(golden));
102    mix64(key.wrapping_add(counter.wrapping_add(1).wrapping_mul(golden)))
103}
104type Key = (usize, usize, usize);
105pub struct Engine {
106    pub config: Arc<Config>,
107    pub state: State,
108    cache: HashMap<Key, readers::Shard>,
109    pending: HashMap<Key, Receiver<Result<readers::Shard>>>,
110    // None reads the still-cached shard; eviction copies only rows still needed.
111    rows: HashMap<Coordinate, Option<RecordBatch>>,
112    orders: Vec<Vec<Vec<usize>>>,
113    window: Option<Key>,
114    pool: rayon::ThreadPool,
115    client: Arc<crate::storage::Client>,
116}
117pub struct Batch {
118    pub columns: RecordBatch,
119    pub stage: usize,
120    pub index: u64,
121    pub state: State,
122}
123impl Engine {
124    pub fn new(config: Arc<Config>, state: State) -> Result<Self> {
125        let pool = rayon::ThreadPoolBuilder::new()
126            .num_threads(config.threads)
127            .build()?;
128        let runtime = Arc::new(
129            tokio::runtime::Builder::new_multi_thread()
130                .worker_threads(1)
131                .enable_all()
132                .build()?,
133        );
134        let client = Arc::new(crate::storage::Client::new(config.storage.clone(), runtime));
135        for (stage_index, stage) in config.stages.iter().enumerate() {
136            for (source_index, source) in stage.sources.iter().enumerate() {
137                let fields = source.fields.as_deref().unwrap_or(&config.fields);
138                let encoding = source
139                    .encoding
140                    .as_ref()
141                    .or(stage.encoding.as_ref())
142                    .unwrap();
143                let mut found = false;
144                for file in &source.files {
145                    use anyhow::Context;
146                    let sample = readers::read(file, encoding, &client, fields, 0, true, &config)
147                        .with_context(|| {
148                        format!("stage={stage_index} source={source_index}")
149                    })?;
150                    if sample.columns.num_rows() > 0 {
151                        found = true;
152                        break;
153                    }
154                }
155                ensure!(
156                    found,
157                    "stage {stage_index} source {source_index} {}: no record for field {}",
158                    source.files[0].uri,
159                    fields[0].name
160                );
161            }
162        }
163        let orders = config
164            .stages
165            .iter()
166            .enumerate()
167            .map(|(stage_index, stage)| {
168                stage
169                    .sources
170                    .iter()
171                    .enumerate()
172                    .map(|(source_index, source)| {
173                        let mut order: Vec<_> = (0..source.files.len()).collect();
174                        if stage.shuffle.is_some() {
175                            for (draw, i) in (1..order.len()).rev().enumerate() {
176                                let j = random(
177                                    config.seed,
178                                    stage_index,
179                                    "shards",
180                                    source_index as u64,
181                                    draw as u64,
182                                ) % (i as u64 + 1);
183                                order.swap(i, j as usize);
184                            }
185                        }
186                        order
187                    })
188                    .collect()
189            })
190            .collect();
191        let mut engine = Self {
192            config,
193            state,
194            cache: HashMap::new(),
195            pending: HashMap::new(),
196            rows: HashMap::new(),
197            orders,
198            window: None,
199            pool,
200            client,
201        };
202        engine.restore_rows()?;
203        Ok(engine)
204    }
205    fn heads(&self) -> impl Iterator<Item = Key> + '_ {
206        self.state
207            .positions
208            .iter()
209            .enumerate()
210            .filter(|(_, p)| !p.done)
211            .map(|(source, p)| {
212                (
213                    self.state.stage,
214                    source,
215                    self.orders[self.state.stage][source][p.file],
216                )
217            })
218    }
219    fn schedule(
220        &mut self,
221        stage_index: usize,
222        source: usize,
223        file: usize,
224        row: usize,
225        required: bool,
226    ) {
227        let key = (stage_index, source, file);
228        if self.cache.contains_key(&key) || self.pending.contains_key(&key) {
229            return;
230        }
231        // Future shards cannot take the slots needed by other sources' current shards.
232        let reserved = if required {
233            0
234        } else {
235            self.heads()
236                .filter(|k| !self.cache.contains_key(k) && !self.pending.contains_key(k))
237                .count()
238        };
239        if self.cache.len() + self.pending.len() + reserved
240            >= self.config.threads * self.config.lookahead
241        {
242            return;
243        }
244        let config = self.config.clone();
245        let client = self.client.clone();
246        let (send, recv) = bounded(1);
247        self.pool.spawn(move || {
248            let stage = &config.stages[stage_index];
249            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
250                readers::read(
251                    &stage.sources[source].files[file],
252                    stage.sources[source]
253                        .encoding
254                        .as_ref()
255                        .or(stage.encoding.as_ref())
256                        .unwrap(),
257                    &client,
258                    stage.sources[source]
259                        .fields
260                        .as_deref()
261                        .unwrap_or(&config.fields),
262                    row,
263                    false,
264                    &config,
265                )
266            }))
267            .unwrap_or_else(|_| Err(anyhow::anyhow!("shard reader panicked")));
268            let result = result
269                .map_err(|error| anyhow::anyhow!("stage={stage_index} source={source}: {error:#}"));
270            let _ = send.send(result);
271        });
272        self.pending.insert(key, recv);
273    }
274    fn lookahead(&mut self, source: usize, file: usize) {
275        if self.window == Some((self.state.stage, source, file)) {
276            return;
277        }
278        self.window = Some((self.state.stage, source, file));
279        let stage = &self.config.stages[self.state.stage];
280        let (count, infinite) = (stage.sources[source].files.len(), stage.infinite);
281        // At least the next shard, so one thread reads it during the current;
282        // an infinite source continues into its next epoch.
283        let window = self.config.threads.max(2);
284        let ahead = if infinite {
285            window.min(count)
286        } else {
287            window.min(count - file)
288        };
289        for k in 0..ahead {
290            self.schedule(
291                self.state.stage,
292                source,
293                self.orders[self.state.stage][source][(file + k) % count],
294                if k == 0 {
295                    self.state.positions[source].row
296                } else {
297                    0
298                },
299                k == 0,
300            );
301        }
302    }
303    fn load(&mut self, stage_index: usize, source: usize, file: usize, row: usize) -> Result<()> {
304        let key = (stage_index, source, file);
305        if self.cache.get(&key).is_some_and(|s| s.offset <= row) {
306            return Ok(());
307        }
308        self.evict(key)?;
309        if !self.pending.contains_key(&key) {
310            ensure!(
311                self.cache.len() + self.pending.len() < self.config.threads * self.config.lookahead,
312                "reader slots exhausted"
313            );
314            self.schedule(stage_index, source, file, row, true);
315        }
316        let wait = crate::profile::Timer::new(7);
317        let result = self
318            .pending
319            .remove(&key)
320            .unwrap()
321            .recv()
322            .map_err(|_| anyhow::anyhow!("shard worker stopped"))??;
323        drop(wait);
324        if result.offset > row {
325            return self.load(stage_index, source, file, row);
326        }
327        self.cache.insert(key, result);
328        Ok(())
329    }
330    fn retain_row(&mut self, c: Coordinate) {
331        self.rows.insert(c, None);
332    }
333    fn evict(&mut self, key: Key) -> Result<()> {
334        if let Some(shard) = self.cache.get(&key) {
335            for (c, row) in &mut self.rows {
336                if row.is_none() && (c.stage, c.source, c.file) == key {
337                    *row = Some(arrow::compute::interleave_record_batch(
338                        &[&shard.columns],
339                        &[(0, c.row - shard.offset)],
340                    )?);
341                }
342            }
343        }
344        self.cache.remove(&key);
345        Ok(())
346    }
347    fn restore_rows(&mut self) -> Result<()> {
348        let mut groups = std::collections::BTreeMap::<Key, Vec<Coordinate>>::new();
349        for c in self.state.buffer.iter().chain(&self.state.partial) {
350            groups
351                .entry((c.stage, c.source, c.file))
352                .or_default()
353                .push(c.clone());
354        }
355        let heads: HashSet<_> = self.heads().collect();
356        let mut groups: Vec<_> = groups.into_iter().collect();
357        // Restore old shards first; retain current shards for their unread suffixes.
358        groups.sort_by_key(|(key, _)| heads.contains(key));
359        for (key, coordinates) in groups {
360            let mut start = coordinates.iter().map(|c| c.row).min().unwrap();
361            if heads.contains(&key) {
362                start = start.min(self.state.positions[key.1].row);
363            }
364            self.load(key.0, key.1, key.2, start)?;
365            for c in coordinates {
366                self.retain_row(c);
367            }
368            if !heads.contains(&key) {
369                self.evict(key)?;
370            }
371        }
372        Ok(())
373    }
374    fn source_next(&mut self, source: usize) -> Result<Option<Coordinate>> {
375        let stage = self.state.stage;
376        let count = self.orders[stage][source].len();
377        let infinite = self.config.stages[stage].infinite;
378        let mut empty = 0;
379        loop {
380            let p = &self.state.positions[source];
381            if p.done {
382                return Ok(None);
383            }
384            let (position, row, epoch) = (p.file, p.row, p.epoch);
385            let file = self.orders[stage][source][position];
386            self.lookahead(source, position);
387            self.load(stage, source, file, row)?;
388            let end = self.cache[&(stage, source, file)].num_rows();
389            let result = if row < end {
390                let c = Coordinate {
391                    stage,
392                    source,
393                    file,
394                    row,
395                    epoch,
396                };
397                self.retain_row(c.clone());
398                Some(c)
399            } else {
400                None
401            };
402            let p = &mut self.state.positions[source];
403            p.row += usize::from(result.is_some());
404            if p.row >= end {
405                self.evict((stage, source, file))?;
406                let p = &mut self.state.positions[source];
407                p.row = 0;
408                p.file += 1;
409                if p.file == count {
410                    p.epoch += 1;
411                    if infinite {
412                        p.file = 0;
413                    } else {
414                        p.done = true;
415                    }
416                }
417            }
418            if result.is_some() {
419                return Ok(result);
420            }
421            empty += 1;
422            ensure!(
423                !infinite || empty <= count,
424                "infinite source has no records"
425            );
426        }
427    }
428    fn mixed(&mut self) -> Result<Option<Coordinate>> {
429        loop {
430            let groups = &self.config.stages[self.state.stage].sources;
431            let total: f64 = groups
432                .iter()
433                .zip(&self.state.positions)
434                .filter(|(_, p)| !p.done)
435                .map(|(g, _)| g.weight.unwrap_or(1.0))
436                .sum();
437            if total == 0.0 {
438                return Ok(None);
439            }
440            let mut chosen = 0;
441            if groups.len() > 1 {
442                let draw = (random(
443                    self.config.seed,
444                    self.state.stage,
445                    "mix",
446                    0,
447                    self.state.mix_counter,
448                ) >> 11) as f64
449                    * (1.0 / ((1u64 << 53) as f64));
450                self.state.mix_counter = self.state.mix_counter.wrapping_add(1);
451                let mut target = draw * total;
452                for (i, (g, p)) in groups.iter().zip(&self.state.positions).enumerate() {
453                    if p.done || g.weight.unwrap_or(1.0) == 0.0 {
454                        continue;
455                    }
456                    chosen = i;
457                    target -= g.weight.unwrap_or(1.0);
458                    if target < 0.0 {
459                        break;
460                    }
461                }
462            }
463            if let Some(c) = self.source_next(chosen)? {
464                return Ok(Some(c));
465            }
466        }
467    }
468    fn coordinate(&mut self) -> Result<Option<Coordinate>> {
469        let capacity = self.config.stages[self.state.stage]
470            .shuffle
471            .as_ref()
472            .map_or(0, |s| s.buffer_rows);
473        if capacity == 0 && self.state.buffer.is_empty() {
474            return self.mixed();
475        }
476        while self.state.buffer.len() < capacity {
477            if let Some(c) = self.mixed()? {
478                self.state.buffer.push(c);
479            } else {
480                break;
481            }
482        }
483        if self.state.buffer.is_empty() {
484            return Ok(None);
485        }
486        let i = (random(
487            self.config.seed,
488            self.state.stage,
489            "shuffle",
490            0,
491            self.state.shuffle_counter,
492        ) % self.state.buffer.len() as u64) as usize;
493        self.state.shuffle_counter = self.state.shuffle_counter.wrapping_add(1);
494        let replacement = if self.state.buffer.len() <= capacity {
495            self.mixed()?
496        } else {
497            None
498        };
499        let result = if let Some(c) = replacement {
500            std::mem::replace(&mut self.state.buffer[i], c)
501        } else {
502            self.state.buffer.swap_remove(i)
503        };
504        Ok(Some(result))
505    }
506    fn retire(&mut self) {
507        let keep: HashSet<_> = self
508            .state
509            .buffer
510            .iter()
511            .chain(&self.state.partial)
512            .collect();
513        self.rows.retain(|c, _| keep.contains(c));
514    }
515    fn materialize(&self, coordinates: &[Coordinate]) -> Result<RecordBatch> {
516        let _assembly = crate::profile::Timer::new(6);
517        let mut rows = Vec::new();
518        let mut shards = HashMap::new();
519        let mut order = Vec::with_capacity(coordinates.len());
520        for c in coordinates {
521            match &self.rows[c] {
522                Some(row) => {
523                    order.push((rows.len(), 0));
524                    rows.push(row);
525                }
526                None => {
527                    let key = (c.stage, c.source, c.file);
528                    let shard = &self.cache[&key];
529                    let at = *shards.entry(key).or_insert_with(|| {
530                        let at = rows.len();
531                        rows.push(&shard.columns);
532                        at
533                    });
534                    order.push((at, c.row - shard.offset));
535                }
536            }
537        }
538        Ok(arrow::compute::interleave_record_batch(&rows, &order)?)
539    }
540    /// Assembles the next global batch and takes its index, regardless of rank.
541    fn assemble(&mut self) -> Result<Option<(Vec<Coordinate>, u64)>> {
542        let mut short_epochs = 0;
543        loop {
544            let stage = &self.config.stages[self.state.stage];
545            let size = stage.batch_size;
546            let drop_last = stage.drop_last;
547            let epoch_boundary = stage.sources.len() == 1 && stage.shuffle.is_none();
548            let mut coordinates = std::mem::take(&mut self.state.partial);
549            while coordinates.len() < size {
550                let Some(c) = self.coordinate()? else {
551                    break;
552                };
553                if epoch_boundary
554                    && coordinates
555                        .first()
556                        .is_some_and(|first| first.epoch != c.epoch)
557                {
558                    self.state.partial.push(c);
559                    break;
560                }
561                coordinates.push(c);
562            }
563            if coordinates.len() < size && drop_last {
564                if self.state.partial.is_empty() {
565                    return Ok(None);
566                }
567                // Two short batches in a row mean a whole epoch fits in one.
568                short_epochs += 1;
569                ensure!(
570                    short_epochs < 2,
571                    "dropLast discards every batch: an epoch has fewer than batchSize rows"
572                );
573                continue;
574            }
575            if coordinates.is_empty() {
576                return Ok(None);
577            }
578            let index = self.state.batch_index;
579            self.state.batch_index += 1;
580            return Ok(Some((coordinates, index)));
581        }
582    }
583    pub fn next(&mut self) -> Result<Option<Batch>> {
584        loop {
585            let Some((coordinates, index)) = self.assemble()? else {
586                return Ok(None);
587            };
588            if index % self.config.world_size != self.config.rank {
589                self.retire();
590                continue;
591            }
592            let columns = self.materialize(&coordinates)?;
593            self.retire();
594            // Finish the round of world_size batches before yielding, so every
595            // rank yields the same count and a finite tail that cannot fill a
596            // round is dropped on all ranks.
597            while !self
598                .state
599                .batch_index
600                .is_multiple_of(self.config.world_size)
601            {
602                if self.assemble()?.is_none() {
603                    return Ok(None);
604                }
605                self.retire();
606            }
607            self.state.batches_yielded += 1;
608            self.state.rows_yielded += coordinates.len() as u64;
609            return Ok(Some(Batch {
610                columns,
611                stage: self.state.stage,
612                index,
613                state: self.state.clone(),
614            }));
615        }
616    }
617    /// Draws the global batches owned by other ranks up to the next multiple of
618    /// world_size. Ranks that advance after the same number of local batches
619    /// then switch stages from one identical cursor and stay partitioned.
620    pub fn align(&mut self) -> Result<()> {
621        while !self
622            .state
623            .batch_index
624            .is_multiple_of(self.config.world_size)
625        {
626            if self.assemble()?.is_none() {
627                break;
628            }
629            self.retire();
630        }
631        Ok(())
632    }
633}
634
635impl Drop for Engine {
636    fn drop(&mut self) {
637        for (_, pending) in self.pending.drain() {
638            let _ = pending.recv();
639        }
640    }
641}