I just took a look at the implementation of torch.autograd.functional.jacobian 8 and as it appears it does the following (simplified for single input and output tensor):
input.requires_grad = True
output = func(input)
jac_rows = []
for o in output:
    input.grad.zero_()
    o.backward(retain_graph=True)
    jac_rows.append(input.grad.data)
jac = torch.stack(jac_rows)

So if we have 1d-input and -output tensors of shape (N,) and (M,) respectively then this performs M backward passes through the graph. In order to estimate the Jacobian one could also use Finite Difference Approximation 39 which requires N forward passes instead. One can also assume that the cost of a forward and backward pass are roughly the same.
Now the thing is, I’m using this jacobian function specifically because I want to use the Levenberg-Marquardt algorithm 8 to fit a small network (~ 100 parameters) which is more efficient than other gradient-based optimizers. The Levenberg-Marquardt algorithm requires that M >= N and hence this backward Jacobian approach is less efficient than forward difference approximation (M vs. N steps). Now I’m wondering why this implementation has been chosen for jacobian and if there’s a more efficient way. After all there must be a reason to use jacobian rather than just one of the available optimizers. If the reason is to implement a custom optimizer using Jacobian then most likely M > N and the implementation is inefficient (e.g. in one of my cases I had N = 84, M = 576 so using Autograd Jacobian is about 7 times slower than Finite Difference Approximation).
Also it seems strange to compute the backward pass separately for each of the output tensor’s elements. This is like using an explicit for loop over the batch dimension for the forward pass (which we don’t do of course). The forward pass uses all parallelization capabilities for the batch dimension, so I’m wondering whether something similar can be used for the backward pass and Jacobian, i.e. performing all the computations for output at once.