Skip to main content

eredu_runtime/
automatic_support.rs

1//! Portable resource sizing and telemetry for automatic execution planning.
2
3use 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/// Invalid exact resource sizing for selected bounded execution.
17#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
18#[non_exhaustive]
19pub enum BoundedResidencySizingError {
20    /// The authoritative selected materialization contract is invalid.
21    #[error(transparent)]
22    Materialization(#[from] ReplicatedTextContractError),
23    /// An exact byte or unit count cannot be represented.
24    #[error("selected {0} overflowed")]
25    ArithmeticOverflow(&'static str),
26}
27
28/// Computes the pinned bytes and largest group-local device window.
29///
30/// Excluded logical parameters belong to independent storage and contribute no
31/// ordinary residency bytes. Missing unit indices retain their zero-byte
32/// positions, while memory and time depend on the number of selected tasks,
33/// rather than the largest unit index. No payload or native mechanism is used.
34pub 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
123/// Projects a neutral residency snapshot into its stable telemetry document.
124pub 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(&parameters, 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(&parameters, 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(&parameters, 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(&parameters, host(2), &BTreeSet::new())
251                .unwrap()
252                .window_bytes,
253            17
254        );
255        assert_eq!(
256            size(&parameters, 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(&parameters, 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(&parameters, 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}