Skip to content

← nn.Module Mechanics step 1 of 7

Medium Primitives

The layers the optimiser never saw

class Net(nn.Module):
    def __init__(self, depth):
        super().__init__()
        self.layers = [nn.Linear(4, 4) for _ in range(depth)]
len(list(Net(3).parameters()))      # 0

Not six. Zero.

Why

nn.Module overrides __setattr__. When you assign a Parameter or a Module to an attribute, it is intercepted and put into the module’s registry. That registry is what parameters(), state_dict(), .to() and train() all walk.

A plain Python list is not a Module, so the assignment is ordinary. The list holds the layers, the layers hold their parameters, and the module knows about none of it.

What that costs

Everything, quietly:

  • model.parameters() is empty, so the optimiser is constructed over nothing and step() updates nothing. Training runs. The loss does not move.
  • state_dict() is empty, so checkpoints save nothing.
  • model.to(device) moves nothing.
  • model.eval() does not reach the layers, so dropout stays on during evaluation.

The forward pass works perfectly, which is what makes this hard to spot. Most people find it when the loss is flat and they check whether the optimiser has any parameters at all.

The containers

nn.ModuleList([...])           # a list, registered
nn.ModuleDict({...})           # a dict, registered
nn.Sequential(*layers)         # a list that also defines forward
nn.ParameterList([...])        # for bare Parameters

ModuleList holds modules and you write the loop yourself. Sequential calls them in order for you, which is right when the forward really is just “apply each in turn” and wrong as soon as there is a skip connection.

The same trap, one level down

self.weights = [torch.nn.Parameter(...) for _ in range(n)]     # invisible
self.weights = nn.ParameterList([...])                          # registered

and the buffer version of it, which the devices track covered. The rule is the same every time: if the module should know about it, assign something the module can recognise.

Your task

Fix Net so its layers are registered, then:

def net_facts(depth: int) -> dict

builds a Net(depth) and reports the number of parameter tensors, the number of state_dict entries, and the output length of a forward pass on a 4-element input.