Skip to main content

weir/
fixed_point_resident_batch.rs

1//! Resident fixed-point batch facade.
2//!
3//! This module composes the resident graph cache, borrowed resident plans, and
4//! reusable resident frontier scratch so hot fixed-point analyses do not
5//! repeatedly upload CSR layouts or allocate frontier buffers.
6
7#![allow(clippy::too_many_arguments)]
8mod analysis_methods;
9#[cfg(test)]
10mod tests;
11
12use crate::fixed_point_execution_plan_cache::{
13    FixedPointExecutionPlanCache, FixedPointExecutionPlanCacheStats,
14};
15use crate::fixed_point_graph::FixedPointAnalysisKind;
16use crate::fixed_point_graph::FixedPointForwardGraph;
17use crate::fixed_point_resident_cache::{
18    FixedPointResidentGraphCache, FixedPointResidentGraphCacheStats,
19};
20use crate::fixed_point_resident_frontier::FixedPointResidentFrontierScratch;
21use vyre::ResidentGraphReuseTelemetry;
22
23/// Runtime counters for [`FixedPointResidentBatch`].
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct FixedPointResidentBatchStats {
26    /// Resident graph cache counters.
27    pub graph_cache: FixedPointResidentGraphCacheStats,
28    /// Execution-plan cache counters.
29    pub execution_plan_cache: FixedPointExecutionPlanCacheStats,
30    /// Resident frontier allocation or resize events observed by this facade.
31    pub frontier_allocations: u64,
32    /// Solves that reused the existing resident frontier allocation.
33    pub frontier_reuses: u64,
34    /// Resident frontier clears performed by [`FixedPointResidentBatch::free_all`].
35    pub frontier_clears: u64,
36    /// Successful resident fixed-point dispatches executed through this facade.
37    pub resident_dispatches: u64,
38    /// Host-to-device frontier seed bytes uploaded through resident solves.
39    pub frontier_upload_bytes: u64,
40    /// Device-to-host result bytes read back through resident solves.
41    pub result_readback_bytes: u64,
42    /// Resident graph cache misses observed by this facade.
43    pub resident_graph_cold_uploads: u64,
44    /// Resident graph cache hits observed by this facade.
45    pub resident_graph_warm_reuses: u64,
46    /// Resident graph bytes uploaded by cold graph-cache misses.
47    pub resident_graph_upload_bytes: u64,
48    /// Resident graph upload bytes avoided by warm graph-cache hits.
49    pub resident_graph_avoided_upload_bytes: u64,
50    /// Whether the reusable resident frontier pair is currently allocated.
51    pub frontier_resident: bool,
52    /// Current resident frontier byte width.
53    pub frontier_bytes: usize,
54}
55
56impl FixedPointResidentBatchStats {
57    /// Backend-neutral resident graph reuse telemetry for this batch snapshot.
58    #[must_use]
59    pub const fn resident_graph_reuse_telemetry(self) -> ResidentGraphReuseTelemetry {
60        ResidentGraphReuseTelemetry::from_counters(
61            self.resident_graph_cold_uploads,
62            self.resident_graph_warm_reuses,
63            self.resident_graph_upload_bytes,
64            self.resident_graph_avoided_upload_bytes,
65        )
66    }
67}
68
69/// Reusable resident fixed-point execution state.
70#[derive(Clone, Debug, PartialEq)]
71pub struct FixedPointResidentBatch {
72    graph_cache: FixedPointResidentGraphCache,
73    execution_plan_cache: FixedPointExecutionPlanCache,
74    resident_frontier: FixedPointResidentFrontierScratch,
75    host_scratch: crate::fixed_point_scratch::FixedPointScratch,
76    max_frontier_bytes: Option<usize>,
77    frontier_allocations: u64,
78    frontier_reuses: u64,
79    frontier_clears: u64,
80    resident_dispatches: u64,
81    frontier_upload_bytes: u64,
82    result_readback_bytes: u64,
83    resident_graph_reuse: ResidentGraphReuseTelemetry,
84    observed_graph_cache_reuse: ResidentGraphReuseTelemetry,
85}
86
87impl Default for FixedPointResidentBatch {
88    fn default() -> Self {
89        Self {
90            graph_cache: FixedPointResidentGraphCache::new(),
91            execution_plan_cache: FixedPointExecutionPlanCache::new(),
92            resident_frontier: FixedPointResidentFrontierScratch::default(),
93            host_scratch: crate::fixed_point_scratch::FixedPointScratch::default(),
94            max_frontier_bytes: None,
95            frontier_allocations: 0,
96            frontier_reuses: 0,
97            frontier_clears: 0,
98            resident_dispatches: 0,
99            frontier_upload_bytes: 0,
100            result_readback_bytes: 0,
101            resident_graph_reuse: ResidentGraphReuseTelemetry::default(),
102            observed_graph_cache_reuse: ResidentGraphReuseTelemetry::default(),
103        }
104    }
105}
106
107impl FixedPointResidentBatch {
108    /// Create a resident fixed-point batch facade with an unbounded graph cache.
109    #[must_use]
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Create a resident fixed-point batch facade with a soft graph-cache byte bound.
115    #[must_use]
116    pub fn with_max_retained_bytes(max_retained_bytes: usize) -> Self {
117        Self {
118            graph_cache: FixedPointResidentGraphCache::with_max_retained_bytes(max_retained_bytes),
119            ..Self::default()
120        }
121    }
122
123    /// Create a resident fixed-point batch facade with explicit graph-cache
124    /// byte and execution-plan entry bounds.
125    #[must_use]
126    pub fn with_cache_bounds(max_retained_bytes: usize, max_execution_plans: usize) -> Self {
127        Self {
128            graph_cache: FixedPointResidentGraphCache::with_max_retained_bytes(max_retained_bytes),
129            execution_plan_cache: FixedPointExecutionPlanCache::with_max_entries(
130                max_execution_plans,
131            ),
132            ..Self::default()
133        }
134    }
135
136    /// Create a resident fixed-point batch facade with explicit graph-cache,
137    /// execution-plan cache, and resident frontier byte bounds.
138    #[must_use]
139    pub fn with_memory_budget(
140        max_retained_bytes: usize,
141        max_execution_plans: usize,
142        max_frontier_bytes: usize,
143    ) -> Self {
144        Self {
145            graph_cache: FixedPointResidentGraphCache::with_max_retained_bytes(max_retained_bytes),
146            execution_plan_cache: FixedPointExecutionPlanCache::with_max_entries(
147                max_execution_plans,
148            ),
149            max_frontier_bytes: Some(max_frontier_bytes),
150            ..Self::default()
151        }
152    }
153
154    /// Inspect the resident graph cache.
155    #[must_use]
156    pub fn graph_cache(&self) -> &FixedPointResidentGraphCache {
157        &self.graph_cache
158    }
159
160    /// Inspect resident graph cache counters.
161    #[must_use]
162    pub fn graph_cache_stats(&self) -> FixedPointResidentGraphCacheStats {
163        self.graph_cache.stats()
164    }
165
166    /// Inspect execution-plan cache counters.
167    #[must_use]
168    pub fn execution_plan_cache_stats(&self) -> FixedPointExecutionPlanCacheStats {
169        self.execution_plan_cache.stats()
170    }
171
172    /// Inspect resident batch counters.
173    #[must_use]
174    pub fn stats(&self) -> FixedPointResidentBatchStats {
175        FixedPointResidentBatchStats {
176            graph_cache: self.graph_cache.stats(),
177            execution_plan_cache: self.execution_plan_cache.stats(),
178            frontier_allocations: self.frontier_allocations,
179            frontier_reuses: self.frontier_reuses,
180            frontier_clears: self.frontier_clears,
181            resident_dispatches: self.resident_dispatches,
182            frontier_upload_bytes: self.frontier_upload_bytes,
183            result_readback_bytes: self.result_readback_bytes,
184            resident_graph_cold_uploads: self.resident_graph_reuse.cold_uploads,
185            resident_graph_warm_reuses: self.resident_graph_reuse.warm_reuses,
186            resident_graph_upload_bytes: self.resident_graph_reuse.upload_bytes,
187            resident_graph_avoided_upload_bytes: self.resident_graph_reuse.avoided_upload_bytes,
188            frontier_resident: self.resident_frontier.is_allocated(),
189            frontier_bytes: self.resident_frontier.byte_len(),
190        }
191    }
192
193    /// Latest host-observed frontier density from the last fixed-point solve.
194    #[must_use]
195    pub fn frontier_density(&self) -> crate::fixed_point_scratch::FrontierDensityTelemetry {
196        self.host_scratch.frontier_density()
197    }
198
199    /// Sparse/dense execution family recommended from the last solve's frontier density.
200    #[must_use]
201    pub fn recommended_frontier_execution_mode(
202        &self,
203    ) -> crate::fixed_point_scratch::FrontierExecutionMode {
204        self.host_scratch
205            .frontier_density()
206            .recommended_execution_mode()
207    }
208
209    /// Build a scale-aware execution plan for a prepared graph using the last
210    /// observed frontier density.
211    pub fn execution_plan_for_prepared_graph(
212        &self,
213        graph: &FixedPointForwardGraph,
214    ) -> Result<crate::fixed_point_execution_plan::FixedPointExecutionPlan, String> {
215        crate::fixed_point_execution_plan::plan_prepared_graph(graph, self.frontier_density())
216    }
217
218    /// Return a cached scale-aware execution plan for a prepared graph and
219    /// analysis family using the last observed frontier density.
220    pub fn cached_execution_plan_for_prepared_graph(
221        &mut self,
222        backend: &dyn vyre::VyreBackend,
223        kind: FixedPointAnalysisKind,
224        graph: &FixedPointForwardGraph,
225    ) -> Result<crate::fixed_point_execution_plan::FixedPointExecutionPlan, String> {
226        self.execution_plan_cache
227            .get_or_plan(backend, kind, graph, self.frontier_density())
228    }
229    /// Free retained resident frontier and graph resources.
230    pub fn free_all(&mut self, backend: &dyn vyre::VyreBackend) -> Result<(), vyre::BackendError> {
231        let frontier_result = if self.resident_frontier.is_allocated() {
232            let result = self.resident_frontier.clear(backend);
233            if result.is_ok() {
234                self.frontier_clears = self
235                    .frontier_clears
236                    .checked_add(1)
237                    .ok_or_else(|| {
238                        vyre::BackendError::new(
239                            "weir fixed-point resident batch frontier clear counter overflowed u64. Fix: rotate the batch facade before clearing more resident frontiers.",
240                        )
241                    })?;
242            }
243            result
244        } else {
245            Ok(())
246        };
247        let cache_result = self.graph_cache.free_all(backend);
248        match (frontier_result, cache_result) {
249            (Ok(()), Ok(())) => Ok(()),
250            (Err(error), _) | (_, Err(error)) => Err(error),
251        }
252    }
253
254    fn frontier_state(&self) -> (bool, usize) {
255        (
256            self.resident_frontier.is_allocated(),
257            self.resident_frontier.byte_len(),
258        )
259    }
260
261    fn record_frontier_reuse(&mut self, before: (bool, usize)) -> Result<(), String> {
262        let after = self.frontier_state();
263        if !after.0 {
264            return Ok(());
265        }
266        if before.0 && before.1 == after.1 {
267            self.frontier_reuses = self
268                .frontier_reuses
269                .checked_add(1)
270                .ok_or_else(|| {
271                    "weir fixed-point resident batch frontier reuse counter overflowed u64. Fix: rotate the batch facade before reusing more resident frontiers."
272                        .to_string()
273                })?;
274        } else {
275            self.frontier_allocations = self
276                .frontier_allocations
277                .checked_add(1)
278                .ok_or_else(|| {
279                    "weir fixed-point resident batch frontier allocation counter overflowed u64. Fix: rotate the batch facade before allocating more resident frontiers."
280                        .to_string()
281                })?;
282        }
283        Ok(())
284    }
285
286    fn record_execution_plan(
287        &mut self,
288        backend: &dyn vyre::VyreBackend,
289        kind: FixedPointAnalysisKind,
290        graph: &FixedPointForwardGraph,
291    ) -> Result<(), String> {
292        let _ =
293            self.execution_plan_cache
294                .get_or_plan(backend, kind, graph, self.frontier_density())?;
295        self.record_graph_residency_from_cache_stats()?;
296        Ok(())
297    }
298
299    fn record_graph_residency_from_cache_stats(&mut self) -> Result<(), String> {
300        let graph_cache_reuse = self.graph_cache.stats().resident_graph_reuse_telemetry();
301        let graph_cache_delta = graph_cache_reuse
302            .checked_delta_since(self.observed_graph_cache_reuse)
303            .map_err(|error| {
304                format!(
305                    "weir fixed-point resident batch observed graph cache telemetry regressed: {error}"
306                )
307            })?;
308        self.resident_graph_reuse = self
309            .resident_graph_reuse
310            .checked_add(graph_cache_delta)
311            .map_err(|error| error.to_string())?;
312        self.observed_graph_cache_reuse = graph_cache_reuse;
313        Ok(())
314    }
315
316    fn record_resident_dispatch_io(
317        &mut self,
318        seed_words: usize,
319        result_words: usize,
320    ) -> Result<(), String> {
321        self.resident_dispatches = self
322            .resident_dispatches
323            .checked_add(1)
324            .ok_or_else(|| {
325                "weir fixed-point resident batch dispatch counter overflowed u64. Fix: rotate the batch facade before dispatch accounting saturates."
326                    .to_string()
327            })?;
328        let logical_seed_bytes = seed_words
329            .checked_mul(std::mem::size_of::<u32>())
330            .ok_or_else(|| {
331                "weir fixed-point resident seed byte count overflowed usize. Fix: shard the seed frontier before resident dispatch."
332                    .to_string()
333            })?;
334        let seed_bytes = logical_seed_bytes.checked_mul(2).ok_or_else(|| {
335            "weir fixed-point resident physical seed byte count overflowed usize. Fix: shard the seed frontier before resident dispatch."
336                .to_string()
337        })?;
338        let seed_bytes_u64 = u64::try_from(seed_bytes).map_err(|error| {
339            format!(
340                "weir fixed-point resident seed byte count does not fit u64: {error}. Fix: shard the seed frontier before resident dispatch."
341            )
342        })?;
343        self.frontier_upload_bytes = self
344            .frontier_upload_bytes
345            .checked_add(seed_bytes_u64)
346            .ok_or_else(|| {
347                "weir fixed-point resident frontier upload byte counter overflowed u64. Fix: rotate the batch facade or shard the workload."
348                    .to_string()
349            })?;
350        let result_bytes = result_words
351            .checked_mul(std::mem::size_of::<u32>())
352            .ok_or_else(|| {
353                "weir fixed-point resident result byte count overflowed usize. Fix: shard the result frontier before resident readback."
354                    .to_string()
355            })?;
356        let result_bytes_u64 = u64::try_from(result_bytes).map_err(|error| {
357            format!(
358                "weir fixed-point resident result byte count does not fit u64: {error}. Fix: shard the result frontier before resident readback."
359            )
360        })?;
361        self.result_readback_bytes = self
362            .result_readback_bytes
363            .checked_add(result_bytes_u64)
364            .ok_or_else(|| {
365                "weir fixed-point resident result readback byte counter overflowed u64. Fix: rotate the batch facade or shard the workload."
366                    .to_string()
367            })?;
368        Ok(())
369    }
370
371    fn record_resident_changed_flag_sequence_window_io(
372        &mut self,
373        seed_words: usize,
374        result_words: usize,
375    ) -> Result<(), String> {
376        let readback_words = result_words.checked_add(1).ok_or_else(|| {
377            "weir fixed-point resident changed-flag sequence-window readback word count overflowed usize. Fix: shard the result frontier before resident readback."
378                .to_string()
379        })?;
380        self.record_resident_dispatch_io(seed_words, readback_words)
381    }
382
383    fn require_frontier_budget(&self, graph: &FixedPointForwardGraph) -> Result<(), String> {
384        let Some(max_frontier_bytes) = self.max_frontier_bytes else {
385            return Ok(());
386        };
387        let node_count = usize::try_from(graph.node_count()).map_err(|error| {
388            format!(
389                "weir resident fixed-point frontier budget cannot represent node_count={} as usize: {error}. Fix: shard the graph before resident dispatch.",
390                graph.node_count()
391            )
392        })?;
393        let frontier_bytes = node_count
394            .checked_add(31)
395            .and_then(|bits| bits.checked_div(32))
396            .and_then(|words| words.checked_mul(std::mem::size_of::<u32>()))
397            .ok_or_else(|| {
398                format!(
399                    "weir resident fixed-point frontier budget overflowed for node_count={}. Fix: shard the graph before resident dispatch.",
400                    graph.node_count()
401                )
402            })?;
403        if frontier_bytes > max_frontier_bytes {
404            return Err(format!(
405                "weir resident fixed-point frontier requires {frontier_bytes} bytes but the configured device-memory budget allows {max_frontier_bytes}. Fix: increase max_frontier_bytes, reuse a smaller graph shard, or lower the analysis batch size before dispatch."
406            ));
407        }
408        Ok(())
409    }
410}