Core API
The core package contains the tensor object and operation base class used by
every differentiable computation.
Variable
Variable wraps a NumPy array, stores an optional accumulated gradient, and
points to the Function that produced it.
import numpy as np
from MiniTorch import Variable
x = Variable(np.array([1.0, 2.0], dtype=np.float32), name="x")
loss = (x ** 2).sum()
loss.backward()
Note
MiniTorch gradients are themselves Variable objects. Read the raw array
through x.grad.data.
Variable
Variable(data: Optional[ndarray], name: Optional[str] = None)
Source code in MiniTorch/core/variable.py
| def __init__(self, data: Optional[np.ndarray], name: Optional[str] = None) -> None:
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported")
self.data: Optional[np.ndarray] = data
self.name: Optional[str] = name
self.grad: Optional[Variable] = None
self.creator: Optional[Function] = None
self.generation: int = 0
|
backward
backward(retain_grad: bool = False, create_graph: bool = False) -> None
Source code in MiniTorch/core/variable.py
| def backward(self, retain_grad: bool = False, create_graph: bool = False) -> None:
if not Config.enable_backprob:
raise RuntimeError("backward() is not allowed in no_grad mode")
if self.grad is None:
self.grad = Variable(np.ones_like(self.data))
if self.creator is None:
return
# Build a compact topological tape in O(functions + edges). Forward
# execution already defines a DAG, so backward can simply replay this
# tape in reverse instead of repeatedly sorting a ready list.
tape: list[Function] = []
discovered: set[Function] = set()
stack: list[tuple[Function, bool]] = [(self.creator, False)]
while stack:
func, expanded = stack.pop()
if expanded:
tape.append(func)
continue
if func in discovered:
continue
discovered.add(func)
stack.append((func, True))
if func.inputs is not None:
for input_var in func.inputs:
if input_var.creator is not None and input_var.creator not in discovered:
stack.append((input_var.creator, False))
for func in reversed(tape):
if func.outputs is None or func.inputs is None:
continue
outputs = [output_ref() for output_ref in func.outputs]
if create_graph:
output_grads = [
output.grad if output is not None else None
for output in outputs
]
with using_config("enable_backprob", True):
input_grads = func.backward(*output_grads)
if not isinstance(input_grads, tuple):
input_grads = (input_grads,)
for input_var, grad in zip(func.inputs, input_grads):
if grad is None:
continue
input_var.grad = grad if input_var.grad is None else input_var.grad + grad
else:
output_grad_arrays = [
output.grad.data
if output is not None and output.grad is not None
else None
for output in outputs
]
array_backward = getattr(func, "backward_array", None)
try:
if array_backward is None:
raise NotImplementedError
input_grad_arrays = array_backward(*output_grad_arrays)
except NotImplementedError:
# Compatibility path for third-party operations that have
# not implemented the raw-array training API yet.
output_grads = [
output.grad if output is not None else None
for output in outputs
]
with using_config("enable_backprob", False):
legacy_grads = func.backward(*output_grads)
if not isinstance(legacy_grads, tuple):
legacy_grads = (legacy_grads,)
input_grad_arrays = tuple(
grad.data if isinstance(grad, Variable) else grad
for grad in legacy_grads
)
if not isinstance(input_grad_arrays, tuple):
input_grad_arrays = (input_grad_arrays,)
for input_var, grad_array in zip(func.inputs, input_grad_arrays):
if grad_array is None:
continue
grad_array = np.asarray(grad_array)
if input_var.grad is None:
input_var.grad = Variable(grad_array)
else:
# Allocate the accumulation result instead of mutating
# in place: some derivative rules intentionally return
# aliased upstream gradients.
input_var.grad.data = np.asarray(
input_var.grad.data + grad_array
)
if not retain_grad and not create_graph:
for output in outputs:
if output is not None and output is not self:
output.grad = None
|
clear_grad
Source code in MiniTorch/core/variable.py
| def clear_grad(self) -> None:
self.grad = None
|
reshape
reshape(*shape: Any) -> Variable
Source code in MiniTorch/core/variable.py
| def reshape(self, *shape: Any) -> Variable:
resolved: Any = shape[0] if (len(shape) == 1 and isinstance(shape[0], (tuple, list))) else shape
from MiniTorch.ops.reshape import reshape
return reshape(self, resolved)
|
Function
Subclass Function to implement a differentiable operation. The ordinary
first-order path can provide backward_array to propagate raw NumPy arrays
without building another autograd graph.
Function
Base class for all differentiable operations.
Source code in MiniTorch/core/function.py
| def __init__(self) -> None:
self.inputs: Optional[list[Variable]] = None
self.outputs: Optional[list[Any]] = None
self.generation: int = 0
|
forward
forward(*xs: ndarray) -> np.ndarray | tuple[np.ndarray, ...]
Source code in MiniTorch/core/function.py
| def forward(self, *xs: np.ndarray) -> np.ndarray | tuple[np.ndarray, ...]:
raise NotImplementedError
|
backward
backward(*gys: Optional[Variable]) -> Any
Source code in MiniTorch/core/function.py
| def backward(self, *gys: Optional[Variable]) -> Any:
raise NotImplementedError
|
input_data(index: int) -> np.ndarray
Return a validated saved input for a raw NumPy backward kernel.
Source code in MiniTorch/core/function.py
| def input_data(self, index: int) -> np.ndarray:
"""Return a validated saved input for a raw NumPy backward kernel."""
if self.inputs is None:
raise RuntimeError("operation input data is no longer available")
data = self.inputs[index].data
if data is None:
raise RuntimeError("operation input data is no longer available")
return data
|
output_data
output_data(index: int) -> np.ndarray
Return a validated output for a raw NumPy backward kernel.
Source code in MiniTorch/core/function.py
| def output_data(self, index: int) -> np.ndarray:
"""Return a validated output for a raw NumPy backward kernel."""
if self.outputs is None:
raise RuntimeError("operation output data is no longer available")
output = self.outputs[index]()
if output is None or output.data is None:
raise RuntimeError("operation output data is no longer available")
return output.data
|
Gradient configuration
no_grad
Source code in MiniTorch/core/config.py
| def no_grad():
return using_config("enable_backprob", False)
|
with_grad
Source code in MiniTorch/core/config.py
| def with_grad():
return using_config("enable_backprob", True)
|