[Tensor] Rework bindings for Tensor constructors
Context
Completed Tensor constructors python bindings implementation.
For constructors, now manage correctly:
- np array 0-rank arrays scalar,
- python int|float scalars,
- np scalars np.dtype() (different from 0-rank array scalars),
- construction with dims=..., now a kw only argument,
- np arrays of rank >= 1 as before.
Actually re-implemented the scalar constructor to have better control over what pybind default conversions does and to support bare numpy scalars.
Examples:
x = Tensor() # undefined tensor
x.resize((1, 2))
x.set_backend("cpu")
x[0] = 1.0
x = Tensor(dims=(1, 2)) # uninitialized float32 Tensor of given dims
x.set_backend("cpu")
x[0] = 1.0
Tensor(np.array([1, 2], dtype="float64")) # float64 2-D tensor
Tensor(np.array(1, dtype="int8")) # int8 scalar (0-rank) tensor
Tensor(1.0) # float32 scalar (0-rank) tensor
Tensor(np.float64(1.0)) # float64 scalar (0-rank) tensor
Tensor(np.int8(1.0)) # int8 scalar (0-rank) tensor
# Behavior kept because it is the legacy, but may change to always cast to float32?
Tensor(1) # int32 scalar (0-rank) tensor
Tensor(2**60) # int64 scalar (0-rank) tensor
Tensor(10**30) # float32 scalar (0-rank) tensor
For setters, now manage correctly conversion without loss of precision and do not raise in case of overflow/type-downcast (i.e. always force cast to the tensor dtype) as we should probably expect (numpy behavior). Examples:
t = Tensor(np.uint8(1))
t[0] += 256 # Do not throw an exception on overflow
print(t[0]) # outputs 1 (i.e. 0 == (1+ 256) % 256)
Added support in ctors()/get()/set() for some missing types, now support [u]int(8|16|32|64).
Modified files
pybind_Tensor.cpp:
- redefine constructors for Tensors
- add helper code for managing Numpy scalars
- redefine setters
- complete missing types from the list above where necessary
aidge_core/unit_tests/test_tensor_scalar.py:
- add unit tests for scalar tensor with np 0-rank arrays of numpy scalars
Detailed major modifications
There is a user facing modification for construction of uninitialized tensors with:
t = Tensor(dims=(2, 3))
Previously one could do:
t = Tensor((2, 3))
Which basically worked because a tuple or bare python list was not be default casted to a numpy array, and hence this called the "dims only" constructor overload.
Actually, I propose to break this old behavior and have dims a kw args only argument, in addition it was confusing for the user I suppose to pass an array of value and have it not interpreted as values, but as dims.
Now the behavior is:
t = Tensor((2, 3))
# ValueError: Unsupported python type passed to Tensor constructor
t = Tensor([2, 3])
# ValueError: Unsupported python type passed to Tensor constructor
Possible discussions
Handling of numpy scalars
I took a long time trying to manage in a reasonable way auto promotions of types done by numpy and it could work to have a simpler code as it was in the previous version.
Though as soon as we want to handle numpy scalars, it breaks because in this case pybind automatic conversions come into place with incomplete/inconsistent behaviors when promoting numpy scalars to either numpy arrays or bare python scalars.
In the end I preferred to choose a bare py::object interface for the passed value and handle precisely the different scalar cases
and possibly would allow to change the legacy behavior (int32
-> int64
-> float32
conversion) more easily or
add a dtype argument for instance (which will require conversion without loss of precision and hence should not use the default pybind scheme for casting).
TODO
-
add some unit tests for scalar construction in test_tensor_scalar.py
-
close discussions/questions