use std::{mem::size_of, sync::Arc};
use laddu_data::{
BatchLayout,
data::{CacheStorage, Dataset},
schema::Precision as DataPrecision,
};
use laddu_expr::{ExprId, ValueKind};
use laddu_memory::{FootprintOverflow, MemoryFootprint};
use num::complex::Complex64;
use super::cache::{CachedFactorSlot, CachedSlot, CachedSolveRowSlot};
use super::{
CpuCachedBatch, CpuCachedDataset, CpuPlan, CpuPreparedDataset, DynamicLu, RuntimeError,
RuntimeResult,
};
use crate::execution::Execution;
use crate::preparation::{DatasetPreparation, LocalDatasetStats};
impl CpuPlan {
pub fn cache_dataset(&self, dataset: &Dataset) -> RuntimeResult<CpuCachedDataset> {
self.cache_dataset_with_plan(dataset, dataset.read_plan())
}
pub fn cache_memory_estimate(&self, events: usize) -> usize {
self.cache_memory_footprint()
.map(|footprint| usize::try_from(footprint.peak_bytes(events)).unwrap_or(usize::MAX))
.unwrap_or(usize::MAX)
}
fn cache_memory_footprint(&self) -> Result<MemoryFootprint, FootprintOverflow> {
let mut fixed = MemoryFootprint::fixed(0);
for (count, bytes) in [
(self.cache_plan.entries().len(), size_of::<CachedSlot>()),
(self.factor_matrices.len(), size_of::<CachedFactorSlot>()),
(self.solve_row_keys.len(), size_of::<CachedSolveRowSlot>()),
(self.cache_plan.entries().len(), size_of::<ExprId>()),
(self.factor_matrices.len(), size_of::<ExprId>()),
(
self.solve_row_keys.len(),
size_of::<(ExprId, usize, usize)>(),
),
] {
fixed = fixed.checked_add(
MemoryFootprint::from_usize_checked(bytes, 0)?.checked_scale_usize(count)?,
)?;
}
let mut per_event = MemoryFootprint::per_event(size_of::<f64>() as u64);
for entry in self.cache_plan.entries() {
let bytes = match entry.value_kind() {
ValueKind::Real => MemoryFootprint::per_event(size_of::<f64>() as u64),
ValueKind::Complex => MemoryFootprint::per_event(size_of::<Complex64>() as u64),
ValueKind::Vector { len } => {
MemoryFootprint::per_event(size_of::<Complex64>() as u64)
.checked_scale_usize(len)?
}
ValueKind::Matrix { rows, cols } => {
MemoryFootprint::per_event(size_of::<Complex64>() as u64)
.checked_scale_usize(rows)?
.checked_scale_usize(cols)?
}
};
per_event = per_event.checked_add(bytes)?;
}
for (_, dimension) in &self.factor_matrices {
let bytes = MemoryFootprint::per_event(size_of::<DynamicLu>() as u64)
.checked_add(
MemoryFootprint::per_event(size_of::<Complex64>() as u64)
.checked_scale_usize(*dimension)?
.checked_scale_usize(*dimension)?,
)?
.checked_add(
MemoryFootprint::per_event(size_of::<usize>() as u64)
.checked_scale_usize(*dimension)?,
)?;
per_event = per_event.checked_add(bytes)?;
}
for (_, _, dimension) in &self.solve_row_keys {
per_event = per_event.checked_add(
MemoryFootprint::per_event(size_of::<Complex64>() as u64)
.checked_scale_usize(*dimension)?,
)?;
}
fixed.checked_add(per_event)
}
fn cache_dataset_with_plan(
&self,
dataset: &Dataset,
read_plan: laddu_data::io::ReadPlan,
) -> RuntimeResult<CpuCachedDataset> {
let mut batches = Vec::new();
let mut sum_weights = 0.0;
for batch in dataset
.stream_with_plan(read_plan)
.map_err(|err| RuntimeError::Data(err.to_string()))?
{
let batch = batch.map_err(|err| RuntimeError::Data(err.to_string()))?;
let cached = CpuCachedBatch::from_cache(self.cache_event_batch(&batch)?);
sum_weights += cached.sum_weights();
batches.push(cached);
}
Ok(CpuCachedDataset::from_parts(batches, sum_weights))
}
pub fn prepare_dataset(
&self,
execution: &Execution,
dataset: &Dataset,
) -> RuntimeResult<CpuPreparedDataset> {
let preparation = DatasetPreparation::new(execution, dataset);
let mut planning = preparation.runtime_plan()?;
let initial_read_plan = planning.read_plan();
let schema = dataset
.schema()
.map_err(|error| RuntimeError::Data(error.to_string()))?;
let source_footprint = BatchLayout::from_schema(&schema)
.schema_footprint(DataPrecision::F64)
.map_err(|error| RuntimeError::Data(format!("source working-set overflow: {error}")))?;
let cache_footprint = self
.cache_memory_footprint()
.map_err(|error| RuntimeError::Data(format!("cache working-set overflow: {error}")))?;
let source_bytes_per_event =
usize::try_from(source_footprint.bytes_per_event).unwrap_or(usize::MAX);
let cache_zero = usize::try_from(cache_footprint.fixed_bytes).unwrap_or(usize::MAX);
let cache_bytes_per_event =
usize::try_from(cache_footprint.bytes_per_event).unwrap_or(usize::MAX);
let local_event_limit = planning.event_limit();
let host_remaining = execution.host_memory().remaining();
let resident_plan = resident_cache_plan(
cache_zero,
cache_bytes_per_event,
source_bytes_per_event.saturating_mul(2),
local_event_limit,
usize::try_from(host_remaining).unwrap_or(usize::MAX),
);
let source_staging = source_footprint
.checked_scale(2)
.and_then(|footprint| {
MemoryFootprint::fixed(footprint.bytes_per_event)
.checked_add(MemoryFootprint::per_event(cache_footprint.bytes_per_event))
})
.map_err(|error| RuntimeError::Data(format!("source working-set overflow: {error}")))?;
let minimum = MemoryFootprint::fixed(cache_footprint.fixed_bytes)
.checked_add(source_staging)
.map_err(|error| RuntimeError::Data(format!("cache working-set overflow: {error}")))?
.peak_bytes(local_event_limit);
let resident_bytes = resident_plan
.map(|(bytes, _)| u64::try_from(bytes).unwrap_or(u64::MAX))
.unwrap_or(minimum);
planning.select_storage(
dataset.memory_policy(),
"host",
resident_plan.is_some(),
resident_bytes,
host_remaining,
)?;
let requested_storage = planning.storage();
planning.reserve_storage(Some(execution.host_memory()), || {
RuntimeError::Data("CPU execution has no host memory pool".into())
})?;
let persistent_lease = planning.take_memory_lease();
let available_for_batch = execution.host_memory().remaining();
let transient_footprint = if requested_storage == CacheStorage::Streaming {
cache_footprint
.checked_add(source_footprint.checked_scale(2).map_err(|error| {
RuntimeError::Data(format!("source working-set overflow: {error}"))
})?)
.map_err(|error| {
RuntimeError::Data(format!("cache working-set overflow: {error}"))
})?
} else {
source_footprint.checked_scale(2).map_err(|error| {
RuntimeError::Data(format!("source working-set overflow: {error}"))
})?
};
let decision = planning.fit_staging(
"CPU prepared dataset",
transient_footprint,
available_for_batch,
if requested_storage == CacheStorage::Resident {
"resident"
} else {
"streaming"
},
)?;
planning.clamp_read_plan(initial_read_plan.chunk_size, decision.chunk_events);
let decision = planning
.take_decisions()
.into_iter()
.next()
.expect("CPU preparation records one staging decision");
execution.record_memory_decision(decision.clone());
let read_plan = planning.read_plan();
match requested_storage {
CacheStorage::Resident => {
let dataset =
preparation.coordinate(self.cache_dataset_with_plan(dataset, read_plan))?;
let stats = preparation.finish_stats(
LocalDatasetStats::new(
dataset.len(),
dataset.batches().len(),
dataset.sum_weights(),
),
dataset.resident_bytes(),
requested_storage,
);
let memory_lease = persistent_lease.ok_or_else(|| {
RuntimeError::Data(
"resident dataset preparation did not reserve host memory".into(),
)
})?;
Ok(CpuPreparedDataset::Resident {
dataset: Arc::new(dataset),
stats,
memory_lease,
})
}
CacheStorage::Streaming => {
let local = preparation.scan()?;
Ok(CpuPreparedDataset::Streaming {
dataset: dataset.clone(),
stats: preparation.finish_stats(local, 0, requested_storage),
read_plan,
transient_bytes: decision.estimated_peak_bytes,
})
}
}
}
}
pub(super) fn resident_cache_plan(
fixed_per_batch: usize,
cache_bytes_per_event: usize,
source_bytes_per_event: usize,
events: usize,
available: usize,
) -> Option<(usize, usize)> {
if events == 0 {
return Some((fixed_per_batch, 1));
}
let event_cache = cache_bytes_per_event.checked_mul(events)?;
let minimum = event_cache
.checked_add(fixed_per_batch)?
.checked_add(source_bytes_per_event)?;
if minimum > available {
return None;
}
let mut chunk = events;
for _ in 0..16 {
let batches = events.saturating_add(chunk - 1) / chunk;
let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
let next = available
.saturating_sub(resident)
.checked_div(source_bytes_per_event.max(1))?
.min(events);
if next == 0 {
return None;
}
if next == chunk {
return Some((resident, chunk));
}
chunk = next;
}
let batches = events.saturating_add(chunk - 1) / chunk;
let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
(resident.checked_add(source_bytes_per_event.checked_mul(chunk)?)? <= available)
.then_some((resident, chunk))
}