1use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use eredu_core::{
6 residency::{MemoryTier, TransferDirection},
7 BoundedResidencyRequirement, DurationSeconds, ResidencyTelemetry, TransferTelemetry,
8};
9
10use crate::{
11 replicated_text_materialization_tasks, selected_materialization_task_bytes,
12 LayerWeightResidency, ReplicatedTextContractError, ReplicatedTextParameterOwner,
13 ResidencyReport, SelectedReplicatedTextRealization,
14};
15
16#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
18#[non_exhaustive]
19pub enum BoundedResidencySizingError {
20 #[error(transparent)]
22 Materialization(#[from] ReplicatedTextContractError),
23 #[error("selected {0} overflowed")]
25 ArithmeticOverflow(&'static str),
26}
27
28pub fn selected_text_bounded_requirement(
35 selected: &SelectedReplicatedTextRealization,
36 excluded: &BTreeSet<String>,
37) -> Result<BoundedResidencyRequirement, BoundedResidencySizingError> {
38 let tasks = replicated_text_materialization_tasks(selected)?;
39 bounded_requirement(
40 tasks.iter().map(|task| {
41 (task.name(), task.owner(), || {
42 selected_materialization_task_bytes(task)
43 })
44 }),
45 selected.residency(),
46 excluded,
47 )
48}
49
50fn bounded_requirement<'a, F>(
51 parameters: impl IntoIterator<Item = (&'a str, &'a ReplicatedTextParameterOwner, F)>,
52 residency: LayerWeightResidency,
53 excluded: &BTreeSet<String>,
54) -> Result<BoundedResidencyRequirement, BoundedResidencySizingError>
55where
56 F: FnOnce() -> Result<u64, ReplicatedTextContractError>,
57{
58 use BoundedResidencySizingError::ArithmeticOverflow;
59
60 let mut static_bytes = 0u64;
61 let mut groups = BTreeMap::<&str, BTreeMap<usize, u64>>::new();
62 for (name, owner, bytes) in parameters {
63 if excluded.contains(name) {
64 continue;
65 }
66 let bytes = bytes()?;
67 match owner {
68 ReplicatedTextParameterOwner::StaticRole(_) => {
69 static_bytes = static_bytes
70 .checked_add(bytes)
71 .ok_or(ArithmeticOverflow("static parameter bytes"))?;
72 }
73 ReplicatedTextParameterOwner::ExecutionUnit { group, unit } => {
74 let total = groups.entry(group).or_default().entry(*unit).or_default();
75 *total = total
76 .checked_add(bytes)
77 .ok_or(ArithmeticOverflow("execution-unit bytes"))?;
78 }
79 }
80 }
81 let unit_count = groups.values().try_fold(0usize, |total, units| {
82 let count = units
83 .last_key_value()
84 .map_or(Some(0), |(unit, _)| unit.checked_add(1))
85 .ok_or(ArithmeticOverflow("execution-group unit count"))?;
86 total
87 .checked_add(count)
88 .ok_or(ArithmeticOverflow("execution unit count"))
89 })?;
90 let depth = residency.device_depth(unit_count);
91 let mut window_bytes = 0u64;
92 if depth != 0 {
93 for units in groups.values() {
94 let mut window = VecDeque::<(usize, u64)>::new();
95 let mut current = 0u64;
96 for (&unit, &bytes) in units {
97 while window
98 .front()
99 .is_some_and(|(first, _)| unit - first >= depth)
100 {
101 let (_, expired) = window.pop_front().expect("window is nonempty");
102 current -= expired;
103 }
104 current = current
105 .checked_add(bytes)
106 .ok_or(ArithmeticOverflow("device-window bytes"))?;
107 window.push_back((unit, bytes));
108 window_bytes = window_bytes.max(current);
109 }
110 }
111 }
112 let required_bytes = static_bytes
113 .checked_add(window_bytes)
114 .ok_or(ArithmeticOverflow("bounded-residency bytes"))?;
115 Ok(BoundedResidencyRequirement {
116 static_bytes,
117 window_bytes,
118 required_bytes,
119 depth,
120 })
121}
122
123pub fn residency_telemetry(report: &ResidencyReport) -> ResidencyTelemetry {
125 let offload = report.offload();
126 let planned = offload.planned_bytes();
127 let current = offload.resident_bytes();
128 let peak = offload.peak_resident_bytes();
129 let transfers = TransferDirection::ALL
130 .into_iter()
131 .map(|direction| {
132 let metrics = offload.transfer(direction);
133 TransferTelemetry {
134 direction: match direction {
135 TransferDirection::DeviceToHost => "device_to_host",
136 TransferDirection::DeviceToDisk => "device_to_disk",
137 TransferDirection::HostToDevice => "host_to_device",
138 TransferDirection::HostToDisk => "host_to_disk",
139 TransferDirection::DiskToDevice => "disk_to_device",
140 TransferDirection::DiskToHost => "disk_to_host",
141 }
142 .into(),
143 count: metrics.count(),
144 bytes: metrics.bytes(),
145 seconds: DurationSeconds(metrics.duration().as_secs_f64()),
146 }
147 })
148 .collect();
149 ResidencyTelemetry {
150 planned_disk_bytes: planned.get(MemoryTier::Disk),
151 planned_host_bytes: planned.get(MemoryTier::Host),
152 planned_device_bytes: planned.get(MemoryTier::Device),
153 current_host_bytes: current.get(MemoryTier::Host),
154 current_device_bytes: current.get(MemoryTier::Device),
155 peak_host_bytes: peak.get(MemoryTier::Host),
156 peak_device_bytes: peak.get(MemoryTier::Device),
157 transfers,
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use eredu_core::residency::{OffloadConfig, OffloadTelemetry, TierByteTotals};
165
166 fn unit(group: &str, unit: usize) -> ReplicatedTextParameterOwner {
167 ReplicatedTextParameterOwner::ExecutionUnit {
168 group: group.into(),
169 unit,
170 }
171 }
172
173 fn host(depth: usize) -> LayerWeightResidency {
174 LayerWeightResidency::LayerwiseHost(crate::LayerwiseLoadOptions::new(
175 OffloadConfig::new(None, None, depth).unwrap(),
176 ))
177 }
178
179 fn size(
180 parameters: &[(&str, ReplicatedTextParameterOwner, u64)],
181 residency: LayerWeightResidency,
182 excluded: &BTreeSet<String>,
183 ) -> Result<BoundedResidencyRequirement, BoundedResidencySizingError> {
184 bounded_requirement(
185 parameters
186 .iter()
187 .map(|(name, owner, bytes)| (*name, owner, || Ok(*bytes))),
188 residency,
189 excluded,
190 )
191 }
192
193 #[test]
194 fn sizing_keeps_group_windows_and_excluded_parameters_exact() {
195 let parameters = [
196 (
197 "embedding",
198 ReplicatedTextParameterOwner::StaticRole("input".into()),
199 7,
200 ),
201 ("a", unit("decoder", 0), 2),
202 ("b", unit("decoder", 1), 3),
203 ("c", unit("decoder", 1), 5),
204 ("d", unit("decoder", 2), 11),
205 ("e", unit("vision", 0), 13),
206 ("f", unit("vision", 2), 17),
207 ("bank", unit("decoder", usize::MAX), u64::MAX),
208 ];
209 let excluded = BTreeSet::from(["bank".into()]);
210 assert_eq!(
211 size(¶meters, host(2), &excluded).unwrap(),
212 BoundedResidencyRequirement {
213 static_bytes: 7,
214 window_bytes: 19,
215 required_bytes: 26,
216 depth: 2,
217 },
218 );
219 assert_eq!(
220 size(¶meters, host(3), &excluded).unwrap().window_bytes,
221 30
222 );
223 let streamed = LayerWeightResidency::DenseDiskStream(
224 crate::DenseDiskStreamLoadOptions::new(1024, 2048, 5, 2).unwrap(),
225 );
226 assert_eq!(size(¶meters, streamed, &excluded).unwrap().depth, 2);
227 }
228
229 #[test]
230 fn excluded_parameters_do_not_evaluate_byte_geometry() {
231 let owner = unit("decoder", usize::MAX);
232 let requirement = bounded_requirement(
233 [("bank", &owner, || {
234 panic!("excluded byte geometry must not be queried")
235 })],
236 host(3),
237 &BTreeSet::from(["bank".into()]),
238 )
239 .unwrap();
240 assert_eq!(requirement.required_bytes, 0);
241 }
242
243 #[test]
244 fn sparse_unit_indices_do_not_expand_all_intervening_positions() {
245 let parameters = [
246 ("a", unit("decoder", 0), 11),
247 ("b", unit("decoder", usize::MAX - 1), 17),
248 ];
249 assert_eq!(
250 size(¶meters, host(2), &BTreeSet::new())
251 .unwrap()
252 .window_bytes,
253 17
254 );
255 assert_eq!(
256 size(¶meters, host(usize::MAX), &BTreeSet::new())
257 .unwrap()
258 .window_bytes,
259 28
260 );
261 }
262
263 #[test]
264 fn sparse_windows_equal_the_dense_geometry_for_every_small_occupancy() {
265 for occupied in 0u16..256 {
266 let bytes = (0..8)
267 .map(|unit| {
268 if occupied & (1 << unit) == 0 {
269 0
270 } else {
271 unit as u64 + 1
272 }
273 })
274 .collect::<Vec<_>>();
275 let parameters = bytes
276 .iter()
277 .enumerate()
278 .filter(|(_, bytes)| **bytes != 0)
279 .map(|(index, bytes)| ("weight", unit("decoder", index), *bytes))
280 .collect::<Vec<_>>();
281 for depth in 1..=10 {
282 let expected = (0..bytes.len())
283 .map(|start| bytes[start..].iter().take(depth).sum::<u64>())
284 .max()
285 .unwrap_or(0);
286 assert_eq!(
287 size(¶meters, host(depth), &BTreeSet::new())
288 .unwrap()
289 .window_bytes,
290 expected
291 );
292 }
293 }
294 }
295
296 #[test]
297 fn every_byte_and_unit_count_overflow_is_rejected() {
298 use BoundedResidencySizingError::ArithmeticOverflow;
299 let pinned = ReplicatedTextParameterOwner::StaticRole("input".into());
300 for (parameters, expected) in [
301 (
302 vec![("a", pinned.clone(), u64::MAX), ("b", pinned.clone(), 1)],
303 "static parameter bytes",
304 ),
305 (
306 vec![("a", unit("g", 0), u64::MAX), ("b", unit("g", 0), 1)],
307 "execution-unit bytes",
308 ),
309 (
310 vec![("a", unit("g", 0), u64::MAX), ("b", unit("g", 1), 1)],
311 "device-window bytes",
312 ),
313 (
314 vec![("a", pinned, u64::MAX), ("b", unit("g", 0), 1)],
315 "bounded-residency bytes",
316 ),
317 (
318 vec![("a", unit("g", usize::MAX), 1)],
319 "execution-group unit count",
320 ),
321 (
322 vec![("a", unit("g", usize::MAX - 1), 1), ("b", unit("h", 0), 1)],
323 "execution unit count",
324 ),
325 ] {
326 assert_eq!(
327 size(¶meters, host(2), &BTreeSet::new()),
328 Err(ArithmeticOverflow(expected))
329 );
330 }
331 }
332
333 #[test]
334 fn residency_projection_preserves_every_tier_and_ordered_transfer() {
335 let mut source = OffloadTelemetry::default();
336 source.set_planned_bytes(TierByteTotals::new(303, 202, 101));
337 source.set_resident_bytes(MemoryTier::Host, 90);
338 source.set_resident_bytes(MemoryTier::Host, 70);
339 source.set_resident_bytes(MemoryTier::Device, 60);
340 source.set_resident_bytes(MemoryTier::Device, 40);
341 for (index, direction) in TransferDirection::ALL.into_iter().enumerate() {
342 source.record_transfer(
343 direction,
344 10 + index as u64,
345 std::time::Duration::from_millis(125),
346 );
347 source.record_transfer(direction, 20, std::time::Duration::from_millis(375));
348 }
349 let diagnostics = eredu_checkpoint::store::WeightStoreDiagnostics {
350 backend: eredu_checkpoint::store::WeightStoreBackend::Memory,
351 cache_hits: 0,
352 cache_misses: 0,
353 evictions: 0,
354 currently_cached_shards: 0,
355 touched_shard_paths: vec![],
356 payload_shard_paths: vec![],
357 physical_reads: 0,
358 physical_read_bytes: 0,
359 coalesced_group_hits: 0,
360 };
361 let report = ResidencyReport::new(true, source.snapshot(), vec![], vec![], diagnostics);
362 assert_eq!(
363 residency_telemetry(&report),
364 ResidencyTelemetry {
365 planned_disk_bytes: 101,
366 planned_host_bytes: 202,
367 planned_device_bytes: 303,
368 current_host_bytes: 70,
369 current_device_bytes: 40,
370 peak_host_bytes: 90,
371 peak_device_bytes: 60,
372 transfers: [
373 "device_to_host",
374 "device_to_disk",
375 "host_to_device",
376 "host_to_disk",
377 "disk_to_device",
378 "disk_to_host"
379 ]
380 .into_iter()
381 .enumerate()
382 .map(|(index, direction)| TransferTelemetry {
383 direction: direction.into(),
384 count: 2,
385 bytes: 30 + index as u64,
386 seconds: DurationSeconds(0.5),
387 })
388 .collect(),
389 }
390 );
391 }
392}