Good experiment. Now it proves that requires_grad=False will calculate gradients with respect to that parameter (layer2.weight) but will not set gradient on it, so that gradient can still flow to layer1, but layer2 is not affected.
There is one tiny error in your comment:
-# gradients of backward_hook  ((gradient for W, gradient for x), gradient for y)
+# gradients of backward_hook  ((gradient for x, gradient for W), gradient for y)

And @googlebot detach() is definetely inappropriate here, since it will detach the whole remaining chain from backward computation completely, so layer1 will not receive any gradients:
import torch.nn as nn
import torch.optim as optim
import torch

class Demo(nn.Module):

    def __init__(self):
        super(Demo, self).__init__()
        self.layer1 = nn.Linear(1, 1, bias=False)
        self.layer2 = nn.Linear(1, 1, bias=False)
        self.layer3 = nn.Linear(1, 1, bias=False)

        self.layer1.weight = nn.Parameter(torch.ones([1, 1]).float())
        self.layer2.weight = nn.Parameter(torch.ones([1, 1]).float() * 2)
        self.layer3.weight = nn.Parameter(torch.ones([1, 1]).float() * 3)

        # self.layer1.register_forward_hook(lambda _, x_in, x_out: print('layer1', x_in, x_out))
        # self.layer2.register_forward_hook(lambda _, x_in, x_out: print('layer2', x_in, x_out))
        # self.layer3.register_forward_hook(lambda _, x_in, x_out: print('layer3', x_in, x_out))

        self.layer1.register_backward_hook(lambda _, grad_in, grad_out: print('layer1', grad_in, grad_out))
        self.layer2.register_backward_hook(lambda _, grad_in, grad_out: print('layer2', grad_in, grad_out))
        self.layer3.register_backward_hook(lambda _, grad_in, grad_out: print('layer3', grad_in, grad_out))

        # self.layer2.weight.requires_grad = False

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out).detach()
        out = self.layer3(out)

        return out


if __name__ == '__main__':
    model = Demo()
    optimizer = optim.SGD(params=model.parameters(), lr=0.1)
    optimizer.zero_grad()

    print(model.layer1.weight, model.layer2.weight, model.layer3.weight)
    # for i in range(10):
    x = torch.ones((1, 1))
    out = model(x)
    out.backward()
    optimizer.step()
    optimizer.zero_grad()
    print(model.layer1.weight, model.layer2.weight, model.layer3.weight)

will print:
Parameter containing:
tensor([[1.]], requires_grad=True) 
Parameter containing:
tensor([[2.]], requires_grad=True) 
Parameter containing:
tensor([[3.]], requires_grad=True)
layer3 (None, tensor([[2.]])) (tensor([[1.]]),)
Parameter containing:
tensor([[1.]], requires_grad=True) 
Parameter containing:
tensor([[2.]], requires_grad=True) 
Parameter containing:
tensor([[2.8000]], requires_grad=True)

gradient of layer1 is not calculated at all. therefore, requires_grad=True is a correct solution.