Both examples should work with a single optimizer*.
Basically, every tensor stores some information about how to calculate the gradient, and the gradient. The gradient is (when initialized), the same shape but full of 0s. When you do backward, this info is used to calculate the gradients. These gradients are added to each tensor’s .grad. When you do step, the optimizer updates the weights based on the gradients. When you do zero_grad, the gradients are set to 0.
Most optimizers store extra data (eg momentum of the weights). However, this is the same size as the weights, and so is independent of the batch size.
*If you have two different models or parts of a model that you train separately, it makes sense to create two different optimizers. As long as you provide the correct parameters to the optimizer, this will work.