Repository: EPFL-LAP/dynamatic
Pull request #356: [Buffers][Refactoring] Remove Internal Buffers in Mux/Merge/CMerge
Author: login: QinYuan2000
Created: 2025-03-21T11:40:41Z | Merged: 2025-05-16T22:02:20Z

This PR addresses Issue #342. See the [description](https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2887385308) for details.


Diff:
diff --git a/data/rtl-config-verilog.json b/data/rtl-config-verilog.json
index 21b531797c..d7b8e1332b 100644
--- a/data/rtl-config-verilog.json
+++ b/data/rtl-config-verilog.json
@@ -566,7 +566,6 @@
     ],
     "generic": "$DYNAMATIC/data/verilog/handshake/dataless/mux.v",
     "module-name": "mux_dataless",
-    "dependencies": ["tehb_dataless"],
     "hdl": "verilog"
   },
   {
@@ -577,7 +576,6 @@
       { "name": "SELECT_TYPE", "type": "dataflow", "data-lb": 1, "extra-eq": 0 }
     ],
     "generic": "$DYNAMATIC/data/verilog/handshake/mux.v",
-    "dependencies": ["tehb"],
     "hdl": "verilog"
   },
   {
@@ -595,7 +593,7 @@
     ],
     "generic": "$DYNAMATIC/data/verilog/handshake/dataless/control_merge.v",
     "module-name": "control_merge_dataless",
-    "dependencies": ["merge_notehb_dataless", "tehb", "fork_dataless"],
+    "dependencies": ["merge_dataless", "tehb", "fork_dataless"],
     "hdl": "verilog"
   },
   {
@@ -623,7 +621,6 @@
     ],
     "generic": "$DYNAMATIC/data/verilog/handshake/dataless/merge.v",
     "module-name": "merge_dataless",
-    "dependencies": ["tehb_dataless"],
     "hdl": "verilog"
   },
   {
@@ -633,7 +630,6 @@
       { "name": "DATA_TYPE", "type": "dataflow", "data-lb": 1, "extra-eq": 0 }
     ],
     "generic": "$DYNAMATIC/data/verilog/handshake/merge.v",
-    "dependencies": ["tehb"],
     "hdl": "verilog"
   },
   {
@@ -818,15 +814,6 @@
     "generic": "$DYNAMATIC/data/verilog/support/mc_support.v",
     "hdl": "verilog"
   },
-  {
-    "generic": "$DYNAMATIC/data/verilog/support/merge_notehb.v",
-    "hdl": "verilog"
-  },
-  {
-    "generic": "$DYNAMATIC/data/verilog/support/dataless/merge_notehb.v",
-    "module-name": "merge_notehb_dataless",
-    "hdl": "verilog"
-  },
   {
     "name": "mem_to_bram",
     "parameters": [
diff --git a/data/rtl-config-vhdl.json b/data/rtl-config-vhdl.json
index 93883a9c02..e27dedfd35 100644
--- a/data/rtl-config-vhdl.json
+++ b/data/rtl-config-vhdl.json
@@ -563,8 +563,7 @@
       { "name": "SELECT_TYPE", "type": "dataflow", "data-lb": 1, "extra-eq": 0 }
     ],
     "generic": "$DYNAMATIC/data/vhdl/handshake/dataless/mux.vhd",
-    "module-name": "mux_dataless",
-    "dependencies": ["tehb_dataless"]
+    "module-name": "mux_dataless"
   },
   {
     "name": "handshake.mux",
@@ -574,7 +573,7 @@
       { "name": "SELECT_TYPE", "type": "dataflow", "data-lb": 1, "extra-eq": 0 }
     ],
     "generic": "$DYNAMATIC/data/vhdl/handshake/mux.vhd",
-    "dependencies": ["types", "tehb"]
+    "dependencies": ["types"]
   },
   {
     "name": "handshake.control_merge",
@@ -591,7 +590,7 @@
     ],
     "generic": "$DYNAMATIC/data/vhdl/handshake/dataless/control_merge.vhd",
     "module-name": "control_merge_dataless",
-    "dependencies": ["merge_notehb_dataless", "tehb", "fork_dataless"]
+    "dependencies": ["merge_dataless", "tehb", "fork_dataless"]
   },
   {
     "name": "handshake.control_merge",
@@ -616,8 +615,7 @@
       }
     ],
     "generic": "$DYNAMATIC/data/vhdl/handshake/dataless/merge.vhd",
-    "module-name": "merge_dataless",
-    "dependencies": ["tehb_dataless"]
+    "module-name": "merge_dataless"
   },
   {
     "name": "handshake.merge",
@@ -626,7 +624,7 @@
       { "name": "DATA_TYPE", "type": "dataflow", "data-lb": 1, "extra-eq": 0 }
     ],
     "generic": "$DYNAMATIC/data/vhdl/handshake/merge.vhd",
-    "dependencies": ["types", "tehb", "merge_notehb"]
+    "dependencies": ["types"]
   },
   {
     "name": "handshake.br",
@@ -816,14 +814,6 @@
     "generic": "$DYNAMATIC/data/vhdl/support/mc_support.vhd",
     "dependencies": ["types"]
   },
-  {
-    "generic": "$DYNAMATIC/data/vhdl/support/merge_notehb.vhd",
-    "dependencies": ["types"]
-  },
-  {
-    "generic": "$DYNAMATIC/data/vhdl/support/dataless/merge_notehb.vhd",
-    "module-name": "merge_notehb_dataless"
-  },
   {
     "generic": "$DYNAMATIC/data/vhdl/support/types.vhd"
   },
diff --git a/data/verilog/handshake/dataless/control_merge.v b/data/verilog/handshake/dataless/control_merge.v
index b3a88f34b0..3a479b6790 100644
--- a/data/verilog/handshake/dataless/control_merge.v
+++ b/data/verilog/handshake/dataless/control_merge.v
@@ -37,8 +37,8 @@ module control_merge_dataless #(
   end
 
   // Instantiate Merge_dataless
-  merge_notehb_dataless #(
-    .INPUTS(SIZE)
+  merge_dataless #(
+    .SIZE(SIZE)
   ) merge_ins (
     .clk        (clk          ),
     .rst        (rst          ),
diff --git a/data/verilog/handshake/dataless/merge.v b/data/verilog/handshake/dataless/merge.v
index d4ce5cd0b5..6b39773c76 100644
--- a/data/verilog/handshake/dataless/merge.v
+++ b/data/verilog/handshake/dataless/merge.v
@@ -12,28 +12,22 @@ module merge_dataless #(
   input  outs_ready
 );
 
-  // Internal signal declaration
-  wire tehb_pvalid;
-  wire tehb_ready;
+  reg tmp_valid_out;
+  reg [SIZE - 1 : 0] tmp_ready_out;
+  integer i;
 
-  merge_notehb_dataless #(
-    .INPUTS(SIZE)
-  ) merge_ins (
-    .clk        (clk          ),
-    .rst        (rst          ),
-    .ins_valid  (ins_valid    ),
-    .ins_ready  (ins_ready    ),
-    .outs_valid (tehb_pvalid),
-    .outs_ready (tehb_ready)
-  );
-
-  tehb_dataless tehb (
-    .clk        (clk         ),
-    .rst        (rst         ),
-    .ins_valid  (tehb_pvalid ),
-    .outs_ready (outs_ready  ),
-    .outs_valid (outs_valid  ),
-    .ins_ready  (tehb_ready  )
-  );
+  always @(*) begin
+    tmp_valid_out = 0;
+    tmp_ready_out = {SIZE{1'b0}}; 
+    for (i = 0; i < SIZE; i = i + 1) begin
+      if (ins_valid[i] && !tmp_valid_out) begin
+        tmp_valid_out = 1;
+        tmp_ready_out[i] = outs_ready;
+      end
+    end
+  end
+  
+  assign outs_valid = tmp_valid_out;
+  assign ins_ready = tmp_ready_out;
 
 endmodule
diff --git a/data/verilog/handshake/dataless/mux.v b/data/verilog/handshake/dataless/mux.v
index 6c034d69a7..d8743d4511 100644
--- a/data/verilog/handshake/dataless/mux.v
+++ b/data/verilog/handshake/dataless/mux.v
@@ -16,18 +16,14 @@ module mux_dataless #(
   output outs_valid,
   input  outs_ready
 );
-  // Internal signal defintion
-  wire tehb_ins_valid;
-  wire tehb_ins_ready;
   reg selectedData_valid;
-
   integer i;
 
   always @(*) begin
     selectedData_valid = 0;
 
     for (i = SIZE - 1; i >= 0 ; i = i - 1) begin
-      if (((i[SELECT_TYPE - 1 : 0] == index) & index_valid & ins_valid[i] & tehb_ins_ready) | ~ins_valid[i])
+      if (((i[SELECT_TYPE - 1 : 0] == index) & index_valid & ins_valid[i] & outs_ready) | ~ins_valid[i])
         ins_ready[i] = 1;
       else
         ins_ready[i] = 0;
@@ -38,18 +34,7 @@ module mux_dataless #(
     end
   end
 
-  assign tehb_ins_valid = selectedData_valid;
-  assign index_ready = ~index_valid | (selectedData_valid & tehb_ins_ready);
-
-  // Instantiate the tehb_dataless module
-  tehb_dataless tehb (
-    .clk        (clk           ),
-    .rst        (rst           ),
-    .ins_valid  (tehb_ins_valid),
-    .ins_ready  (tehb_ins_ready),
-    .outs_valid (outs_valid    ),
-    .outs_ready (outs_ready    )
-  );
-
+  assign outs_valid = selectedData_valid;
+  assign index_ready = ~index_valid | (selectedData_valid & outs_ready);
 
 endmodule
diff --git a/data/verilog/handshake/merge.v b/data/verilog/handshake/merge.v
index 416f28d603..2ae0044225 100644
--- a/data/verilog/handshake/merge.v
+++ b/data/verilog/handshake/merge.v
@@ -14,35 +14,33 @@ module merge # (
   output outs_valid,
   input  outs_ready
 );
-  wire [DATA_TYPE - 1 : 0] tehb_data_in;
-  wire tehb_pvalid;
-  wire tehb_ready;
 
-  merge_notehb #(
-    .INPUTS(SIZE),
-    .DATA_TYPE(DATA_TYPE)
-  ) merge_ins (
-    .clk        (clk          ),
-    .rst        (rst          ),
-    .ins        (ins          ),
-    .ins_valid  (ins_valid    ),
-    .ins_ready  (ins_ready    ),
-    .outs       (tehb_data_in ),
-    .outs_valid (tehb_pvalid  ),
-    .outs_ready (tehb_ready   )
-  );
+  reg tmp_valid_out;
+  reg [SIZE - 1 : 0] tmp_ready_out;
+  reg [DATA_TYPE - 1 : 0] tmp_data_out;
+  integer i;
+  integer cnt;
 
-  tehb #(
-    .DATA_TYPE(DATA_TYPE)
-  ) tehb_inst (
-    .clk        (clk         ),
-    .rst        (rst         ),
-    .ins_valid  (tehb_pvalid ),
-    .outs_ready (outs_ready  ),
-    .outs_valid (outs_valid  ),
-    .ins_ready  (tehb_ready  ),
-    .ins        (tehb_data_in),
-    .outs       (outs        )
-  );
+  always @(*) begin
+    tmp_valid_out = 0;
+    tmp_ready_out = {SIZE{1'b0}}; 
+    tmp_data_out = ins[0 +: DATA_TYPE];
+
+    cnt = 1;
+    for (i = 0; i < INPUTS; i = i + 1) begin
+      if (cnt == 1 && ins_valid[i]) begin
+        tmp_data_out = ins[i * DATA_TYPE +: DATA_TYPE];
+        tmp_valid_out = 1;
+        tmp_ready_out[i] = outs_ready;
+        cnt = 0;
+      end
+    end
+  end
+  
+  // The outs channel is not persistent, meaning the data payload 
+  // may change while valid remains high
+  assign outs = tmp_data_out;
+  assign outs_valid = tmp_valid_out;
+  assign ins_ready = tmp_ready_out;
 
 endmodule
diff --git a/data/verilog/handshake/mux.v b/data/verilog/handshake/mux.v
index 94ba384370..dde3cf07c1 100644
--- a/data/verilog/handshake/mux.v
+++ b/data/verilog/handshake/mux.v
@@ -19,10 +19,6 @@ module mux #(
   output outs_valid,
   input  outs_ready
 );
-  wire [DATA_TYPE - 1 : 0] tehb_ins;
-  wire tehb_ins_ready;
-  wire tehb_ins_valid;
-
   reg [DATA_TYPE - 1 : 0] selectedData;
   reg selectedData_valid;
 
@@ -41,7 +37,7 @@ module mux #(
       selectedData_valid = 0;
 
       for (i = SIZE - 1; i >= 0; i = i - 1) begin
-        if (((i[SELECT_TYPE - 1 : 0] == index) & index_valid & tehb_ins_ready & ins_valid[i]) | ~ins_valid[i]) begin
+        if (((i[SELECT_TYPE - 1 : 0] == index) & index_valid & outs_ready & ins_valid[i]) | ~ins_valid[i]) begin
           ins_ready[i] = 1;
         end else begin
           ins_ready[i] = 0;
@@ -56,22 +52,8 @@ module mux #(
 
   end
 
-  assign index_ready = ~index_valid | (selectedData_valid & tehb_ins_ready);
-  assign tehb_ins = selectedData;
-  assign tehb_ins_valid = selectedData_valid;
-
-  // Instantiate the tehb module
-  tehb #(
-    .DATA_TYPE(DATA_TYPE)
-  ) tehb_inst (
-    .clk        (clk           ),
-    .rst        (rst           ),
-    .ins        (tehb_ins      ),
-    .ins_valid  (tehb_ins_valid),
-    .ins_ready  (tehb_ins_ready),
-    .outs       (outs          ),
-    .outs_valid (outs_valid    ),
-    .outs_ready (outs_ready    )
-  );
+  assign index_ready = ~index_valid | (selectedData_valid & outs_ready);
+  assign outs = selectedData;
+  assign outs_valid = selectedData_valid;
 
 endmodule
diff --git a/data/vhdl/handshake/dataless/control_merge.vhd b/data/vhdl/handshake/dataless/control_merge.vhd
index a6a23def5d..636f2098ef 100644
--- a/data/vhdl/handshake/dataless/control_merge.vhd
+++ b/data/vhdl/handshake/dataless/control_merge.vhd
@@ -37,7 +37,7 @@ begin
     end loop;
   end process;
 
-  merge_ins : entity work.merge_notehb_dataless(arch) generic map (SIZE)
+  merge_ins : entity work.merge_dataless(arch) generic map (SIZE)
     port map(
       clk        => clk,
       rst        => rst,
diff --git a/data/vhdl/handshake/dataless/merge.vhd b/data/vhdl/handshake/dataless/merge.vhd
index f0f6f62765..0e2b7f1fac 100644
--- a/data/vhdl/handshake/dataless/merge.vhd
+++ b/data/vhdl/handshake/dataless/merge.vhd
@@ -18,26 +18,24 @@ entity merge_dataless is
 end entity;
 
 architecture arch of merge_dataless is
-  signal tehb_pvalid : std_logic;
-  signal tehb_ready  : std_logic;
 begin
-  merge_ins : entity work.merge_notehb_dataless(arch) generic map (SIZE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs_valid => tehb_pvalid
-    );
+  process (ins_valid, outs_ready)
+    variable tmp_valid_out : std_logic;
+    variable tmp_ready_out : std_logic_vector(SIZE - 1 downto 0);
+  begin
+    tmp_valid_out := '0';
+    tmp_ready_out := (others => '0');
+    
+    for i in 0 to (SIZE - 1) loop
+      if (ins_valid(i) = '1') then
+        tmp_valid_out := '1';
+        tmp_ready_out(i) := outs_ready;
+        exit;
+      end if;
+    end loop;
 
-  tehb : entity work.tehb_dataless(arch)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready
-    );
+    outs_valid <= tmp_valid_out;
+    ins_ready <= tmp_ready_out;
+  end process;
+  
 end architecture;
diff --git a/data/vhdl/handshake/dataless/mux.vhd b/data/vhdl/handshake/dataless/mux.vhd
index f9d2e2957a..bca3412fc7 100644
--- a/data/vhdl/handshake/dataless/mux.vhd
+++ b/data/vhdl/handshake/dataless/mux.vhd
@@ -24,9 +24,8 @@ entity mux_dataless is
 end entity;
 
 architecture arch of mux_dataless is
-  signal tehb_ins_valid, tehb_ins_ready : std_logic;
 begin
-  process (ins_valid, outs_ready, index, index_valid, tehb_ins_ready)
+  process (ins_valid, outs_ready, index, index_valid)
     variable selectedData_valid, indexEqual : std_logic;
   begin
     selectedData_valid := '0';
@@ -41,22 +40,10 @@ begin
       if indexEqual and index_valid and ins_valid(i) then
         selectedData_valid := '1';
       end if;
-      ins_ready(i) <= (indexEqual and index_valid and ins_valid(i) and tehb_ins_ready) or (not ins_valid(i));
+      ins_ready(i) <= (indexEqual and index_valid and ins_valid(i) and outs_ready) or (not ins_valid(i));
     end loop;
 
-    index_ready    <= (not index_valid) or (selectedData_valid and tehb_ins_ready);
-    tehb_ins_valid <= selectedData_valid;
+    index_ready    <= (not index_valid) or (selectedData_valid and outs_ready);
+    outs_valid     <= selectedData_valid;
   end process;
-
-  tehb : entity work.tehb_dataless(arch)
-    port map(
-      clk => clk,
-      rst => rst,
-      -- input channel
-      ins_valid => tehb_ins_valid,
-      ins_ready => tehb_ins_ready,
-      -- output channel
-      outs_valid => outs_valid,
-      outs_ready => outs_ready
-    );
 end architecture;
diff --git a/data/vhdl/handshake/merge.vhd b/data/vhdl/handshake/merge.vhd
index d2202cd53d..acff8fcfb6 100644
--- a/data/vhdl/handshake/merge.vhd
+++ b/data/vhdl/handshake/merge.vhd
@@ -22,32 +22,30 @@ entity merge is
 end entity;
 
 architecture arch of merge is
-  signal tehb_data_in : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_pvalid  : std_logic;
-  signal tehb_ready   : std_logic;
 begin
-  
-  merge_ins : entity work.merge_notehb(arch) generic map (SIZE, DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins        => ins,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs       => tehb_data_in,
-      outs_valid => tehb_pvalid
-    );
+  process (ins_valid, ins, outs_ready)
+    variable tmp_data_out  : unsigned(DATA_TYPE - 1 downto 0);
+    variable tmp_valid_out : std_logic;
+    variable tmp_ready_out : std_logic_vector(SIZE - 1 downto 0);
+  begin
+    tmp_data_out  := unsigned(ins(0));
+    tmp_valid_out := '0';
+    tmp_ready_out := (others => '0');
 
-  tehb : entity work.tehb(arch) generic map (DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready,
-      ins        => tehb_data_in,
-      outs       => outs
-    );
+    for I in 0 to (SIZE - 1) loop
+      if (ins_valid(I) = '1') then
+        tmp_data_out  := unsigned(ins(I));
+        tmp_valid_out := '1';
+        tmp_ready_out(i) := outs_ready;
+        exit;
+      end if;
+    end loop;
+    
+    -- The outs channel is not persistent, meaning the data payload 
+    -- may change while valid remains high
+    outs <= std_logic_vector(resize(tmp_data_out, DATA_TYPE));
+    outs_valid  <= tmp_valid_out;
+    ins_ready <= tmp_ready_out;
+  end process;
+  
 end architecture;
diff --git a/data/vhdl/handshake/mux.vhd b/data/vhdl/handshake/mux.vhd
index 97c60bb09b..e9067e6c6b 100644
--- a/data/vhdl/handshake/mux.vhd
+++ b/data/vhdl/handshake/mux.vhd
@@ -28,10 +28,8 @@ entity mux is
 end entity;
 
 architecture arch of mux is
-  signal tehb_ins                       : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_ins_valid, tehb_ins_ready : std_logic;
 begin
-  process (ins, ins_valid, outs_ready, index, index_valid, tehb_ins_ready)
+  process (ins, ins_valid, outs_ready, index, index_valid)
     variable selectedData                   : std_logic_vector(DATA_TYPE - 1 downto 0);
     variable selectedData_valid, indexEqual : std_logic;
   begin
@@ -48,28 +46,11 @@ begin
         selectedData       := ins(i);
         selectedData_valid := '1';
       end if;
-      ins_ready(i) <= (indexEqual and index_valid and ins_valid(i) and tehb_ins_ready) or (not ins_valid(i));
+      ins_ready(i) <= (indexEqual and index_valid and ins_valid(i) and outs_ready) or (not ins_valid(i));
     end loop;
 
-    index_ready    <= (not index_valid) or (selectedData_valid and tehb_ins_ready);
-    tehb_ins       <= selectedData;
-    tehb_ins_valid <= selectedData_valid;
+    index_ready    <= (not index_valid) or (selectedData_valid and outs_ready);
+    outs           <= selectedData;
+    outs_valid     <= selectedData_valid;
   end process;
-
-  tehb : entity work.tehb(arch)
-    generic map(
-      DATA_TYPE => DATA_TYPE
-    )
-    port map(
-      clk => clk,
-      rst => rst,
-      -- input channel
-      ins       => tehb_ins,
-      ins_valid => tehb_ins_valid,
-      ins_ready => tehb_ins_ready,
-      -- output channel
-      outs       => outs,
-      outs_valid => outs_valid,
-      outs_ready => outs_ready
-    );
 end architecture;
diff --git a/data/vhdl/support/dataless/merge_notehb.vhd b/data/vhdl/support/dataless/merge_notehb.vhd
deleted file mode 100644
index 0f343c6396..0000000000
--- a/data/vhdl/support/dataless/merge_notehb.vhd
+++ /dev/null
@@ -1,41 +0,0 @@
-library ieee;
-use ieee.std_logic_1164.all;
-use ieee.numeric_std.all;
-
-entity merge_notehb_dataless is
-  generic (
-    INPUTS : integer
-  );
-  port (
-    clk, rst : in std_logic;
-    -- input channels
-    ins_valid : in  std_logic_vector(INPUTS - 1 downto 0);
-    ins_ready : out std_logic_vector(INPUTS - 1 downto 0);
-    -- output channel
-    outs_valid : out std_logic;
-    outs_ready : in  std_logic
-  );
-end entity;
-
-architecture arch of merge_notehb_dataless is
-begin
-  process (ins_valid, outs_ready)
-    variable tmp_valid_out : std_logic;
-    variable tmp_ready_out : std_logic_vector(INPUTS - 1 downto 0);
-  begin
-    tmp_valid_out := '0';
-    tmp_ready_out := (others => '0');
-    
-    for i in 0 to (INPUTS - 1) loop
-      if (ins_valid(i) = '1') then
-        tmp_valid_out := '1';
-        tmp_ready_out(i) := outs_ready;
-        exit;
-      end if;
-    end loop;
-
-    outs_valid <= tmp_valid_out;
-    ins_ready <= tmp_ready_out;
-  end process;
-
-end architecture;
\ No newline at end of file
diff --git a/data/vhdl/support/merge_notehb.vhd b/data/vhdl/support/merge_notehb.vhd
deleted file mode 100644
index 9243375de3..0000000000
--- a/data/vhdl/support/merge_notehb.vhd
+++ /dev/null
@@ -1,49 +0,0 @@
-library ieee;
-use ieee.std_logic_1164.all;
-use ieee.numeric_std.all;
-use work.types.all;
-
-entity merge_notehb is
-  generic (
-    INPUTS   : integer;
-    DATA_TYPE : integer
-  );
-  port (
-    clk, rst : in std_logic;
-    -- input channels
-    ins       : in  data_array(INPUTS - 1 downto 0)(DATA_TYPE - 1 downto 0);
-    ins_valid : in  std_logic_vector(INPUTS - 1 downto 0);
-    ins_ready : out std_logic_vector(INPUTS - 1 downto 0);
-    -- output channel
-    outs       : out std_logic_vector(DATA_TYPE - 1 downto 0);
-    outs_valid : out std_logic;
-    outs_ready : in  std_logic
-  );
-end entity;
-
-architecture arch of merge_notehb is
-begin
-  process (ins_valid, ins, outs_ready)
-    variable tmp_data_out  : unsigned(DATA_TYPE - 1 downto 0);
-    variable tmp_valid_out : std_logic;
-    variable tmp_ready_out : std_logic_vector(INPUTS - 1 downto 0);
-  begin
-    tmp_data_out  := unsigned(ins(0));
-    tmp_valid_out := '0';
-    tmp_ready_out := (others => '0');
-
-    for I in 0 to (INPUTS - 1) loop
-      if (ins_valid(I) = '1') then
-        tmp_data_out  := unsigned(ins(I));
-        tmp_valid_out := '1';
-        tmp_ready_out(i) := outs_ready;
-        exit;
-      end if;
-    end loop;
-
-    outs <= std_logic_vector(resize(tmp_data_out, DATA_TYPE));
-    outs_valid  <= tmp_valid_out;
-    ins_ready <= tmp_ready_out;
-  end process;
-
-end architecture;
diff --git a/include/dynamatic/Dialect/Handshake/HandshakeAttributes.h b/include/dynamatic/Dialect/Handshake/HandshakeAttributes.h
index 7f4f5b65f8..ebbe683a2b 100644
--- a/include/dynamatic/Dialect/Handshake/HandshakeAttributes.h
+++ b/include/dynamatic/Dialect/Handshake/HandshakeAttributes.h
@@ -83,6 +83,8 @@ struct ChannelBufProps {
   unsigned minOpaque;
   /// Maximum number of opaque slots allowed on the channel (inclusive).
   std::optional<unsigned> maxOpaque;
+  /// Minimum number of buffer slots allowed on the channel (inclusive).
+  unsigned minSlots;
   /// Combinational delay (in ns) from the output port to the buffer's input, if
   /// a buffer is placed on the channel.
   double inDelay;
@@ -100,6 +102,7 @@ struct ChannelBufProps {
                   std::optional<unsigned> maxTrans = std::nullopt,
                   unsigned minOpaque = 0,
                   std::optional<unsigned> maxOpaque = std::nullopt,
+                  unsigned minSlots = 0,
                   double inDelay = 0.0, double outDelay = 0.0,
                   double delay = 0.0);
 
@@ -126,7 +129,8 @@ template <typename Os>
 Os &operator<<(Os &os, ChannelBufProps &props) {
   os << "{\n\ttransparent slots: [" << props.minTrans << ", "
      << getMaxStr(props.maxTrans) << "\n\topaque slots: [" << props.minOpaque
-     << ", " << getMaxStr(props.maxOpaque) << "\n\tin/out delays: ("
+     << ", " << getMaxStr(props.maxOpaque) << "\n\tTotal slots: " 
+     << props.minSlots << ", " << "\n\tin/out delays: ("
      << props.inDelay << ", " << props.outDelay << ")"
      << "\n\ttotal delay: " << props.delay << "\n}\n";
   return os;
diff --git a/include/dynamatic/Dialect/Handshake/HandshakeAttributes.td b/include/dynamatic/Dialect/Handshake/HandshakeAttributes.td
index 91d7018f20..7e2c8ae705 100644
--- a/include/dynamatic/Dialect/Handshake/HandshakeAttributes.td
+++ b/include/dynamatic/Dialect/Handshake/HandshakeAttributes.td
@@ -293,6 +293,7 @@ def ChannelBufPropsAttr : Handshake_Attr<"ChannelBufProps", "channelBufProps"> {
                         "std::optional<unsigned>":$maxTrans,
                         "unsigned":$minOpaque,
                         "std::optional<unsigned>":$maxOpaque,
+                        "unsigned":$minSlots,
                         "mlir::FloatAttr":$inDelay,
                         "mlir::FloatAttr":$outDelay,
                         "mlir::FloatAttr":$delay
@@ -301,7 +302,7 @@ def ChannelBufPropsAttr : Handshake_Attr<"ChannelBufProps", "channelBufProps"> {
   let builders = [
     AttrBuilder<(ins "dynamatic::handshake::ChannelBufProps&":$props), [{
       return $_get(context, props.minTrans, props.maxTrans, props.minOpaque, 
-        props.maxOpaque, 
+        props.maxOpaque, props.minSlots,
         FloatAttr::get(mlir::Float32Type::get(context), props.inDelay), 
         FloatAttr::get(mlir::Float32Type::get(context), props.outDelay),
         FloatAttr::get(mlir::Float32Type::get(context), props.delay));
@@ -318,7 +319,7 @@ def ChannelBufPropsAttr : Handshake_Attr<"ChannelBufProps", "channelBufProps"> {
     /// attribute for easier data manipulation.
     inline dynamatic::handshake::ChannelBufProps getProps() {
       return dynamatic::handshake::ChannelBufProps(getMinTrans(), getMaxTrans(),
-        getMinOpaque(), getMaxOpaque(), getInDelay().getValueAsDouble(),
+        getMinOpaque(), getMaxOpaque(), getMinSlots(), getInDelay().getValueAsDouble(),
         getOutDelay().getValueAsDouble(), getDelay().getValueAsDouble());
     }
   }];
diff --git a/include/dynamatic/Transforms/BackAnnotate.h b/include/dynamatic/Transforms/BackAnnotate.h
index 172d080851..d48d7ad84b 100644
--- a/include/dynamatic/Transforms/BackAnnotate.h
+++ b/include/dynamatic/Transforms/BackAnnotate.h
@@ -80,6 +80,7 @@
 //          "maximum-trans": <integer>, /* optional, default infinity */
 //          "minimum-opaque": <integer>, /* optional, default 0 */
 //          "maximum-opaque": <integer>, /* optional, default infinity */
+//          "minimum-slots": <integer>, /* optional, default 0 */
 //          "input-delay": <float>, /* optional, default 0.0 */
 //          "output-delay": <float>, /* optional, default 0.0 */
 //          "unbuf-delay": <float> /* optional, default 0.0 */
diff --git a/lib/Dialect/Handshake/HandshakeAttributes.cpp b/lib/Dialect/Handshake/HandshakeAttributes.cpp
index 533362d7e5..c10ad08793 100644
--- a/lib/Dialect/Handshake/HandshakeAttributes.cpp
+++ b/lib/Dialect/Handshake/HandshakeAttributes.cpp
@@ -311,9 +311,10 @@ ChannelBufProps::ChannelBufProps(unsigned minTrans,
                                  std::optional<unsigned> maxTrans,
                                  unsigned minOpaque,
                                  std::optional<unsigned> maxOpaque,
+                                 unsigned minSlots,
                                  double inDelay, double outDelay, double delay)
     : minTrans(minTrans), maxTrans(maxTrans), minOpaque(minOpaque),
-      maxOpaque(maxOpaque), inDelay(inDelay), outDelay(outDelay),
+      maxOpaque(maxOpaque), minSlots(minSlots), inDelay(inDelay), outDelay(outDelay),
       delay(delay) {};
 
 bool ChannelBufProps::isSatisfiable() const {
@@ -328,8 +329,8 @@ bool ChannelBufProps::isBufferizable() const {
 
 bool ChannelBufProps::operator==(const ChannelBufProps &rhs) const {
   return (this->minTrans == rhs.minTrans) && (this->maxTrans == rhs.maxTrans) &&
-         (this->minOpaque == rhs.minOpaque) &&
-         (this->maxOpaque == rhs.maxOpaque) && (this->inDelay == rhs.inDelay) &&
+         (this->minOpaque == rhs.minOpaque) && (this->maxOpaque == rhs.maxOpaque) &&
+         (this->minSlots == rhs.minSlots) && (this->inDelay == rhs.inDelay) &&
          (this->outDelay == rhs.outDelay) && (this->delay == rhs.delay);
 }
 
@@ -372,6 +373,10 @@ Attribute ChannelBufPropsAttr::parse(AsmParser &odsParser, Type odsType) {
       odsParser.parseComma() || parseMaxSlots(odsParser, props.maxOpaque))
     return nullptr;
 
+  // Parse minimum number of slots
+  if (odsParser.parseComma() || odsParser.parseInteger(props.minSlots))
+    return nullptr;
+
   // Parse the delays
   if (odsParser.parseComma() || odsParser.parseFloat(props.inDelay) ||
       odsParser.parseComma() || odsParser.parseFloat(props.outDelay) ||
@@ -384,6 +389,7 @@ Attribute ChannelBufPropsAttr::parse(AsmParser &odsParser, Type odsType) {
 void ChannelBufPropsAttr::print(AsmPrinter &odsPrinter) const {
   odsPrinter << "[" << getMinTrans() << "," << getMaxStr(getMaxTrans()) << ", ["
              << getMinOpaque() << "," << getMaxStr(getMaxOpaque()) << ", "
+             << getMinSlots() << ", "
              << getInDelay().getValueAsDouble() << ", "
              << getOutDelay().getValueAsDouble() << ", "
              << getDelay().getValueAsDouble();
diff --git a/lib/Transforms/BackAnnotate.cpp b/lib/Transforms/BackAnnotate.cpp
index 495f648ac0..25770d82f4 100644
--- a/lib/Transforms/BackAnnotate.cpp
+++ b/lib/Transforms/BackAnnotate.cpp
@@ -133,7 +133,8 @@ static const ljson::Array *getAnnotationArray(const ljson::Value &topValue,
 /// Allowed keys for "buffering-properties" attribute type
 static const StringLiteral MINIMUM_TRANS("minimum-trans"),
     MAXIMUM_TRANS("maximum-trans"), MINIMUM_OPAQUE("minimum-opaque"),
-    MAXIMUM_OPAQUE("maximum-opaque"), INPUT_DELAY("input-delay"),
+    MAXIMUM_OPAQUE("maximum-opaque"), MINIMUM_SLOTS("minimum-slots"),
+    INPUT_DELAY("input-delay"),
     OUTPUT_DELAY("output-delay"), UNBUF_DELAY("unbuf-delay");
 
 /// Deserializes a JSON value into a handshake::ChannelBufPropsAttr. See
@@ -165,6 +166,9 @@ static bool fromJSON(const ljson::Value &value,
   keys[MAXIMUM_OPAQUE] = [&]() {
     return fromJSONIfPresent(*data, MAXIMUM_OPAQUE, props.maxOpaque, path);
   };
+  keys[MINIMUM_SLOTS] = [&]() {
+    return fromJSONIfPresent(*data, MINIMUM_SLOTS, props.minSlots, path);
+  };
   keys[INPUT_DELAY] = [&]() {
     return fromJSONIfPresent(*data, INPUT_DELAY, props.inDelay, path);
   };
diff --git a/lib/Transforms/BufferPlacement/FPGA20Buffers.cpp b/lib/Transforms/BufferPlacement/FPGA20Buffers.cpp
index 69a76308e4..12c05d2d60 100644
--- a/lib/Transforms/BufferPlacement/FPGA20Buffers.cpp
+++ b/lib/Transforms/BufferPlacement/FPGA20Buffers.cpp
@@ -125,8 +125,12 @@ void FPGA20Buffers::addCustomChannelConstraints(Value channel) {
     // Force the MILP to place a minimum number of transparent slots
     model.addConstr(chVars.bufNumSlots >= props.minTrans + dataBuf,
                     "custom_minTrans");
+  } else if (props.minSlots > 0) {
+    // Force the MILP to place a minimum number of slots
+    model.addConstr(chVars.bufNumSlots >= props.minSlots,
+                    "custom_minSlots");
   }
-  if (props.minOpaque + props.minTrans > 0)
+  if (props.minOpaque + props.minTrans + props.minSlots > 0)
     model.addConstr(chVars.bufPresent == 1, "custom_forceBuffers");
 
   // Set a maximum number of slots to be placed
diff --git a/lib/Transforms/BufferPlacement/FPL22Buffers.cpp b/lib/Transforms/BufferPlacement/FPL22Buffers.cpp
index 80d87fea19..4690a68df9 100644
--- a/lib/Transforms/BufferPlacement/FPL22Buffers.cpp
+++ b/lib/Transforms/BufferPlacement/FPL22Buffers.cpp
@@ -105,7 +105,7 @@ void FPL22BuffersBase::addCustomChannelConstraints(Value channel) {
   ChannelVars &chVars = vars.channelVars[channel];
 
   // Force buffer presence if at least one slot is requested
-  unsigned minSlots = props.minOpaque + props.minTrans;
+  unsigned minSlots = std::max(props.minOpaque + props.minTrans, props.minSlots);
   if (minSlots > 0) {
     model.addConstr(chVars.bufPresent == 1, "custom_forceBuffers");
     model.addConstr(chVars.bufNumSlots >= minSlots, "custom_minSlots");
diff --git a/lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp b/lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
index db11ec084a..559a0ea6fa 100644
--- a/lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
+++ b/lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
@@ -25,6 +25,7 @@
 #include "dynamatic/Transforms/HandshakeMaterialize.h"
 #include "mlir/IR/Value.h"
 #include "llvm/ADT/STLExtras.h"
+#include "dynamatic/Support/CFG.h"
 
 using namespace dynamatic;
 using namespace dynamatic::buffer;
@@ -108,12 +109,53 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // A merge with more than one input should have at least one
+  // buffer slot at its output, and this is necessary only if 
+  // the merge is on a cycle.
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        if (isChannelOnCycle(mergeRes)) {
+          channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+        }
+      }
+    }
+  }
+
+  // https://github.com/EPFL-LAP/dynamatic/issues/388
+  // To mitigate the latency asymmetry between LSQ group allocation 
+  // and the Store/Load operations, we set a minimum number of buffer 
+  // slots at Store/Load's input.
+  // This is a temporary workaround and a better solution is needed.
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    auto memOp = findMemInterface(storeOp.getAddressResult());
+    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
+    }
+  }
+
+  for (handshake::LoadOp loadOp : funcOp.getOps<handshake::LoadOp>()) {
+    auto memOp = findMemInterface(loadOp.getAddressResult());
+    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
+      continue;
+
+    for (Value operand : loadOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && 
+          !isa<handshake::MemoryOpInterface, handshake::ConstantOp>(defOp)) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
     }
   }
   
diff --git a/test/Transforms/BufferPlacement/handshake-set-buffering-properties.mlir b/test/Transforms/BufferPlacement/handshake-set-buffering-properties.mlir
index 6681197c31..e00a8bbd83 100644
--- a/test/Transforms/BufferPlacement/handshake-set-buffering-properties.mlir
+++ b/test/Transforms/BufferPlacement/handshake-set-buffering-properties.mlir
@@ -3,9 +3,8 @@
 
 // CHECK-LABEL:   handshake.func @mergeBufferTwoInputs(
 // CHECK-SAME:                                         %[[VAL_0:.*]]: !handshake.control<>, ...) -> !handshake.control<> attributes {argNames = ["start"], resNames = ["out0"]} {
-// CHECK:           %[[VAL_1:.*]]:2 = fork [2] %[[VAL_0]] {handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
+// CHECK:           %[[VAL_1:.*]]:2 = fork [2] %[[VAL_0]] {handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
 // CHECK:           %[[VAL_2:.*]] = merge %[[VAL_1]]#0, %[[VAL_1]]#1 : <>
-// CHECK:           end {handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} %[[VAL_2]] : <>
 // CHECK:         }
 handshake.func @mergeBufferTwoInputs(%start: !handshake.control<>) -> !handshake.control<> {
   %fork:2 = fork [2] %start : <>
@@ -17,9 +16,9 @@ handshake.func @mergeBufferTwoInputs(%start: !handshake.control<>) -> !handshake
 
 // CHECK-LABEL:   handshake.func @mcUnbuffered(
 // CHECK-SAME:                                 %[[VAL_0:.*]]: memref<64xi32>, %[[VAL_1:.*]]: !handshake.channel<i32>, %[[VAL_2:.*]]: !handshake.control<>, ...) -> (!handshake.channel<i32>, !handshake.control<>) attributes {argNames = ["memref", "addr", "start"], resNames = ["out0", "out1"]} {
-// CHECK:           %[[VAL_3:.*]], %[[VAL_4:.*]] = mem_controller{{\[}}%[[VAL_0]] : memref<64xi32>] %[[VAL_5:.*]]#0 (%[[VAL_6:.*]]) %[[VAL_5]]#1 {connectedBlocks = [0 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.channel<i32>) -> !handshake.channel<i32>
-// CHECK:           %[[VAL_5]]:2 = fork [2] %[[VAL_2]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
-// CHECK:           %[[VAL_6]], %[[VAL_7:.*]] = load{{\[}}%[[VAL_1]]] %[[VAL_3]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
+// CHECK:           %[[VAL_3:.*]], %[[VAL_4:.*]] = mem_controller{{\[}}%[[VAL_0]] : memref<64xi32>] %[[VAL_5:.*]]#0 (%[[VAL_6:.*]]) %[[VAL_5]]#1 {connectedBlocks = [0 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.channel<i32>) -> !handshake.channel<i32>
+// CHECK:           %[[VAL_5]]:2 = fork [2] %[[VAL_2]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
+// CHECK:           %[[VAL_6]], %[[VAL_7:.*]] = load{{\[}}%[[VAL_1]]] %[[VAL_3]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
 // CHECK:           end %[[VAL_7]], %[[VAL_4]] : <i32>, <>
 // CHECK:         }
 handshake.func @mcUnbuffered(%memref: memref<64xi32>, %addr: !handshake.channel<i32>, %start: !handshake.control<>) -> (!handshake.channel<i32>, !handshake.control<>) {
@@ -33,9 +32,9 @@ handshake.func @mcUnbuffered(%memref: memref<64xi32>, %addr: !handshake.channel<
 
 // CHECK-LABEL:   handshake.func @lsqUnbuffered(
 // CHECK-SAME:                                  %[[VAL_0:.*]]: memref<64xi32>, %[[VAL_1:.*]]: !handshake.channel<i32>, %[[VAL_2:.*]]: !handshake.control<>, ...) -> (!handshake.channel<i32>, !handshake.control<>) attributes {argNames = ["memref", "addr", "start"], resNames = ["out0", "out1"]} {
-// CHECK:           %[[VAL_3:.*]]:2 = lsq{{\[}}%[[VAL_0]] : memref<64xi32>] (%[[VAL_4:.*]]#0, %[[VAL_4]]#1, %[[VAL_5:.*]], %[[VAL_4]]#2)  {groupSizes = [1 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "3": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.control<>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>) -> (!handshake.channel<i32>, !handshake.control<>)
-// CHECK:           %[[VAL_4]]:3 = fork [3] %[[VAL_2]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
-// CHECK:           %[[VAL_5]], %[[VAL_6:.*]] = load{{\[}}%[[VAL_1]]] %[[VAL_3]]#0 {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
+// CHECK:           %[[VAL_3:.*]]:2 = lsq{{\[}}%[[VAL_0]] : memref<64xi32>] (%[[VAL_4:.*]]#0, %[[VAL_4]]#1, %[[VAL_5:.*]], %[[VAL_4]]#2)  {groupSizes = [1 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "3": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.control<>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>) -> (!handshake.channel<i32>, !handshake.control<>)
+// CHECK:           %[[VAL_4]]:3 = fork [3] %[[VAL_2]] {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
+// CHECK:           %[[VAL_5]], %[[VAL_6:.*]] = load{{\[}}%[[VAL_1]]] %[[VAL_3]]#0 {handshake.bb = 0 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
 // CHECK:           end %[[VAL_6]], %[[VAL_3]]#1 : <i32>, <>
 // CHECK:         }
 handshake.func @lsqUnbuffered(%memref: memref<64xi32>, %addr: !handshake.channel<i32>, %start: !handshake.control<>) -> (!handshake.channel<i32>, !handshake.control<>) {
@@ -49,21 +48,21 @@ handshake.func @lsqUnbuffered(%memref: memref<64xi32>, %addr: !handshake.channel
 
 // CHECK-LABEL:   handshake.func @lsqBufferControlPath(
 // CHECK-SAME:                                         %[[VAL_0:.*]]: memref<64xi32>, %[[VAL_1:.*]]: !handshake.control<>, ...) -> (!handshake.channel<i32>, !handshake.control<>) attributes {argNames = ["memref", "start"], resNames = ["out0", "out1"]} {
-// CHECK:           %[[VAL_2:.*]]:4 = lsq{{\[}}%[[VAL_0]] : memref<64xi32>] (%[[VAL_3:.*]]#4, %[[VAL_3]]#0, %[[VAL_4:.*]], %[[VAL_5:.*]]#0, %[[VAL_6:.*]], %[[VAL_7:.*]]#0, %[[VAL_8:.*]], %[[VAL_7]]#2)  {groupSizes = [1 : i32, 1 : i32, 1 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "3": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "4": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "5": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "6": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "7": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.control<>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>) -> (!handshake.channel<i32>, !handshake.channel<i32>, !handshake.channel<i32>, !handshake.control<>)
-// CHECK:           %[[VAL_9:.*]] = merge %[[VAL_1]], %[[VAL_5]]#2 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,inf], [1,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
-// CHECK:           %[[VAL_3]]:5 = lazy_fork [5] %[[VAL_9]] {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
-// CHECK:           %[[VAL_10:.*]] = constant %[[VAL_3]]#1 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = false} : <>, <i1>
-// CHECK:           %[[VAL_11:.*]] = constant %[[VAL_3]]#2 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 0 : i32} : <>, <i32>
-// CHECK:           %[[VAL_4]], %[[VAL_12:.*]] = load{{\[}}%[[VAL_11]]] %[[VAL_2]]#0 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
-// CHECK:           %[[VAL_13:.*]], %[[VAL_14:.*]] = cond_br %[[VAL_10]], %[[VAL_3]]#3 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,inf], [1,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i1>, <>
+// CHECK:           %[[VAL_2:.*]]:4 = lsq{{\[}}%[[VAL_0]] : memref<64xi32>] (%[[VAL_3:.*]]#4, %[[VAL_3]]#0, %[[VAL_4:.*]], %[[VAL_5:.*]]#0, %[[VAL_6:.*]], %[[VAL_7:.*]]#0, %[[VAL_8:.*]], %[[VAL_7]]#2)  {groupSizes = [1 : i32, 1 : i32, 1 : i32], handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "2": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "3": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "4": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "5": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "6": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "7": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : (!handshake.control<>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>, !handshake.channel<i32>, !handshake.control<>) -> (!handshake.channel<i32>, !handshake.channel<i32>, !handshake.channel<i32>, !handshake.control<>)
+// CHECK:           %[[VAL_9:.*]] = merge %[[VAL_1]], %[[VAL_5]]#2 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00, "1": [0,inf], [1,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
+// CHECK:           %[[VAL_3]]:5 = lazy_fork [5] %[[VAL_9]] {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [0,inf], [0,inf], 1, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <>
+// CHECK:           %[[VAL_10:.*]] = constant %[[VAL_3]]#1 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = false} : <>, <i1>
+// CHECK:           %[[VAL_11:.*]] = constant %[[VAL_3]]#2 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 0 : i32} : <>, <i32>
+// CHECK:           %[[VAL_4]], %[[VAL_12:.*]] = load{{\[}}%[[VAL_11]]] %[[VAL_2]]#0 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
+// CHECK:           %[[VAL_13:.*]], %[[VAL_14:.*]] = cond_br %[[VAL_10]], %[[VAL_3]]#3 {handshake.bb = 1 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,inf], [1,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i1>, <>
 // CHECK:           sink %[[VAL_12]] : <i32>
 // CHECK:           %[[VAL_5]]:3 = lazy_fork [3] %[[VAL_13]] {handshake.bb = 2 : ui32} : <>
-// CHECK:           %[[VAL_15:.*]] = constant %[[VAL_5]]#1 {handshake.bb = 2 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 1 : i32} : <>, <i32>
-// CHECK:           %[[VAL_6]], %[[VAL_16:.*]] = load{{\[}}%[[VAL_15]]] %[[VAL_2]]#1 {handshake.bb = 2 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
+// CHECK:           %[[VAL_15:.*]] = constant %[[VAL_5]]#1 {handshake.bb = 2 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 1 : i32} : <>, <i32>
+// CHECK:           %[[VAL_6]], %[[VAL_16:.*]] = load{{\[}}%[[VAL_15]]] %[[VAL_2]]#1 {handshake.bb = 2 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
 // CHECK:           sink %[[VAL_16]] : <i32>
 // CHECK:           %[[VAL_7]]:3 = lazy_fork [3] %[[VAL_14]] {handshake.bb = 3 : ui32} : <>
-// CHECK:           %[[VAL_17:.*]] = constant %[[VAL_7]]#1 {handshake.bb = 3 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 2 : i32} : <>, <i32>
-// CHECK:           %[[VAL_8]], %[[VAL_18:.*]] = load{{\[}}%[[VAL_17]]] %[[VAL_2]]#2 {handshake.bb = 3 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
+// CHECK:           %[[VAL_17:.*]] = constant %[[VAL_7]]#1 {handshake.bb = 3 : ui32, handshake.bufProps = #handshake<bufProps{"0": [1,inf], [0,inf], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>, value = 2 : i32} : <>, <i32>
+// CHECK:           %[[VAL_8]], %[[VAL_18:.*]] = load{{\[}}%[[VAL_17]]] %[[VAL_2]]#2 {handshake.bb = 3 : ui32, handshake.bufProps = #handshake<bufProps{"1": [0,0], [0,0], 0, 0.000000e+00, 0.000000e+00, 0.000000e+00}>} : <i32>, <i32>, <i32>, <i32>
 // CHECK:           end {handshake.bb = 3 : ui32} %[[VAL_18]], %[[VAL_2]]#3 : <i32>, <>
 // CHECK:         }
 handshake.func @lsqBufferControlPath(%memref: memref<64xi32>, %start: !handshake.control<>) -> (!handshake.channel<i32>, !handshake.control<>) {


Reviews:
user: login: shundroid
body: Thank you for your changes!!
A few comments:
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T19:01:07Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T01:23:38Z
user: login: murphe67
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T17:12:35Z
user: login: shundroid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T18:42:01Z
user: login: Jiahui17
body: @QinYuan2000 You are right, removing min/max Trans/Opaque should be a separate PR. I have a small comment
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-05T20:24:56Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T08:33:12Z
user: login: shundroid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T15:06:14Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T22:40:02Z
user: login: Jiahui17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T17:57:36Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T19:30:10Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T19:37:49Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T19:51:09Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T19:51:34Z
user: login: Jiahui17
body: Very good! A very small suggestion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T21:04:03Z
user: login: QinYuan2000
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T21:34:49Z
user: login: Jiahui17
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T22:00:37Z

Review comments:
diff_hunk: @@ -22,32 +22,29 @@ entity merge is
 end entity;
 
 architecture arch of merge is
-  signal tehb_data_in : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_pvalid  : std_logic;
-  signal tehb_ready   : std_logic;
 begin
-  
-  merge_ins : entity work.merge_notehb(arch) generic map (SIZE, DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins        => ins,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs       => tehb_data_in,
-      outs_valid => tehb_pvalid
-    );
 
-  tehb : entity work.tehb(arch) generic map (DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready,
-      ins        => tehb_data_in,
-      outs       => outs
-    );
path: data/vhdl/handshake/merge.vhd
user: login: shundroid
body: if you remove tehb from merge, isn't it the same as `merge_notehb`?
created_at: 2025-03-21T18:52:00Z
author_association: COLLABORATOR
diff_hunk: @@ -116,6 +116,20 @@ void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
     }
   }
 
+  for (handshake::ControlMergeOp cmergeOp : funcOp.getOps<handshake::ControlMergeOp>()) {
+    if (cmergeOp->getNumOperands() > 1) {
+      Channel channel(cmergeOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
+
+  for (handshake::MuxOp muxOp : funcOp.getOps<handshake::MuxOp>()) {
+    if (muxOp->getNumOperands() > 1) {
+      Channel channel(muxOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: shundroid
body: This is not an urgent change but I believe these also should be HandshakeInterface, like `RequiringBufferInterface`, because people who add new handshake ops cannot understand this specification is needed unless they find this place by chance. (CC: @murphe67)

created_at: 2025-03-21T18:56:14Z
author_association: COLLABORATOR
diff_hunk: @@ -22,32 +22,29 @@ entity merge is
 end entity;
 
 architecture arch of merge is
-  signal tehb_data_in : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_pvalid  : std_logic;
-  signal tehb_ready   : std_logic;
 begin
-  
-  merge_ins : entity work.merge_notehb(arch) generic map (SIZE, DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins        => ins,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs       => tehb_data_in,
-      outs_valid => tehb_pvalid
-    );
 
-  tehb : entity work.tehb(arch) generic map (DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready,
-      ins        => tehb_data_in,
-      outs       => outs
-    );
path: data/vhdl/handshake/merge.vhd
user: login: QinYuan2000
body: Yes. I planned to remove `merge_notehb`. Now it's done.
created_at: 2025-03-22T01:23:38Z
author_association: COLLABORATOR
diff_hunk: @@ -116,6 +116,20 @@ void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
     }
   }
 
+  for (handshake::ControlMergeOp cmergeOp : funcOp.getOps<handshake::ControlMergeOp>()) {
+    if (cmergeOp->getNumOperands() > 1) {
+      Channel channel(cmergeOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
+
+  for (handshake::MuxOp muxOp : funcOp.getOps<handshake::MuxOp>()) {
+    if (muxOp->getNumOperands() > 1) {
+      Channel channel(muxOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: murphe67
body: I don't understand this, probably you can explain in our meeting?
created_at: 2025-03-22T17:12:34Z
author_association: COLLABORATOR
diff_hunk: @@ -116,6 +116,20 @@ void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
     }
   }
 
+  for (handshake::ControlMergeOp cmergeOp : funcOp.getOps<handshake::ControlMergeOp>()) {
+    if (cmergeOp->getNumOperands() > 1) {
+      Channel channel(cmergeOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
+
+  for (handshake::MuxOp muxOp : funcOp.getOps<handshake::MuxOp>()) {
+    if (muxOp->getNumOperands() > 1) {
+      Channel channel(muxOp.getResult(), true);
+      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+    }
+  }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: shundroid
body: `RequiringBufferInterface` provides one method called `getMinTransBuffer(OpOperand &operand)`. If the op requires buffer placement (e.g., Mux, CMerge, Load), it implements this method and returns the necessary buffer size per operand.

And the buffer placement pass just calls that method if provided.
created_at: 2025-03-22T18:42:01Z
author_association: COLLABORATOR
diff_hunk: @@ -108,15 +108,40 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
     }
   }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+  
+    if (!connectedToLSQ)
+      continue;
   
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      if (channel.props->maxTrans.value_or(1) > 0) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 2U);
+      } else {
+        storeOp->emitWarning()
+            << "Store input channel (connected to LSQ) should have transparent buffer, but not allowed";
+      }
+    }
+  }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: This addresses #388, right? Should it be a separate fix (also, I am not convinced that we need to put two transparent at the input of every store)?
created_at: 2025-05-05T20:23:53Z
author_association: MEMBER
diff_hunk: @@ -108,15 +108,40 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
     }
   }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+  
+    if (!connectedToLSQ)
+      continue;
   
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      if (channel.props->maxTrans.value_or(1) > 0) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 2U);
+      } else {
+        storeOp->emitWarning()
+            << "Store input channel (connected to LSQ) should have transparent buffer, but not allowed";
+      }
+    }
+  }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: These are separate issues. My concern is that if we remove the internal buffers first and only address #388 later, Dynamatic might perform poorly in the meantime. I’d rather not risk users picking a bad version without realizing it. How about creating a separate PR to fix it and merging the two with minimal delay?

> (also, I am not convinced that we need to put two transparent at the input of every store)

Yes. This is not always necessary. In my opinion, it is necessary to place a transparent buffer only when 

- The store and LSQ share the same upstream fork, and
- The store path has equal or lower latency (latency = token occupancy × II) than the other path.

One slot is enough; two are used to prevent TEHB from replacing a transparent FIFO. This can be refined later.
created_at: 2025-05-06T08:33:12Z
author_association: COLLABORATOR
diff_hunk: @@ -22,32 +22,28 @@ entity merge is
 end entity;
 
 architecture arch of merge is
-  signal tehb_data_in : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_pvalid  : std_logic;
-  signal tehb_ready   : std_logic;
 begin
-  
-  merge_ins : entity work.merge_notehb(arch) generic map (SIZE, DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins        => ins,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs       => tehb_data_in,
-      outs_valid => tehb_pvalid
-    );
+  process (ins_valid, ins, outs_ready)
+    variable tmp_data_out  : unsigned(DATA_TYPE - 1 downto 0);
+    variable tmp_valid_out : std_logic;
+    variable tmp_ready_out : std_logic_vector(SIZE - 1 downto 0);
+  begin
+    tmp_data_out  := unsigned(ins(0));
+    tmp_valid_out := '0';
+    tmp_ready_out := (others => '0');
+
+    for I in 0 to (SIZE - 1) loop
+      if (ins_valid(I) = '1') then
+        tmp_data_out  := unsigned(ins(I));
+        tmp_valid_out := '1';
+        tmp_ready_out(i) := outs_ready;
+        exit;
+      end if;
+    end loop;
 
-  tehb : entity work.tehb(arch) generic map (DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready,
-      ins        => tehb_data_in,
-      outs       => outs
-    );
+    outs <= std_logic_vector(resize(tmp_data_out, DATA_TYPE));
path: data/vhdl/handshake/merge.vhd
user: login: shundroid
body: Could you add a comment like "The `outs` channel is not persistent, meaning the data payload may change while `valid` remains high." somewhere? I think it's important to either include a comment in this file or add documentation about the persistency of the channel. Something like "the non-persistency affects components like the eager fork and we may need to insert buffers between them (though I'm not yet sure what kind)". See the discussion in #373 for context.
created_at: 2025-05-14T15:06:13Z
author_association: COLLABORATOR
diff_hunk: @@ -22,32 +22,28 @@ entity merge is
 end entity;
 
 architecture arch of merge is
-  signal tehb_data_in : std_logic_vector(DATA_TYPE - 1 downto 0);
-  signal tehb_pvalid  : std_logic;
-  signal tehb_ready   : std_logic;
 begin
-  
-  merge_ins : entity work.merge_notehb(arch) generic map (SIZE, DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins        => ins,
-      ins_valid  => ins_valid,
-      outs_ready => tehb_ready,
-      ins_ready  => ins_ready,
-      outs       => tehb_data_in,
-      outs_valid => tehb_pvalid
-    );
+  process (ins_valid, ins, outs_ready)
+    variable tmp_data_out  : unsigned(DATA_TYPE - 1 downto 0);
+    variable tmp_valid_out : std_logic;
+    variable tmp_ready_out : std_logic_vector(SIZE - 1 downto 0);
+  begin
+    tmp_data_out  := unsigned(ins(0));
+    tmp_valid_out := '0';
+    tmp_ready_out := (others => '0');
+
+    for I in 0 to (SIZE - 1) loop
+      if (ins_valid(I) = '1') then
+        tmp_data_out  := unsigned(ins(I));
+        tmp_valid_out := '1';
+        tmp_ready_out(i) := outs_ready;
+        exit;
+      end if;
+    end loop;
 
-  tehb : entity work.tehb(arch) generic map (DATA_TYPE)
-    port map(
-      clk        => clk,
-      rst        => rst,
-      ins_valid  => tehb_pvalid,
-      outs_ready => outs_ready,
-      outs_valid => outs_valid,
-      ins_ready  => tehb_ready,
-      ins        => tehb_data_in,
-      outs       => outs
-    );
+    outs <= std_logic_vector(resize(tmp_data_out, DATA_TYPE));
path: data/vhdl/handshake/merge.vhd
user: login: QinYuan2000
body: Updated. Thanks!
created_at: 2025-05-14T22:40:02Z
author_association: COLLABORATOR
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: Also it is necessary only if the merge is on any cycles
created_at: 2025-05-16T17:53:20Z
author_association: MEMBER
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: I think this is a bit cleaner:

https://github.com/EPFL-LAP/dynamatic/blob/aab53187654209c1353cd0d427205f36c6ca6b79/lib/Support/TimingModels.cpp#L136-L138
created_at: 2025-05-16T17:56:09Z
author_association: MEMBER
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
+    }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: Link to the issue + write down the reason for the change.
created_at: 2025-05-16T17:56:46Z
author_association: MEMBER
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
+    }
+  }
+
+  for (handshake::LoadOp loadOp : funcOp.getOps<handshake::LoadOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : loadOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : loadOp->getOperands()) {
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: Link to the issue + write down the reason for the change.
created_at: 2025-05-16T17:57:01Z
author_association: MEMBER
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: Updated:
```c++
for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
    auto memOp = findMemInterface(storeOp.getAddressResult());
    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
      continue;

    for (Value operand : storeOp->getOperands()) {
      Channel channel(operand, true);
      Operation *defOp = operand.getDefiningOp();

      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
      }
    }
  }

  for (handshake::LoadOp loadOp : funcOp.getOps<handshake::LoadOp>()) {
    auto memOp = findMemInterface(loadOp.getAddressResult());
    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
      continue;

    for (Value operand : loadOp->getOperands()) {
      Channel channel(operand, true);
      Operation *defOp = operand.getDefiningOp();

      if (defOp && 
          !isa<handshake::MemoryOpInterface, handshake::ConstantOp>(defOp)) {
        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
      }
    }
  }
  ```
created_at: 2025-05-16T19:30:09Z
author_association: COLLABORATOR
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: Updated:

```c++
// A merge with more than one input should have at least one
  // buffer slot at its output, and this is necessary only if 
  // the merge is on a cycle.
  for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
    if (mergeOp->getNumOperands() > 1) {
      for (OpResult mergeRes : mergeOp->getResults()) {
        Channel channel(mergeRes, true);
        if (isChannelOnCycle(mergeRes)) {
          channel.props->minSlots = std::max(channel.props->minSlots, 1U);
        }
      }
    }
  }
```
created_at: 2025-05-16T19:37:49Z
author_association: COLLABORATOR
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
+    }
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: Updated:

```c++
// https://github.com/EPFL-LAP/dynamatic/issues/388
// To mitigate the latency asymmetry between LSQ group allocation 
// and the Store/Load operations, we set a minimum number of buffer 
// slots at Store/Load's input.
// This is a temporary workaround and a better solution is needed.
for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
  auto memOp = findMemInterface(storeOp.getAddressResult());
  if (!isa<handshake::LSQOp>(memOp))
    continue;

  for (Value operand : storeOp->getOperands()) {
    Channel channel(operand, true);
    Operation *defOp = operand.getDefiningOp();

    if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
    }
  }
}
```

created_at: 2025-05-16T19:51:09Z
author_association: COLLABORATOR
diff_hunk: @@ -108,12 +108,58 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // Merges with more than one input should have at least one
+  // buffer slot at their output
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+      }
+    }
+  }
+
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : storeOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
+        channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      }
+    }
+  }
+
+  for (handshake::LoadOp loadOp : funcOp.getOps<handshake::LoadOp>()) {
+    bool connectedToLSQ = false;
+    for (Operation *user : loadOp->getUsers()) {
+      if (isa<handshake::LSQOp>(user)) {
+        connectedToLSQ = true;
+        break;
+      }
+    }
+
+    if (!connectedToLSQ)
+      continue;
+
+    for (Value operand : loadOp->getOperands()) {
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: Addressed in the update above.
created_at: 2025-05-16T19:51:34Z
author_association: COLLABORATOR
diff_hunk: @@ -108,12 +109,53 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // A merge with more than one input should have at least one
+  // buffer slot at its output, and this is necessary only if 
+  // the merge is on a cycle.
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        if (isChannelOnCycle(mergeRes)) {
+          channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+        }
+      }
+    }
+  }
+
+  // https://github.com/EPFL-LAP/dynamatic/issues/388
+  // To mitigate the latency asymmetry between LSQ group allocation 
+  // and the Store/Load operations, we set a minimum number of buffer 
+  // slots at Store/Load's input.
+  // This is a temporary workaround and a better solution is needed.
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    auto memOp = findMemInterface(storeOp.getAddressResult());
+    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: Jiahui17
body: I don't think we need to check for MemoryOpInterface for stores (the predecessors of the store are always operations in the circuit
created_at: 2025-05-16T20:59:02Z
author_association: MEMBER
diff_hunk: @@ -108,12 +109,53 @@ static void setLSQControlConstraints(handshake::LSQOp lsqOp) {
 }
 
 void dynamatic::buffer::setFPGA20Properties(handshake::FuncOp funcOp) {
-  // Merges with more than one input should have at least a transparent slot
-  // at their output
+  // A merge with more than one input should have at least one
+  // buffer slot at its output, and this is necessary only if 
+  // the merge is on a cycle.
   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
     if (mergeOp->getNumOperands() > 1) {
-      Channel channel(mergeOp.getResult(), true);
-      channel.props->minTrans = std::max(channel.props->minTrans, 1U);
+      for (OpResult mergeRes : mergeOp->getResults()) {
+        Channel channel(mergeRes, true);
+        if (isChannelOnCycle(mergeRes)) {
+          channel.props->minSlots = std::max(channel.props->minSlots, 1U);
+        }
+      }
+    }
+  }
+
+  // https://github.com/EPFL-LAP/dynamatic/issues/388
+  // To mitigate the latency asymmetry between LSQ group allocation 
+  // and the Store/Load operations, we set a minimum number of buffer 
+  // slots at Store/Load's input.
+  // This is a temporary workaround and a better solution is needed.
+  for (handshake::StoreOp storeOp : funcOp.getOps<handshake::StoreOp>()) {
+    auto memOp = findMemInterface(storeOp.getAddressResult());
+    if (!mlir::isa_and_present<handshake::LSQOp>(memOp))
+      continue;
+
+    for (Value operand : storeOp->getOperands()) {
+      Channel channel(operand, true);
+      Operation *defOp = operand.getDefiningOp();
+
+      if (defOp && !isa<handshake::MemoryOpInterface>(defOp)) {
path: lib/Transforms/BufferPlacement/HandshakeSetBufferingProperties.cpp
user: login: QinYuan2000
body: Updated.
created_at: 2025-05-16T21:34:49Z
author_association: COLLABORATOR

PR comments:
user: login: Jiahui17
created_at: 2025-03-21T11:45:15Z
body: @Carmine50 @lana555 @kb29x37 @gioelegott @shundroid @murphe67 @paolo-ienne 

What do you think about this change?
author_association: MEMBER
user: login: Jiahui17
created_at: 2025-03-21T11:54:10Z
body: @QinYuan2000 Could you update the PR descriptions with the following info?

- Describe the motivation on your side: This deducting internal buffers makes the MILP -> buffer instantiation logic very complex.
- How does it affect our results (summarize what happens in the PDF). If there is no performance/resource penalty, highlight why.
- This is for people who aren't familiar with buffer placement: Tell us where are all the buffering rules maintained.
author_association: MEMBER
user: login: murphe67
created_at: 2025-03-21T12:24:35Z
body: I actually had no idea these had buffers in them til Louis brought it up.

Though I have been talking to @shundroid recently about the tehbs for the load operations/memory controllers, we don't understand how they work, seems spiritually similiar.
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-03-21T13:12:31Z
body: @murphe67 

> I actually had no idea these had buffers in them til Louis brought it up.

So in the spirit of "what is an operation", it makes sense to remove them? If we explain to someone what is a merge, we shouldn't tell them that this merge has a transparent buffer inside. 

This information (i.e., a merge has to have a buffer at its output) is only relevant when we talk about "what are the rules that we need to follow when connecting a merge to other units".

> Though I have been talking to @shundroid recently about the tehbs for the load operations/memory controllers, we don't understand how they work, seems spiritually similiar.

I guess we should handle them in the same way as Merge/Mux/CMerge (centralize all the buffering constraints in one file and avoid any buffers inside).

---

side note for buffers in the load: They are there for avoiding deadlocks (the resource sharing problem) although solved in a very hacky way. 

author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-03-21T15:15:15Z
body: @Jiahui17 Sure.

> * Describe the motivation on your side: This deducting internal buffers makes the MILP -> buffer instantiation logic very complex.

This is explained in the Problem section.

> * How does it affect our results (summarize what happens in the PDF). If there is no performance/resource penalty, highlight why.

This is explained in the Result section.

> * This is for people who aren't familiar with buffer placement: Tell us where are all the buffering rules maintained.

This is explained in the Motivation section.
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-03-22T15:53:38Z
body: @Jiahui17 

Hello Jiahui,

Simply removing the TEHB in HDL files will lower the throughput. As I mentioned in Issue #342, the MILPs are not aware of the token reordering problem. They only balance token occupancies to optimize throughput and use as few buffer slots as possible. But the problem is not that “there must be a TEHB on the channel”, but rather that “there must be one more TEHB on the channel than indicated by the MILP solution to maintain optimal throughput”. So only telling the MILP that there’s at least one TEHB is not enough. That’s why the previous implementation actually didn’t deduct the internal buffer—they deliberately set the deducted slots to 0 incorrectly to ensure there was one more TEHB than the MILP solution, in order to avoid this problem.

I’m not clear whether the LSQ’s problem is a “there must be a TEHB on the channel” problem, or a “there must be one more TEHB on the channel than the MILP solution” problem. If it is the former, then there are two problems which should be handled in different ways.

So I propose adding the variables `extraTrans` and `extraOpaque` to the struct `ChannelBufProps` to indicate that there should be extra buffer slots on top of the MILP solution.

```c++
struct ChannelBufProps {
  /// Minimum number of transparent slots allowed on the channel (inclusive).
  unsigned minTrans;
  /// Maximum number of transparent slots allowed on the channel (inclusive).
  std::optional<unsigned> maxTrans;
  /// Minimum number of opaque slots allowed on the channel (inclusive).
  unsigned minOpaque;
  /// Maximum number of opaque slots allowed on the channel (inclusive).
  std::optional<unsigned> maxOpaque;
  /// Number of additional transparent slots on the channel.
  unsigned extraTrans;
  /// Number of additional opaque slots on the channel.
  unsigned extraOpaque;
```

And add them directly to the MILP results in the `extractResult` function of the MILP classes. 

If both LSQ's problem and internal buffers are the “there must be one more TEHB on the channel than the MILP solution” problem, then we do not need the new variables and can let `minTrans` and `minOpaque` handle that.

Does this approach make sense to you?
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-03-22T16:49:14Z
body: @QinYuan2000

I don't see why 
> Simply removing the TEHB in HDL files will lower the throughput.

Could you show two DOT files showing the placement before and after?
author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-03-22T19:21:02Z
body: > Could you show two DOT files showing the placement before and after?

Sure. These are the DOT files (in TXT form) before and after the revision. Specifically, the TEHB number on the output channel of Merge/CMerge/Mux is one more than before to compensate the removed internal TEHBs.
[bicg_before.txt](https://github.com/user-attachments/files/19405145/bicg_before.txt)
[bicg_after.txt](https://github.com/user-attachments/files/19405144/bicg_after.txt)

author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-03-22T19:25:36Z
body: Let's post the examples on #342 

Also, maybe I missed something, from your results there is no throughput drop in bicg?
author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-03-22T19:47:08Z
body: > Also, maybe I missed something, from your results there is no throughput drop in bicg?

Yes, the redemption by `extraTrans` makes throughput the same as before under most benchmarks. But there're still some benchmarks having a little throughput degration. This might due to the CMerge unit, whose internal TEHB is not completely on the output (it links to a fork, which is completely on the output).
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-03-26T15:32:40Z
body: Once #362 is merged, the same revision should apply to `merge.v/vhd`, as it will fully replace `merge_notehb.v/vhd`.
author_association: COLLABORATOR
user: login: shundroid
created_at: 2025-03-30T00:32:59Z
body: Thank you for updating the pull request description!

> As for merge and mux, unlike cmerge, their internal TEHBs can be replaced by buffers on the channel. Therefore, this PR will remove their internal TEHBs.

I'm not understanding this- I expect "As for mux, unlike merge or cmerge, " instead. TEHB inside `merge` is also for preventing token reordering (which happened in our case). Could you tell me why only `cmerge` is special?
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-04-01T16:25:57Z
body: > Could you tell me why only `cmerge` is special?

As mentioned in the updated description:

> Put simply, this is because the internal structure of cmerge is a `merge–TEHB–fork` sequence, which inherently forms the pattern described in https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2745567695 that can lead to token reordering.

This suggests that the token reordering issue arises when a `mux` or `merge` is followed by a `fork`, and can be resolved by placing a buffer on the channel between them. However, `cmerge` is problematic on its own because it contains this pattern internally. Therefore, a buffer should be added inside `cmerge` itself to avoid token reordering.

Thank you for pointing this out. I will revise the description for better clarity.
author_association: COLLABORATOR
user: login: shundroid
created_at: 2025-04-01T16:46:52Z
body: Thanks for updating the description and clarifying this! This is a minor point, but I don't think a buffer is needed for mux to prevent reordering, am I right?

I agree that the internal TEHB of the merge op _can_ be replaced with a buffer on the channel, but the output channel without TEHB violates the handshake protocol (SELF) since the value could change after valid is set to high (which leads to the token reordering). I don't think that's acceptable, so I'd prefer to keep the internal TEHB in the merge. What do you think?

EDIT: So,

> Note that, according to https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2745567695, a buffer is only necessary when the consumer is a fork.

I think a buffer is always necessary to satisfy the handshaking protocol.
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-04-01T17:40:11Z
body: > I don't think a buffer is needed for mux to prevent reordering, am I right?
 
According to #342, yes.

> but the output channel without TEHB violates the handshake protocol (SELF) since the value could change after valid is set to high (which leads to the token reordering). I don't think that's acceptable, so I'd prefer to keep the internal TEHB in the merge. What do you think?
> 
> EDIT: So,
> 
> > Note that, according to [#342 (comment)](https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2745567695), a buffer is only necessary when the consumer is a fork.
> 
> I think a buffer is always necessary to satisfy the handshaking protocol.

Are token reordering and handshake protocol violations always the same thing? My understanding is that the value changes because the token travels through the back edge and arrives at the other input of the `merge`. If that’s the case, then only a `fork` — or a combinational loop (which is not allowed to occur) — can lead to this situation. This is because only a `fork` can replicate the token and allow one copy to travel across the back edge (i.e., change the value) while the previous token is still unconsumed (i.e., valid is high).

By the way, the current assumption that:

> a buffer is only necessary when the consumer is a fork

does overlook one situation: when a fork exists in the cycle but is not directly connected as the consumer of the `merge` (e.g., there’s another unit in between), a buffer is still needed.

However, I believe that if there is no fork in the cycle at all, then no buffer is necessary.
author_association: COLLABORATOR
user: login: shundroid
created_at: 2025-04-02T23:35:11Z
body: > Are token reordering and handshake protocol violations always the same thing?

A handshake protocol violation causes token reordering. We only observe token reordering because, currently, only the eager fork assumes the value remains the same after valid is high. I also realized that the speculator assumes this property (i.e., the actual condition remains the same after it's high), leading to different problems.

> My understanding is that the value changes because the token travels through the back edge and arrives at the other input of the merge.

I don't think backedge is essential in this phenomenon?

Consider a merge (`merge1`) with two inputs (`input1` and `input2`), neither of which are backedges. `merge1` is connected to an eager fork (`eagerfork1`) with two outputs (`output1` and `output2`). (This setup matches the figure in [this comment](https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2745567695), but without a backedge.)

In the first cycle, (`input1`, `input2`) = (not valid, valid) and (`output1`, `output2`) = (ready, not ready). `output1` accepts `input2`, but `eagerfork1` doesn’t emit `ready` to `merge1` since `output2` is stalled. In the next cycle, `input1` becomes valid, and `output2` becomes ready. Without TEHB, `merge1` may change its output to `input1` while `valid` is high. Then `output2` accepts `input1` first, and `eagerfork1` accepts the input from `merge1`. In the next cycle, both `output1` and `output2` accept `input2`?
(If I'm misunderstanding anything, please let me know)

If `merge1` is connected to the actual condition port of a speculator, the issue changes.

---

I think this stems from the handshake protocol not being explicitly specified. It’s not just a case of the value changing in some conditions and not in others. It also makes developing new operations difficult.
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-04-03T09:13:22Z
body: Thank you for the clarification!

> Consider a merge (`merge1`) with two inputs (`input1` and `input2`), neither of which are backedges. `merge1` is connected to an eager fork (`eagerfork1`) with two outputs (`output1` and `output2`). (This setup matches the figure in [this comment](https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2745567695), but without a backedge.)

Is your concern similar to [this other comment](https://github.com/EPFL-LAP/dynamatic/issues/342#issuecomment-2743156755)? I completely agree if there’s a chance that tokens arrive at different inputs within a short time, even without a back edge. In that case, should we include logic to specify the input index? Designs like this seem fragile without it.

It’s certainly important to discuss for generality. I’m just not aware of any specific cases in Dynamatic where this happens yet. Could you clarify how this relates to your recent speculator implementation? As I see it, there are two possible solutions: either keep the internal buffers, or add extra logic to prevent this scenario.

Please let me know if I’ve misunderstood your point.
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-04-03T12:55:38Z
body: @shundroid 

How about “setting a buffering constraint between any merge and fork/speculator”?
author_association: MEMBER
user: login: shundroid
created_at: 2025-04-03T14:40:30Z
body: It looks like my concern aligns exactly with @QinYuan2000's comment, sorry for overlooking it! I completely agree.

> In that case, should we include logic to specify the input index?

Is that Mux?
We actually use Merges with multiple inputs in some cases, like in speculation or fast-token-delivery, though Jiahui mentioned "We don't have 2-input merges in any MILP," (which I didn't quite understand though)

> Could you clarify how this relates to your recent speculator implementation?

I realized it's mostly the same as the eager fork case. Initially, I thought the inner FSM might also assume this behavior, but for now, it seems that's not the case.

> How about “setting a buffering constraint between any merge and fork/speculator”?

It depends on how we define our handshaking protocol. At first, I assumed that once the valid bit is high, the data value shouldn't change, meaning the merge itself would need an internal buffer to enforce this. But if we allow the value to change, then it's fine without it.
I've opened an issue to discuss the handshaking protocol further, so please check it out! #373
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-04-07T12:54:43Z
body: @QinYuan2000 Is it strictly necessary to also include the buffer type changes in this PR?
author_association: MEMBER
user: login: Jiahui17
created_at: 2025-04-07T13:06:34Z
body: I know it is a bit annoying to enforce the rules, but it would be much easier to review and discuss if we only focus on the two changes that we promised:

- Remove internal buffers from Mux/Merge
- Add buffer constraint for Merge
author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-04-08T00:01:57Z
body: > @QinYuan2000 Is it strictly necessary to also include the buffer type changes in this PR?

If PR #317 is merged beforehand, then only the relevant part would remain in this PR.
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-04-08T00:12:31Z
body: > I know it is a bit annoying to enforce the rules, but it would be much easier to review and discuss if we only focus on the two changes that we promised:
> 
> * Remove internal buffers from Mux/Merge
> * Add buffer constraint for Merge

The result data in the description reflects the version that also adds buffer constraints to Mux.
The results for the canonical version, which includes exactly the two changes listed above, are available here: [Results.pdf](https://github.com/user-attachments/files/19640561/SP25_InternalBuf_Full_canonical.pdf).
Interestingly, in the canonical version, throughput degrades on more benchmarks. I’ve tried a few things, but so far I still don’t have a clear explanation for it.
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-04-08T10:13:47Z
body: I converted this to draft. Will undraft it when #317 is merged
author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-04-29T18:45:03Z
body: > I converted this to draft. Will undraft it when #317 is merged

Now the differences reflect exactly what this PR does. It has been undrafted.
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-04-30T12:40:03Z
body: Is min/maxOpaque/Trans still needed if we add minSlots?
author_association: MEMBER
user: login: kb29x37
created_at: 2025-04-30T13:09:42Z
body: Can you constraint independently TEHBs and OEHBs with minSlots ? If yes, you can remove, otherwise, I would keep them. I use these min/max/Opaque/Trans all the time...
Thanks!
author_association: CONTRIBUTOR
user: login: Jiahui17
created_at: 2025-04-30T13:18:07Z
body: >Can you constraint independently TEHBs and OEHBs with minSlots ? If yes, you can remove, otherwise, I would keep them. I use these min/max/Opaque/Trans all the time...
Thanks!

@kb29x37 

I think at one point we decided to have:
- minSlots
- forceBreakDV
- forceBreakR

So if we have forceBreakDV = true and minSlots >= 1, then it will for sure create at least one OEHB. Do you have a use case where this is not flexible enough?
author_association: MEMBER
user: login: kb29x37
created_at: 2025-04-30T13:34:02Z
body: Say you want to constraint a channel to at most 1 OEHB slot and at most n TEHB slots. In that case, forceBreakDV and forceBreakR are both 0 I suppose ? minSlots would not allow you to constraint OEHB and TEHBs separately no ?
author_association: CONTRIBUTOR
user: login: kb29x37
created_at: 2025-04-30T13:39:05Z
body: As long as the behavior of this equation is maintained, I am fine with whatever you people decided.

https://github.com/EPFL-LAP/dynamatic/blob/main/lib/Transforms/BufferPlacement/FPL22Buffers.cpp#L151
author_association: CONTRIBUTOR
user: login: QinYuan2000
created_at: 2025-04-30T15:37:55Z
body: > Is min/maxOpaque/Trans still needed if we add minSlots?

Since minSlots cannot fully reproduce the functions of minOpaque/Trans, they are keeped for now. 

> Say you want to constraint a channel to at most 1 OEHB slot and at most n TEHB slots.

Although maxOpaque/Trans cannot be fully replaced by other variables, from what I’ve seen so far, they are only set to 0 to prevent some channels from placing any buffers. However, I’m not aware of any specific use cases where max values greater than 0 are actually needed.
author_association: COLLABORATOR
user: login: kb29x37
created_at: 2025-04-30T15:45:12Z
body: Ok sounds good :)

I generate a bunch of the max constraints to limit the number of buffers on elastic edges to what an hardened architecture for elastic circuits may provide... So as long as these are kept I am happy.

Thanks !
author_association: CONTRIBUTOR
user: login: QinYuan2000
created_at: 2025-04-30T15:49:46Z
body: > I think at one point we decided to have:
> 
> * minSlots
> * forceBreakDV
> * forceBreakR
> 
> So if we have forceBreakDV = true and minSlots >= 1, then it will for sure create at least one OEHB. Do you have a use case where this is not flexible enough?

If minOpaque/Trans are always 0 or 1, and maxOpaque/Trans are always 0 when they are used, then we can represent them using minSlots, forceBreakDV, and forceBreakR, assuming that `opaque` corresponds to breaking DV and `trans` corresponds to breaking R. However, since the meanings of `opaque` and `trans` vary across different MILPs, we always need a translation within the MILPs.
author_association: COLLABORATOR
user: login: QinYuan2000
created_at: 2025-04-30T15:57:16Z
body: > Ok sounds good :)
> 
> I generate a bunch of the max constraints to limit the number of buffers on elastic edges to what an hardened architecture for elastic circuits may provide... So as long as these are kept I am happy.
> 
> Thanks !

Thanks for the information about using positive values for maxOpaque/Trans. We’ll ensure the current functionality is preserved accordingly.
author_association: COLLABORATOR
user: login: Jiahui17
created_at: 2025-05-01T12:57:50Z
body: @kb29x37 

>I generate a bunch of the max constraints to limit the number of buffers on elastic edges to what an hardened architecture for elastic circuits may provide... So as long as these are kept I am happy.

Thanks. Now I understand the constraints from your side a bit better. I guess between two non-buffer elements in any DFG, you need to limit the number of buffer slots of different types (e.g., <= 5 OEHB && <= 5 TEHB && <= 5 Transparent && <= 5 total slots).

## forceBreakDV / forceBreakR are not enough

In FPGA20/FPL22, you get a solution like this for each channel:
- Cut DV?
- Cut R?
- Slots?

I think **what I wrote before doesn't make sense** (e.g., I only had "forceBreakDV", but I don't have "forceNotBreakDV".

Furthermore, we plan to also extend the solution to sth like:
- Cut DV?
- Cut R?
- Slots?
- LatencyDV? (multiple cascaded OEHB?)
- SharedEnable? (shift register with a single shared enable signal)







author_association: MEMBER
user: login: Jiahui17
created_at: 2025-05-01T13:33:20Z
body: I feel like there are too many variables in MILP that we want to put a custom constraint on. So, it seems like HandshakeSetBufferingPropertyPass is giving you flexibility, but it actually makes adding new features more complicated. Imagine I need to add a new variable called "sharedEn", then I need to do the following:

1. Add the variable and constrain its values in the buffer pass
2. Adapt the `ChannelBufProps` data structure for custom constraints on the variable.
3. Implement how we want to constrain that variable in a custom way in `ChannelBufProps`.
4. Write the actual constraints to be inserted into the MILP in the buffer pass (!?)

Problems here:

- I imagine that MOST of the people don't know that `HandshakeSetBufferingPropertyPass` exists. 
- The analysis about buffer placement constraint (step 3 above, in the `HandshakeSetBufferingPropertyPass`) and the place where the constraint is formulated (step 4 above, in the HandshakePlaceBuffersPass) are split into two separate places. So we need to invent a new data structure (`ChannelBufProps`) to carry the constraints down.

---

So, maybe it makes more sense to get rid of that pass and implement the constraints as separate functions in the buffer pass. For example, if we need to force a buffer placed at the output of the merge, we can just load the following function into the setup() function in the buffer placement pass:

```c++
void dynamatic::buffer::setMergeBufferingConstraints(handshake::FuncOp funcOp) {
  // Merges with more than one input should have at least a transparent slot
  // at their output
  for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
    if (mergeOp->getNumOperands() > 1) {
      Channel channel(mergeOp.getResult(), true);
      ChannelVars &chVars = vars.channelVars[channel];
      model.addConstr(chVars.bufNumSlots >= 1);
    }
  }
}
```

And Luis' buffering constraint can also be easily added:

```c++
void dynamatic::buffer::setElasticCGRABufferConstraints(Value channel) {

  ChannelVars &chVars = vars.channelVars[channel];
  model.addConstr(chVars.bufNumSlots <= 5);
  if (/* some condition */)
    // This channel cannot have any OEHB!
    model.addConstr(chVars.signalVars[SignalType::DATA].bufPresent == 0);
}
```


Maybe we should add a separate buffer placement algorithm or a separate flag that enables certain architecture constraints for Luis' use case?

@kb29x37 






author_association: MEMBER
user: login: Jiahui17
created_at: 2025-05-01T13:44:11Z
body: Disclaimer: I am not trying to take away what maxOpaque / maxTrans intend to give you--I just feel like breaking the buffer placement pass into two separate things (set buffering property and buffer placement pass) adds too much overhead (the channel buf property and all the logic around it) and it is used exactly in the same way as the other regular constraints in `HandshakePlaceBuffersPass`.

This channel buffer property is also something that you can not possibly understand (or be aware that thing exists) from reading the tablegen stuff.
author_association: MEMBER
user: login: QinYuan2000
created_at: 2025-05-04T13:35:30Z
body: > I feel like there are too many variables in MILP that we want to put a custom constraint on. So, it seems like HandshakeSetBufferingPropertyPass is giving you flexibility, but it actually makes adding new features more complicated. Imagine I need to add a new variable called "sharedEn", then I need to do the following:
> 
> 1. Add the variable and constrain its values in the buffer pass
> 2. Adapt the `ChannelBufProps` data structure for custom constraints on the variable.
> 3. Implement how we want to constrain that variable in a custom way in `ChannelBufProps`.
> 4. Write the actual constraints to be inserted into the MILP in the buffer pass (!?)
> 
> Problems here:
> 
> * I imagine that MOST of the people don't know that `HandshakeSetBufferingPropertyPass` exists.
> * The analysis about buffer placement constraint (step 3 above, in the `HandshakeSetBufferingPropertyPass`) and the place where the constraint is formulated (step 4 above, in the HandshakePlaceBuffersPass) are split into two separate places. So we need to invent a new data structure (`ChannelBufProps`) to carry the constraints down.
> 
> So, maybe it makes more sense to get rid of that pass and implement the constraints as separate functions in the buffer pass. For example, if we need to force a buffer placed at the output of the merge, we can just load the following function into the setup() function in the buffer placement pass:
> 
> ```c++
> void dynamatic::buffer::setMergeBufferingConstraints(handshake::FuncOp funcOp) {
>   // Merges with more than one input should have at least a transparent slot
>   // at their output
>   for (handshake::MergeOp mergeOp : funcOp.getOps<handshake::MergeOp>()) {
>     if (mergeOp->getNumOperands() > 1) {
>       Channel channel(mergeOp.getResult(), true);
>       ChannelVars &chVars = vars.channelVars[channel];
>       model.addConstr(chVars.bufNumSlots >= 1);
>     }
>   }
> }
> ```
> 
> And Luis' buffering constraint can also be easily added:
> 
> ```c++
> void dynamatic::buffer::setElasticCGRABufferConstraints(Value channel) {
> 
>   ChannelVars &chVars = vars.channelVars[channel];
>   model.addConstr(chVars.bufNumSlots <= 5);
>   if (/* some condition */)
>     // This channel cannot have any OEHB!
>     model.addConstr(chVars.signalVars[SignalType::DATA].bufPresent == 0);
> }
> ```
> 
> Maybe we should add a separate buffer placement algorithm or a separate flag that enables certain architecture constraints for Luis' use case?
> 
> @kb29x37

Should we open another PR for this?

As for removing internal buffers, this current PR could be merged first, assuming there’s no objection to adding the `minSlots` field while keeping the other buffer attributes unchanged.

Also, the test file has changed a lot due to the addition of a new field in `handshake.bufProps`. However, I noticed that some values need to be modified to make the tests pass. It’s strange — I’m not sure why it also affects unrelated units like `lazy_fork`.
author_association: COLLABORATOR
user: login: shundroid
created_at: 2025-05-14T15:08:42Z
body: Should we update the RTL files in the beta backend here as well? Or will it be handled elsewhere?
Like the [merge.py](https://github.com/EPFL-LAP/dynamatic/blob/main/experimental/tools/unit-generators/vhdl/generators/handshake/merge.py).

I also think the beta backend is still using and only supports the old buffers (tehb, tfifo, oehb, ofifo).
author_association: COLLABORATOR