Spaces:
Running
Running
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)
|