English · Español
Lab 00 — Tensor skeleton: the class, no ops yet¶
Goal: create the
Tensorclass insrc/minitorch/tensor.pywith all the structural pieces (data,grad,_prev,_op,_backward,requires_grad,backward()) wired up — but with no operations defined yet. Confirm that callingbackward()on a "leaf"Tensoris a no-op, that topo sort handles a hand-constructed DAG, and that the class passesmypy --strict.Estimated time: 60–90 minutes.
Prereqs:
src/minigrad/scalar.pyfrom Phase 7.src/minitorch/BLUEPRINT.mdreviewed.
What you produce¶
A new module src/minitorch/ with:
__init__.pyre-exportingTensor.tensor.pycontaining theTensorclass skeleton.- A unit test
tests/test_tensor_skeleton.pyproving the structural contract.
No numpy operations beyond construction. No _backward closures for any specific op. The skeleton is "what Tensor looks like before we teach it any math".
🇪🇸 Pista: la fase 7 fue
Value. La fase 8 esTensor. La diferencia visible es trivial: cambiamosfloatpornp.ndarray. La diferencia oculta — broadcasting reverso, ops por familias — la abriremos en los siguientes labs.
TODOs¶
Block A — module boilerplate¶
In src/minitorch/:
- Create
__init__.pywithfrom .tensor import Tensor. - Create
tensor.pywith:
from __future__ import annotations
from collections.abc import Callable
import numpy as np
import numpy.typing as npt
ArrayLike = npt.ArrayLike
FloatArray = npt.NDArray[np.floating]
class Tensor:
"""Autograd-enabled tensor. See docs/phase-08-tensor-autograd/theory/."""
data: FloatArray
grad: FloatArray | None
_prev: tuple[Tensor, ...]
_op: str
_backward: Callable[[], None]
requires_grad: bool
def __init__(
self,
data: ArrayLike,
*,
requires_grad: bool = False,
_prev: tuple[Tensor, ...] = (),
_op: str = "",
) -> None: ...
def backward(self) -> None: ...
def zero_grad(self) -> None: ...
@property
def shape(self) -> tuple[int, ...]: ...
def __repr__(self) -> str: ...
- Implement
__init__to: - Convert
datato a NumPy array vianp.asarray(data, dtype=np.float64)(FP64 default for Phase 8; reconsider in Phase 26). - Initialise
grad = None(allocated lazily, the first time backward touches it). - Store
_prev,_op,_backward = lambda: None. - Store
requires_grad. - Implement
backward()to: - Assert
self.datais a scalar (self.data.shape == ()); otherwise raise. (Backward only makes sense from a scalar root in Phase 8.) - Topo-sort the DAG via DFS over
_prev. - Set
self.grad = np.array(1.0). - For each node in reverse topological order, call
node._backward(). - Implement
zero_grad()to setself.grad = None(ornp.zeros_like(self.data)— pick and document). - Implement
__repr__to print"Tensor(shape=..., data=..., grad=..., op=...)"truncated for large arrays.
Block B — structural unit test¶
In tests/test_tensor_skeleton.py:
-
test_leaf_construction:t = Tensor([1.0, 2.0, 3.0])produces a Tensor withshape==(3,),_prev==(),_op=="",requires_grad==False. -
test_grad_is_lazy: A freshly constructedTensorhasgrad is None. -
test_leaf_backward_noop:t = Tensor(5.0); t.backward()does not raise andt.grad == 1.0. -
test_topo_handles_diamond: Manually construct a small DAG by setting_prevdirectly (no ops yet):Assert the topo order visitsa = Tensor(1.0) b = Tensor(2.0, _prev=(a,), _op="dummy") c = Tensor(3.0, _prev=(a,), _op="dummy") d = Tensor(4.0, _prev=(b, c), _op="dummy") d.backward()alast (after bothbandc). Use a side-effect closure in each node's_backwardto record visit order. -
test_repr_does_not_crash:repr(Tensor(np.zeros((100, 100))))returns a string and doesn't print 10,000 floats. -
test_mypy_strict: not a test method per se; verify viajust mypyin CI.
Block C — sanity check vs Value¶
- Look at your Phase 7
scalar.py. Side-by-side, confirm the class skeletons are isomorphic:datais the only field whose type differs (floatvsnp.ndarray), andbackward()has the same topo + reverse-traverse structure. If you find a non-trivial structural difference, you've probably already drifted — restart the diff comparison until it's clean.
Constraints¶
- No ops yet. Don't define
__add__,__mul__, etc. Those are Lab 01. - No
_backwardfor any op. The skeleton's only_backwardis the defaultlambda: None. mypy --strictmust pass. Usenumpy.typingfor array hints. Ifnumpy.typingraises errors, add a# type: ignore[name-of-error]with a TODO and move on — track inPHASE_08_PLAN.mdrevisions.- No PyTorch imports in
src/minitorch/. PyTorch is test-only.
Pitfalls¶
gradinitialised to a zero array always. Wastes memory; allocates(B, V)arrays for tensors that will never backprop. LazyNone+ first-touch allocate is the right pattern.backward()not asserting scalar root. Callingbackward()on a non-scalar Tensor without an upstream grad is a bug we want to catch loudly. Assert, don't guess.- DFS topo with
set()ofTensorobjects. Tensors need to be hashable by identity. The default__hash__works. Do not override__eq__(np.allclose(t1.data, t2.data)) — it breaks hashability. - Recursive DFS exceeding Python's limit. For ~1000 nodes you're fine. Phase 18's training loop may push this; if so, switch to iterative DFS then. Note in BLUEPRINT.
Stop conditions¶
Done when:
src/minitorch/tensor.pyexists with the full skeleton.tests/test_tensor_skeleton.pyhas 5 tests, all green.mypy --strict src/minitorch/is clean.ruff check src/minitorch/is clean.- You can explain why
gradisNone | FloatArrayand not justFloatArray.
When to consult solutions/¶
After all five tests pass. solutions/00-tensor-skeleton-ref.md (at phase open) compares your class to the reference shape.
Next lab: lab/01-elementwise-ops.md.