Reusing Jacobian and Hessian computational graph

contraidees: <p>Hello,</p>
<p>I’m using PyTorch as an audodiff tool to compute the first and second derivatives of a cost function to be used in a (non-deep-learning) optimization tool (ipopt). The cost function depends about 10 parameters. I am using the new <code>torch.autograd.functional.jacobian</code> and <code>torch.autograd.functional.hessian</code> added to PyTorch 1.5. The Jacobian and Hessian get called several times (about 100) with different input parameters until the function is minimized.</p>
<p>To be more specific, I am computing the Jacobians and Hessians as:</p>
<pre><code class="lang-auto">def jac(f, x):
    x_tensor = torch.tensor(x, requires_grad=True, dtype=torch.float64)   # Convert input variable to torch tensor
    jac = torch.autograd.functional.jacobian(f, x_tensor)
    return jac.numpy()

def hess(f, x):
    x_tensor = torch.tensor(x, requires_grad=True, dtype=torch.float64)   # Convert input variable to torch tensor
    hess = torch.autograd.functional.hessian(f, x_tensor)
    return hess.numpy()
</code></pre>
<p>Since the functions to compute the derivatives are always the same, I was wondering if there was a way (or if it makes sense) to save the computational graph to avoid computing many times the same quantity, and thus speed up the calculation.</p>
<p>Thanks,</p>
<p>Daniel</p>

albanD: <p>Hi,</p>
<p>The short answer is:<br>
Pytorch is actually built in such a way that the overhead of the graph creation is low enough (compared to all the computations you do in the forward) that you can do it at every iteration.</p>
<p>The long answer is:<br>
This is true for neural network and when each op is quite “large” but unfortunately, if you have only very small ops, then the overhead might become noticeable.</p>
<p>The usual answer is: “use the jit to get the last drop of perf for your production evaluation”. But since you want to actually use backward here, you might run into troubles.<br>
You can try to jit your function <code>f</code>. And I would be curious to know if it leads to any improvement.</p>

contraidees: <p>Thanks for your answer.</p>
<p>I have tried to use jit to speed up the computation. It doesn’t work. I have been able to jit the original function, but this does not lead to a relevant speedup. I can’t jit the gradients and the Hessian:</p>
<aside class="onebox githubgist">
  <header class="source">
      <a href="https://gist.github.com/darteaga/8df73f503518de9c8c783d11bae4e6ee" target="_blank" rel="nofollow noopener">gist.github.com</a>
  </header>
  <article class="onebox-body">
    <h4><a href="https://gist.github.com/darteaga/8df73f503518de9c8c783d11bae4e6ee" target="_blank" rel="nofollow noopener">https://gist.github.com/darteaga/8df73f503518de9c8c783d11bae4e6ee</a></h4>
<h5>compare_ipyopt_jax.ipynb</h5>
<pre><code class="Jupyter Notebook">{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import numpy as np\n",</code></pre>
This file has been truncated. <a href="https://gist.github.com/darteaga/8df73f503518de9c8c783d11bae4e6ee" target="_blank" rel="nofollow noopener">show original</a>

<p>
</p>

  </article>
  <div class="onebox-metadata">
    
    
  </div>
  <div style="clear: both"></div>
</aside>

<p>In the same gist I compare also to <a href="https://github.com/google/jax" rel="nofollow noopener">JAX</a> for gradient and hessian. With JAX it is possible to precompute the gradients using jit. It seems that PyTorch is more or less at the level as JAX for the gradient computation, but not for Hessian computation: Hessian is about 20x slower in PyTorch than in JAX.</p>

albanD: <blockquote>
<p>It seems that PyTorch is more or less at the level as JAX for the gradient computation</p>
</blockquote>
<p>That’s good news, that needs that your ops are big enough that the creation of the graph is negligible.</p>
<blockquote>
<p>Hessian is about 20x slower in PyTorch than in JAX.</p>
</blockquote>
<p>That might be due to other reasons, mainly the way the Hessian is computed: jax can use forward mode AD to speed this up while pytorch does not have forward mode AD (yet <img src="https://discuss.pytorch.org/images/emoji/apple/wink.png?v=12" title=":wink:" class="emoji" alt=":wink:" loading="lazy" width="20" height="20"> ).</p>

bradbell: <p>Consider the example case of computing determinants using expansion by minors. (I know this is not an efficient way to compute determinants, but it demonstrates a case that has lots of operations on scalar values).</p>
<p>I am trying to re-use the graph for this case and my method of doing so works for 2 by 2 matrices and fails for 3 by 3 matrices (see the pythong programs below) and do not know why ?</p>
<p>det_22.py</p>
<pre><code class="lang-auto"># Test reusing graph for derivatives of determinant of 2 by 2 matrix.
# The output generated by this program is below:
#
# First gradient passed check.
# Second gradient passed check.
#
# imports
import sys
import torch
import numpy
#
# check_grad
def check_grad(ax) :
   # ok, eps99
   ok    = True
   eps99 = 99.0 * numpy.finfo(float).eps
   #
   # ok
   check = ax.data[1,1]
   if abs( ax.grad[0,0] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[0,0] = {ax.grad[0,0]}, check = {check}' )
   #
   # ok
   check = -ax.data[1,0]
   if abs( ax.grad[0,1] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[0,1] = {ax.grad[0,1]}, check = {check}' )
   #
   # ok
   check = -ax.data[0,1]
   if abs( ax.grad[1,0] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[1,0] = {ax.grad[1,0]}, check = {check}' )
   #
   # ok
   check = ax.data[0,0]
   if abs( ax.grad[1,1] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[1,1] = {ax.grad[1,1]}, check = {check}' )
   #
   return ok
#
# main
def main() :
   #
   # ok
   ok = True
   #
   # n
   n = 2
   #
   # ax
   x  = numpy.random.uniform(0.0, 1.0, (n , n) )
   ax = torch.tensor(x, requires_grad = True)
   #
   # az
   az  = ax[0,0] * ax[1,1] - ax[0,1] * ax[1,0]
   #
   # ax.grad
   az.backward(retain_graph = True)
   #
   # check_grad
   if check_grad(ax) :
      print( 'First gradient passed check.' )
   else :
      print( 'First gradient failed check.' )
   #
   #
   # ax.data
   x  = numpy.random.uniform(0.0, 1.0, (n, n) )
   for i in range(n) :
      for j in range(n) :
         ax.data[i,j] = x[i,j]
   #
   # ax.grad
   ax.grad.zero_()
   az.backward(retain_graph = True)
   #
   # check_grad
   if check_grad(ax) :
      print( 'Second gradient passed check.' )
   else :
      print( 'Second gradient failed check.' )
#
main()
</code></pre>
<p>det_33.py:</p>
<pre><code class="lang-auto"># Test reusing graph for derivatives of determinant of 3 by 3 matrix.
# The output generated by this program is below. The actual numbers
# in the output will vary becasue a different random matrix is chosen
# for each evaluation.
#
# First gradient passed check.
# ax.grad[0,0] = 0.07585514040844837, check = 0.4295608074373773
# ax.grad[0,1] = -0.6133183512861293, check = -0.11782369019260797
# ax.grad[0,2] = 0.5337097801031835, check = 0.040633019648616306
# Second gradient failed check.
#
#
# imports
import torch
import numpy
#
# check_grad
def check_grad(ax) :
   # ok, eps99
   ok    = True
   eps99 = 99.0 * numpy.finfo(float).eps
   #
   # ok
   check = ( ax[1,1] * ax[2,2] - ax[1,2] * ax[2,1] )
   if abs( ax.grad[0,0] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[0,0] = {ax.grad[0,0]}, check = {check}' )
   #
   # ok
   check = - ( ax[1,0] * ax[2,2] - ax[1,2] * ax[2,0] )
   if abs( ax.grad[0,1] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[0,1] = {ax.grad[0,1]}, check = {check}' )
   #
   # ok
   check = ( ax[1,0] * ax[2,1] - ax[1,1] * ax[2,0] )
   if abs( ax.grad[0,2] - check ) &gt; eps99 :
      ok = False
      print( f'ax.grad[0,2] = {ax.grad[0,2]}, check = {check}' )
   #
   return ok
#
# main
def main() :
   #
   # ok
   ok = True
   #
   # n
   n = 3
   #
   # ax
   x  = numpy.random.uniform(0.0, 1.0, (n, n))
   ax = torch.tensor(x, requires_grad = True)
   #
   #  ax[0,0]  ax[0,1]  ax[0,2]
   #  ax[1,0]  ax[1,1]  ax[1,2]
   #  ax[2,0]  ax[2,1]  ax[2,2]
   #
   # az
   az  = ax[0,0] * ( ax[1,1] * ax[2,2] - ax[1,2] * ax[2,1] )
   az -= ax[0,1] * ( ax[1,0] * ax[2,2] - ax[1,2] * ax[2,0] )
   az += ax[0,2] * ( ax[1,0] * ax[2,1] - ax[1,1] * ax[2,0] )
   #
   #
   # ax.grad
   az.backward(retain_graph = True)
   #
   # check_grad
   if check_grad(ax) :
      print( 'First gradient passed check.' )
   else :
      print( 'First gradient failed check.' )
   #
   #
   # ax.data
   x  = numpy.random.uniform(0.0, 1.0, (n, n) )
   for i in range(n) :
      for j in range(n) :
         ax.data[i,j] = x[i,j]
   #
   # ax.grad
   ax.grad.zero_()
   az.backward(retain_graph = True)
   #
   # check_grad
   if check_grad(ax) :
      print( 'Second gradient passed check.' )
   else :
      print( 'Second gradient failed check.' )
#
main()
</code></pre>

Luigi: <p>Hi! <img src="https://discuss.pytorch.org/images/emoji/apple/slightly_smiling_face.png?v=12" title=":slightly_smiling_face:" class="emoji" alt=":slightly_smiling_face:" loading="lazy" width="20" height="20"></p>
<p>I’m using <code>torch.autograd.functional.jacobian</code> to include the gradient of the outputs wrt the inputs as a loss for the training of a NN.</p>
<p>However, once the gradient is computed and added to the <code>loss</code>, its graph is freed. As a result <code>loss.backward()</code> cannot flow inside the gradient-based loss.</p>
<p>Is there a solution for that, e.g. setting in any way <code>retain_graph=True</code> for <code>torch.autograd.functional.jacobian</code> ?</p>
<p>Thanks in advance for helping!</p>

albanD: <p>Hey<br>
You’re looking for the “create_graph=True” flag I think</p>