Note
Go to the end to download the full example code.
Data Types and Type Casting¶
Author: Hongzheng Chen (hzchen@cs.cornell.edu)
This document will discuss the Allo-supported data types in detail.
All the data types are defined in the allo.ir.types
module.
import allo
from allo.ir.types import int16, int32, float32, Int, UInt, Float, Fixed
Currently, Allo supports three base data types for mathematical operations:
Integers:
Int(bitwdith)
,UInt(bitwidth)
Floating points:
Float(bitwidth, fracs)
(we can use float32, float64, etc. as shorthands)Fixed points:
Fixed(bitwidth, frac)
,UFixed(bitwidth, frac)
For example, one can declare a 15-bit integer as Int(15)
and an unsigned 8-bit fixed-point number with 3 fractional bits as UFixed(8, 3)
.
For all the C/C++ supported data types, we provide shorthands like float32
and int16
to easily declare them.
Notice different from native Python, Allo requires the program to be strongly and statically typed. The variable types are either declared explicitly or inferred from the context. For a variable that first appears in the program, we should declare it with an expected data type using Python’s type hint notation:
a: int32
Once the data types are defined, an important consideration is how to handle operations between variables of different types. Allo supports two types of casting: (1) implicit casting that is automatically done by the Allo compiler; and (2) explicit casting that is manually done by the user.
Implicit Casting¶
Allo has a strong type system that follows the MLIR convention to enforce the operand types are the same for the arithmetic operations. However, it is burdensome for users to cast the variables every time, and it is also error-prone to avoid overflow when performing computations. Therefore, Allo is equipped with builtin casting rules to automatically cast the variables to the same type before the operation, which is called implicit casting. An example is shown below:
def add(a: int32, b: int32) -> int32:
return a + b
s = allo.customize(add)
print(s.module)
module {
func.func @add(%arg0: i32, %arg1: i32) -> i32 attributes {itypes = "ss", otypes = "s"} {
%0 = arith.extsi %arg0 : i32 to i33
%1 = arith.extsi %arg1 : i32 to i33
%2 = arith.addi %0, %1 : i33
%3 = arith.trunci %2 : i33 to i32
return %3 : i32
}
}
We can see that a
and b
are firstly casted to int33
, added
together, and converted back to int32
.
This is to avoid overflow and is automatically inferred by the Allo compiler.
Explicit Casting¶
One can also explicitly cast the variable to a specific type by creating an intermediate variable,
or use Python-builtin functions like float()
and int()
to explicitly cast a variable to float32
or int32
.
Another example is shown below:
def cast(a: int32) -> int16:
b: float32 = a # explicit
c: float32 = b * 2
d: float32 = float(a) * 2
e: int16 = c + d
return e
s = allo.customize(cast)
print(s.module)
module {
func.func @cast(%arg0: i32) -> i16 attributes {itypes = "s", otypes = "s"} {
%0 = arith.sitofp %arg0 : i32 to f32
%alloc = memref.alloc() {name = "b"} : memref<f32>
affine.store %0, %alloc[] {to = "b"} : memref<f32>
%c2_i32 = arith.constant 2 : i32
%1 = arith.sitofp %c2_i32 : i32 to f32
%2 = affine.load %alloc[] {from = "b"} : memref<f32>
%3 = arith.mulf %2, %1 : f32
%alloc_0 = memref.alloc() {name = "c"} : memref<f32>
affine.store %3, %alloc_0[] {to = "c"} : memref<f32>
%4 = arith.sitofp %arg0 : i32 to f32
%c2_i32_1 = arith.constant 2 : i32
%5 = arith.sitofp %c2_i32_1 : i32 to f32
%6 = arith.mulf %4, %5 : f32
%alloc_2 = memref.alloc() {name = "d"} : memref<f32>
affine.store %6, %alloc_2[] {to = "d"} : memref<f32>
%7 = affine.load %alloc_0[] {from = "c"} : memref<f32>
%8 = affine.load %alloc_2[] {from = "d"} : memref<f32>
%9 = arith.addf %7, %8 : f32
%10 = arith.fptosi %9 : f32 to i16
%alloc_3 = memref.alloc() {name = "e"} : memref<i16>
affine.store %10, %alloc_3[] {to = "e"} : memref<i16>
%11 = affine.load %alloc_3[] {from = "e"} : memref<i16>
%12 = affine.load %alloc_3[] {from = "e"} : memref<i16>
return %12 : i16
}
}
By explicitly creating an intermediate variable b
, we can cast the int32
variable a
to the desired floating-point type.
Similarly, calling float(a)
can also cast a
to a floating-point type.
Note
The above stated explicit casting between integers and floating points preserves the value but the precision may be changed.
If you want to use a union type to represent both integers and floating points, please use the .bitcast() API instead. For example, a.bitcast()
can convert int32
to float32
representation with the bit pattern preserved.
Bit Operations¶
As hardware accelerators have ability to manipulate each bit of the data, Allo supports bit operations on
those integer types. For example, we can access a specific bit in an integer a
using the indexing operator:
a[15]
We can also extract a chunk of bits from an integer using the slicing operator:
a[0:16]
Note
Allo follows the Python convention that the upper bound is not included, so [0:16]
means
extracting the first 16 bits, which is different from the Xilinx HLS convention that uses [0:15]
to indicate the first 16 bits.
Not only constant values are supported, but also variables can be used as the index or the slice range.
Total running time of the script: (0 minutes 0.171 seconds)