use arrow::{
array::{ArrayRef, Int32Builder, ListBuilder},
datatypes::{DataType, Field, Schema},
record_batch::RecordBatch,
};
use o41_training_data::{Config, Loader};
use std::{
alloc::{GlobalAlloc, Layout, System},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
struct Counting;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size();
PEAK.fetch_max(live, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(pointer, layout) }
}
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
if size > layout.size() {
let before = LIVE.fetch_add(size - layout.size(), Ordering::Relaxed);
PEAK.fetch_max(before + size, Ordering::Relaxed);
} else {
LIVE.fetch_sub(layout.size() - size, Ordering::Relaxed);
}
unsafe { System.realloc(pointer, layout, size) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
struct Case {
widths: Vec<usize>,
shards: usize,
rows: usize,
group_rows: usize,
batch: usize,
}
fn measure(case: &Case) -> (u64, u64) {
let root = std::env::temp_dir().join(format!(
"o41-memory-{}-{}-{}",
std::process::id(),
case.widths[0],
case.batch
));
std::fs::create_dir_all(&root).unwrap();
let item = Arc::new(Field::new("item", DataType::Int32, true));
let schema = Arc::new(Schema::new(
(0..case.widths.len())
.map(|i| Field::new(format!("f{i}"), DataType::List(item.clone()), false))
.collect::<Vec<_>>(),
));
let mut files = Vec::new();
for shard in 0..case.shards {
let path = root.join(format!("{shard}.parquet"));
let properties = parquet::file::properties::WriterProperties::builder()
.set_max_row_group_row_count(Some(case.group_rows))
.set_compression(parquet::basic::Compression::SNAPPY)
.build();
let mut writer = parquet::arrow::ArrowWriter::try_new(
std::fs::File::create(&path).unwrap(),
schema.clone(),
Some(properties),
)
.unwrap();
for first in (0..case.rows).step_by(case.group_rows) {
let columns = case
.widths
.iter()
.enumerate()
.map(|(column, &width)| {
let mut lists = ListBuilder::new(Int32Builder::new());
for row in first..(first + case.group_rows).min(case.rows) {
lists.values().append_slice(
&(0..width)
.map(|i| ((shard * 7919 + row * 31 + i + column) % 100_003) as i32)
.collect::<Vec<_>>(),
);
lists.append(true);
}
Arc::new(lists.finish()) as ArrayRef
})
.collect();
writer
.write(&RecordBatch::try_new(schema.clone(), columns).unwrap())
.unwrap();
writer.flush().unwrap();
}
writer.close().unwrap();
files.push(serde_json::json!({"uri": path, "rows": case.rows}));
}
let fields: Vec<_> = (0..case.widths.len())
.map(|i| serde_json::json!({"name": format!("f{i}"), "from": format!("f{i}"), "dtype": "i32", "shape": [case.widths[i]]}))
.collect();
let config: Config = serde_json::from_value(serde_json::json!({
"fields": fields,
"stages": [{"encoding": "parquet", "sources": [{"files": files}], "batchSize": case.batch, "infinite": false}],
"seed": 0, "rank": 0, "worldSize": 1, "threads": 2, "prefetchBatches": 4, "lookahead": 2,
"memoryLimitBytes": u64::MAX, "maxRecordBytes": 4 << 20, "maxRowGroupBytes": 64 << 20,
"maxShardRows": 1 << 17,
}))
.unwrap();
let bound = config.memory_bound().unwrap();
let start = LIVE.load(Ordering::Relaxed);
PEAK.store(start, Ordering::Relaxed);
let mut loader = Loader::open(config, None).unwrap();
let mut rows = 0;
while let Some(batch) = loader.next().unwrap() {
rows += batch.columns.num_rows();
}
assert_eq!(rows, case.shards * case.rows);
drop(loader);
std::fs::remove_dir_all(root).unwrap();
(bound, (PEAK.load(Ordering::Relaxed) - start) as u64)
}
fn check(cases: &[Case]) {
for case in cases {
let (bound, peak) = measure(case);
println!(
"widths {:?} shards {} rows {} group {} batch {}: bound {bound} peak {peak}",
case.widths, case.shards, case.rows, case.group_rows, case.batch
);
assert!(peak <= bound, "peak {peak} over bound {bound}");
}
}
#[test]
fn the_bound_holds_wide_and_narrow_rows() {
check(&[
Case {
widths: vec![1 << 20; 2],
shards: 6,
rows: 8,
group_rows: 2,
batch: 2,
},
Case {
widths: vec![512; 2],
shards: 2,
rows: 4096,
group_rows: 1024,
batch: 64,
},
]);
}
#[test]
#[ignore]
fn the_bound_holds_the_reported_shards() {
check(&[
Case {
widths: vec![644809; 3],
shards: 1,
rows: 100,
group_rows: 4,
batch: 1,
},
Case {
widths: vec![1 << 20; 2],
shards: 4,
rows: 25,
group_rows: 2,
batch: 1,
},
Case {
widths: vec![1 << 20; 2],
shards: 4,
rows: 25,
group_rows: 2,
batch: 25,
},
Case {
widths: vec![5544; 2],
shards: 1,
rows: 80000,
group_rows: 8192,
batch: 64,
},
]);
}