Broadcasting
Broadcasting refers to how TML binary operators and assignments behave when their tensor operands differ in number of dimensions or lenght per dimension. TML automatically broadcasts tensors in binary operations, if possible. If broadcasting is not possible due to incompatible dimensions, an error is raised. If dimensions are the same, broadcasting is not performed. TML has different broadcasting rules for logical, relational and arithmetic operations.
Info
Broadcasting rules are inspired by Octave Broadcasting and Numpy Broadcasting.
The difference between TML and Octave/Numpy is support for nested tensor types.
For nested types, broadcasting is applied recursively, as 3x3x3 and 3x(3x3) objects
are not semantically equal.
Logical, relational and bitwise operations
For broadcasting in binary operation A <op> B to be possible,
following conditions must hold:
- One operand is tensor and other operand is scalar, or both operands are tensors of the same nesting level and same dimensions on all nesting levels
- Scalar/root scalar type must be compatible
- For relational operations, scalar type must be numeric
- For logical operations, scalar type must be
bool - For bitwise operations, scalar type must be
uint
Result of broadcasting is a tensor with the same nesting level and
same dimensions on all nesting levels as the operands,
with bool as root scalar type.
These rules differ from broadcasting rules used in arithmetic operations because adding elements implicitly when doing comparisons or bitwise operations is counterintuitive and provides no benefit.
Following example shows result of broadcasting on a scalar and a tensor:
a = 1
b = [1, 2, 3]
c = [b, b, b]
d = a > b # 1 > [1, 2, 3]
# [1, 1, 1] > [1, 2, 3]
# [false, false, false]
d = a > c # 1 > [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
# [[1, 1, 1], [1, 1, 1], [1, 1, 1]] > [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
# [[false, false, false], [false, false, false], [false, false, false]]
Following example shows result of broadcasting on two compatible tensors:
a = [1, 2, 3]
b = [4, 5, -1]
c = [a, a, a]
d = [b, b, b]
x = a > b # [1, 2, 3] > [4, 5, -1]
# [false, false, true]
y = c > d # [[1, 2, 3], [1, 2, 3], [1, 2, 3]] > [[4, 5, -1], [4, 5, -1], [4, 5, -1]]
# [[false, false, true], [false, false, true], [false, false, true]]
Arithmetic operations
For broadcasting in binary operation A <op> B to be possible,
following conditions must hold:
- Length of corresponding tensor dimensions must be equal, or one of the corresponding dimensions must be 1
- Base type of tensors must be compatible
- For basic types, type inference must be possible
- For tensor types, broadcasting must be possible
Broadcasting is performed following these rules:
- If number of dimensions is not the same, dimensions with length of 1 are added to the left
- Dimensions with length of 1 are broadcast to the greater length
- Base type is inferred to a compatible type
- For basic types, type inference is applied
- For tensor types, broadcasting is applied
Broadcasting and tensor type order
Tensors can be compatible in binary operations even if they do not meet rules
for assignment compatibility. Resulting tensor C maintains the propery that
type(C) <= type(A) and type(C) <= type(B)
Following example shows result of broadcasting on two compatible tensors:
a = [1, 2, 3] # with broadcasting applied:
# a = [
# 1, 2, 3;
# 1, 2, 3; <- added
# 1, 2, 3 <- added
# ]
b = [ # with broadcasting applied:
1; # b = [
2; # 1, 1, 1;
3 # 2, 2, 2;
] # 3, 3, 3
# ^ ^
# | |
# | --added
# -----added
# ]
c = a + b # c = [
# 2, 3, 4;
# 3, 4, 5;
# 4, 5, 6
# ]
Following example shows two incompatible tensors:
a = [
1, 2;
1, 2
]
b = [
1, 2, 3;
1, 2, 3
]
c = a + b # Error: Broadcasting is not possible!
Assignments
Rules for broadcasting in assignments are similar to the rules for broadcasting in binary operations, with the difference that it must be possible to broadcast RHS type to LHS type, without affecting LHS type. This means that, in constrast to binary opearations, no new type is produced.
For broadcasting in assignment A = B to be possible, following conditions must hold:
- Length of corresponding
AandBtensor dimensions must be equal, or length of corresponding dimension ofBmust be 1 Bmust have the same or smaller number of dimensions thanA- Base type of
Amust be the same or greater thanB(i.e.base_type(B) <= base_type(A))- For basic types, type rules for assignment must be met
(i.e. scalar type
Bmust be smaller than scalar typeA) - For tensor types, broadcasting rules for assignment must be met (ie. tensor type
Bmust be smaller than tensor typeA)
- For basic types, type rules for assignment must be met
(i.e. scalar type
Broadcasting is performed following these rules:
- If number of dimensions is not the same, dimensions with length of 1 are added to the left
- Dimensions with length of 1 are broadcast to the greater length
- Base type is inferred to a compatible type
- For basic types, type inference is applied
- For tensor types, assignment broadcasting is applied
Following example shows result of broadcasting on two compatible tensors in an assignment:
a = [0, 0, 0] # type(a) = tensor<int, 3>
a = 1 # broadcasts to:
# a = [1, 1, 1]
b = [ # type(b) = tensor<int, 3, 3>
0, 0, 0;
0, 0, 0;
0, 0, 0
]
b = [1, 1, 1] # broadcasts to:
# b = [
# 1, 1, 1;
# 1, 1, 1; <- added
# 1, 1, 1 <- added
# ]
Following example shows two incompatible tensors in assignment:
tensor<int, 3> a = 0 # a = [0, 0, 0]
a = [1, 3] # Error: Assignment not possible!
# a has length of 3, and array with length of 2
# cannot be broadcast
Index access broadcasting (IAB)
Currently, we have two type kinds:
- Base types (scalars)
- Tensors
We also support broadcasting, which enables writing generic code like:
c = a + b
Here, a could be a tensor, such as tensor<real, 2, 3>, while b could be a
scalar of type real. Broadcasting ensures that b is expanded to match the
dimensions of a before performing the element-wise + operation.
Similarly, if b is tensor<real, 2>, its dimensionality is expanded until it
becomes compatible with a.
This concept is extended further with Index Access Broadcasting (IAB), which allows indexed access at an arbitrary length along each dimension of size 1.
In simple terms, bounded index access is enforced for dimensions greater than 1, while dimensions of size 1 allow unbounded index access.
Example
Consider a tensor a of type tensor<real, 1, 2>:
a = [3, 4]
As expected, direct indexing works:
a[0][1] == 4
However, following the IAB rules, unbounded indexing is allowed along dimensions of size 1:
a[999][1] == 4
Effectively, a is broadcasted along its first dimension to an arbitrary length.
Indexing can also be extended over additional dimensions indefinitely:
a[999][1][1000][2000] == 4
a[999][0][1000][2000] == 3
This means that a is broadcasted over an arbitrary number of dimensions.
Scalars and Index Access Broadcasting
Although TML distinguishes between scalar and tensor types, treating scalars as tensors can be beneficial in certain cases. This approach allows writing generic code that handles both scalar and tensor model variables consistently, even when element-wise operations are performed manually.
IAB extends to scalars, making each scalar indexable over an arbitrary number of
dimensions. For example, if a is an int with a value of 42, the following
statements hold:
a == 42
a[0] == 42
a[500] == 42
a[4][5] == 42
Conceptually, a is broadcasted across infinite dimensions.
Indexing Rules for Scalars
- Scalars allow indexing over an arbitrary number of dimensions indefinitely.
- Indexing a scalar is ignored and does not change the value.
- Indexing applies to both lvalues and rvalues.
Strings and Indexing
Since strings are used to pass configuration data within the model scope, they follow these indexing rules:
- The
strtype is treated as a tensor, meaning indexing astrreturns achar. - A
charis treated as a scalar, meaning infinite indexing is allowed.
Syntax and Examples
Examples of index access broadcasting are shown below:
a = 1
b = a[1] # 1
c = a[1][2][3] # 1
a[1] = 2 # a = 2
a[1][2][3] = 3 # a = 3
# signs has values such as "-" and "+-+"
for i=0:signs.len:
# if signs == "+", signs is a char and IAB is applied
# if signs == "+-+", signs is a char and normal indexing is applied
if signs[i] == "+":
x[i] += y[i]
end
end
Narrowcasting
When working with tensors, it is common to have tensors with some (or even all) dimensions equal to 1. One common type of this problem is having a single input terminal in input terminal group. Those dimensions are effectively useless and can make tensor usage harder in code. To overcome this problem, TML has support for a mechanism opposite to broadcasting that is called narrowcasting.
Narrowcasting is performed only on the top level type and is not performed recursively. Narrowcasting expressions can be nested arbitrarily. In contrast to broadcasting, narrowcasting never produces an error.
Narrowcasting is performed following these rules:
- If a scalar is passed, type of the result is unchanged
(e.g.
intstaysint) - If a tensor is passed, dimensions equal to 1 are removed
(e.g.
tensor<T, 1, 5, 1>becomestensor<T, 5>) - If a tensor is passed, and no dimensions are equal to 1,
type of the result is unchanged
(e.g.
tensor<T, 5>staystensor<T, 5>) - If a tensor is passed, and all dimensions are equal to 1,
type of the result is tensor base type
(e.g.
tensor<T, 1, 1, 1>becomesT)
Narrowcasting syntax and examples are shown below.
int a = 1
b = narrow(b) # int
tensor<int, 1> c = 1
d = narrow(c) # int
tensor<int, 1, 5, 1> e = 1
f = narrow(e) # tensor<int, 5>
tensor<int, 5, 5> g = 1
h = narrow(g) # tensor<int, 5, 5>
tensor<tensor<int, 1>, 5, 5> i = 1
j = narrow(i) # tensor<tensor<int, 1>, 5, 5>