English · Español
Lab 00 — Parameter and Module skeleton¶
Goal: implement
Parameter(a 2-line subclass ofTensor) andModule(the ~25-line base class with__setattr__registration, recursiveparameters(),zero_grad,state_dict,load_state_dict, and__repr__). Getmypy --strictclean. Write the structural tests that prove the registration mechanic works.Estimated time: 90–120 minutes.
Prereqs: Phase 8 closed (
minitorch.Tensoravailable). Theory 00 + 01 read.
What you produce¶
A new module src/minimodel/nn/ with:
__init__.pyre-exportingParameter,Module.module.pycontaining both classes.
And tests/test_module_skeleton.py with the registration mechanic tests.
🇪🇸 Toda la fase 9 descansa sobre este lab. Si
Moduleno descubre los parámetros correctamente, niLinearniSequentialni los optimizadores funcionan. Saca 90 minutos para que pase limpio antes de pasar a Lab 01.
TODOs¶
Block A — Parameter¶
In src/minimodel/nn/module.py:
- Define
Parameter(Tensor): __init__(self, data, requires_grad: bool = True)— callsuper().__init__(data, requires_grad=requires_grad).- Nothing else.
Block B — Module base class¶
- Define
Module: __init__(self) -> None:- Use
object.__setattr__(self, "_parameters", {})andobject.__setattr__(self, "_modules", {}). - Also
object.__setattr__(self, "training", True).
- Use
__setattr__(self, name, value) -> None:- Pop
namefrom both_parametersand_modules(handle the re-assignment case). - If
isinstance(value, Parameter): register in_parameters[name]. - Elif
isinstance(value, Module): register in_modules[name]. - In all cases, also
object.__setattr__(self, name, value).
- Pop
parameters(self) -> Iterator[Parameter]:- Yield from
_parameters.values(). - For each submodule in
_modules.values(), recursivelyyield from submodule.parameters().
- Yield from
zero_grad(self) -> None:- For each
pinself.parameters(), setp.grad = None.
- For each
state_dict(self, prefix: str = "") -> dict[str, np.ndarray]:- Flat dict; keys are dot-separated paths.
- Recursively walk
_modules.
load_state_dict(self, state, prefix: str = "") -> None:- In-place copy
state[key]into the matchingParameter.data. - Recursively walk
_modules.
- In-place copy
train(self, mode: bool = True) -> Noneandeval(self) -> None: setself.training; stub for Phase 9 (no-op semantically, but the attribute must exist).forward(self, *args, **kwargs) -> Any: raiseNotImplementedError.__call__(self, *args, **kwargs): dispatch toself.forward(*args, **kwargs).__repr__(self) -> str: emit a tree-style print of submodules and parameter shapes.
Tests¶
In tests/test_module_skeleton.py:
Block C — basic registration¶
-
test_parameter_is_tensor: -
test_module_init_no_parameters: -
test_single_parameter_registered: -
test_submodule_registered:(Note: yield order isclass Inner(Module): def __init__(self): super().__init__() self.w = Parameter(np.zeros(3)) class Outer(Module): def __init__(self): super().__init__() self.inner = Inner() self.b = Parameter(np.zeros(1)) m = Outer() params = list(m.parameters()) assert len(params) == 2 assert params[0] is m.b # direct Parameter first assert params[1] is m.inner.w # then submodule's parameter_parametersthen_modules. Lock this convention.)
Block D — re-assignment / overwrite¶
-
test_parameter_reassign_replaces_registration: -
test_parameter_overwritten_with_module: Setself.w = Parameter(...), thenself.w = Linear(2, 3)(you can use a temp stub class for Linear here — Lab 01 builds the real one). Verify_parametersno longer has"w"and_modulesdoes. -
test_parameter_overwritten_with_none: Setself.w = Parameter(...), thenself.w = None. Verify_parametersno longer has"w", andself.w is None.
Block E — zero_grad, state_dict, load_state_dict¶
-
test_zero_grad_clears_grads: Construct a module with 2 parameters. Manually assignp.grad = np.ones_like(p.data). Callm.zero_grad(). Assert all parametergrads areNone. -
test_state_dict_keys: -
test_load_state_dict_roundtrip: m1 = Outer(). Capturestate = m1.state_dict().m2 = Outer()(different random init).m2.load_state_dict(state).- Assert
m1.b.data is not m2.b.data(different objects). - Assert
np.array_equal(m1.b.data, m2.b.data)(same values).
Block F — edge cases¶
-
Document this as expected behavior (tied embeddings). Lab 02 reflection asks whether to deduplicate.test_shared_parameter_yields_twice: Two attributes pointing at the sameParameter: -
test_repr_does_not_crash:repr(M())returns a string and doesn't recurse forever. -
test_call_dispatches_to_forward: SubclassModule, overrideforward(self, x)to returnx * 2. Verifym(5) == 10. -
test_forward_not_implemented_raises:Module()(any_arg)should raiseNotImplementedError.
Block G — mypy --strict clean¶
- No
Anytypes except where they appear in the upstreamTensorAPI. -
parametersis typedIterator[Parameter]. -
state_dictis typeddict[str, np.ndarray]. -
forwardis typedAny(placeholder; subclasses narrow).
Constraints¶
- No
Linear, noSequential, no activations. Those are Lab 01. Moduleis the only abstract base. Don't introduceLayer,Container,LossModuleetc. KISS — Phase 10+ may refactor if needed.- No PyTorch in
src/minimodel/. PyTorch lives intests/only (some integration tests in Lab 02 use it). __init_subclass__is forbidden. The framework cleverness is limited to__setattr__. No metaclasses, no class decorators.
Pitfalls¶
__setattr__recursion. IfModule.__setattr__writes viaself.X = value(regular assignment), it triggers itself. Always useobject.__setattr__(self, name, value)for the actual storage.super().__init__()forgotten. Test it by constructing a subclass that forgets to call super, then assigning a Parameter — should raiseAttributeErrorbecause_parametersdoesn't exist. Document the expected error.parameters()returns a generator, not a list. Consumers must convert (list(model.parameters())) if they need to iterate twice. The optimizer base class does the conversion internally.state_dictkey collisions. If two submodules have the same name (shouldn't happen but could via weird subclassing), the second overwrites the first. Add a test (test_state_dict_no_collision) that asserts unique keys.
Stop conditions¶
Done when:
Parameteris a 2-line subclass.Moduleis ≤ 60 lines including type hints and the repr.- All 12+ tests above are green.
mypy --strict src/minimodel/nn/module.pyclean.ruff check src/minimodel/nn/module.pyclean.- You can explain why we use
object.__setattr__in__init__(not in__setattr__body).
When to consult solutions/¶
After all tests pass. solutions/00-parameter-and-module-skeleton-ref.md (at phase open) compares your Module against the canonical implementation and points out where the cleverness can be even shorter (or longer, with better error messages).
Next lab: lab/01-linear-and-activations.md.