1use std::{mem::size_of, sync::Arc};
2
3use laddu_data::{
4 BatchLayout,
5 data::{CacheStorage, Dataset},
6 schema::Precision as DataPrecision,
7};
8use laddu_expr::{ExprId, ValueKind};
9use laddu_memory::{FootprintOverflow, MemoryFootprint};
10use num::complex::Complex64;
11
12use super::cache::{CachedFactorSlot, CachedSlot, CachedSolveRowSlot};
13use super::{
14 CpuCachedBatch, CpuCachedDataset, CpuPlan, CpuPreparedDataset, DynamicLu, RuntimeError,
15 RuntimeResult,
16};
17use crate::execution::Execution;
18use crate::preparation::{DatasetPreparation, LocalDatasetStats};
19
20impl CpuPlan {
21 pub fn cache_dataset(&self, dataset: &Dataset) -> RuntimeResult<CpuCachedDataset> {
28 self.cache_dataset_with_plan(dataset, dataset.read_plan())
29 }
30
31 pub fn cache_memory_estimate(&self, events: usize) -> usize {
33 self.cache_memory_footprint()
34 .map(|footprint| usize::try_from(footprint.peak_bytes(events)).unwrap_or(usize::MAX))
35 .unwrap_or(usize::MAX)
36 }
37
38 fn cache_memory_footprint(&self) -> Result<MemoryFootprint, FootprintOverflow> {
39 let mut fixed = MemoryFootprint::fixed(0);
40 for (count, bytes) in [
41 (self.cache_plan.entries().len(), size_of::<CachedSlot>()),
42 (self.factor_matrices.len(), size_of::<CachedFactorSlot>()),
43 (self.solve_row_keys.len(), size_of::<CachedSolveRowSlot>()),
44 (self.cache_plan.entries().len(), size_of::<ExprId>()),
45 (self.factor_matrices.len(), size_of::<ExprId>()),
46 (
47 self.solve_row_keys.len(),
48 size_of::<(ExprId, usize, usize)>(),
49 ),
50 ] {
51 fixed = fixed.checked_add(
52 MemoryFootprint::from_usize_checked(bytes, 0)?.checked_scale_usize(count)?,
53 )?;
54 }
55
56 let mut per_event = MemoryFootprint::per_event(size_of::<f64>() as u64);
57 for entry in self.cache_plan.entries() {
58 let bytes = match entry.value_kind() {
59 ValueKind::Real => MemoryFootprint::per_event(size_of::<f64>() as u64),
60 ValueKind::Complex => MemoryFootprint::per_event(size_of::<Complex64>() as u64),
61 ValueKind::Vector { len } => {
62 MemoryFootprint::per_event(size_of::<Complex64>() as u64)
63 .checked_scale_usize(len)?
64 }
65 ValueKind::Matrix { rows, cols } => {
66 MemoryFootprint::per_event(size_of::<Complex64>() as u64)
67 .checked_scale_usize(rows)?
68 .checked_scale_usize(cols)?
69 }
70 };
71 per_event = per_event.checked_add(bytes)?;
72 }
73 for (_, dimension) in &self.factor_matrices {
74 let bytes = MemoryFootprint::per_event(size_of::<DynamicLu>() as u64)
75 .checked_add(
76 MemoryFootprint::per_event(size_of::<Complex64>() as u64)
77 .checked_scale_usize(*dimension)?
78 .checked_scale_usize(*dimension)?,
79 )?
80 .checked_add(
81 MemoryFootprint::per_event(size_of::<usize>() as u64)
82 .checked_scale_usize(*dimension)?,
83 )?;
84 per_event = per_event.checked_add(bytes)?;
85 }
86 for (_, _, dimension) in &self.solve_row_keys {
87 per_event = per_event.checked_add(
88 MemoryFootprint::per_event(size_of::<Complex64>() as u64)
89 .checked_scale_usize(*dimension)?,
90 )?;
91 }
92 fixed.checked_add(per_event)
93 }
94
95 fn cache_dataset_with_plan(
96 &self,
97 dataset: &Dataset,
98 read_plan: laddu_data::io::ReadPlan,
99 ) -> RuntimeResult<CpuCachedDataset> {
100 let mut batches = Vec::new();
101 let mut sum_weights = 0.0;
102 for batch in dataset
103 .stream_with_plan(read_plan)
104 .map_err(|err| RuntimeError::Data(err.to_string()))?
105 {
106 let batch = batch.map_err(|err| RuntimeError::Data(err.to_string()))?;
107 let cached = CpuCachedBatch::from_cache(self.cache_event_batch(&batch)?);
108 sum_weights += cached.sum_weights();
109 batches.push(cached);
110 }
111 Ok(CpuCachedDataset::from_parts(batches, sum_weights))
112 }
113
114 pub fn prepare_dataset(
126 &self,
127 execution: &Execution,
128 dataset: &Dataset,
129 ) -> RuntimeResult<CpuPreparedDataset> {
130 let preparation = DatasetPreparation::new(execution, dataset);
131 let mut planning = preparation.runtime_plan()?;
132 let initial_read_plan = planning.read_plan();
133 let schema = dataset
134 .schema()
135 .map_err(|error| RuntimeError::Data(error.to_string()))?;
136 let source_footprint = BatchLayout::from_schema(&schema)
137 .schema_footprint(DataPrecision::F64)
138 .map_err(|error| RuntimeError::Data(format!("source working-set overflow: {error}")))?;
139 let cache_footprint = self
140 .cache_memory_footprint()
141 .map_err(|error| RuntimeError::Data(format!("cache working-set overflow: {error}")))?;
142 let source_bytes_per_event =
143 usize::try_from(source_footprint.bytes_per_event).unwrap_or(usize::MAX);
144 let cache_zero = usize::try_from(cache_footprint.fixed_bytes).unwrap_or(usize::MAX);
145 let cache_bytes_per_event =
146 usize::try_from(cache_footprint.bytes_per_event).unwrap_or(usize::MAX);
147 let local_event_limit = planning.event_limit();
148 let host_remaining = execution.host_memory().remaining();
149 let resident_plan = resident_cache_plan(
150 cache_zero,
151 cache_bytes_per_event,
152 source_bytes_per_event.saturating_mul(2),
153 local_event_limit,
154 usize::try_from(host_remaining).unwrap_or(usize::MAX),
155 );
156 let source_staging = source_footprint
157 .checked_scale(2)
158 .and_then(|footprint| {
159 MemoryFootprint::fixed(footprint.bytes_per_event)
160 .checked_add(MemoryFootprint::per_event(cache_footprint.bytes_per_event))
161 })
162 .map_err(|error| RuntimeError::Data(format!("source working-set overflow: {error}")))?;
163 let minimum = MemoryFootprint::fixed(cache_footprint.fixed_bytes)
164 .checked_add(source_staging)
165 .map_err(|error| RuntimeError::Data(format!("cache working-set overflow: {error}")))?
166 .peak_bytes(local_event_limit);
167 let resident_bytes = resident_plan
168 .map(|(bytes, _)| u64::try_from(bytes).unwrap_or(u64::MAX))
169 .unwrap_or(minimum);
170 planning.select_storage(
171 dataset.memory_policy(),
172 "host",
173 resident_plan.is_some(),
174 resident_bytes,
175 host_remaining,
176 )?;
177 let requested_storage = planning.storage();
178 planning.reserve_storage(Some(execution.host_memory()), || {
179 RuntimeError::Data("CPU execution has no host memory pool".into())
180 })?;
181 let persistent_lease = planning.take_memory_lease();
182 let available_for_batch = execution.host_memory().remaining();
183 let transient_footprint = if requested_storage == CacheStorage::Streaming {
187 cache_footprint
188 .checked_add(source_footprint.checked_scale(2).map_err(|error| {
189 RuntimeError::Data(format!("source working-set overflow: {error}"))
190 })?)
191 .map_err(|error| {
192 RuntimeError::Data(format!("cache working-set overflow: {error}"))
193 })?
194 } else {
195 source_footprint.checked_scale(2).map_err(|error| {
196 RuntimeError::Data(format!("source working-set overflow: {error}"))
197 })?
198 };
199 let decision = planning.fit_staging(
200 "CPU prepared dataset",
201 transient_footprint,
202 available_for_batch,
203 if requested_storage == CacheStorage::Resident {
204 "resident"
205 } else {
206 "streaming"
207 },
208 )?;
209 planning.clamp_read_plan(initial_read_plan.chunk_size, decision.chunk_events);
210 let decision = planning
211 .take_decisions()
212 .into_iter()
213 .next()
214 .expect("CPU preparation records one staging decision");
215 execution.record_memory_decision(decision.clone());
216 let read_plan = planning.read_plan();
217 match requested_storage {
218 CacheStorage::Resident => {
219 let dataset =
220 preparation.coordinate(self.cache_dataset_with_plan(dataset, read_plan))?;
221 let stats = preparation.finish_stats(
222 LocalDatasetStats::new(
223 dataset.len(),
224 dataset.batches().len(),
225 dataset.sum_weights(),
226 ),
227 dataset.resident_bytes(),
228 requested_storage,
229 );
230 let memory_lease = persistent_lease.ok_or_else(|| {
231 RuntimeError::Data(
232 "resident dataset preparation did not reserve host memory".into(),
233 )
234 })?;
235 Ok(CpuPreparedDataset::Resident {
236 dataset: Arc::new(dataset),
237 stats,
238 memory_lease,
239 })
240 }
241 CacheStorage::Streaming => {
242 let local = preparation.scan()?;
243 Ok(CpuPreparedDataset::Streaming {
244 dataset: dataset.clone(),
245 stats: preparation.finish_stats(local, 0, requested_storage),
246 read_plan,
247 transient_bytes: decision.estimated_peak_bytes,
248 })
249 }
250 }
251 }
252}
253
254pub(super) fn resident_cache_plan(
255 fixed_per_batch: usize,
256 cache_bytes_per_event: usize,
257 source_bytes_per_event: usize,
258 events: usize,
259 available: usize,
260) -> Option<(usize, usize)> {
261 if events == 0 {
262 return Some((fixed_per_batch, 1));
263 }
264 let event_cache = cache_bytes_per_event.checked_mul(events)?;
265 let minimum = event_cache
266 .checked_add(fixed_per_batch)?
267 .checked_add(source_bytes_per_event)?;
268 if minimum > available {
269 return None;
270 }
271 let mut chunk = events;
272 for _ in 0..16 {
273 let batches = events.saturating_add(chunk - 1) / chunk;
274 let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
275 let next = available
276 .saturating_sub(resident)
277 .checked_div(source_bytes_per_event.max(1))?
278 .min(events);
279 if next == 0 {
280 return None;
281 }
282 if next == chunk {
283 return Some((resident, chunk));
284 }
285 chunk = next;
286 }
287 let batches = events.saturating_add(chunk - 1) / chunk;
288 let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
289 (resident.checked_add(source_bytes_per_event.checked_mul(chunk)?)? <= available)
290 .then_some((resident, chunk))
291}