But, how we calculate the gradients for non-leaf variables?

You don’t need it because all the nn.Parameter() are leafs. So .backward() will always compute their gradient. And so the optimizer will always have the .grad to work with.

So, in no_grad mode, .backward() and optimizer.step() will have no effect?

Since the Tensor have requires_grad=False, you cannot call .backward() on it (you will get an error).
You can still do optimizer.step() (remember this is a nn construct, unrelated to the autograd) and it will perform its update based on the current .grad (these can be None if no .backward() was called before).
Side note, for some optimizers (like Adam or SGD with momentum or SGD with weight decay), even with a gradient of 0, the optimizer step will still change the weights.