Skip to content

English · Español

Lab 00 — Tensor skeleton: the class, no ops yet

Goal: create the Tensor class in src/minitorch/tensor.py with all the structural pieces (data, grad, _prev, _op, _backward, requires_grad, backward()) wired up — but with no operations defined yet. Confirm that calling backward() on a "leaf" Tensor is a no-op, that topo sort handles a hand-constructed DAG, and that the class passes mypy --strict.

Estimated time: 60–90 minutes.

Prereqs: src/minigrad/scalar.py from Phase 7. src/minitorch/BLUEPRINT.md reviewed.


What you produce

A new module src/minitorch/ with:

  • __init__.py re-exporting Tensor.
  • tensor.py containing the Tensor class skeleton.
  • A unit test tests/test_tensor_skeleton.py proving 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 es Tensor. La diferencia visible es trivial: cambiamos float por np.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__.py with from .tensor import Tensor.
  • Create tensor.py with:
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 data to a NumPy array via np.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.data is 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 set self.grad = None (or np.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 with shape==(3,), _prev==(), _op=="", requires_grad==False.
  • test_grad_is_lazy: A freshly constructed Tensor has grad is None.
  • test_leaf_backward_noop: t = Tensor(5.0); t.backward() does not raise and t.grad == 1.0.
  • test_topo_handles_diamond: Manually construct a small DAG by setting _prev directly (no ops yet):
    a = 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()
    
    Assert the topo order visits a last (after both b and c). Use a side-effect closure in each node's _backward to 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 via just mypy in CI.

Block C — sanity check vs Value

  • Look at your Phase 7 scalar.py. Side-by-side, confirm the class skeletons are isomorphic: data is the only field whose type differs (float vs np.ndarray), and backward() 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 _backward for any op. The skeleton's only _backward is the default lambda: None.
  • mypy --strict must pass. Use numpy.typing for array hints. If numpy.typing raises errors, add a # type: ignore[name-of-error] with a TODO and move on — track in PHASE_08_PLAN.md revisions.
  • No PyTorch imports in src/minitorch/. PyTorch is test-only.

Pitfalls

  • grad initialised to a zero array always. Wastes memory; allocates (B, V) arrays for tensors that will never backprop. Lazy None + first-touch allocate is the right pattern.
  • backward() not asserting scalar root. Calling backward() 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() of Tensor objects. 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:

  1. src/minitorch/tensor.py exists with the full skeleton.
  2. tests/test_tensor_skeleton.py has 5 tests, all green.
  3. mypy --strict src/minitorch/ is clean.
  4. ruff check src/minitorch/ is clean.
  5. You can explain why grad is None | FloatArray and not just FloatArray.

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.