code: def push_out_add_scan(fgraph, node):
      r"""Push `Add` operations performed at the end of the inner graph to the outside.
  
      Like `push_out_seq_scan`, this optimization aims to replace many operations
      on small tensors by few operations on large tensors. It can also lead to
      increased memory usage.
      """
      # Don't perform the optimization on `as_while` `Scan`s. Because these
      # `Scan`s don't run for a predetermined number of steps, handling them is
      # more complicated and this optimization doesn't support it at the moment.
      if not (isinstance(node.op, Scan) and not node.op.info.as_while):
          return False
  
      op = node.op
  
      # Use `ScanArgs` to parse the inputs and outputs of scan for ease of
      # use
      args = ScanArgs(
          node.inputs,
          node.outputs,
          op.inner_inputs,
          op.inner_outputs,
          op.info,
      )
  
      clients = {}
      local_fgraph_topo = io_toposort(
          args.inner_inputs, args.inner_outputs, clients=clients
      )
  
      for nd in local_fgraph_topo:
          if (
              isinstance(nd.op, Elemwise)
              and isinstance(nd.op.scalar_op, ps.Add)
              and nd.out in args.inner_out_sit_sot
              and inner_sitsot_only_last_step_used(fgraph, nd.out, args)
          ):
              # Ensure that one of the input to the add is the output of
              # the add from a previous iteration of the inner function
              sitsot_idx = args.inner_out_sit_sot.index(nd.out)
              if args.inner_in_sit_sot[sitsot_idx] in nd.inputs:
                  # Ensure that the other input to the add is a dot product
                  # between 2 matrices which will become a tensor3 and a
                  # matrix if pushed outside of the scan. Also make sure
                  # that the output of the Dot is ONLY used by the 'add'
                  # otherwise doing a Dot in the outer graph will only
                  # duplicate computation.
  
                  sitsot_in_idx = nd.inputs.index(
                      args.inner_in_sit_sot[sitsot_idx]
                  )
  
                  # 0 if sitsot_in_idx==1, 1 if sitsot_in_idx==0
                  dot_in_idx = 1 - sitsot_in_idx
  
                  dot_input = nd.inputs[dot_in_idx]
  
                  if (
                      dot_input.owner is not None
                      and isinstance(dot_input.owner.op, Dot)
                      and len(clients[dot_input]) == 1
                      and dot_input.owner.inputs[0].ndim == 2
                      and dot_input.owner.inputs[1].ndim == 2
                      and get_outer_ndim(
                          dot_input.owner.inputs[0], args
                      )
                      == 3
                      and get_outer_ndim(
                          dot_input.owner.inputs[1], args
                      )
                      == 3
                  ):
                      # The optimization can be be applied in this case.
  
                      # Move out of scan the two inputs to the Dot and
                      # perform a dot outside of scan on these two inputs
                      inner_dot_inputs = nd.inputs[
                          dot_in_idx
                      ].owner.inputs
                      (
                          outer_dot_inputs,
                          new_scan_args,
                          replacements,
                      ) = push_out_inner_vars(
                          fgraph, inner_dot_inputs, node, args
                      )
  
                      # Collapse some of the dimensions of the tensors
                      # so that they become matrices. This is because a
                      # dot is usually faster on two large matrices than
                      # a bunch of small ones
                      outer_dot_inputs[0] = pt.flatten(
                          outer_dot_inputs[0].dimshuffle(1, 0, 2),
                          ndim=2,
                      )
  
                      shape_input1 = shape(outer_dot_inputs[1])
                      outer_dot_inputs[1] = outer_dot_inputs[1].reshape(
                          (
                              shape_input1[0] * shape_input1[1],
                              shape_input1[2],
                          )
                      )
  
                      # Perform the dot on the newly obtained matrices and
                      # add the initial value
                      outer_dot_output = dot(*outer_dot_inputs)
                      init_value = new_scan_args.outer_in_sit_sot[
                          sitsot_idx
                      ][0]
                      replacement = outer_dot_output + init_value
  
                      # Alter the outer graph to use the output of the
                      # external Dot instead of the output of scan
                      # Modify the outer graph to add the outer Dot
                      outer_sitsot = new_scan_args.outer_out_sit_sot[
                          sitsot_idx
                      ]
                      subtensor_node = fgraph.clients[outer_sitsot][0][
                          0
                      ]
                      outer_sitsot_last_step = subtensor_node.outputs[0]
  
                      replacements[outer_sitsot_last_step] = replacement
                      return replacements
  
      return False
  
imports: import copy
  import dataclasses
  from itertools import chain
  from sys import maxsize
  from typing import Optional, cast
  import numpy
  import pytensor
  from pytensor import scalar
  from pytensor import tensor
  from pytensor.compile import optdb
  from pytensor.compile.function.types import deep_copy_op
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, Constant, NominalVariable, Variable, ancestors, apply_depends_on, equal_computations, graph_inputs, io_toposort
  from pytensor.graph.destroyhandler import DestroyHandler
  from pytensor.graph.features import ReplaceValidate
  from pytensor.graph.fg import FunctionGraph
  from pytensor.graph.op import compute_test_value
  from pytensor.graph.replace import clone_replace
  from pytensor.graph.rewriting.basic import GraphRewriter, copy_stack_trace, in2out, node_rewriter
  from pytensor.graph.rewriting.db import EquilibriumDB, SequenceDB
  from pytensor.graph.rewriting.utils import get_clients_at_depth
  from pytensor.graph.type import HasShape
  from pytensor.graph.utils import InconsistencyError
  from pytensor.raise_op import Assert
  from pytensor.scalar import ScalarConstant
  from pytensor.scan.op import Scan, ScanInfo
  from pytensor.scan.utils import ScanArgs, compress_outs, expand_empty, reconstruct_graph, safe_new, scan_can_remove_outs
  from pytensor.tensor.basic import Alloc, AllocEmpty, get_underlying_scalar_constant_value
  from pytensor.tensor.elemwise import DimShuffle, Elemwise
  from pytensor.tensor.exceptions import NotScalarConstantError
  from pytensor.tensor.math import Dot, dot, maximum, minimum
  from pytensor.tensor.rewriting.basic import constant_folding, local_useless_switch
  from pytensor.tensor.rewriting.elemwise import local_upcast_elemwise_constant_inputs
  from pytensor.tensor.rewriting.math import local_abs_merge, local_mul_switch_sink
  from pytensor.tensor.shape import shape
  from pytensor.tensor.subtensor import IncSubtensor, Subtensor, get_canonical_form_slice, get_idx_list, get_slice_elements, set_subtensor
  from pytensor.tensor.variable import TensorConstant, get_unique_constant_value
label_desc: Use image node_id 8 for calling a global function with example usage: push_out_add_scan(fgraph, node) and returns: False, False, replacements