File size: 8,088 Bytes
8565ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1708397
8565ab7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""Renderer-independent computation store and typed information flow."""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

import numpy as np

from math_expression import Expr, operation_expression, snapshot
from backward_rules import ScalarLocalRule, TensorBackwardRule
from backward_explanation import BackwardExplanation, explain_step, explain_stop
from scalar_rules import scalar_rule_from_record


class BackwardEdgeType(Enum):
    ADJOINT_INTRODUCTION = 'adjoint_introduction'
    ADJOINT_PROPAGATION = 'adjoint_propagation'
    CONTRIBUTION = 'contribution'


@dataclass
class QuantityState:
    uid: str
    name: str
    primal_value: Any
    adjoint_value: Any
    differentiable: bool
    is_input: bool
    shape: tuple
    is_constant: bool = False


@dataclass
class Operation:
    quantity: QuantityState
    operands: list[QuantityState]
    kind: str
    function: str | None = None
    exponent: float | None = None
    index: int | None = None
    formula: Expr = field(init=False)
    stop_explanation: BackwardExplanation | None = None

    def __post_init__(self):
        self.formula = operation_expression(self)


@dataclass
class BackwardStep:
    uid: str
    source_quantity: QuantityState
    destination_quantity: QuantityState
    incoming_adjoint: Any
    operation: Operation
    rule: ScalarLocalRule | TensorBackwardRule
    local_value: Any
    contribution: Any
    operand_occurrence: int
    explanation: BackwardExplanation | None = None


@dataclass
class Accumulation:
    uid: str
    destination_quantity: QuantityState
    contributions: list[BackwardStep]
    resulting_adjoint: Any


@dataclass
class BackwardEdge:
    source: str
    destination: str
    kind: BackwardEdgeType
    value: Any
    quantity: QuantityState | None = None
    # A single contribution is allowed to collapse into an introduction edge.
    contribution_step: BackwardStep | None = None
    seed: bool = False


@dataclass
class OutputState:
    uid: str
    quantity: QuantityState


@dataclass
class ForwardEdge:
    source: str
    destination: str
    value: Any
    operand_occurrence: int | None = None


@dataclass
class ComputationModel:
    quantities: list[QuantityState]
    operations: list[Operation]
    root: QuantityState
    output_name: str
    formula: str
    forward_only: bool
    output: OutputState | None = None
    steps: list[BackwardStep] = field(default_factory=list)
    accumulations: list[Accumulation] = field(default_factory=list)
    forward_edges: list[ForwardEdge] = field(default_factory=list)
    backward_edges: list[BackwardEdge] = field(default_factory=list)


def build_computation_model(graph, engine, output_name='out', forward_only=False):
    """Adapt numerical engine records once; formatting does not infer edge roles."""
    if engine not in {'scalar', 'tensor'}:
        raise ValueError('Unknown numerical engine')
    quantities, operations, steps = [], [], []
    by_id = {}
    for node in graph.nodes:
        differentiable = not node.is_constant if node.is_input else any(by_id[a.uid].differentiable for a in node.args)
        q = QuantityState(node.uid, node.name, snapshot(node.val),
                          None if forward_only or not differentiable or not node.live else snapshot(node.grad),
                          differentiable, node.is_input, np.shape(node.val),
                          is_constant=getattr(node, 'is_constant', False))
        by_id[q.uid] = q
        quantities.append(q)
        if node.is_input:
            continue
        if engine == 'scalar':
            names = {'Add': 'add', 'Mul': 'mul', 'Pow': 'pow'}
            func = node.expr.func.__name__
            operation = Operation(q, [by_id[a.uid] for a in node.args], names.get(func, 'func'), func)
        else:
            operation = Operation(q, [by_id[a.uid] for a in node.args], node.op_type,
                                  node.func_name, node.pow_exp, node.pick_index)
        operations.append(operation)
        is_nograd = func == 'nograd' if engine == 'scalar' else node.func_name == 'nograd'
        if is_nograd and not forward_only and q.differentiable:
            operation.stop_explanation = explain_stop(operation)
        if forward_only or not getattr(node, 'live', True) or is_nograd:
            continue
        records = node.local_derivatives if engine == 'scalar' else node.backward_rules
        for record in records:
            occurrence = record['operand_occurrence']
            destination = by_id[record['arg_node'].uid]
            if not destination.differentiable:
                continue
            rule = (scalar_rule_from_record(operation, occurrence, record)
                    if engine == 'scalar' else record['rule'])
            local_value = record['local_val'] if engine == 'scalar' else record.get('local_val')
            step = BackwardStep(
                f'der_{node.uid}_{occurrence}', q, destination, snapshot(node.grad),
                operation, rule, snapshot(local_value),
                snapshot(record['grad_contrib'] if engine == 'scalar' else record['contrib_val']), occurrence,
            )
            values = {a.uid: a.primal_value for a in operation.operands}
            values[q.uid] = q.primal_value
            values['adjoint:' + q.uid] = step.incoming_adjoint
            step.explanation = explain_step(step, values)
            steps.append(step)
    root_q = by_id[graph.root.uid]
    output_state = OutputState(f'output_{root_q.uid}', root_q)
    model = ComputationModel(quantities, operations, root_q, output_name,
                         str(graph.orig_expr) if engine == 'scalar' else graph.orig_expr_str,
                         forward_only, output_state, steps)
    for operation in operations:
        for i, operand in enumerate(operation.operands):
            model.forward_edges.append(ForwardEdge(operand.uid, operation.quantity.uid, operand.primal_value, i))
    # The final operation/result sends its primal result forward to the output presentation node
    model.forward_edges.append(ForwardEdge(model.root.uid, model.output.uid, model.root.primal_value, None))
    if forward_only or not model.root.differentiable:
        return model
    model.backward_edges.append(BackwardEdge(model.output.uid, model.root.uid,
        BackwardEdgeType.ADJOINT_INTRODUCTION, model.root.adjoint_value, model.root, seed=True))
    contributions = {q.uid: [] for q in quantities}
    for step in steps:
        contributions[step.destination_quantity.uid].append(step)
        model.backward_edges.append(BackwardEdge(step.source_quantity.uid, step.uid,
            BackwardEdgeType.ADJOINT_PROPAGATION, step.incoming_adjoint, step.source_quantity))
    for quantity in quantities:
        incoming = contributions[quantity.uid]
        if len(incoming) > 1:
            accumulation = Accumulation('sum_' + quantity.uid, quantity, incoming, quantity.adjoint_value)
            model.accumulations.append(accumulation)
            for step in incoming:
                model.backward_edges.append(BackwardEdge(step.uid, accumulation.uid,
                    BackwardEdgeType.CONTRIBUTION, step.contribution, contribution_step=step))
            model.backward_edges.append(BackwardEdge(accumulation.uid, quantity.uid,
                BackwardEdgeType.ADJOINT_INTRODUCTION, quantity.adjoint_value, quantity))
        elif incoming:
            step = incoming[0]
            model.backward_edges.append(BackwardEdge(step.uid, quantity.uid,
                BackwardEdgeType.ADJOINT_INTRODUCTION, quantity.adjoint_value, quantity, step))
    return model


def tensor_operation(node):
    """Numerical tensor-node adapter; rule factories depend only on this record."""
    def quantity(n):
        return QuantityState(n.uid, n.name, n.val, n.grad, not n.is_constant,
                             n.is_input, n.shape, n.is_constant)
    return Operation(quantity(node), [quantity(a) for a in node.args],
                     node.op_type, node.func_name, node.pow_exp, node.pick_index)