Skip to main content

cubecl_opt/
version.rs

1use core::mem::take;
2
3use alloc::vec::Vec;
4use cubecl_ir::{Id, Instruction, Memory, Operation, Type, Value, ValueKind};
5use hashbrown::{HashMap, HashSet};
6use petgraph::visit::EdgeRef;
7
8use crate::{EdgeIndex, Function, GlobalState, NodeIndex};
9
10/// The state required by the SSA transform
11#[derive(Debug)]
12pub struct SsaState<'a> {
13    versions: HashMap<Id, Id>,
14    visited_blocks: &'a mut HashSet<NodeIndex>,
15    visited_edges: &'a mut HashSet<EdgeIndex>,
16}
17
18/// An entry in the phi instruction. Contains the value ID that should be used when coming from
19/// `block`.
20#[derive(Debug, Clone, PartialEq)]
21pub struct PhiEntry {
22    pub block: NodeIndex,
23    pub value: Value,
24}
25
26/// A phi node that picks its value based on the `BasicBlock` that came immediately before.
27/// For more information, see <https://en.wikipedia.org/wiki/Static_single-assignment_form>
28///
29/// # Example
30/// ```ignore
31/// if cond {
32///     result = "heads";
33/// } else {
34///     result = "tails";
35/// }
36/// ```
37/// would translate to the following SSA graph:
38/// ```ignore
39/// bb1: {
40///     branch if cond { bb2 } else { bb3 };
41/// }
42///
43/// bb2: {
44///     let result.v1 = "heads";
45///     branch bb4;
46/// }
47///
48/// bb3: {
49///     let result.v2 = "tails";
50///     branch bb4;
51/// }
52///
53/// bb4: {
54///     let result.v3 = phi [bb2: result.v1] [bb3: result.v2];
55/// }
56/// ```
57#[derive(Debug, Clone, PartialEq)]
58pub struct PhiInstruction {
59    /// The out value for the phi instruction
60    pub out: Value,
61    /// The set of `block`-`value` pairs for the phi instruction
62    pub entries: Vec<PhiEntry>,
63}
64
65impl Function {
66    /// Version all variables in the program so they are each assigned to exactly once.
67    pub(crate) fn version_program(&mut self, global_state: &GlobalState) {
68        let locals = self.destructurable_local_memories();
69        let versions: HashMap<_, _> = locals.keys().map(|key| (*key, 0)).collect();
70        let mut visited_blocks = HashSet::new();
71        let mut visited_edges = HashSet::new();
72        let initial_state = SsaState {
73            versions,
74            visited_blocks: &mut visited_blocks,
75            visited_edges: &mut visited_edges,
76        };
77        self.version_block(global_state, self.root, initial_state);
78    }
79
80    fn version_block(
81        &mut self,
82        global_state: &GlobalState,
83        block: NodeIndex,
84        mut state: SsaState<'_>,
85    ) {
86        self.version_block_ops(global_state, block, &mut state);
87
88        let edges: Vec<_> = self.edges(block).map(|it| (it.id(), it.target())).collect();
89        let state = &mut state;
90        for (edge_id, target) in edges {
91            let edge_visited = state.visited_edges.contains(&edge_id);
92            state.visited_edges.insert(edge_id);
93            let block_visited = state.visited_blocks.contains(&target);
94            state.visited_blocks.insert(block);
95
96            let new_state = SsaState {
97                versions: state.versions.clone(),
98                visited_blocks: state.visited_blocks,
99                visited_edges: state.visited_edges,
100            };
101
102            if !edge_visited {
103                self.version_phi(target, block, &new_state);
104            }
105            if !block_visited {
106                self.version_block(global_state, target, new_state);
107            }
108        }
109    }
110
111    /// Version the phi entry for this edge
112    fn version_phi(&mut self, target: NodeIndex, source: NodeIndex, state: &SsaState<'_>) {
113        let phi = self[target].phi_nodes.clone();
114        for node in phi.borrow_mut().iter_mut() {
115            let entry = node
116                .entries
117                .iter_mut()
118                .find(|it| it.block == source)
119                .unwrap();
120            if let Some((id, item)) = as_local(entry.value)
121                && self.destructurable_local_memories().contains_key(&id)
122            {
123                let id = state.versions[&id];
124                entry.value = Value::new(id, item);
125            }
126        }
127    }
128
129    /// Version the operations for this block
130    fn version_block_ops(
131        &mut self,
132        global_state: &GlobalState,
133        block: NodeIndex,
134        state: &mut SsaState<'_>,
135    ) {
136        for phi in self[block].phi_nodes.borrow_mut().iter_mut() {
137            if let Some((id, item)) = as_local(phi.out)
138                && self.destructurable_local_memories().contains_key(&id)
139            {
140                let version = state.versions.get_mut(&id).unwrap();
141                let out = global_state.root_scope.create_value(item);
142                *version = out.id();
143                phi.out = out;
144            }
145        }
146
147        let ops = take(&mut *self[block].ops.borrow_mut());
148        let ops = ops.into_iter().flat_map(|(_, mut instruction)| {
149            self.version_loads(&mut instruction, state);
150            self.version_stores(&mut instruction, state, global_state);
151            if let Operation::DeclareVariable { .. } = &instruction.operation
152                && state.versions.contains_key(&instruction.out().id())
153            {
154                None
155            } else {
156                Some(instruction)
157            }
158        });
159        *self[block].ops.borrow_mut() = ops.collect();
160    }
161
162    fn version_loads(&mut self, inst: &mut Instruction, state: &mut SsaState<'_>) {
163        if let Operation::Memory(Memory::Load(ptr)) = inst.operation
164            && let Some(id) = state.versions.get(&ptr.id())
165        {
166            let new_val = Value::new(*id, inst.out().ty);
167            *inst = Instruction::new(Operation::Copy(new_val), inst.out())
168        }
169    }
170
171    fn version_stores(
172        &mut self,
173        inst: &mut Instruction,
174        state: &mut SsaState<'_>,
175        global_state: &GlobalState,
176    ) {
177        if let Operation::Memory(Memory::Store(store)) = &mut inst.operation
178            && let Some(version) = state.versions.get_mut(&store.ptr.id())
179        {
180            let new_val = global_state.root_scope.create_value(store.value.ty);
181            *version = new_val.id();
182            *inst = Instruction::new(Operation::Copy(store.value), new_val);
183        }
184    }
185}
186
187fn as_local(val: Value) -> Option<(Id, Type)> {
188    match val.kind {
189        ValueKind::Value { id } => Some((id, val.ty)),
190        _ => None,
191    }
192}