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#[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#[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}
123fn lookahead() -> usize {
124 2
125}
126fn record_limit() -> usize {
127 4 * 1024 * 1024
128}
129fn row_group_limit() -> usize {
130 64 * 1024 * 1024
131}
132fn row_limit() -> usize {
133 65536
134}
135impl Config {
136 pub fn memory_limit(&self) -> Result<u64> {
137 if let Some(limit) = self.memory_limit_bytes {
138 return Ok(limit);
139 }
140 let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
141 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
142 ensure!(
143 pages > 0 && page_size > 0,
144 "cannot read host physical memory"
145 );
146 Ok(((pages as u128 * page_size as u128) / 2).try_into()?)
147 }
148 pub fn memory_bound(&self) -> Result<u64> {
149 let groups: Vec<_> = self.stages.iter().flat_map(|s| &s.sources).collect();
150 let row: u128 = self
151 .fields
152 .iter()
153 .map(|f| {
154 f.width() as u128
155 * if matches!(f.dtype.as_str(), "i64" | "f64") {
156 8
157 } else {
158 4
159 }
160 })
161 .sum();
162 let metadata = std::iter::once(&self.fields)
163 .chain(groups.iter().filter_map(|g| g.fields.as_ref()))
164 .map(|fields| {
165 fields
166 .iter()
167 .map(|f| (4096 + f.name.len() + f.from.len()) as u128)
168 .sum::<u128>()
169 })
170 .max()
171 .unwrap_or(0);
172 let rows = groups
173 .iter()
174 .flat_map(|g| &g.files)
175 .map(|f| f.rows.unwrap_or(self.max_shard_rows))
176 .max()
177 .unwrap_or(0) as u128;
178 let batch_rows = self.stages.iter().map(|s| s.batch_size).max().unwrap_or(0) as u128;
179 let batch = (row + 128) * batch_rows + metadata;
180 let shuffle = self
181 .stages
182 .iter()
183 .filter_map(|s| s.shuffle.as_ref())
184 .map(|s| s.buffer_rows)
185 .max()
186 .unwrap_or(0) as u128;
187 let sources = self
188 .stages
189 .iter()
190 .map(|s| s.sources.len())
191 .max()
192 .unwrap_or(0) as u128;
193 let parquet = self.stages.iter().any(|s| {
194 s.sources.iter().any(|g| {
195 g.encoding
196 .as_ref()
197 .or(s.encoding.as_ref())
198 .is_some_and(|e| e == "parquet")
199 })
200 });
201 let shard = row * rows;
202 let fetched = crate::storage::HELD as u128
206 * if parquet {
207 (self.max_row_group_bytes as u64).max(crate::storage::WINDOW) as u128
208 } else {
209 crate::storage::WINDOW as u128
210 }
211 + crate::storage::PARTS as u128;
212 let workspace = (64 * self.max_record_bytes as u128).max(if parquet {
215 8 * self.max_row_group_bytes as u128
216 } else {
217 0
218 }) + 16 * row * rows.min(128)
219 + 2 * shard
220 + metadata * rows.div_ceil(128)
221 + fetched
222 + 32 * 1024 * 1024;
223 let buffered = (shuffle + batch_rows + 1) * (row + 1024);
224 let snapshots = 64 * (shuffle + sources);
225 let bound = (self.threads as u128)
226 .checked_mul(self.lookahead as u128)
227 .and_then(|n| n.checked_mul(shard + metadata))
228 .and_then(|n| {
229 (self.threads as u128)
230 .checked_mul(workspace)
231 .and_then(|w| n.checked_add(w))
232 })
233 .and_then(|n| n.checked_add(buffered))
234 .and_then(|n| {
235 (self.prefetch_batches as u128 + 3)
236 .checked_mul(batch * 3 + snapshots)
237 .and_then(|q| n.checked_add(q))
238 })
239 .ok_or_else(|| anyhow::anyhow!("memory bound exceeds supported range"))?;
240 Ok(bound.try_into()?)
241 }
242
243 pub fn validate(&self) -> Result<u64> {
246 ensure!(!self.stages.is_empty(), "no stages");
247 ensure!(
248 !self.fields.is_empty(),
249 "fields must be a nonempty allowlist"
250 );
251 ensure!(
252 self.lookahead > 0
253 && self.max_record_bytes > 0
254 && self.max_shard_rows > 0
255 && self.max_row_group_bytes > 0,
256 "invalid memory limits"
257 );
258 ensure!(
259 self.world_size > 0 && self.rank < self.world_size,
260 "invalid rank layout"
261 );
262 ensure!(self.threads > 0, "threads must be positive");
263 for (stage_index, stage) in self.stages.iter().enumerate() {
264 ensure!(
265 stage.sources.len() <= self.threads.saturating_mul(self.lookahead),
266 "threads * lookahead must hold one current shard per source"
267 );
268 ensure!(
269 stage.batch_size > 0 && !stage.sources.is_empty(),
270 "empty stage"
271 );
272 ensure!(
273 stage.shuffle.as_ref().is_none_or(|s| s.buffer_rows > 0),
274 "empty shuffle buffer"
275 );
276 let mut sum = 0.0;
277 for (source_index, group) in stage.sources.iter().enumerate() {
278 ensure!(
279 group.manifest_uri.is_none() && group.split.is_none(),
280 "stage {stage_index} source {source_index}: manifest source is not resolved"
281 );
282 ensure!(
283 matches!(
284 group
285 .encoding
286 .as_ref()
287 .or(stage.encoding.as_ref())
288 .map(String::as_str),
289 Some("msgpack.zstf" | "jsonl.gz" | "parquet")
290 ),
291 "stage {stage_index} source {source_index}: unknown encoding"
292 );
293 let w = group.weight.unwrap_or(1.0);
294 ensure!(
295 w.is_finite() && w >= 0.0 && !group.files.is_empty(),
296 "stage {stage_index} source {source_index}: invalid source group"
297 );
298 ensure!(
299 group
300 .files
301 .iter()
302 .all(|f| f.rows.is_none_or(|n| n <= self.max_shard_rows)),
303 "manifest rows exceed maxShardRows"
304 );
305 sum += w;
306 }
307 ensure!(sum.is_finite() && sum > 0.0, "invalid total weight");
308 }
309 for fields in std::iter::once(&self.fields).chain(
310 self.stages
311 .iter()
312 .flat_map(|s| &s.sources)
313 .filter_map(|g| g.fields.as_ref()),
314 ) {
315 let mut names = std::collections::HashSet::new();
316 for field in fields {
317 ensure!(names.insert(&field.name), "duplicate projected name");
318 ensure!(
319 matches!(field.dtype.as_str(), "i32" | "i64" | "f32" | "f64" | "bool"),
320 "unknown dtype"
321 );
322 ensure!(
323 field.length.is_none_or(|n| n > 0 && n <= i32::MAX as usize),
324 "invalid field length"
325 );
326 ensure!(
327 field.shape.is_none() || field.length.is_none(),
328 "use length or shape, not both"
329 );
330 if let Some(shape) = &field.shape {
331 ensure!(
332 !shape.is_empty() && shape.iter().all(|&n| n > 0),
333 "invalid field shape"
334 );
335 ensure!(
336 shape
337 .iter()
338 .try_fold(1usize, |a, &b| a.checked_mul(b))
339 .is_some_and(|n| n <= i32::MAX as usize),
340 "field shape is too large"
341 );
342 }
343 }
344 }
345 for (stage_index, stage) in self.stages.iter().enumerate() {
346 for (source_index, source) in stage.sources.iter().enumerate() {
347 let fields = source.fields.as_ref().unwrap_or(&self.fields);
348 ensure!(
349 fields.len() == self.fields.len(),
350 "stage {stage_index} source {source_index} {}: field count differs",
351 source.files[0].uri
352 );
353 for (a, b) in fields.iter().zip(&self.fields) {
354 ensure!(
355 a.name == b.name && a.dtype == b.dtype && a.dimensions() == b.dimensions(),
356 "stage {stage_index} source {source_index} {}: incompatible field {}",
357 source.files[0].uri,
358 b.name
359 );
360 }
361 }
362 }
363 let bound = self.memory_bound()?;
364 let limit = self.memory_limit()?;
365 ensure!(
366 bound <= limit,
367 "memory bound {bound} bytes exceeds memoryLimitBytes {limit}"
368 );
369 Ok(bound)
370 }
371}