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    /// Keep a rank's batch when the finite stream cannot fill every rank.
111    /// Synchronized training keeps the default, false.
112    #[serde(default, skip_serializing_if = "is_false")]
113    pub allow_incomplete_group: bool,
114    pub threads: usize,
115    pub prefetch_batches: usize,
116    #[serde(default = "lookahead")]
117    pub lookahead: usize,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub memory_limit_bytes: Option<u64>,
120    #[serde(default = "record_limit")]
121    pub max_record_bytes: usize,
122    #[serde(default = "row_group_limit")]
123    pub max_row_group_bytes: usize,
124    #[serde(default = "row_limit")]
125    pub max_shard_rows: usize,
126}
127/// Decoded bytes a Parquet reader's batch of wide rows holds: rows of up to
128/// 64 KiB decode 128 at a time.
129const DECODE_BYTES: u128 = 8 * 1024 * 1024;
130fn is_false(value: &bool) -> bool {
131    !*value
132}
133fn lookahead() -> usize {
134    2
135}
136fn record_limit() -> usize {
137    4 * 1024 * 1024
138}
139fn row_group_limit() -> usize {
140    64 * 1024 * 1024
141}
142fn row_limit() -> usize {
143    65536
144}
145impl Config {
146    pub fn memory_limit(&self) -> Result<u64> {
147        if let Some(limit) = self.memory_limit_bytes {
148            return Ok(limit);
149        }
150        let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
151        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
152        ensure!(
153            pages > 0 && page_size > 0,
154            "cannot read host physical memory"
155        );
156        Ok(((pages as u128 * page_size as u128) / 2).try_into()?)
157    }
158    /// Bytes the memory bound counts for a row: eight for each `i64` or `f64`
159    /// element, four for any other.
160    fn row_bytes(&self) -> u128 {
161        self.fields
162            .iter()
163            .map(|f| {
164                f.width() as u128
165                    * if matches!(f.dtype.as_str(), "i64" | "f64") {
166                        8
167                    } else {
168                        4
169                    }
170            })
171            .sum()
172    }
173    /// Rows a Parquet reader decodes at once: 128, or as many as fit
174    /// [`DECODE_BYTES`], at least one.
175    pub(crate) fn decode_rows(&self) -> usize {
176        (DECODE_BYTES / self.row_bytes().max(1)).clamp(1, 128) as usize
177    }
178    /// Decoded shards held at once, and the largest's rows: each reader slot
179    /// holds one, and a file takes at most one slot.
180    fn shards(&self) -> (u128, u128) {
181        let files = self
182            .stages
183            .iter()
184            .flat_map(|s| &s.sources)
185            .flat_map(|g| &g.files);
186        let slots =
187            (self.threads as u128 * self.lookahead as u128).min(files.clone().count() as u128);
188        let rows = files
189            .map(|f| f.rows.unwrap_or(self.max_shard_rows))
190            .max()
191            .unwrap_or(0) as u128;
192        (slots, rows)
193    }
194    pub fn memory_bound(&self) -> Result<u64> {
195        let groups: Vec<_> = self.stages.iter().flat_map(|s| &s.sources).collect();
196        let row = self.row_bytes();
197        let metadata = std::iter::once(&self.fields)
198            .chain(groups.iter().filter_map(|g| g.fields.as_ref()))
199            .map(|fields| {
200                fields
201                    .iter()
202                    .map(|f| (4096 + f.name.len() + f.from.len()) as u128)
203                    .sum::<u128>()
204            })
205            .max()
206            .unwrap_or(0);
207        let (slots, rows) = self.shards();
208        let batch_rows = self.stages.iter().map(|s| s.batch_size).max().unwrap_or(0) as u128;
209        let batch = (row + 128) * batch_rows + metadata;
210        let shuffle = self
211            .stages
212            .iter()
213            .filter_map(|s| s.shuffle.as_ref())
214            .map(|s| s.buffer_rows)
215            .max()
216            .unwrap_or(0) as u128;
217        let sources = self
218            .stages
219            .iter()
220            .map(|s| s.sources.len())
221            .max()
222            .unwrap_or(0) as u128;
223        let encodings: Vec<_> = self
224            .stages
225            .iter()
226            .flat_map(|s| {
227                s.sources
228                    .iter()
229                    .map(|g| g.encoding.as_ref().or(s.encoding.as_ref()))
230            })
231            .collect();
232        let parquet = encodings.iter().any(|e| e.is_some_and(|e| e == "parquet"));
233        // A Parquet reader writes its rows into the shard it returns; any
234        // other projects them into chunks, then copies them into one.
235        let streamed = encodings.iter().any(|e| e.is_none_or(|e| e != "parquet"));
236        let decoded = if streamed {
237            128
238        } else {
239            self.decode_rows() as u128
240        };
241        let shard = row * rows;
242        let fetched = crate::storage::HELD as u128
243            * if parquet {
244                (self.max_row_group_bytes as u64).max(crate::storage::WINDOW) as u128
245            } else {
246                crate::storage::WINDOW as u128
247            }
248            + crate::storage::PARTS as u128;
249        let workspace = (64 * self.max_record_bytes as u128).max(if parquet {
250            8 * self.max_row_group_bytes as u128
251        } else {
252            0
253        }) + 16 * row * rows.min(decoded)
254            + if streamed { 2 * shard } else { 0 }
255            + metadata * rows.div_ceil(128)
256            + fetched
257            + 32 * 1024 * 1024;
258        let buffered = (shuffle + batch_rows + 1) * (row + 1024);
259        let snapshots = 64 * (shuffle + sources);
260        let bound = slots
261            .checked_mul(shard + metadata)
262            .and_then(|n| {
263                (self.threads as u128)
264                    .min(slots)
265                    .checked_mul(workspace)
266                    .and_then(|w| n.checked_add(w))
267            })
268            .and_then(|n| n.checked_add(buffered))
269            .and_then(|n| {
270                (self.prefetch_batches as u128 + 3)
271                    .checked_mul(batch * 3 + snapshots)
272                    .and_then(|q| n.checked_add(q))
273            })
274            .ok_or_else(|| anyhow::anyhow!("memory bound exceeds supported range"))?;
275        Ok(bound.try_into()?)
276    }
277
278    /// Structural checks that do not need storage. Every source must already be
279    /// resolved to files; manifest references are resolved by `manifest::resolve`.
280    pub fn validate(&self) -> Result<u64> {
281        ensure!(!self.stages.is_empty(), "no stages");
282        ensure!(
283            !self.fields.is_empty(),
284            "fields must be a nonempty allowlist"
285        );
286        ensure!(
287            self.lookahead > 0
288                && self.max_record_bytes > 0
289                && self.max_shard_rows > 0
290                && self.max_row_group_bytes > 0,
291            "invalid memory limits"
292        );
293        ensure!(
294            self.world_size > 0 && self.rank < self.world_size,
295            "invalid rank layout"
296        );
297        ensure!(self.threads > 0, "threads must be positive");
298        for (stage_index, stage) in self.stages.iter().enumerate() {
299            ensure!(
300                stage.sources.len() <= self.threads.saturating_mul(self.lookahead),
301                "threads * lookahead must hold one current shard per source"
302            );
303            ensure!(
304                stage.batch_size > 0 && !stage.sources.is_empty(),
305                "empty stage"
306            );
307            ensure!(
308                stage.shuffle.as_ref().is_none_or(|s| s.buffer_rows > 0),
309                "empty shuffle buffer"
310            );
311            let mut sum = 0.0;
312            for (source_index, group) in stage.sources.iter().enumerate() {
313                ensure!(
314                    group.manifest_uri.is_none() && group.split.is_none(),
315                    "stage {stage_index} source {source_index}: manifest source is not resolved"
316                );
317                ensure!(
318                    matches!(
319                        group
320                            .encoding
321                            .as_ref()
322                            .or(stage.encoding.as_ref())
323                            .map(String::as_str),
324                        Some("msgpack.zstf" | "jsonl.gz" | "parquet")
325                    ),
326                    "stage {stage_index} source {source_index}: unknown encoding"
327                );
328                let w = group.weight.unwrap_or(1.0);
329                ensure!(
330                    w.is_finite() && w >= 0.0 && !group.files.is_empty(),
331                    "stage {stage_index} source {source_index}: invalid source group"
332                );
333                ensure!(
334                    group
335                        .files
336                        .iter()
337                        .all(|f| f.rows.is_none_or(|n| n <= self.max_shard_rows)),
338                    "manifest rows exceed maxShardRows"
339                );
340                sum += w;
341            }
342            ensure!(sum.is_finite() && sum > 0.0, "invalid total weight");
343        }
344        for fields in std::iter::once(&self.fields).chain(
345            self.stages
346                .iter()
347                .flat_map(|s| &s.sources)
348                .filter_map(|g| g.fields.as_ref()),
349        ) {
350            let mut names = std::collections::HashSet::new();
351            for field in fields {
352                ensure!(names.insert(&field.name), "duplicate projected name");
353                ensure!(
354                    matches!(field.dtype.as_str(), "i32" | "i64" | "f32" | "f64" | "bool"),
355                    "unknown dtype"
356                );
357                ensure!(
358                    field.length.is_none_or(|n| n > 0 && n <= i32::MAX as usize),
359                    "invalid field length"
360                );
361                ensure!(
362                    field.shape.is_none() || field.length.is_none(),
363                    "use length or shape, not both"
364                );
365                if let Some(shape) = &field.shape {
366                    ensure!(
367                        !shape.is_empty() && shape.iter().all(|&n| n > 0),
368                        "invalid field shape"
369                    );
370                    ensure!(
371                        shape
372                            .iter()
373                            .try_fold(1usize, |a, &b| a.checked_mul(b))
374                            .is_some_and(|n| n <= i32::MAX as usize),
375                        "field shape is too large"
376                    );
377                }
378            }
379        }
380        for (stage_index, stage) in self.stages.iter().enumerate() {
381            for (source_index, source) in stage.sources.iter().enumerate() {
382                let fields = source.fields.as_ref().unwrap_or(&self.fields);
383                ensure!(
384                    fields.len() == self.fields.len(),
385                    "stage {stage_index} source {source_index} {}: field count differs",
386                    source.files[0].uri
387                );
388                for (a, b) in fields.iter().zip(&self.fields) {
389                    ensure!(
390                        a.name == b.name && a.dtype == b.dtype && a.dimensions() == b.dimensions(),
391                        "stage {stage_index} source {source_index} {}: incompatible field {}",
392                        source.files[0].uri,
393                        b.name
394                    );
395                }
396            }
397        }
398        let bound = self.memory_bound()?;
399        let limit = self.memory_limit()?;
400        let (slots, rows) = self.shards();
401        ensure!(
402            bound <= limit,
403            "memory bound {bound} bytes exceeds memoryLimitBytes {limit}: it holds {slots} \
404             decoded shards of up to {rows} rows of {} bytes; shards of fewer rows lower it",
405            self.row_bytes()
406        );
407        Ok(bound)
408    }
409}