Yes you got it right.
Now what is “intermediate” is a whole other can of worms 
Here again, for memory optimization, we only keep what is strictly necessary. And so every Function is responsible to save only what it needs.
This means that there is not general rule of what is an intermediate value, it is dependent on the Function itself.
If you take the multiplication as an example, the definition of it’s derivative is given here 4 as:
- name: mul(Tensor self, Tensor other) -> Tensor
  self: grad * other
  other: grad * self

This means that the gradient for self is given by grad * other and the gradient for other is given by grad * self.
As you can see, if the gradient for self is needed, only other is required for the backward and only this will be saved.
A hacky way (works in this example, might not work all the time) to see if something is needed for backward is to modify it inplace and see if the backward fails:
import torch

self = torch.rand(10, requires_grad=True).clone() # This one requires gradients
# We do a clone to be allowed to do inplace ops on it later
other = torch.rand(10) # This one does NOT requires gradients

# Backward works
output = self * other
output.backward(torch.ones_like(output))

# Here we only ask for gradients for self. So self should not be saved:
output = self * other
self += 1
output.backward(torch.ones_like(output)) # Works !

# But we need the value of other:
output = self * other
other += 1
output.backward(torch.ones_like(output)) # Fails !