mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-13 07:53:45 +08:00
22a8575199
Keep official openpilot ancestry and dependency licenses. Store resources directly in Git, remove LFS setup requirements, and leave the downloadable big model outside the source tree.
421 lines
18 KiB
Python
421 lines
18 KiB
Python
from __future__ import annotations
|
|
import math, functools
|
|
from tinygrad.tensor import Tensor
|
|
from tinygrad.dtype import dtypes
|
|
from tinygrad.helpers import prod, make_tuple, flatten, USE_ATOMICS, TRAINING
|
|
from tinygrad.nn import optim, state, datasets # noqa: F401
|
|
|
|
class BatchNorm:
|
|
"""
|
|
Applies Batch Normalization over a 2D or 3D input.
|
|
|
|
- Paper: https://arxiv.org/abs/1502.03167v3
|
|
|
|
See: `Tensor.batchnorm`
|
|
|
|
```python exec="true" session="tensor"
|
|
from tinygrad import Tensor, dtypes, nn
|
|
import numpy as np
|
|
np.set_printoptions(precision=4)
|
|
```
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.BatchNorm(3)
|
|
t = Tensor.rand(2, 3, 4, 4)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = norm(t)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
"""
|
|
def __init__(self, sz:int, eps=1e-5, affine=True, track_running_stats=True, momentum=0.1):
|
|
self.eps, self.track_running_stats, self.momentum = eps, track_running_stats, momentum
|
|
|
|
self.weight: Tensor|None = Tensor.ones(sz) if affine else None
|
|
self.bias: Tensor|None = Tensor.zeros(sz) if affine else None
|
|
|
|
self.num_batches_tracked = Tensor.zeros(dtype='long').is_param_(False)
|
|
if track_running_stats: self.running_mean, self.running_var = Tensor.zeros(sz).is_param_(False), Tensor.ones(sz).is_param_(False)
|
|
|
|
def calc_stats(self, x:Tensor) -> tuple[Tensor, Tensor]:
|
|
shape_mask: list[int] = [1, -1, *([1]*(x.ndim-2))]
|
|
if self.track_running_stats and not TRAINING: return self.running_mean, self.running_var.reshape(shape=shape_mask)
|
|
# This requires two full memory accesses to x
|
|
# https://github.com/pytorch/pytorch/blob/c618dc13d2aa23625cb0d7ada694137532a4fa33/aten/src/ATen/native/cuda/Normalization.cuh
|
|
# There's "online" algorithms that fix this, like https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_Online_algorithm
|
|
batch_mean = x.mean(axis=(reduce_axes:=tuple(x for x in range(x.ndim) if x != 1)))
|
|
y = (x - batch_mean.detach().reshape(shape=shape_mask)) # d(var)/d(mean) = 0
|
|
batch_var = (y*y).mean(axis=reduce_axes)
|
|
return batch_mean, batch_var
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
batch_mean, batch_var = self.calc_stats(x)
|
|
# NOTE: wow, this is done all throughout training in most PyTorch models
|
|
if self.track_running_stats and TRAINING:
|
|
self.running_mean.assign((1-self.momentum) * self.running_mean + self.momentum * batch_mean.detach())
|
|
self.running_var.assign((1-self.momentum) * self.running_var + self.momentum * x.numel()/(x.numel()-x.shape[1]) * batch_var.detach())
|
|
self.num_batches_tracked += 1
|
|
return x.batchnorm(self.weight, self.bias, batch_mean, batch_var.add(self.eps).rsqrt())
|
|
BatchNorm2d = BatchNorm3d = BatchNorm
|
|
|
|
def Conv1d(in_channels:int, out_channels:int, kernel_size:int, stride=1, padding:int|str=0, dilation=1, groups=1, bias=True) -> Conv2d:
|
|
"""
|
|
Applies a 1D convolution over an input signal composed of several input planes.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.Conv1d
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
conv = nn.Conv1d(1, 1, 3)
|
|
t = Tensor.rand(1, 1, 4)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = conv(t)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
return Conv2d(in_channels, out_channels, (kernel_size,), stride, padding, dilation, groups, bias)
|
|
|
|
class Conv2d:
|
|
"""
|
|
Applies a 2D convolution over an input signal composed of several input planes.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.Conv2d
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
conv = nn.Conv2d(1, 1, 3)
|
|
t = Tensor.rand(1, 1, 4, 4)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = conv(t)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
def __init__(self, in_channels:int, out_channels:int, kernel_size:int|tuple[int, ...], stride=1, padding:int|tuple[int, ...]|str=0,
|
|
dilation=1, groups=1, bias=True):
|
|
self.kernel_size = make_tuple(kernel_size, 2)
|
|
if isinstance(padding, str):
|
|
if padding.lower() != 'same': raise ValueError(f"Invalid padding string {padding!r}, only 'same' is supported")
|
|
if stride != 1: raise ValueError("padding='same' is not supported for strided convolutions")
|
|
pad = [(d*(k-1)//2, d*(k-1) - d*(k-1)//2) for d,k in zip(make_tuple(dilation, len(self.kernel_size))[::-1], self.kernel_size[::-1])]
|
|
padding = tuple(flatten(pad))
|
|
self.stride, self.dilation, self.groups, self.padding = stride, dilation, groups, padding
|
|
scale = 1 / math.sqrt(in_channels * prod(self.kernel_size))
|
|
self.weight = Tensor.uniform(out_channels, in_channels//groups, *self.kernel_size, low=-scale, high=scale)
|
|
self.bias: Tensor|None = Tensor.uniform(out_channels, low=-scale, high=scale) if bias else None
|
|
|
|
def __call__(self, x:Tensor) -> Tensor: return x.conv2d(self.weight, self.bias, self.groups, self.stride, self.dilation, self.padding)
|
|
|
|
def ConvTranspose1d(in_channels:int, out_channels:int, kernel_size:int, stride=1, padding=0, output_padding=0, dilation=1,
|
|
groups=1, bias=True) -> ConvTranspose2d:
|
|
"""
|
|
Applies a 1D transposed convolution operator over an input signal composed of several input planes.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose1d
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
conv = nn.ConvTranspose1d(1, 1, 3)
|
|
t = Tensor.rand(1, 1, 4)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = conv(t)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
return ConvTranspose2d(in_channels, out_channels, (kernel_size,), stride, padding, output_padding, dilation, groups, bias)
|
|
|
|
class ConvTranspose2d(Conv2d):
|
|
"""
|
|
Applies a 2D transposed convolution operator over an input image.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose2d
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
conv = nn.ConvTranspose2d(1, 1, 3)
|
|
t = Tensor.rand(1, 1, 4, 4)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = conv(t)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
def __init__(self, in_channels:int, out_channels:int, kernel_size:int|tuple[int, ...], stride=1, padding=0, output_padding=0,
|
|
dilation=1, groups=1, bias=True):
|
|
super().__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias)
|
|
scale = 1 / math.sqrt(in_channels * prod(self.kernel_size))
|
|
self.weight = Tensor.uniform(in_channels, out_channels//groups, *self.kernel_size, low=-scale, high=scale)
|
|
self.output_padding = output_padding
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
return x.conv_transpose2d(self.weight, self.bias, self.groups, self.stride, self.dilation, self.padding, self.output_padding)
|
|
|
|
class Linear:
|
|
"""
|
|
Applies a linear transformation to the incoming data.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.Linear
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
lin = nn.Linear(3, 4)
|
|
t = Tensor.rand(2, 3)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = lin(t)
|
|
print(t.numpy())
|
|
```
|
|
"""
|
|
def __init__(self, in_features:int, out_features:int, bias=True):
|
|
bound = 1 / math.sqrt(in_features)
|
|
self.weight = Tensor.uniform(out_features, in_features, low=-bound, high=bound)
|
|
self.bias = Tensor.uniform(out_features, low=-bound, high=bound) if bias else None
|
|
|
|
def __call__(self, x:Tensor) -> Tensor: return x.linear(self.weight.transpose(), self.bias)
|
|
|
|
class GroupNorm:
|
|
"""
|
|
Applies Group Normalization over a mini-batch of inputs.
|
|
|
|
- Paper: https://arxiv.org/abs/1803.08494v3
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.GroupNorm(2, 12)
|
|
t = Tensor.rand(2, 12, 4, 4) * 2 + 1
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = norm(t)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
"""
|
|
def __init__(self, num_groups:int, num_channels:int, eps=1e-5, affine=True):
|
|
self.num_groups, self.num_channels, self.eps = num_groups, num_channels, eps
|
|
self.weight: Tensor|None = Tensor.ones(num_channels) if affine else None
|
|
self.bias: Tensor|None = Tensor.zeros(num_channels) if affine else None
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
# reshape for layernorm to work as group norm
|
|
# subtract mean and divide stddev
|
|
x = x.reshape(x.shape[0], self.num_groups, -1).layernorm(eps=self.eps).reshape(x.shape)
|
|
|
|
if self.weight is None or self.bias is None: return x
|
|
# elementwise_affine on channels
|
|
return x * self.weight.reshape(1, -1, *[1] * (x.ndim-2)) + self.bias.reshape(1, -1, *[1] * (x.ndim-2))
|
|
|
|
class InstanceNorm:
|
|
"""
|
|
Applies Instance Normalization over a mini-batch of inputs.
|
|
|
|
- Paper: https://arxiv.org/abs/1607.08022v3
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.InstanceNorm(3)
|
|
t = Tensor.rand(2, 3, 4, 4) * 2 + 1
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = norm(t)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
"""
|
|
def __init__(self, num_features:int, eps:float=1e-5, affine:bool=True):
|
|
self.num_features, self.eps = num_features, eps
|
|
self.weight: Tensor|None = Tensor.ones(num_features) if affine else None
|
|
self.bias: Tensor|None = Tensor.zeros(num_features) if affine else None
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
x = x.reshape(x.shape[0], self.num_features, -1).layernorm(eps=self.eps).reshape(x.shape)
|
|
if self.weight is None or self.bias is None: return x
|
|
return x * self.weight.reshape(1, -1, *[1] * (x.ndim-2)) + self.bias.reshape(1, -1, *[1] * (x.ndim-2))
|
|
|
|
class LayerNorm:
|
|
"""
|
|
Applies Layer Normalization over a mini-batch of inputs.
|
|
|
|
- Paper: https://arxiv.org/abs/1607.06450v1
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.LayerNorm(3)
|
|
t = Tensor.rand(2, 5, 3) * 2 + 1
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = norm(t)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
"""
|
|
def __init__(self, normalized_shape:int|tuple[int, ...], eps:float=1e-5, elementwise_affine:bool=True):
|
|
self.normalized_shape: tuple[int, ...] = make_tuple(normalized_shape, 1)
|
|
self.axis, self.eps = tuple(-1-i for i in range(len(self.normalized_shape))), eps
|
|
self.weight: Tensor|None = Tensor.ones(*self.normalized_shape) if elementwise_affine else None
|
|
self.bias: Tensor|None = Tensor.zeros(*self.normalized_shape) if elementwise_affine else None
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
assert self.normalized_shape == x.shape[-len(self.normalized_shape):], f"last dimensions of {x.shape} must match {self.normalized_shape}"
|
|
x = x.layernorm(eps=self.eps, axis=self.axis)
|
|
if self.weight is None or self.bias is None: return x
|
|
return x * self.weight + self.bias
|
|
|
|
class LayerNorm2d(LayerNorm):
|
|
"""
|
|
Applies Layer Normalization over a mini-batch of 2D inputs.
|
|
|
|
See: `LayerNorm`
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.LayerNorm2d(3)
|
|
t = Tensor.rand(2, 3, 4, 4) * 2 + 1
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
t = norm(t)
|
|
print(t.mean().item(), t.std().item())
|
|
```
|
|
"""
|
|
def __call__(self, x: Tensor) -> Tensor: return super().__call__(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
|
|
|
|
class RMSNorm:
|
|
"""
|
|
Applies Root Mean Square Normalization to input.
|
|
|
|
- Paper: https://arxiv.org/abs/1910.07467
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
norm = nn.RMSNorm(4)
|
|
t = Tensor.arange(12, dtype=dtypes.float).reshape(3, 4)
|
|
print(t.numpy())
|
|
```
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
print(norm(t).numpy())
|
|
```
|
|
"""
|
|
def __init__(self, dim:int, eps=1e-6, elementwise_affine=True):
|
|
self.eps = eps
|
|
self.weight = Tensor.ones(dim) if elementwise_affine else None
|
|
|
|
def _norm(self, x:Tensor) -> Tensor: return x * (x.square().mean(-1, keepdim=True) + self.eps).rsqrt()
|
|
|
|
def __call__(self, x:Tensor) -> Tensor:
|
|
x = self._norm(x.float()).cast(x.dtype)
|
|
return x if self.weight is None else x * self.weight
|
|
|
|
from tinygrad.uop.ops import UOp, KernelInfo, Ops, AxisType
|
|
def _embedding_bwd(grad_emb:UOp, call:UOp) -> tuple:
|
|
weight, idx = (a for a in call.src[1:] if not a.unsharded_base.is_unbound)
|
|
is_vocab_sharded = isinstance(weight.device, tuple) and weight.axis == 0
|
|
# for multi-device: replicate grad_emb and idx on all devices
|
|
if isinstance(weight.device, tuple):
|
|
assert weight.axis is None or weight.axis == 0, "only vocab (axis=0) sharding supported on Embedding with USE_ATOMICS"
|
|
grad_emb = grad_emb.copy_to_device(weight.device)
|
|
idx = idx.copy_to_device(weight.device)
|
|
if is_vocab_sharded:
|
|
ndev = len(weight.device)
|
|
local_vocab_size = weight.shape[0] // ndev
|
|
grad_weight_uop = Tensor.empty(local_vocab_size, weight.shape[1], dtype=dtypes.float, device=weight.device).uop.unshard(axis=0)
|
|
else:
|
|
# weight is replicated (or single device), grad_weight should match
|
|
grad_weight_uop = Tensor.empty(weight.shape, dtype=dtypes.float, device=weight.device).uop
|
|
|
|
# TODO: how do we remove this dumb kernel and use Tensor.zeros?
|
|
def _zero_kernel(out:UOp) -> UOp:
|
|
i = UOp.range(out.numel(), 0)
|
|
return out.flatten()[i].store(0).end(i).sink(arg=KernelInfo(name="zero"))
|
|
grad_weight_uop = grad_weight_uop.custom_kernel(fxn=_zero_kernel)[0]
|
|
|
|
# TODO: do we have a universal helper for this?
|
|
device = call.device.split(":")[0] if not isinstance(call.device, tuple) else call.device[0].split(":")[0]
|
|
|
|
# this is the real atomic kernel
|
|
def _embedding_bwd_kernel(grad_weight:UOp, grad_emb:UOp, idx:UOp) -> UOp:
|
|
idx_flat, grad_emb_flat = idx.flatten(), grad_emb.reshape((idx.numel(), grad_weight.shape[-1]))
|
|
|
|
embed_size = grad_weight.shape[-1]
|
|
BLOCK_J = min(256, embed_size)
|
|
n_j_blocks = (embed_size + BLOCK_J - 1) // BLOCK_J
|
|
i = UOp.range(grad_emb_flat.shape[0], 0) # batch_size * sequence_length -> GLOBAL
|
|
j_inner = UOp.range(BLOCK_J, 2, AxisType.WEAK if device in ("CPU", "NULL") else AxisType.LOCAL) # BLOCK_J threads per workgroup
|
|
j_outer = UOp.range(n_j_blocks, 1)
|
|
j = j_outer * BLOCK_J + j_inner
|
|
# mask padded embed
|
|
j_ok = j < embed_size
|
|
j_idx = j.clip(0, embed_size-1)
|
|
|
|
if is_vocab_sharded:
|
|
# each device owns [offset, offset+local_vocab_size) of the global vocabulary
|
|
dnum = UOp.range(ndev, -1, AxisType.DEVICE)
|
|
offset = dnum * local_vocab_size
|
|
global_token_id = idx_flat[i].cast(dtypes.weakint)
|
|
local_token_id = (global_token_id - offset).clip(0, grad_weight.shape[0]-1)
|
|
in_range = (global_token_id >= offset) & (global_token_id < (offset + local_vocab_size)) & j_ok
|
|
grad_val = in_range.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
|
|
else:
|
|
local_token_id = idx_flat[i].clip(0, grad_weight.shape[0]-1).cast(dtypes.weakint)
|
|
grad_val = j_ok.where(grad_emb_flat[i, j_idx].load().cast(dtypes.float), 0.0)
|
|
# atomic scatter-add: grad_weight[token_id, j] += grad_emb_flat[i, j]
|
|
if device in ("CPU", "NULL"): atomic_arg = "__atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED);"
|
|
elif device == "AMD": atomic_arg = "__hip_atomic_fetch_add({0}, {1}, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT);"
|
|
else: raise NotImplementedError(f"no atomics for device {device}")
|
|
atomic = UOp(Ops.CUSTOM, src=(grad_weight.index(local_token_id, j_idx), grad_val), arg=(atomic_arg, dtypes.void))
|
|
return atomic.end(i, j_outer, j_inner).sink(arg=KernelInfo(name="embedding_bwd", opts_to_apply=()))
|
|
|
|
grad_weight_uop = grad_weight_uop.custom_kernel(grad_emb, idx, fxn=_embedding_bwd_kernel)[0]
|
|
|
|
return (grad_weight_uop.cast(weight.dtype), None)
|
|
|
|
def _embedding_fwd(weight:Tensor, idx:Tensor) -> Tensor:
|
|
arange = Tensor.arange(weight.shape[0])
|
|
return (arange == idx.unsqueeze(-1)).unsqueeze(-1).where(weight, 0).sum(-2, dtype=weight.dtype)
|
|
|
|
@functools.cache
|
|
def _embedding_fwd_fxn(wp, ip, device):
|
|
return _embedding_fwd(Tensor(wp, device=device), Tensor(ip, device=device))
|
|
|
|
class Embedding:
|
|
"""
|
|
A simple lookup table that stores embeddings of a fixed dictionary and size.
|
|
|
|
See: https://pytorch.org/docs/stable/generated/torch.nn.Embedding
|
|
|
|
```python exec="true" source="above" session="tensor" result="python"
|
|
emb = nn.Embedding(10, 3)
|
|
print(emb(Tensor([1, 2, 3, 1])).numpy())
|
|
```
|
|
"""
|
|
def __init__(self, vocab_size:int, embed_size:int):
|
|
self.weight = Tensor.glorot_uniform(vocab_size, embed_size)
|
|
|
|
def __call__(self, idx:Tensor) -> Tensor:
|
|
if not dtypes.is_int(idx.dtype): raise TypeError(f"Expected integer dtype for index in embedding, got {idx.dtype}")
|
|
if USE_ATOMICS:
|
|
fxn = _embedding_fwd_fxn(self.weight.as_param(0).uop, idx.as_param(1).uop, self.weight.device)
|
|
return Tensor.call(self.weight, idx, fxn=fxn, grad_fxn=_embedding_bwd)
|
|
return _embedding_fwd(self.weight, idx)
|
|
|
|
class LSTMCell:
|
|
"""
|
|
A long short-term memory (LSTM) cell.
|
|
|
|
Args:
|
|
input_size: The number of expected features in the input `x`
|
|
hidden_size: The number of features in the hidden state `h`
|
|
bias: If ``False``, then the layer does not use bias weights `b_ih` and `b_hh`
|
|
"""
|
|
def __init__(self, input_size:int, hidden_size:int, bias:bool=True):
|
|
stdv = 1.0 / math.sqrt(hidden_size)
|
|
self.weight_ih = Tensor.uniform(hidden_size*4, input_size, low=-stdv, high=stdv)
|
|
self.weight_hh = Tensor.uniform(hidden_size*4, hidden_size, low=-stdv, high=stdv)
|
|
self.bias_ih: Tensor|None = Tensor.zeros(hidden_size*4) if bias else None
|
|
self.bias_hh: Tensor|None = Tensor.zeros(hidden_size*4) if bias else None
|
|
|
|
def __call__(self, x:Tensor, hc:tuple[Tensor, Tensor]|None=None) -> tuple[Tensor, Tensor]:
|
|
if hc is None: hc = (Tensor.zeros(x.size(0), self.weight_hh.size(1), dtype=x.dtype, buffer=False),)*2
|
|
gates = x.linear(self.weight_ih.T, self.bias_ih) + hc[0].linear(self.weight_hh.T, self.bias_hh)
|
|
i, f, g, o = gates.chunk(4, dim=1)
|
|
i, f, g, o = i.sigmoid(), f.sigmoid(), g.tanh(), o.sigmoid()
|
|
new_c = f * hc[1] + i * g
|
|
return o * new_c.tanh(), new_c
|