Exported Calyx doesn't add memory

Hi everyone. I’m testing a simple copy function as following in MLIR:

module {
  func.func @copy(%arg0: memref<20xi32>, %arg1: memref<20xi32>) {
    affine.for %arg2 = 0 to 20 {
      %0 = affine.load %arg0[%arg2] : memref<20xi32>
      affine.store %0, %arg1[%arg2] : memref<20xi32>
    }
    return
  }
}

For Lowering I used this command:

circt-opt copy.mlir -convert-affine-to-pipeline -lower-static-logic-to-calyx | circt-translate -export-calyx -o copy.futil

The intermediate calyx dialect code in MLIR is:

module attributes {calyx.entrypoint = "copy"} {
  calyx.component @copy(%ext_mem0_read_data: i32 {mem = {id = 0 : i32, tag = "read_data"}}, %ext_mem0_done: i1 {mem = {id = 0 : i32, tag = "done"}}, %ext_mem1_read_data: i32 {mem = {id = 1 : i32, tag = "read_data"}}, %ext_mem1_done: i1 {mem = {id = 1 : i32, tag = "done"}}, %clk: i1 {clk}, %reset: i1 {reset}, %go: i1 {go}) -> (%ext_mem0_write_data: i32 {mem = {id = 0 : i32, tag = "write_data"}}, %ext_mem0_addr0: i5 {mem = {addr_idx = 0 : i32, id = 0 : i32, tag = "addr"}}, %ext_mem0_write_en: i1 {mem = {id = 0 : i32, tag = "write_en"}}, %ext_mem1_write_data: i32 {mem = {id = 1 : i32, tag = "write_data"}}, %ext_mem1_addr0: i5 {mem = {addr_idx = 0 : i32, id = 1 : i32, tag = "addr"}}, %ext_mem1_write_en: i1 {mem = {id = 1 : i32, tag = "write_en"}}, %done: i1 {done}) {
    %c1_i32 = hw.constant 1 : i32
    %c20_i32 = hw.constant 20 : i32
    %c0_i32 = hw.constant 0 : i32
    %true = hw.constant true
    %std_slice_1.in, %std_slice_1.out = calyx.std_slice @std_slice_1 : i32, i5
    %std_slice_0.in, %std_slice_0.out = calyx.std_slice @std_slice_0 : i32, i5
    %std_add_0.left, %std_add_0.right, %std_add_0.out = calyx.std_add @std_add_0 : i32, i32, i32
    %std_lt_0.left, %std_lt_0.right, %std_lt_0.out = calyx.std_lt @std_lt_0 : i32, i32, i1
    %stage_0_register_0_reg.in, %stage_0_register_0_reg.write_en, %stage_0_register_0_reg.clk, %stage_0_register_0_reg.reset, %stage_0_register_0_reg.out, %stage_0_register_0_reg.done = calyx.register @stage_0_register_0_reg : i32, i1, i1, i1, i32, i1
    %while_0_arg0_reg.in, %while_0_arg0_reg.write_en, %while_0_arg0_reg.clk, %while_0_arg0_reg.reset, %while_0_arg0_reg.out, %while_0_arg0_reg.done = calyx.register @while_0_arg0_reg : i32, i1, i1, i1, i32, i1
    calyx.wires {
      calyx.group @assign_while_0_init_0 {
        calyx.assign %while_0_arg0_reg.in = %c0_i32 : i32
        calyx.assign %while_0_arg0_reg.write_en = %true : i1
        calyx.group_done %while_0_arg0_reg.done : i1
      }
      calyx.comb_group @bb0_0 {
        calyx.assign %std_lt_0.left = %while_0_arg0_reg.out : i32
        calyx.assign %std_lt_0.right = %c20_i32 : i32
      }
      calyx.group @bb0_1 {
        calyx.assign %std_slice_1.in = %while_0_arg0_reg.out : i32
        calyx.assign %ext_mem0_addr0 = %std_slice_1.out : i5
        calyx.assign %stage_0_register_0_reg.in = %ext_mem0_read_data : i32
        calyx.assign %stage_0_register_0_reg.write_en = %true : i1
        calyx.group_done %stage_0_register_0_reg.done : i1
      }
      calyx.group @bb0_2 {
        calyx.assign %std_add_0.left = %while_0_arg0_reg.out : i32
        calyx.assign %std_add_0.right = %c1_i32 : i32
        calyx.assign %while_0_arg0_reg.in = %std_add_0.out : i32
        calyx.assign %while_0_arg0_reg.write_en = %true : i1
        calyx.group_done %while_0_arg0_reg.done : i1
      }
      calyx.group @bb0_3 {
        calyx.assign %std_slice_0.in = %while_0_arg0_reg.out : i32
        calyx.assign %ext_mem1_addr0 = %std_slice_0.out : i5
        calyx.assign %ext_mem1_write_data = %stage_0_register_0_reg.out : i32
        calyx.assign %ext_mem1_write_en = %true : i1
        calyx.group_done %ext_mem1_done : i1
      }
    }
    calyx.control {
      calyx.seq {
        calyx.par {
          calyx.enable @assign_while_0_init_0
        }
        calyx.par {
          calyx.enable @bb0_1
          calyx.enable @bb0_2
        }
        calyx.while %std_lt_0.out with @bb0_0 {
          calyx.par {
            calyx.enable @bb0_1
            calyx.enable @bb0_2
            calyx.enable @bb0_3
          }
        } {bound = 19 : i64}
        calyx.par {
          calyx.enable @bb0_3
        }
      }
    }
  } {toplevel}
}

And the exported calyx code is:

import "primitives/core.futil";
component copy<"toplevel"=1>(ext_mem0_read_data: 32, ext_mem0_done: 1, ext_mem1_read_data: 32, ext_mem1_done: 1, @clk clk: 1, @reset reset: 1, @go go: 1) -> (ext_mem0_write_data: 32, ext_mem0_addr0: 5, ext_mem0_write_en: 1, ext_mem1_write_data: 32, ext_mem1_addr0: 5, ext_mem1_write_en: 1, @done done: 1) {
  cells {
    std_slice_1 = std_slice(32, 5);
    std_slice_0 = std_slice(32, 5);
    std_add_0 = std_add(32);
    std_lt_0 = std_lt(32);
    stage_0_register_0_reg = std_reg(32);
    while_0_arg0_reg = std_reg(32);
  }
  wires {
    group assign_while_0_init_0 {
      while_0_arg0_reg.in = 32'd0;
      while_0_arg0_reg.write_en = 1'd1;
      assign_while_0_init_0[done] = while_0_arg0_reg.done;
    }
    comb group bb0_0 {
      std_lt_0.left = while_0_arg0_reg.out;
      std_lt_0.right = 32'd20;
    }
    group bb0_1 {
      std_slice_1.in = while_0_arg0_reg.out;
      ext_mem0_addr0 = std_slice_1.out;
      stage_0_register_0_reg.in = ext_mem0_read_data;
      stage_0_register_0_reg.write_en = 1'd1;
      bb0_1[done] = stage_0_register_0_reg.done;
    }
    group bb0_2 {
      std_add_0.left = while_0_arg0_reg.out;
      std_add_0.right = 32'd1;
      while_0_arg0_reg.in = std_add_0.out;
      while_0_arg0_reg.write_en = 1'd1;
      bb0_2[done] = while_0_arg0_reg.done;
    }
    group bb0_3 {
      std_slice_0.in = while_0_arg0_reg.out;
      ext_mem1_addr0 = std_slice_0.out;
      ext_mem1_write_data = stage_0_register_0_reg.out;
      ext_mem1_write_en = 1'd1;
      bb0_3[done] = ext_mem1_done;
    }
  }
  control {
    seq {
      par {
        assign_while_0_init_0;
      }
      par {
        bb0_1;
        bb0_2;
      }
      @bound(19) while std_lt_0.out with bb0_0 {
        par {
          bb0_1;
          bb0_2;
          bb0_3;
        }
      }
      par {
        bb0_3;
      }
    }
  }
}

Then I changed the component name to main and tried to use native calyx compiler to run this program with Xilinx tools as these steps:

For me the first and third steps works fine. There are sv and xml files generated. But with the second step, I always get this error saying:

Error: Program has no memories marked with attribute @external. Please make sure that at least one memory is marked as @external.

Then I tried to add the @external attribute to the memory within cells block, but there is none!
I’m wondering if this is something wrong with the pass or translate or I should add memory usage myself, which either is problematic.

Please make sure that at least one memory is marked as @external.

Is this a problem with what CIRCT generates, or what Calyx native compiler expects? As far as I understood, this looks like CIRCT generates the right thing for a program whose top-level reads and writes external memories.

@rachitnigam do you know if there needs to be something else in a program like this?

Well, I think it’s something calyx expects. But I don’t know in which we can satisfy this: in the pass or for the calyx to have something.

So the problem here is that there is no memory generated from the affine code. It seems that the memory ports have been “flattened” into the copy component’s signature during compilation from affine to Calyx.

In general, during synthesis compilation, Calyx transforms any memory marked with the @external attribute by removing it from the cells section and flattening its ports into the signature in exactly the same way as the above code. However, in order to generate the correct AXI interfaces and host code, it needs access to the size and width of the memory which is not possible if the memory has already been flattened.

The fix would be to generate memories marked with @external in the CIRCT flow and let the Calyx compiler handle the rest of the lowering process. @HahaLan97 @mikeurbach could one of you open an issue about this in the CIRCT repo and maybe point to this thread? I can do some digging to figure out where the changed would need to occur.

I’ve already opened one, here it is: Exported Calyx doesn’t add memory · Issue #4718 · llvm/circt · GitHub

On reading the issue carefully, everything is behaving as expected. The component passes memories by reference which means it doesn’t have any of its own memories. More details in the issue above

Sorry for the late reply. I’ve seen your comments on GitHub, I get it now;)