Hi, @likethevegetable
I tired a snippet as follows:
random_input = torch.randn(3,3)
random_output = torch.randn(3,3)
criterion = nn.MSELoss()
# model
l1 = nn.Linear(3, 10, bias=False)
l2 = nn.Linear(10, 3, bias=False)
intermediate = l1(random_input)
# intermediate.requires_grad=False  RuntimeError
# intermediate = intermediate.detach()
output = l2(intermediate)

loss = criterion(output ,random_output)
loss.backward()

I think what you confused is about intermediate result and l1.weight. requires_grad=False is set to l1.weight and what the chain rule needs is the intermediate result.
And if you want to set requires_grad=False to intermediate, it will raise an error
 RuntimeError: you can only change requires_grad flags of leaf variables. If you want to use a computed variable in a subgraph that doesn't require differentiation use var_no_grad = var.detach().

So if we want to change requires_grad flags of intermediate variables, we should use .detach(), and then the related sub_graph will not have gradients flow back.
Using the snippet above, it will display l1.weight.grad = None and l2.weight.grad computed normally.