docstr: The logic of this method is almost same as one in visit_function_backward in C++ layer.
docstr_tok: ["The","logic","of","this","method","is","almost","same","as","one","in","visit_function_backward","in","C++","layer","."]
code: def __call__(
      self,
      outputs,
      inputs,
      grad_outputs=None,
      persistent_outputs=[],
      bind_grad_output=False,
  ):
      """
      The logic of this method is almost same as one in visit_function_backward in C++ layer.
      """
      # TODO: address auto_forward is very slow. It may be python overhead since small diff when BS is large.
      # TODO: address auto_forward consumes lots of memory, need to call v.get_unlinked_variable()?
      # TODO: address auto_forward consumes lots of memory, need to use NdArray as inputs?
  
      # Check outputs/inputs
      outputs = self._force_list(outputs)
      if not all([isinstance(o, nn.Variable) for o in outputs]):
          raise ValueError(
              "Element of outputs must be `nnabla.Variable`."
          )
      inputs = self._force_list(inputs)
      if not all([isinstance(i, nn.Variable) for i in inputs]):
          raise ValueError(
              "Element of inputs must be `nnabla.Variable`."
          )
  
      # Check grad_outputs
      if grad_outputs is None:
          grad_outputs = [None] * len(outputs)
      elif isinstance(
          grad_outputs, (int, float, np.ndarray, nn.NdArray)
      ):
          grad_outputs = self._force_list(grad_outputs)
      elif isinstance(grad_outputs, list):
          if len(outputs) != len(grad_outputs):
              raise ValueError(
                  "Length of `grad_outputs` and length of `outputs` must be the same."
              )
          for i in range(len(outputs)):
              o = outputs[i]
              go = grad_outputs[i]
              if not isinstance(
                  go,
                  (
                      type(None),
                      int,
                      float,
                      np.ndarray,
                      nn.NdArray,
                      nn.Variable,
                  ),
              ):
                  raise ValueError(
                      "Element of `grad_outputs` must be "
                      "in (`None`, `int`, `float`, `numpy.ndarray`, "
                      "`nnabla.NdArray`, `nnabla.Variable`) or "
                      "list of (`None`, `int`, `float`, `numpy.ndarray`, "
                      "`nnabla.NdArray`, `nnabla.Variable`)\n"
                      "type(grad_outputs[{}] = {}".format(i, type(go))
                  )
              elif (
                  isinstance(go, (np.ndarray, nn.NdArray, nn.Variable))
                  and go.shape != o.shape
              ):
                  raise ValueError(
                      "Shape of each of outputs and grad_outputs must be same.\n"
                      "output[{}]({}) != grad_output[{}]({})".format(
                          i, o.shape, i, go.shape
                      )
                  )
  
      # Check persistent_outputs
      if len(persistent_outputs) != 0 and len(outputs) != len(
          persistent_outputs
      ):
          raise ValueError(
              "Length of outputs and persistent_outputs "
              "must be the same except for "
              "the case that the length of the persistent_outputs is 0."
          )
  
      # Persistent outputs since outputs are basically losses to be monitored
      persistent_outputs = (
          [True] * len(outputs)
          if persistent_outputs == []
          else persistent_outputs
      )
      for o, p in zip(outputs, persistent_outputs):
          o.persistent = p
  
      # Open list of next search candidate
      ids = {}
  
      def get_id(func):
          if func not in ids.keys():
              size = len(ids)
              ids[func] = size
              return size
          return ids[func]
  
      open = set()
  
      # Map for grad_variables consumed on the gradient graph.
      # End is the special case where d_o = end_f(o) and map[end_f] = {o: [d_o]}
      grad_vars = OrderedDict()  # {F_fwd: {VO_fwd: [VI_bwd]}}
  
      # Set grad_outputs
      for i in range(len(outputs)):
          o = outputs[i]
          go = grad_outputs[i]
          if go is None:
              output = o
          elif isinstance(go, (int, float)):
              go = nn.Variable(o.shape).apply(d=go, need_grad=False)
              output = o * go
          elif isinstance(go, np.ndarray):
              go = nn.Variable(o.shape).apply(d=go, need_grad=False)
              output = o * go
          elif isinstance(go, nn.NdArray):
              go = nn.Variable(o.shape).apply(data=go, need_grad=False)
              output = o * go
          elif isinstance(go, nn.Variable):
              output = o * go
          func = output.parent
          open.add((-output.rank, get_id(func), func))
  
          # Connect the graph and its gradient graph
          grad_output = GradEndFunction()(output).apply(need_grad=False)
          grad_vars[func] = OrderedDict({output: [grad_output]})
  
      # Return grads but
      # replace inputs params with the vars connected with the given graph
      wrt_inputs = self._get_corresponding_vars_on_graph(
          inputs, outputs
      )
      grads = [None] * len(wrt_inputs)
      child_nodes = self._get_children(wrt_inputs)
      wrt_inputs = [
          nn.Variable() if x is None else x for x in wrt_inputs
      ]
  
      # Expand the graph to its gradient graph
      while len(open) != 0:
          open = sorted(open)  # python set is NOT sorted set.
          rank_func = open.pop(0)  # 0 is necessary
          open = set(open)
          f = rank_func[2]
  
          if not f.need_grad:
              continue
          # Connect variables on the gradient graph
          grad_outputs = self._connect_on_gradient_graph(grad_vars, f)
  
          # Check grads w.r.t. inputs
          for inp, grad_out in zip(f.inputs, grad_outputs):
              if (
                  inp not in wrt_inputs
                  or inp.need_grad == False
                  or grad_out is None
              ):
                  continue
              idx = wrt_inputs.index(inp)
              if grads[idx] is None:
                  grads[idx] = grad_out
              else:
                  grads[idx] = grads[idx] + grad_out  # accum at leaf
              if bind_grad_output:
                  inp.grad = grads[idx].data
  
          # Propagate down
          for inp, grad_out in zip(f.inputs, grad_outputs):
              if (
                  inp not in child_nodes
                  or not inp.need_grad
                  or grad_out is None
              ):
                  continue
              p_i = inp.parent
              if not p_i:
                  continue
              open.add((-p_i.rank, get_id(p_i), p_i))
  
      # If the final grads has None, then None becomes zero Variable(s).
      for i in range(len(grads)):
          if grads[i]:
              continue
          grads[i] = F.constant(0, wrt_inputs[i].shape)
      return grads
  
imports: import numpy
  from collections import OrderedDict
  import nnabla
  import nnabla.functions
  from nnabla.function import PythonFunction
  from .backward_functions import registry
label_desc: Use image node_id 6 for calling the Grad obj's underlying member method code with example usage: obj.__call__(outputs, inputs, grad_outputs, persistent_outputs, bind_grad_output) and returns: grads, ids, size