Skip to main content

tract_gpu/memory/
schema.rs

1use std::fmt;
2use std::fmt::Debug;
3use tract_core::internal::num_integer::Integer;
4use tract_core::internal::*;
5
6use crate::fact::DeviceTypedFactExt;
7use crate::sync::{DeviceSync, DeviceSyncKind};
8
9/// Requirement for node outputs from a memory perspective.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct NodeMemReq {
12    pub outlet_id: OutletId,
13    pub lifetime: Lifetime,
14    pub mem_size: TDim,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct Lifetime {
19    pub start: usize,
20    pub end: usize,
21}
22
23impl Lifetime {
24    pub fn is_disjoint(&self, other: &Lifetime) -> bool {
25        self.start >= other.end || other.start >= self.end
26    }
27
28    pub fn is_alive_at_step(&self, step: usize) -> bool {
29        self.start <= step && step < self.end
30    }
31
32    pub fn is_empty(&self) -> bool {
33        self.len() == 0
34    }
35
36    pub fn len(&self) -> usize {
37        self.end - self.start
38    }
39}
40
41fn next_nodes<'a>(model: &'a TypedModel, node: &TypedNode) -> Option<TVec<&'a TypedNode>> {
42    if node.outputs.is_empty() {
43        return None;
44    };
45
46    Some(
47        node.outputs
48            .iter()
49            .flat_map(|o| {
50                o.successors.iter().map(|succ| &model.nodes()[succ.node]).collect::<Vec<_>>()
51            })
52            .collect(),
53    )
54}
55
56pub fn eval_device_mem_req_for_nodes(
57    model: &TypedModel,
58    order: &[usize],
59) -> TractResult<TVec<NodeMemReq>> {
60    let outputs = model.output_outlets()?.to_vec();
61    let flush_lists = order::build_flush_list(model, order, &outputs, |node| {
62        let Ok(facts) = model.node_output_facts(node.id) else { return false };
63
64        let cpu_sync_in_next_nodes = next_nodes(model, node).is_some_and(|nodes| {
65            nodes.iter().any(|it| {
66                it.op_as::<DeviceSync>().is_some_and(|op| op.kind == DeviceSyncKind::ToHost)
67                    || it.outputs.iter().any(|o| o.fact.as_device_fact().is_none())
68            })
69        });
70
71        !cpu_sync_in_next_nodes
72            && facts.iter().any(|it| {
73                it.as_device_fact()
74                    .map(|it| it.is_from_device() && !it.is_state_owned())
75                    .unwrap_or(false)
76            })
77    });
78    let mut scoped_nodes = tvec![];
79
80    for (step, n) in order.iter().enumerate() {
81        let lifetime_start = step;
82
83        let lifetime_end = flush_lists
84            .iter()
85            .enumerate()
86            .find(|(_step, flush_list)| flush_list.contains(n))
87            .map(|it| usize::min(it.0 + 1, order.len()));
88        // Ignore nodes that won't be flushed from Device.
89        let Some(lifetime_end) = lifetime_end else {
90            continue;
91        };
92
93        let out_device_tmp_facts = model
94            .node_output_facts(*n)?
95            .into_iter()
96            .flat_map(|it| it.as_device_fact())
97            .filter(|it| it.is_from_device())
98            .collect::<TVec<_>>();
99
100        if out_device_tmp_facts.is_empty() {
101            continue;
102        }
103
104        for (slot, fact) in out_device_tmp_facts.iter().enumerate() {
105            let outlet_id = OutletId { node: *n, slot };
106            for buff_size in fact.buffer_sizes() {
107                scoped_nodes.push(NodeMemReq {
108                    outlet_id,
109                    lifetime: Lifetime { start: lifetime_start, end: lifetime_end },
110                    mem_size: buff_size,
111                })
112            }
113        }
114    }
115
116    // Walked backwards so a chain of forwarders carries the extension down to
117    // the region it all aliases.
118    for n in order.iter().rev() {
119        let Some(ix) = model.node(*n).op.forwards_input() else { continue };
120        let Some(end) = scoped_nodes
121            .iter()
122            .filter(|req| req.outlet_id.node == *n)
123            .map(|req| req.lifetime.end)
124            .max()
125        else {
126            continue;
127        };
128        let src = model.node(*n).inputs[ix];
129        for req in scoped_nodes.iter_mut().filter(|req| req.outlet_id == src) {
130            req.lifetime.end = req.lifetime.end.max(end);
131        }
132    }
133
134    Ok(scoped_nodes)
135}
136
137fn collect_exotic_facts(model: &TypedModel) -> TractResult<Vec<NodeExoticFacts>> {
138    let mut res: Vec<TVec<Option<Box<dyn ExoticFact>>>> = vec![];
139    for node in model.nodes() {
140        let mut tmp: TVec<Option<Box<dyn ExoticFact>>> = tvec![];
141        for fact in model.node_output_facts(node.id)? {
142            if let Some(dev_fact) = fact.as_device_fact() {
143                tmp.push(dev_fact.exotic_fact.clone());
144            }
145        }
146        res.push(tmp);
147    }
148    Ok(res)
149}
150
151/// A partition is a list of node that have disjoint memory requirement from a lifetime
152/// perspective.
153#[derive(Debug, Clone, PartialEq, Eq, Hash)]
154pub struct Partition {
155    pub nodes: Vec<NodeMemReq>,
156}
157
158impl Partition {
159    pub fn eval_size_to_i64(&self, symbols: &SymbolValues) -> TractResult<i64> {
160        let mut max_size = self
161            .nodes
162            .iter()
163            .map(|it| it.mem_size.eval_to_i64(symbols))
164            .collect::<TractResult<Vec<_>>>()?
165            .into_iter()
166            .max()
167            .unwrap_or(0);
168        max_size = Integer::next_multiple_of(&max_size, &(vector_size() as i64));
169        Ok(max_size)
170    }
171
172    pub fn size(&self) -> TDim {
173        TDim::Max(self.nodes.iter().map(|s| s.mem_size.clone()).collect()).simplify()
174    }
175
176    pub fn has_no_conflict_with_lifetime(&self, lifetime: &Lifetime) -> bool {
177        self.nodes.iter().all(|n| n.lifetime.is_disjoint(lifetime))
178    }
179
180    pub fn find_node_alive_at_step(&self, step: usize) -> Option<&NodeMemReq> {
181        self.nodes.iter().find(|it| it.lifetime.is_alive_at_step(step))
182    }
183}
184
185type NodeExoticFacts = TVec<Option<Box<dyn ExoticFact>>>;
186/// This struct represents a resolved memory schema for a model that contains
187/// GPU operators. This schema is concrete.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct DeviceResolvedMemSchema {
190    pub offsets_by_node: Vec<Option<TVec<TVec<usize>>>>,
191    pub memory_size: usize,
192}
193
194/// This struct represent a memory schema for node output memory that are handled
195/// by a GPU.
196#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub struct DeviceMemSchema {
198    /// Total numbef in the model.
199    pub model_num_nodes: usize,
200    pub by_partition: Vec<Partition>,
201    // vec![vec![Option<NodeMemReq>; num_partitions]; num_steps].
202    pub by_steps: Vec<Vec<Option<NodeMemReq>>>,
203    pub exotic_facts: Vec<NodeExoticFacts>,
204}
205
206impl DeviceMemSchema {
207    /// Returns memory size of each inner partitions.
208    pub fn size_by_partition(&self) -> Vec<TDim> {
209        self.by_partition.iter().map(|it| it.size()).collect()
210    }
211
212    /// Evaluate memory size by partition for given symbol values.
213    pub fn eval_size_by_partition(&self, symbols: &SymbolValues) -> TractResult<Vec<i64>> {
214        self.by_partition.iter().map(|it| it.eval_size_to_i64(symbols)).collect()
215    }
216
217    /// Returns total memory size required for the schema.
218    pub fn memory_size(&self) -> TDim {
219        self.by_partition.iter().map(|it| it.size()).sum()
220    }
221
222    /// Evaluate memory size required for the schema for given symbol values.
223    pub fn eval_memory_size(&self, symbols: &SymbolValues) -> TractResult<i64> {
224        self.by_partition.iter().map(|it| it.eval_size_to_i64(symbols)).sum()
225    }
226
227    /// Compute offsets for each node for given symbols. Node ids
228    /// are indexes in the returned vector.
229    pub fn compute_offset_by_node(
230        &self,
231        symbols: &SymbolValues,
232    ) -> TractResult<Vec<Option<TVec<TVec<usize>>>>> {
233        let mut cursor = 0;
234        let mut offset_by_outlet: Vec<Option<TVec<TVec<usize>>>> = vec![None; self.model_num_nodes];
235
236        for partition in &self.by_partition {
237            for node_mem in &partition.nodes {
238                let node = node_mem.outlet_id.node;
239                let slot = node_mem.outlet_id.slot;
240
241                let slots: &mut TVec<TVec<usize>> =
242                    offset_by_outlet[node].get_or_insert_with(|| tvec![tvec!()]);
243
244                if slot < 1 {
245                    slots[slot].push(cursor);
246                } else {
247                    if slots.len() <= slot {
248                        slots.resize_with(slot + 1, TVec::<usize>::new);
249                    }
250                    slots[slot].push(cursor);
251                }
252            }
253            cursor += partition.eval_size_to_i64(symbols)? as usize;
254        }
255
256        Ok(offset_by_outlet)
257    }
258
259    /// Evaluate peak memory size for given symbols. The return value is lower or equal to the memory
260    /// size of the schema. The difference between peak memory size and memory size represents the
261    /// memory fragmentation introduced by the schema.
262    /// Per-step arena occupancy as symbolic expressions; the peak for a given
263    /// symbol assignment is the max of these evaluated. These do not depend on
264    /// symbol values, so a caller sweeping many assignments should compute them
265    /// once rather than re-summing the symbolic terms for every point.
266    pub fn peak_memory_terms(&self) -> Vec<TDim> {
267        self.by_steps
268            .iter()
269            .map(|active_nodes| active_nodes.iter().flatten().map(|it| it.mem_size.clone()).sum())
270            .collect()
271    }
272
273    pub fn eval_peak_memory_size(&self, symbols: &SymbolValues) -> TractResult<i64> {
274        Ok(self
275            .peak_memory_terms()
276            .iter()
277            .map(|term| term.eval_to_i64(symbols))
278            .collect::<TractResult<Vec<_>>>()?
279            .into_iter()
280            .max()
281            .unwrap_or(0))
282    }
283
284    /// Evaluate the usage for given symbols as the ratio between
285    /// schema memory size and peak memory size. A value of 1.0 means
286    /// that the schema doesn't introduce memory fragmentation.
287    pub fn eval_usage(&self, symbols: &SymbolValues) -> TractResult<f32> {
288        let memory_size = self.eval_memory_size(symbols)? as f32;
289        let peak_memory_size = self.eval_peak_memory_size(symbols)? as f32;
290        Ok(peak_memory_size / memory_size)
291    }
292}
293
294impl fmt::Display for DeviceMemSchema {
295    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
296        for (step, mem_step) in self.by_steps.iter().enumerate() {
297            writeln!(
298                fmt,
299                "step: {:5} => |{}|",
300                step,
301                mem_step
302                    .iter()
303                    .map(|n| -> String {
304                        n.as_ref()
305                            .map(|it| format!("{:^7}/{:^7}", it.outlet_id.node, it.outlet_id.slot))
306                            .unwrap_or(format!("{:^7}", "*"))
307                    })
308                    .collect::<Vec<String>>()
309                    .join("|")
310            )?;
311        }
312        writeln!(fmt, "memory_size: {}", self.memory_size())?;
313        Ok(())
314    }
315}
316
317impl DeviceMemSchema {
318    /// Resolve Memory schema with given symbols.
319    pub fn resolve(&self, symbols: &SymbolValues) -> TractResult<DeviceResolvedMemSchema> {
320        Ok(DeviceResolvedMemSchema {
321            offsets_by_node: self.compute_offset_by_node(symbols)?,
322            memory_size: self.eval_memory_size(symbols)?.try_into()?,
323        })
324    }
325
326    /// Build a memory schema for given model and execution order. The hint is used to optimize
327    /// the memory schema because it is based on symbolic dimensions. That doesn't mean it will be
328    /// optimal for all possible values for symbolic dimensions.
329    pub fn build(
330        model: &TypedModel,
331        order: &[usize],
332        hint: &SymbolValues,
333    ) -> TractResult<DeviceMemSchema> {
334        let mut nodes_mem_req = eval_device_mem_req_for_nodes(model, order)?;
335
336        let exotic_facts = collect_exotic_facts(model)?;
337        let hinted_mem_size = nodes_mem_req
338            .iter()
339            .map(|node_mem| Ok((node_mem.outlet_id, node_mem.mem_size.eval_to_i64(hint)?)))
340            .collect::<TractResult<HashMap<OutletId, i64>>>()?;
341
342        nodes_mem_req.sort_by(|lhs, rhs| {
343            let lhs_hint_mem_size = hinted_mem_size.get(&lhs.outlet_id);
344            let rhs_hint_mem_size = hinted_mem_size.get(&rhs.outlet_id);
345            lhs_hint_mem_size.cmp(&rhs_hint_mem_size).reverse()
346        });
347
348        let mut partitions: Vec<Partition> = vec![];
349        for node_mem in nodes_mem_req {
350            // Find partitions where node lifetime is disjoint from existing.
351            let mut available = partitions
352                .iter_mut()
353                .filter(|it| it.has_no_conflict_with_lifetime(&node_mem.lifetime))
354                .collect::<Vec<_>>();
355
356            available.sort_by_cached_key(|n| {
357                -n.nodes.iter().flat_map(|it| hinted_mem_size.get(&it.outlet_id)).sum::<i64>()
358            });
359
360            match available.first_mut() {
361                Some(available) => {
362                    available.nodes.push(node_mem);
363                }
364                None => partitions.push(Partition { nodes: vec![node_mem] }),
365            }
366        }
367
368        let by_steps: Vec<Vec<Option<NodeMemReq>>> = (0..order.len())
369            .map(|step| {
370                let mem_step: Vec<_> =
371                    partitions.iter().map(|p| p.find_node_alive_at_step(step).cloned()).collect();
372                ensure!(mem_step.len() <= partitions.len());
373                Ok(mem_step)
374            })
375            .collect::<TractResult<Vec<_>>>()?;
376
377        Ok(DeviceMemSchema {
378            model_num_nodes: model.nodes().len(),
379            by_partition: partitions,
380            by_steps,
381            exotic_facts,
382        })
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_lifetime_is_disjoint() {
392        let l1 = Lifetime { start: 0, end: 5 };
393        let l2 = Lifetime { start: 5, end: 10 };
394        let l3 = Lifetime { start: 3, end: 7 };
395
396        assert!(l1.is_disjoint(&l2));
397        assert!(l2.is_disjoint(&l1));
398        assert!(!l1.is_disjoint(&l3));
399        assert!(!l3.is_disjoint(&l2));
400    }
401
402    #[test]
403    fn test_lifetime_is_alive_at_step() {
404        let lifetime = Lifetime { start: 5, end: 10 };
405
406        assert!(!lifetime.is_alive_at_step(4));
407        assert!(lifetime.is_alive_at_step(5));
408        assert!(lifetime.is_alive_at_step(7));
409        assert!(lifetime.is_alive_at_step(9));
410        assert!(!lifetime.is_alive_at_step(10));
411    }
412
413    #[test]
414    fn test_empty_lifetime() {
415        let lifetime = Lifetime { start: 5, end: 5 };
416        assert!(lifetime.is_empty());
417        assert_eq!(lifetime.len(), 0);
418    }
419
420    #[test]
421    fn test_node_mem_req_basic() {
422        let outlet_id = OutletId { node: 1, slot: 0 };
423        let req = NodeMemReq {
424            outlet_id,
425            lifetime: Lifetime { start: 0, end: 5 },
426            mem_size: 1000.into(),
427        };
428
429        assert_eq!(req.outlet_id.node, 1);
430        assert_eq!(req.lifetime.start, 0);
431        assert_eq!(req.lifetime.end, 5);
432        assert_eq!(req.mem_size.to_i64().unwrap(), 1000);
433    }
434
435    #[test]
436    fn test_partition_has_no_conflict() {
437        let outlet_id = OutletId { node: 1, slot: 0 };
438        let node1 = NodeMemReq {
439            outlet_id,
440            lifetime: Lifetime { start: 0, end: 5 },
441            mem_size: 1000.into(),
442        };
443
444        let partition = Partition { nodes: vec![node1] };
445
446        assert!(partition.has_no_conflict_with_lifetime(&Lifetime { start: 5, end: 10 }));
447        assert!(!partition.has_no_conflict_with_lifetime(&Lifetime { start: 3, end: 7 }));
448    }
449
450    #[test]
451    fn test_partition_find_node() {
452        let outlet_id = OutletId { node: 1, slot: 0 };
453        let node1 = NodeMemReq {
454            outlet_id,
455            lifetime: Lifetime { start: 0, end: 5 },
456            mem_size: 1000.into(),
457        };
458
459        let outlet_id = OutletId { node: 2, slot: 0 };
460        let node2 = NodeMemReq {
461            outlet_id,
462            lifetime: Lifetime { start: 5, end: 10 },
463            mem_size: 2000.into(),
464        };
465
466        let partition = Partition { nodes: vec![node1.clone(), node2.clone()] };
467
468        assert_eq!(partition.find_node_alive_at_step(3), Some(&node1));
469        assert_eq!(partition.find_node_alive_at_step(7), Some(&node2));
470        assert_eq!(partition.find_node_alive_at_step(10), None);
471    }
472}