Skip to main content

tidu/
linearize.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use crate::{ADKey, ADRuleError, ADRuleKind, ADRuleResult, DiffPassId, Primitive};
5use computegraph::resolve::{ResolvedView, ValueDef};
6use computegraph::{GraphOperation, LocalValueId, OperationKey, ValueKey};
7
8use crate::split_builder::SplitGraphBuilder;
9use crate::LinearizedGraph;
10
11/// Linearize a resolved computation graph, producing a linear graph.
12///
13/// The transform walks the reachable DAG from `outputs` in dependency-first
14/// order and delegates primitive-specific JVP generation to
15/// [`crate::Primitive::jvp_rule`].
16///
17/// # Examples
18///
19/// ```ignore
20/// use computegraph::resolve::resolve;
21/// use tidu::linearize;
22///
23/// let view = resolve(vec![primal_graph]);
24/// let mut ctx = ();
25/// let aliases = std::collections::HashMap::new();
26/// let linear = linearize(&view, &[output_key], &[input_key], 1, &mut ctx, &aliases)?;
27/// assert_eq!(linear.tangent_outputs().len(), 1);
28/// # Ok::<(), tidu::ADRuleError>(())
29/// ```
30pub fn linearize<Op: Primitive>(
31    view: &ResolvedView<Op>,
32    outputs: &[ValueKey<Op>],
33    wrt: &[Op::InputKey],
34    pass: DiffPassId,
35    ctx: &mut Op::ADContext,
36    aliases: &HashMap<Op::InputKey, ValueKey<Op>>,
37) -> ADRuleResult<LinearizedGraph<Op>>
38where
39    Op::InputKey: ADKey,
40{
41    let mut builder = SplitGraphBuilder::<Op>::new();
42    let topo_keys = topological_order(view, outputs, aliases);
43    let mut tangent_env: HashMap<ValueKey<Op>, Option<LocalValueId>> = HashMap::new();
44    let mut processed_ops = HashSet::new();
45
46    let mut tangent_inputs = Vec::with_capacity(wrt.len());
47    for wrt_key in wrt {
48        let tangent_key = wrt_key.tangent_of(pass);
49        let (unified_id, linear_id) =
50            builder.add_linear_input(tangent_key, Some(ValueKey::Input(wrt_key.clone())));
51        tangent_env.insert(ValueKey::Input(wrt_key.clone()), Some(unified_id));
52        tangent_inputs.push((wrt_key.clone(), linear_id));
53    }
54
55    for key in topo_keys {
56        if tangent_env.contains_key(&key) {
57            continue;
58        }
59
60        let val_def = match view.resolve_value(&key) {
61            Some(val_def) => val_def,
62            None => continue,
63        };
64
65        match val_def {
66            ValueDef::Input { key: ref input_key } => {
67                if let Some(aliased_key) = aliases.get(input_key) {
68                    let aliased_tangent = tangent_env.get(aliased_key).copied().flatten();
69                    tangent_env.insert(key, aliased_tangent);
70                } else {
71                    tangent_env.insert(key, None);
72                }
73            }
74            ValueDef::Produced {
75                operation,
76                input_keys,
77                role,
78                ..
79            } => {
80                let global_op_key =
81                    OperationKey::new(operation.clone(), input_keys.clone(), role.clone());
82                if !processed_ops.insert(global_op_key.clone()) {
83                    continue;
84                }
85
86                let tangent_in: Vec<Option<LocalValueId>> = input_keys
87                    .iter()
88                    .map(|input_key| tangent_env.get(input_key).copied().flatten())
89                    .collect();
90                let output_keys = output_keys(&global_op_key, operation.output_count());
91
92                if tangent_in.iter().all(Option::is_none) {
93                    for output_key in output_keys {
94                        tangent_env.insert(output_key, None);
95                    }
96                    continue;
97                }
98
99                let tangent_out = operation.jvp_rule(
100                    &mut builder,
101                    &input_keys,
102                    &output_keys,
103                    &tangent_in,
104                    ctx,
105                )?;
106                if let Some(err) = builder.take_error() {
107                    return Err(err);
108                }
109                if tangent_out.len() != output_keys.len() {
110                    return Err(ADRuleError::invalid_input(
111                        format!("{:?}", operation),
112                        ADRuleKind::Jvp,
113                        format!(
114                            "rule returned {} tangents for {} outputs",
115                            tangent_out.len(),
116                            output_keys.len()
117                        ),
118                    ));
119                }
120
121                for (output_key, tangent_output) in output_keys.iter().zip(tangent_out) {
122                    if let Some(tangent_id) = tangent_output {
123                        builder.set_linear_primal(tangent_id, output_key.clone())?;
124                    }
125                    tangent_env.insert(output_key.clone(), tangent_output);
126                }
127            }
128        }
129    }
130
131    let tangent_outputs: Vec<Option<LocalValueId>> = outputs
132        .iter()
133        .map(|key| match tangent_env.get(key).copied().flatten() {
134            Some(unified_id) => builder
135                .linear_local_id(unified_id)
136                .ok_or_else(|| {
137                    ADRuleError::invalid_input(
138                        "tidu::linearize",
139                        ADRuleKind::Jvp,
140                        "JVP rule returned a residual value as an active requested tangent output",
141                    )
142                })
143                .map(Some),
144            None => Ok(None),
145        })
146        .collect::<ADRuleResult<Vec<_>>>()?;
147    let active_outputs: Vec<LocalValueId> = tangent_outputs.iter().filter_map(|id| *id).collect();
148    let (linear, residual, linear_primals) = builder.finish(active_outputs);
149
150    Ok(LinearizedGraph::from_parts(
151        linear,
152        residual,
153        tangent_inputs,
154        tangent_outputs,
155        linear_primals,
156    ))
157}
158
159fn output_keys<Op: GraphOperation>(
160    op_key: &OperationKey<Op>,
161    output_count: usize,
162) -> Vec<ValueKey<Op>> {
163    let op_key = Arc::new(op_key.clone());
164    (0..output_count)
165        .map(|output_slot| ValueKey::Derived {
166            operation: Arc::clone(&op_key),
167            output_slot: output_slot as u8,
168        })
169        .collect()
170}
171
172fn topological_order<Op: GraphOperation>(
173    view: &ResolvedView<Op>,
174    outputs: &[ValueKey<Op>],
175    aliases: &HashMap<Op::InputKey, ValueKey<Op>>,
176) -> Vec<ValueKey<Op>> {
177    fn visit<Op: GraphOperation>(
178        key: &ValueKey<Op>,
179        view: &ResolvedView<Op>,
180        aliases: &HashMap<Op::InputKey, ValueKey<Op>>,
181        visited: &mut HashSet<ValueKey<Op>>,
182        order: &mut Vec<ValueKey<Op>>,
183    ) {
184        if !visited.insert(key.clone()) {
185            return;
186        }
187
188        match view.resolve_value(key) {
189            Some(ValueDef::Produced { input_keys, .. }) => {
190                for input_key in input_keys {
191                    visit(&input_key, view, aliases, visited, order);
192                }
193            }
194            Some(ValueDef::Input { key: input_key }) => {
195                if let Some(aliased_key) = aliases.get(&input_key) {
196                    visit(aliased_key, view, aliases, visited, order);
197                }
198            }
199            None => {}
200        }
201
202        order.push(key.clone());
203    }
204
205    let mut visited = HashSet::new();
206    let mut order = Vec::new();
207    for output_key in outputs {
208        visit(output_key, view, aliases, &mut visited, &mut order);
209    }
210    order
211}