From 3821e442eb9f56b367c2da7ed31cf0b665ac1c8e Mon Sep 17 00:00:00 2001 From: chenyu Date: Tue, 21 Apr 2026 20:24:38 -0400 Subject: [PATCH] _one_hot_along_dim and one_hot to mixin (#15861) --- test/null/test_tensor_uop_mixin.py | 5 +++++ tinygrad/mixin/__init__.py | 21 +++++++++++++++++++++ tinygrad/tensor.py | 23 +---------------------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/test/null/test_tensor_uop_mixin.py b/test/null/test_tensor_uop_mixin.py index 286dea8ae6..bafc257253 100644 --- a/test/null/test_tensor_uop_mixin.py +++ b/test/null/test_tensor_uop_mixin.py @@ -62,6 +62,11 @@ class TestTensorUOpCumalu(unittest.TestCase): def test_cumsum_large(self): _check(self, _t(600), lambda x: x.cumsum()) # exercises _split_cumalu def test_cumprod(self): _check(self, _t(4), lambda x: x.cumprod(0)) +class TestTensorUOpOneHot(unittest.TestCase): + def test_one_hot(self): + t = _t(5) + self.assertIs(_strip_unique(t.one_hot(5).uop), _strip_unique(t.uop.one_hot(5))) + class TestTensorUOpCat(unittest.TestCase): def test_cat_dim0(self): _check(self, _t(2, 3), lambda x: x.cat(x, dim=0)) def test_cat_dim1(self): _check(self, _t(2, 3), lambda x: x.cat(x, dim=1)) diff --git a/tinygrad/mixin/__init__.py b/tinygrad/mixin/__init__.py index 0108a7b575..cb32ded72e 100644 --- a/tinygrad/mixin/__init__.py +++ b/tinygrad/mixin/__init__.py @@ -575,6 +575,27 @@ class OpMixin(ElementwiseMixin, ReduceMixin): """ return self._split_cumalu(axis, Ops.MUL) + # helper function commonly used for indexing + def _one_hot_along_dim(self, num_classes:sint, dim:int=-1) -> Self: + from tinygrad.uop.ops import sint_to_uop + if not dtypes.is_int(self.dtype): raise RuntimeError(f"_one_hot_along_dim expects int index tensor, getting {self.dtype}") + offset = self.ndim - self._resolve_dim(dim) - 1 + dt = dtypes.int64 if sint_to_uop(num_classes).overflows(dtypes.int32) else dtypes.int32 + return self.eq(type(self).arange(num_classes, dtype=dt, device=self.device).reshape((num_classes,) + (1,) * offset)) + + def one_hot(self, num_classes:int) -> Self: + """ + Converts `self` to a one-hot tensor. + + ```python exec="true" source="above" session="tensor" result="python" + t = Tensor([0, 1, 3, 3, 4]) + print(t.one_hot(5).numpy()) + ``` + """ + if not dtypes.is_int(self.dtype): raise RuntimeError(f"expect integer dtype, getting {self.dtype=}") + if num_classes < 0: raise ValueError(f"num_classes must be non-negative, got {num_classes}") + return self[..., None]._one_hot_along_dim(num_classes).where(1, 0) + # ***** functional nn ops ***** def linear(self, weight:Self, bias:Self|None=None, dtype:DTypeLike|None=None) -> Self: diff --git a/tinygrad/tensor.py b/tinygrad/tensor.py index 161eb6d8d9..c93ab490ea 100644 --- a/tinygrad/tensor.py +++ b/tinygrad/tensor.py @@ -11,8 +11,7 @@ from tinygrad.helpers import resolve_pool_pads, IMAGE, FLOAT16, WINO, Metadata, from tinygrad.helpers import suppress_finalizing, disable_gc from tinygrad.gradient import compute_gradient from tinygrad.mixin import OpMixin, ReductionStr -from tinygrad.uop.ops import smax, UOp, Ops, sint, all_metadata, _index_to_concrete_int, sint_to_uop, Variable -from tinygrad.uop.ops import _broadcast_shape +from tinygrad.uop.ops import smax, UOp, Ops, sint, all_metadata, _index_to_concrete_int, Variable, _broadcast_shape from tinygrad.schedule import ExecItem, create_linear_with_vars, linear_to_schedule from tinygrad.device import Buffer, canonicalize_device from tinygrad.engine.realize import run_linear @@ -2006,26 +2005,6 @@ class Tensor(OpMixin): if p == 1: return self.zeros_like() return (Tensor.rand_like(self, requires_grad=False, dtype=dtypes.default_float, contiguous=False) >= p).contiguous().where(self, 0) / (1.0 - p) - # helper function commonly used for indexing - def _one_hot_along_dim(self:Tensor, num_classes:sint, dim:int=-1) -> Tensor: - if not dtypes.is_int(self.dtype): raise RuntimeError(f"_one_hot_along_dim expects int index tensor, getting {self.dtype}") - offset = self.ndim - self._resolve_dim(dim) - 1 - dt = dtypes.int64 if sint_to_uop(num_classes).overflows(dtypes.int32) else dtypes.int32 - return self == Tensor.arange(num_classes, dtype=dt, device=self.device, requires_grad=False).reshape((num_classes,) + (1,) * offset) - - def one_hot(self, num_classes:int) -> Tensor: - """ - Converts `self` to a one-hot tensor. - - ```python exec="true" source="above" session="tensor" result="python" - t = Tensor([0, 1, 3, 3, 4]) - print(t.one_hot(5).numpy()) - ``` - """ - if not dtypes.is_int(self.dtype): raise RuntimeError(f"expect integer dtype, getting {self.dtype=}") - if num_classes < 0: raise ValueError(f"num_classes must be non-negative, got {num_classes}") - return self[..., None]._one_hot_along_dim(num_classes).where(1, 0) - def scaled_dot_product_attention(self, key:Tensor, value:Tensor, attn_mask:Tensor|None=None, dropout_p:float=0.0, is_causal:bool=False, enable_gqa:bool=False) -> Tensor: """