Skip to main content

o41_training_data/
config.rs

1use anyhow::{Result, ensure};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Deserialize, Serialize)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6pub struct Source {
7    pub uri: String,
8    #[serde(default, skip_serializing_if = "Option::is_none")]
9    pub etag: Option<String>,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub size_bytes: Option<u64>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub rows: Option<usize>,
14}
15/// Explicit object-store settings. Nothing is read from the environment.
16#[derive(Clone, Debug, Deserialize, Serialize, Default)]
17#[serde(rename_all = "camelCase", deny_unknown_fields)]
18pub struct Storage {
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub base_url: Option<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub region: Option<String>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub access_key_id: Option<String>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub secret_access_key: Option<String>,
27    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
28    pub virtual_hosted_style_request: bool,
29}
30impl Storage {
31    pub fn without_credentials(&self) -> Self {
32        Self {
33            access_key_id: None,
34            secret_access_key: None,
35            ..self.clone()
36        }
37    }
38}
39#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
40#[serde(deny_unknown_fields)]
41pub struct Field {
42    pub name: String,
43    pub from: String,
44    pub dtype: String,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub length: Option<usize>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub shape: Option<Vec<usize>>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub pad: Option<serde_json::Value>,
51}
52impl Field {
53    pub fn dimensions(&self) -> Vec<usize> {
54        self.shape
55            .clone()
56            .unwrap_or_else(|| self.length.into_iter().collect())
57    }
58    pub fn width(&self) -> usize {
59        self.dimensions().iter().product()
60    }
61}
62/// One weighted source of a stage: either resolved `files` or a manifest
63/// reference that `manifest::resolve` turns into files.
64#[derive(Clone, Debug, Deserialize, Serialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct Group {
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub encoding: Option<String>,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub fields: Option<Vec<Field>>,
71    #[serde(default, skip_serializing_if = "Vec::is_empty")]
72    pub files: Vec<Source>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub manifest_uri: Option<String>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub split: Option<String>,
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub curriculum_stages: Vec<String>,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub weight: Option<f64>,
81}
82#[derive(Clone, Debug, Deserialize, Serialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct Shuffle {
85    pub buffer_rows: usize,
86}
87#[derive(Clone, Debug, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89pub struct Stage {
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub encoding: Option<String>,
92    pub sources: Vec<Group>,
93    pub batch_size: usize,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub shuffle: Option<Shuffle>,
96    pub infinite: bool,
97    #[serde(default)]
98    pub drop_last: bool,
99}
100#[derive(Clone, Debug, Deserialize, Serialize)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102pub struct Config {
103    #[serde(default)]
104    pub storage: Storage,
105    pub stages: Vec<Stage>,
106    pub fields: Vec<Field>,
107    pub seed: u64,
108    pub rank: u64,
109    pub world_size: u64,
110    pub threads: usize,
111    pub prefetch_batches: usize,
112    #[serde(default = "lookahead")]
113    pub lookahead: usize,
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub memory_limit_bytes: Option<u64>,
116    #[serde(default = "record_limit")]
117    pub max_record_bytes: usize,
118    #[serde(default = "row_group_limit")]
119    pub max_row_group_bytes: usize,
120    #[serde(default = "row_limit")]
121    pub max_shard_rows: usize,
122}
123/// Decoded bytes a Parquet reader's batch of wide rows holds: rows of up to
124/// 64 KiB decode 128 at a time.
125const DECODE_BYTES: u128 = 8 * 1024 * 1024;
126fn lookahead() -> usize {
127    2
128}
129fn record_limit() -> usize {
130    4 * 1024 * 1024
131}
132fn row_group_limit() -> usize {
133    64 * 1024 * 1024
134}
135fn row_limit() -> usize {
136    65536
137}
138impl Config {
139    pub fn memory_limit(&self) -> Result<u64> {
140        if let Some(limit) = self.memory_limit_bytes {
141            return Ok(limit);
142        }
143        let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
144        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
145        ensure!(
146            pages > 0 && page_size > 0,
147            "cannot read host physical memory"
148        );
149        Ok(((pages as u128 * page_size as u128) / 2).try_into()?)
150    }
151    /// Bytes the memory bound counts for a row: eight for each `i64` or `f64`
152    /// element, four for any other.
153    fn row_bytes(&self) -> u128 {
154        self.fields
155            .iter()
156            .map(|f| {
157                f.width() as u128
158                    * if matches!(f.dtype.as_str(), "i64" | "f64") {
159                        8
160                    } else {
161                        4
162                    }
163            })
164            .sum()
165    }
166    /// Rows a Parquet reader decodes at once: 128, or as many as fit
167    /// [`DECODE_BYTES`], at least one.
168    pub(crate) fn decode_rows(&self) -> usize {
169        (DECODE_BYTES / self.row_bytes().max(1)).clamp(1, 128) as usize
170    }
171    /// Decoded shards held at once, and the largest's rows: each reader slot
172    /// holds one, and a file takes at most one slot.
173    fn shards(&self) -> (u128, u128) {
174        let files = self
175            .stages
176            .iter()
177            .flat_map(|s| &s.sources)
178            .flat_map(|g| &g.files);
179        let slots =
180            (self.threads as u128 * self.lookahead as u128).min(files.clone().count() as u128);
181        let rows = files
182            .map(|f| f.rows.unwrap_or(self.max_shard_rows))
183            .max()
184            .unwrap_or(0) as u128;
185        (slots, rows)
186    }
187    pub fn memory_bound(&self) -> Result<u64> {
188        let groups: Vec<_> = self.stages.iter().flat_map(|s| &s.sources).collect();
189        let row = self.row_bytes();
190        let metadata = std::iter::once(&self.fields)
191            .chain(groups.iter().filter_map(|g| g.fields.as_ref()))
192            .map(|fields| {
193                fields
194                    .iter()
195                    .map(|f| (4096 + f.name.len() + f.from.len()) as u128)
196                    .sum::<u128>()
197            })
198            .max()
199            .unwrap_or(0);
200        let (slots, rows) = self.shards();
201        let batch_rows = self.stages.iter().map(|s| s.batch_size).max().unwrap_or(0) as u128;
202        let batch = (row + 128) * batch_rows + metadata;
203        let shuffle = self
204            .stages
205            .iter()
206            .filter_map(|s| s.shuffle.as_ref())
207            .map(|s| s.buffer_rows)
208            .max()
209            .unwrap_or(0) as u128;
210        let sources = self
211            .stages
212            .iter()
213            .map(|s| s.sources.len())
214            .max()
215            .unwrap_or(0) as u128;
216        let encodings: Vec<_> = self
217            .stages
218            .iter()
219            .flat_map(|s| {
220                s.sources
221                    .iter()
222                    .map(|g| g.encoding.as_ref().or(s.encoding.as_ref()))
223            })
224            .collect();
225        let parquet = encodings.iter().any(|e| e.is_some_and(|e| e == "parquet"));
226        // A Parquet reader writes its rows into the shard it returns; any
227        // other projects them into chunks, then copies them into one.
228        let streamed = encodings.iter().any(|e| e.is_none_or(|e| e != "parquet"));
229        let decoded = if streamed {
230            128
231        } else {
232            self.decode_rows() as u128
233        };
234        let shard = row * rows;
235        let fetched = crate::storage::HELD as u128
236            * if parquet {
237                (self.max_row_group_bytes as u64).max(crate::storage::WINDOW) as u128
238            } else {
239                crate::storage::WINDOW as u128
240            }
241            + crate::storage::PARTS as u128;
242        let workspace = (64 * self.max_record_bytes as u128).max(if parquet {
243            8 * self.max_row_group_bytes as u128
244        } else {
245            0
246        }) + 16 * row * rows.min(decoded)
247            + if streamed { 2 * shard } else { 0 }
248            + metadata * rows.div_ceil(128)
249            + fetched
250            + 32 * 1024 * 1024;
251        let buffered = (shuffle + batch_rows + 1) * (row + 1024);
252        let snapshots = 64 * (shuffle + sources);
253        let bound = slots
254            .checked_mul(shard + metadata)
255            .and_then(|n| {
256                (self.threads as u128)
257                    .min(slots)
258                    .checked_mul(workspace)
259                    .and_then(|w| n.checked_add(w))
260            })
261            .and_then(|n| n.checked_add(buffered))
262            .and_then(|n| {
263                (self.prefetch_batches as u128 + 3)
264                    .checked_mul(batch * 3 + snapshots)
265                    .and_then(|q| n.checked_add(q))
266            })
267            .ok_or_else(|| anyhow::anyhow!("memory bound exceeds supported range"))?;
268        Ok(bound.try_into()?)
269    }
270
271    /// Structural checks that do not need storage. Every source must already be
272    /// resolved to files; manifest references are resolved by `manifest::resolve`.
273    pub fn validate(&self) -> Result<u64> {
274        ensure!(!self.stages.is_empty(), "no stages");
275        ensure!(
276            !self.fields.is_empty(),
277            "fields must be a nonempty allowlist"
278        );
279        ensure!(
280            self.lookahead > 0
281                && self.max_record_bytes > 0
282                && self.max_shard_rows > 0
283                && self.max_row_group_bytes > 0,
284            "invalid memory limits"
285        );
286        ensure!(
287            self.world_size > 0 && self.rank < self.world_size,
288            "invalid rank layout"
289        );
290        ensure!(self.threads > 0, "threads must be positive");
291        for (stage_index, stage) in self.stages.iter().enumerate() {
292            ensure!(
293                stage.sources.len() <= self.threads.saturating_mul(self.lookahead),
294                "threads * lookahead must hold one current shard per source"
295            );
296            ensure!(
297                stage.batch_size > 0 && !stage.sources.is_empty(),
298                "empty stage"
299            );
300            ensure!(
301                stage.shuffle.as_ref().is_none_or(|s| s.buffer_rows > 0),
302                "empty shuffle buffer"
303            );
304            let mut sum = 0.0;
305            for (source_index, group) in stage.sources.iter().enumerate() {
306                ensure!(
307                    group.manifest_uri.is_none() && group.split.is_none(),
308                    "stage {stage_index} source {source_index}: manifest source is not resolved"
309                );
310                ensure!(
311                    matches!(
312                        group
313                            .encoding
314                            .as_ref()
315                            .or(stage.encoding.as_ref())
316                            .map(String::as_str),
317                        Some("msgpack.zstf" | "jsonl.gz" | "parquet")
318                    ),
319                    "stage {stage_index} source {source_index}: unknown encoding"
320                );
321                let w = group.weight.unwrap_or(1.0);
322                ensure!(
323                    w.is_finite() && w >= 0.0 && !group.files.is_empty(),
324                    "stage {stage_index} source {source_index}: invalid source group"
325                );
326                ensure!(
327                    group
328                        .files
329                        .iter()
330                        .all(|f| f.rows.is_none_or(|n| n <= self.max_shard_rows)),
331                    "manifest rows exceed maxShardRows"
332                );
333                sum += w;
334            }
335            ensure!(sum.is_finite() && sum > 0.0, "invalid total weight");
336        }
337        for fields in std::iter::once(&self.fields).chain(
338            self.stages
339                .iter()
340                .flat_map(|s| &s.sources)
341                .filter_map(|g| g.fields.as_ref()),
342        ) {
343            let mut names = std::collections::HashSet::new();
344            for field in fields {
345                ensure!(names.insert(&field.name), "duplicate projected name");
346                ensure!(
347                    matches!(field.dtype.as_str(), "i32" | "i64" | "f32" | "f64" | "bool"),
348                    "unknown dtype"
349                );
350                ensure!(
351                    field.length.is_none_or(|n| n > 0 && n <= i32::MAX as usize),
352                    "invalid field length"
353                );
354                ensure!(
355                    field.shape.is_none() || field.length.is_none(),
356                    "use length or shape, not both"
357                );
358                if let Some(shape) = &field.shape {
359                    ensure!(
360                        !shape.is_empty() && shape.iter().all(|&n| n > 0),
361                        "invalid field shape"
362                    );
363                    ensure!(
364                        shape
365                            .iter()
366                            .try_fold(1usize, |a, &b| a.checked_mul(b))
367                            .is_some_and(|n| n <= i32::MAX as usize),
368                        "field shape is too large"
369                    );
370                }
371            }
372        }
373        for (stage_index, stage) in self.stages.iter().enumerate() {
374            for (source_index, source) in stage.sources.iter().enumerate() {
375                let fields = source.fields.as_ref().unwrap_or(&self.fields);
376                ensure!(
377                    fields.len() == self.fields.len(),
378                    "stage {stage_index} source {source_index} {}: field count differs",
379                    source.files[0].uri
380                );
381                for (a, b) in fields.iter().zip(&self.fields) {
382                    ensure!(
383                        a.name == b.name && a.dtype == b.dtype && a.dimensions() == b.dimensions(),
384                        "stage {stage_index} source {source_index} {}: incompatible field {}",
385                        source.files[0].uri,
386                        b.name
387                    );
388                }
389            }
390        }
391        let bound = self.memory_bound()?;
392        let limit = self.memory_limit()?;
393        let (slots, rows) = self.shards();
394        ensure!(
395            bound <= limit,
396            "memory bound {bound} bytes exceeds memoryLimitBytes {limit}: it holds {slots} \
397             decoded shards of up to {rows} rows of {} bytes; shards of fewer rows lower it",
398            self.row_bytes()
399        );
400        Ok(bound)
401    }
402}