cornell-zhang/hcl-dialect

[Binding] Generate signless operands for arithmetic operations

Closed this issue · 6 comments

The current LLVM/MLIR ecosystem only accepts signless integers as the inputs and outputs for arithmetic operations, and lets specific operations do the interpretation (e.g., arith.divsi vs arith.divui). The rationale behind using the signless integers instead of signed/unsigned integers (si/ui) can be found here.

Thus, based on this design, the following code is not allowed in MLIR.

module {
    func @main() {
        %a = arith.constant 1 : ui32
        %b = arith.constant 2 : ui32
        %1 = arith.muli %a, %b : ui32
        return
    }
}

The verifier throws the following error:

loc("../test/test.mlir":3:14): error: 'arith.constant' op integer return type must be signless
Error can't load file ../test/test.mlir

This issue seems to come up when we upgrade to LLVM 14 that adds more type checking facilities in the verifier. The error does not only appear in arith.constant but also in arith.addi, arith.andi, and arith.maxsi, etc., so it requires our frontend compiler to map integers to signless types but also find a way to keep the signedness.

I can think of two ways to do that:

  1. Add CastOp for each operand and result. It preserves the signedness for each operand, but it is very inelegant since we need to add lots of cast before and after each operation, and finally the codegen may generate something like unsigned a = (unsigned) ai for cast operations, which is totally redundant.
module {
    func @main() {
        %a = arith.constant 1 : i32
        %au = builtin.unrealized_conversion_cast %a : i32 to ui32
        %b = arith.constant 2 : i32
        %bu = builtin.unrealized_conversion_cast %b : i32 to ui32
        %ai = builtin.unrealized_conversion_cast %au : ui32 to i32
        %bi = builtin.unrealized_conversion_cast %bu : ui32 to i32
        %ci = arith.muli %ai, %bi : i32
        %c = builtin.unrealized_conversion_cast %ci : i32 to ui32
        return
    }
}
  1. Following the paradigm provided by LLVM, we use signless integers all the time, but how to pass the unsigned types from the frontend to the backend becomes the problem.
module {
    func @main() {
        %a = arith.constant 1 : i32
        %b = arith.constant 2 : i32
        %c = arith.muli %a, %b : i32
        return
    }
}

Do you have any comments on this? @zhangzhiru @zzzDavid @hecmay

Signed and unsigned semantics are added to MLIR in D72533. SPIRV had the same need to model signed and unsigned arithmetic operations, their solution is to build in-house integer Add/Mul operations. For example:

spv.func @i8_const() -> () "None" {
    // CHECK: spv.Constant 0 : i8
    %0 = spv.Constant 0 : i8
    // CHECK: spv.Constant -1 : i8
    %1 = spv.Constant 255 : i8

    // CHECK: spv.Constant 0 : si8
    %2 = spv.Constant 0 : si8
    // CHECK: spv.Constant 127 : si8
    %3 = spv.Constant 127 : si8
    // CHECK: spv.Constant -128 : si8
    %4 = spv.Constant -128 : si8

    // CHECK: spv.Constant 0 : i8
    %5 = spv.Constant 0 : ui8
    // CHECK: spv.Constant -1 : i8
    %6 = spv.Constant 255 : ui8

    %10 = spv.IAdd %0, %1: i8
    %11 = spv.IAdd %2, %3: si8
    %12 = spv.IAdd %3, %4: si8
    %13 = spv.IAdd %5, %6: ui8
    spv.Return
  }

Following the paradigm provided by LLVM, we use signless integers all the time, but how to pass the unsigned types from the frontend to the backend becomes the problem.

Can we use signless and attach attributes to pass signed/unsigned type information to HLS backend? I think building our own integer operations may be unnecessary

I adopted @zzzDavid 's advice. My current solution is attaching an unsigned attribute to those operations involving unsigned integers, and unsigned/signed types will not appear in intermediate programs. All the integers are represented in signless type i<bit>.

Specifically, for operations, a unit attribute is simply attached, like

%5 = memref.alloc() {name = "D", unsigned} : memref<32x32xi12>

But for function arguments, I will generate extra_itypes and extra_otypes in the frontend. Each character represents a type. u is unsigned, s is signed, and _ is other types. So the following signature means %arg0 is signed integer, %arg1 is unsigned, and the return type is also unsigned.

func @top(%arg0: memref<32x32xi12>, %arg1: memref<32x32xi12>) -> memref<32x32xi12> attributes {extra_itypes = "us", extra_otypes = "u"} 

I will push the fix later and close this issue.

Let's discuss a bit more about this in the next meeting.