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 #[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}
127const 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 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 pub(crate) fn decode_rows(&self) -> usize {
176 (DECODE_BYTES / self.row_bytes().max(1)).clamp(1, 128) as usize
177 }
178 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 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 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}