cornell-zhang/hcl-dialect

if/else scoping not correct if body is empty (pass)

jcasas00 opened this issue · 8 comments

def test_if_else_scope():
    hcl.init()
    rshape = (1,)
    def kernel():
        f = hcl.scalar(0, "f", dtype='uint32')
        xy = hcl.scalar(0x12340001, "foo", dtype='uint32')
        with hcl.if_(xy.v == 0):
            hcl.print((),"0")
            with hcl.if_(f.v == 0):
                pass
        with hcl.else_():   # should be scoped as the else clause of if_(xy.y == 0)
            hcl.print((),"1")
    #
    s = hcl.create_schedule([], kernel)
    print(hcl.lower(s))

generates the following IR:

module {
  func.func @top() attributes {itypes = "", otypes = ""} {
    %0 = memref.alloc() {name = "f", unsigned} : memref<1xi32>
    %c0_i32 = arith.constant {unsigned} 0 : i32
    affine.store %c0_i32, %0[0] {to = "f", unsigned} : memref<1xi32>
    %1 = memref.alloc() {name = "foo", unsigned} : memref<1xi32>
    %c305397761_i32 = arith.constant {unsigned} 305397761 : i32
    affine.store %c305397761_i32, %1[0] {to = "foo", unsigned} : memref<1xi32>
    %2 = affine.load %1[0] {from = "foo", moved, unsigned} : memref<1xi32>
    %c0_i32_0 = arith.constant {moved} 0 : i32
    %3 = arith.cmpi eq, %2, %c0_i32_0 {moved} : i32
    scf.if %3 {
      %c0_i32_1 = arith.constant 0 : i32
      hcl.print(%c0_i32_1) {format = "0", signedness = "_"} : i32
      %4 = affine.load %0[0] {from = "f", moved, unsigned} : memref<1xi32>
      %5 = arith.cmpi eq, %4, %c0_i32_0 {moved} : i32
      scf.if %5 {
      } else {
        hcl.print(%c0_i32_1) {format = "1", signedness = "_"} : i32
      }
    }
    return
  }
}

The hcl.else_() body got scoped with the hcl.if_(f.v==0) test instead of the hcl.if_(xy.v == 0) test. If instead of pass, there was an hcl statement, it gets scoped correctly.
This case came about because the code we have builds the hcl model based on python variables for configuration and one combination ended up with no hcl statements under the hcl.if_ test (i.e., just like using pass).

This looks like an issue with the insertion point. The insertion point should be reset when the program exits the with hcl.if_ context. However, it seems that the _exit_cb function is not executed when the body is empty.

I found that it is not caused by _exit_cb is not called, but Schedule._IfThenStack not popped. We use Schedule._IfThenStack to keep track of which if operation should the else branch be inserted to. The current implementation only pops Schedule._IfThenStack in hcl.else_. So, in the above case with hcl.if_(f.v == 0): is not popped from stack because it is not closed by any hcl.else_.
I added logic to pop the stack in _exit_cb function if an If op is not closed by hcl.else_: cornell-zhang/heterocl@494d39b

Still seeing an issue with if/else scoping (or at least looks like it).

def test_if_else_multiple():
    hcl.init()
    rshape = (1,)

    stype0 = hcl.Struct({"x": hcl.UInt(8), "y": hcl.UInt(64-8)})
    stype1 = stype0
    stype2 = stype0

    def kernel():
        inst_enc = hcl.scalar(0, "inst_enc", dtype='uint64')
        inst_id = hcl.scalar(0, "inst_id", dtype='uint16')
        inst_enc_v = inst_enc.v

        inst_lat = hcl.scalar(0, "inst_lat", dtype='uint16')

        tmp = hcl.scalar(0, "tmp", dtype=hcl.UInt(1))
        with hcl.if_(inst_lat.v <= 1):
            tmp.v = 1
        with hcl.if_(tmp.v != 0):
            exe_inst = hcl.scalar(inst_enc_v, "exe_inst", dtype='uint64')
            with hcl.if_(exe_inst.v == 0):
                inste = hcl.scalar(exe_inst.v, f"inste0", dtype=stype0).v
            with hcl.else_():
                with hcl.if_(exe_inst.v == 1):
                    inste = hcl.scalar(exe_inst.v, f"inste1", dtype=stype1).v

        r = hcl.compute(rshape, lambda _:0, dtype=hcl.Int(32))
        return r
    #
    s = hcl.create_schedule([], kernel)
    print(hcl.lower(s))
    hcl_res = hcl.asarray(np.zeros(rshape, dtype=np.uint32), dtype=hcl.UInt(32))
    f = hcl.build(s)
    f(hcl_res)
    np_res = hcl_res.asnumpy()
    golden = np.zeros(rshape, dtype=np.int32)
    assert np.array_equal(golden, np_res)

Generates the following error:

      %24 = "affine.load"(%24) {from = "exe_inst", map = #map0, moved, unsigned} : (memref<1xi64>) -> i64
loc("-":60:7): error: definition of SSA value '%24#0' has type 'i64'

This test snippet was derived from a bigger program (took a while to isolate). Both the hcl.if_(inst_lat.v <= 1) test and 2nd level hcl.if_(exec_inst.v == 1) tests are needed to sensitize this issue.
Common to the error in the bigger program, the argument to affine.load is not correct and gets flagged (thankfully) as a type error.
Feel free to open another issue if this is a different issue.

Latest urgent issue as there's no temporary workaround

Added the following lines in dsl.py:if_ seems to fix the issue. Please check if it makes sense to you. If okay, something similar needs to be done for elseif_() .

def if_(cond):
    """Construct an IF branch."""
    hcl_mlir.enable_build_inplace()
    if isinstance(cond, hcl_mlir.ExprOp):
        if_op = hcl_mlir.make_if(cond, ip=hcl_mlir.GlobalInsertionPoint.get())
    else:
        raise RuntimeError("Not implemented")
    hcl_mlir.GlobalInsertionPoint.save(if_op.then_block.operations[0])
+   if len(Schedule._IfElseStack) > Schedule._CurrentIf:
+       Schedule._IfElseStack.pop()
    Schedule._IfElseStack.append(if_op)
    Schedule._CurrentIf += 1
+   assert len(Schedule._IfElseStack) == Schedule._CurrentIf

The issue is that for code like:
with hcl.if_(...):
...
with hcl.if_(...):
...

The second if_ effectively "closes" the first if and pop the stack (meant for an else clause of the first if_).

Hi Jeremy, sorry for the late reply, I am pretty occupied these few days by other things. You are right, the purpose of keep a stack of if operations is to find which if the next elif or else should be inserted to. When a if is followed by another if, the previous if can no longer have else or elif branches, so it should be popped out from the stack. I'll add this fix to the HeteroCL repo, thanks!

No problem. Removed urgent label since this if/else seems to work so far.

The frontend is revamped by cornell-zhang/heterocl#473, and if/elseif/else scope are no longer implemented with a stack. This issue is resolved and a test case is added by cornell-zhang/heterocl@0c8a1c0