8000 Output-stationary matmul with local C stride > 1 writes its result to the wrong scratchpad rows and overwrites other rows · Issue #406 · ucb-bar/gemmini · GitHub
[go: up one dir, main page]

Skip to content
  • Output-stationary matmul with local C stride > 1 writes its result to the wrong scratchpad rows and overwrites other rows #406

    Open

    Description

    @vera-rubin

    With the output-stationary (OS) dataflow and a local C stride of 2 set through config_ex, a preload/compute.preloaded pair writes its 16 result rows 15 scratchpad rows below where they belong. The arithmetic is correct but the placement is not, and the effect is silent local-memory corruption:

    • 16 rows outside the destination are overwritten. These are the odd rows 497-527: 8 of them are below the C base, and the other 8 are the gaps between the intended rows.
    • None of the 16 intended rows (512, 514, … 542) is written, so they keep their previous contents.
    • The commands complete normally, and nothing flags the error.

    The Spike extension runs the same instruction sequence correctly. OS with C stride 1 and WS with C stride 2 also work correctly in the RTL.

    In this issue, "C stride" means the local stride: the step, in scratchpad/accumulator rows, between consecutive C rows that the execute pipeline writes (config_ex rs2[63:48]). It is separate from the main-memory strides that config_mvin and config_mvout set for mvin/mvout. A large main-memory output stride does not trigger this.

    Spec. The README's Preloading section makes preload rs2[31:0] the local scratchpad address of the C matrix. The README's config_ex section documents the A stride (rs1[31:16], "in scratchpad addresses"), but it does not list the C-stride field. That field is defined elsewhere:

    • gemmini_extended3_config_ex in the C library takes C_stride and encodes it in rs2[63:48] for either dataflow (gemmini.h L260-L261).
    • The ISA bundle has a 16-bit c_stride field (GemminiISA.scala L219-L222). ExecuteController latches it whatever the dataflow (L565).
    • The README offers the Spike extension as the functional simulator. It decodes the field for either dataflow (gemmini.cc L408). Its result loop is shared by both dataflows: OS takes the values from the PE state and WS from the computed products. The loop writes output row i to base_sp_addr + c_stride * i (L635-L659; the accumulator write is at L644-L646 and the scratchpad write at L659).

    The model links point at libgemmini's current master, 5b1254a7. Our runs used Gemmini's submodule pin ea8f7ed7, which is seven commits older. At that pin the same code is at L406 and L633-L657. The commits in between do not change it.

    We did not find any note that restricts C stride to WS.

    Test case. This uses the default config (16×16, int8 inputs), with A = identity in rows 0-15 and B row r = r+1 in rows 4096-4111. The sequence is:

    1. config_ex: OS, no activation, shift 0, A stride 1, C stride 2.
    2. preload: D = zero (0xFFFFFFFF), C = row 512, 16×16.
    3. compute.preloaded: A = row 0, B = row 4096.

    C row i should hold i+1 in every column.

    C row i (value i+1) Expected scratchpad row (512 + 2i) RTL writes it to
    0 512 497
    1 514 499
    2 516 501
    … … …
    14 540 525
    15 542 527

    The rows drain in the usual OS order (row 15 first), and every write is 15 rows low. The written rows are all odd and the intended rows are all even, so none of the 256 expected elements is written. An independent audit (details under Tested) pre-filled physical rows 480-575 with -99 and watched all four banks. It ob BD0 served the following:

    • Each odd row from 497 to 527 went from -99 to one full result row. All 256 written values were correct for that row.
    • The 16 intended even rows received no write and still held -99.
    • No other row or bank was written.

    So the problem is not a wrong value. The operation replaces 16 resident rows outside its destination, and the destination keeps stale data.

    Cause. ExecuteController.scala L907-L908 computes the OS write address on L908 as

    mesh.io.resp.bits.tag.addr + (w_total_output_rows - 1.U - output_counter * c_addr_stride)

    The OS drain emits row i = total_rows - 1 - output_counter, so the address should be base + i * c_addr_stride. The stride currently multiplies only output_counter. The two expressions differ by (total_rows - 1) * (c_addr_stride - 1) rows, which is 15 at 16 rows and stride 2, and zero at stride 1. That is why unit stride works. The WS branch on L907 already multiplies the row index by the stride.

    Suggested fix. Reverse the row index first, then apply the stride:

    -    mesh.io.resp.bits.tag.addr + (w_total_output_rows - 1.U - output_counter * c_addr_stride))
    +    mesh.io.resp.bits.tag.addr + ((w_total_output_rows - 1.U - output_counter) * c_addr_stride))

    We ran this change only as a control in our own harness (below). There, OS stride 2 places all 16 rows at 542, 540, … 512 (Verilator data and addresses, Icarus addresses), and the OS stride 1 and WS stride 2 controls still pass. We have not run the project's test suite or a full SoC simulation with it.

    Impact. With OS and a local C stride above 1, a matmul overwrites local rows outside its destination and leaves it 10000 s destination stale. In the tested case:

    • data held in rows 497-511 (below the C base) or in the odd rows 513-527 (between the C rows) is replaced;
    • a later mvout or compute that reads rows 512-542 gets whatever was there before.

    We did not run such a follow-on read in the RTL harness. It follows from the observed writes. From the expression we expect the same kind of shift, (rows - 1) * (stride - 1) rows, for other row counts and strides, but we ran only 16 rows at stride 2.

    Who can hit it. It needs all of the following:

    • a Gemmini build with OS support (the default config supports both dataflows);
    • software that issues Gemmini instructions directly;
    • a local C stride above 1;
    • a preload with a real C address (not the all-ones "no write" value).

    The input values do not matter. Whether a user program can issue Gemmini instructions, as opposed to bare-metal or kernel code, depends on how the core and OS enable the accelerator. We did not check a particular system.

    What follows comes from reading gemmini.h only. It is source evidence, not a survey of real workloads.

    • The tiled matmul path sets local C stride 1 in either dataflow. It does so even when the caller's main-memory output stride is large, because that stride goes to config_mvout (L758-L759). gemmini_config_ex and gemmini_extended_config_ex also pass 1 (L263-L270).
    • The non-unit local C strides are in the convolution paths, and all of them run under WS:
      • tiled_conv sets WS with C stride input_dilation (L2316).
      • sp_tiled_conv sets C stride orows * ocols with set_only_strides (L1639). This keeps the current dataflow (WS here) even though its dataflow argument is 0, because the RTL skips the dataflow update when that bit is set (ExecuteController.scala L550-L562).

    Code that goes through these helpers should not hit this. Code that calls gemmini_extended3_config_ex directly with OS and C stride > 1 does hit it, for example a hand-written kernel or a compiler back end. We did not check other software stacks.

    Ordering (source reading, untested). The reservation station tracks a preload's destination as running from the base to base + rows × C stride (ReservationStation.scala L268-L272), which is rows 512-543 here. Rows 497-511 are outside that range. We would therefore not expect it to order later commands that use those rows behind the stray writes. Our harness has no reservation station and runs one serialized sequence, so overlapping commands are untested. We have not seen corruption or a hang from them.

    Recovery. In the harness the commands completed normally. From source, neither completion nor the interrupt reports a wrong local row:

    A fence waits for outstanding work, and the flush command acts on address translation (Controller.scala L488-L493). Neither restores overwritten rows. To recover, reload every buffer that was held in the overwritten rows, then redo the matmul in a configuration that works. Redoing only the matmul is not enough. For this case, OS with C stride 1 and WS with C stride 2 both pass in our harness. We have not tested a recovery sequence, or what a reset does to scratchpad contents.

    We also did not look at software that shares the scratchpad between jobs or assigns each job a row range. Under such a scheme the stray rows can hold another job's buffers. We make no claim either way about such setups.

    Limits of what we checked:

    • default config only (16×16, int8);
    • scratchpad destination only. Accumulator writes use the same computed address (ExecuteController.scala L909-L912), so we expect the same displacement there, but we did not run it;
    • one C base (512) and one stride (2). We did not try bases near row 0 or near a bank boundary, where the shifted rows could wrap or cross banks;
    • no activation, shift or transposes.

    The RTL result comes from a harness around the unmodified ExecuteController, the mesh and ScratchpadBank, not from a full Chipyard SoC.

    Reproducer. reproduce.sh (in the details below) does the following:

    1. checks the pinned SHAs;
    2. builds libgemmini and runs reference.c on Spike, where it must pass;
    3. elaborates the harness with sbt;
    4. runs two Verilator controls (OS stride 1, WS stride 2), which must pass;
    5. requires the OS stride 2 failure;
    6. if Icarus is installed, repeats the address check there.

    The harness uses GemminiConfigs.defaultConfig and the project's scratchpad response queue. A small bench port fills the A/B rows. There is no CPU, DMA or reservation station. The address calculation does not need them, but see the ordering note above. Output:

    Spike reference: OS stride 2 matrix passed.
    ADDRESS MISMATCH write=0 got=527 expected=542
    WS=0 CSTRIDE=2 SHIFT=0 writes=16 mismatches=272
    ADDRESS MISMATCH write=0 got=527 expected=542
    Reproduced; Spike reference and both Verilator controls passed. See failure.txt.
    

    The 272 mismatches are 16 wrong row addresses plus 256 unwritten expected elements. We did not run reference.c on a full Chipyard Verilator SoC.

    Setup and reproduce.sh
    export GEMMINI_ROOT="$PWD/gemmini" CHIPYARD_ROOT="$PWD/chipyard"
    git clone https://github.com/ucb-bar/gemmini "$GEMMINI_ROOT"
    git -C "$GEMMINI_ROOT" checkout --detach 8c3f9923a44a2fe2c7930587be297d6d4f8c09ca
    git -C "$GEMMINI_ROOT" submodule update --init software/gemmini-rocc-tests software/libgemmini
    git clone https://github.com/ucb-bar/chipyard "$CHIPYARD_ROOT"
    git -C "$CHIPYARD_ROOT" checkout --detach e02074414158c85059c1dcb231ff2f8941254b9a
    git -C "$CHIPYARD_ROOT" submodule update --init \
      generators/rocket-chip generators/hardfloat generators/diplomacy tools/cde sims/firesim
    export SBT_LAUNCH=/path/to/sbt-launch-1.10.7.jar JAVA_BIN=/path/to/java17/bin/java
    export SPIKE_PREFIX=/path/to/spike-install SPIKE="$SPIKE_PREFIX/bin/spike"
    ./reproduce.sh /path/to/empty-build-dir

    Put reproduce.sh, ExecHarness.scala, exec_tb.sv, build.sbt, reference.c, start.S, link.ld and the two small files below in one directory: build.properties contains sbt.version=1.10.7, and repositories contains [repositories], local and maven-central: https://repo.maven.apache.org/maven2 on three lines. Spike was built from riscv-isa-sim d6cfe9efdb531a9dbb2206367deffdf39452434e, because Chipyard's older pinned Spike has an extension API that is incompatible with this libgemmini.

    #!/usr/bin/env bash
    set -euo pipefail
    ulimit -c 0
    src=$(cd -- "$(dirname -- "$0")" && pwd)
    : "${GEMMINI_ROOT:?Point to the pinned Gemmini checkout}"
    : "${CHIPYARD_ROOT:?Point to the checkout at Gemmini CHIPYARD.hash}"
    : "${SBT_LAUNCH:?Path to sbt-launch-1.10.7.jar}"
    : "${SPIKE:?Path to Spike built from d6cfe9efdb531a9dbb2206367deffdf39452434e}"
    : "${SPIKE_PREFIX:?Spike installation prefix, containing include/riscv}"
    java_bin=${JAVA_BIN:-java}
    out=${1:?Supply an empty build directory outside the findings checkout}
    test "$(git -C "$GEMMINI_ROOT" rev-parse HEAD)" = 8c3f9923a44a2fe2c7930587be297d6d4f8c09ca
    test "$(git -C "$CHIPYARD_ROOT" rev-parse HEAD)" = e02074414158c85059c1dcb231ff2f8941254b9a
    test "$(git -C "$GEMMINI_ROOT/software/gemmini-rocc-tests" rev-parse HEAD)" = 7c540b3adf1b86ad93d07f893abe3a73489b568e
    test "$(git -C "$GEMMINI_ROOT/software/libgemmini" rev-parse HEAD)" = ea8f7ed7afd68e001fb06ddccad9a023c990961d
    git -C "$GEMMINI_ROOT" diff --quiet HEAD -- src/main/scala
    git -C "$GEMMINI_ROOT/software/libgemmini" diff --quiet HEAD
    mkdir -p "$out/project" "$out/src/main/scala"
    out=$(cd "$out" && pwd)
    cp "$src/build.sbt" "$out/"
    cp "$src/build.properties" "$out/project/"
    cp "$src/ExecHarness.scala" "$out/src/main/scala/"
    cp "$src/exec_tb.sv" "$out/"
    cd "$out"
    g++ -std=c++17 -shared -fPIC -O3 -I"$SPIKE_PREFIX/include" \
      "$GEMMINI_ROOT/software/libgemmini/gemmini.cc" -o libgemmini.so > lib-build.log 2>&1
    riscv64-unknown-elf-gcc -O2 -march=rv64gc -mabi=lp64d -mcmodel=medany \
      -fno-builtin -nostdlib -nostartfiles -Wl,-T,"$src/link.ld" \
      "$src/start.S" "$src/reference.c" -o reference.elf > reference-build.log 2>&1
    LD_LIBRARY_PATH="$out${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
      "$SPIKE" --extension=gemmini reference.elf > reference.txt 2>&1
    echo 'Spike reference: OS stride 2 matrix passed.'
    "$java_bin" -Xmx6G -Xss8M -XX:ActiveProcessorCount=4 \
      -Dsbt.override.build.repos=true -Dsbt.repository.config="$src/repositories" \
      -Dsbt.log.noformat=true -jar "$SBT_LAUNCH" 'runMain EmitExec' > elaboration.log 2>&1
    verilator --binary --timing --assert -j 4 -Wno-fatal --top-module exec_tb \
      generated/ExecHarness.sv exec_tb.sv > verilator-build.log 2>&1
    obj_dir/Vexec_tb +WS=0 +CSTRIDE=1 > control-os.txt
    obj_dir/Vexec_tb +WS=1 +CSTRIDE=2 > control-ws.txt
    set +e
    obj_dir/Vexec_tb +WS=0 +CSTRIDE=2 > failure.txt 2>&1
    rc=$?
    set -e
    if (( rc == 0 )); then
      echo 'No deviation reproduced: OS stride 2 passed.'
      exit 1
    fi
    rg 'ADDRESS MISMATCH write=0 got=527 expected=542' failure.txt
    rg 'WS=0 CSTRIDE=2 SHIFT=0 writes=16 mismatches=272' failure.txt
    if command -v iverilog >/dev/null; then
      iverilog -g2012 -s exec_tb -o exec.vvp generated/ExecHarness.sv exec_tb.sv > iverilog-build.log 2>&1
      vvp exec.vvp +WS=0 +CSTRIDE=1 +ADDRESS_ONLY=1 > icarus-control.txt
      set +e
      vvp exec.vvp +WS=0 +CSTRIDE=2 +ADDRESS_ONLY=1 > icarus-failure.txt
      rc=$?
      set -e
      test "$rc" != 0
      rg 'ADDRESS MISMATCH write=0 got=527 expected=542' icarus-failure.txt
    fi
    echo 'Reproduced; Spike reference and both Verilator controls passed. See failure.txt.'
    reference.c, start.S, link.ld (Spike program)

    reference.c reads each strided C row back with its own mvout, because mvout source rows are contiguous. It exits with 0 when all 256 elements match. -DWS=1 or -DCSTRIDE=1 give the controls.

    typedef unsigned long u64;
    typedef signed char i8;
    #ifndef CSTRIDE
    #define CSTRIDE 2
    #endif
    #ifndef WS
    #define WS 0
    #endif
    #define CMD(f,a,b) asm volatile(".insn r 0x7b,3,%2,x0,%0,%1"::"r"((u64)(a)),"r"((u64)(b)),"i"(f):"memory")
    #define MAT(a) ((16UL<<48)|(16UL<<32)|(a))
    static i8 a[16][16],b[16][16],c[16][16];
    int main(void) {
      for(int i=0;i<16;i++) for(int j=0;j<16;j++) {a[i][j]=(i==j);b[i][j]=i+1;c[i][j]=-99;}
      CMD(0,0x3f80000000100101UL,16UL);
      CMD(0,2UL,(0x3f800000UL<<32)|16UL);
      CMD(2,a,MAT(0));CMD(2,b,MAT(4096));
      CMD(0,0x3f80000000010000UL|(WS<<2),(u64)CSTRIDE<<48);
      if(WS) {CMD(6,MAT(4096),MAT(512));CMD(4,MAT(0),MAT(0xffffffffUL));}
      else {CMD(6,MAT(0xffffffffUL),MAT(512));CMD(4,MAT(0),MAT(4096));}
      // mvout has contiguous source rows, so read each strided row separately.
      for(int i=0;i<16;i++) CMD(3,c[i],(1UL<<48)|(16UL<<32)|(512UL+CSTRIDE*i));
      asm volatile("fence":::"memory");
      for(int i=0;i<16;i++) for(int j=0;j<16;j++) if(c[i][j]!=i+1) return 1+i*16+j;
      return 0;
    }
    .section .text.init
    .globl _start
    _start:
      la sp, _stack_top
      call main
      slli a0,a0,1
      ori a0,a0,1
      la t0,tohost
      sd a0,0(t0)
    1: j 1b
    .section .tohost,"aw",@progbits
    .balign 64
    .globl tohost
    tohost: .dword 0
    .balign 64
    .globl fromhost
    fromhost: .dword 0
    .section .bss
    .balign 16
    .space 16384
    _stack_top:
    OUTPUT_ARCH(riscv)
    ENTRY(_start)
    SECTIONS {
     . = 0x80000000;
     .text : { *(.text.init) *(.text .text.*) }
     .rodata : { *(.rodata .rodata.*) }
     . = ALIGN(4096);
     .data : { *(.data .data.*) }
     .tohost : { *(.tohost) }
     .bss : { *(.bss .bss.*) *(COMMON) }
    }
    ExecHarness.scala, build.sbt (RTL harness around the unmodified ExecuteController)
    import chisel3._
    import chisel3.util._
    import org.chipsalliance.cde.config.{Parameters, Config}
    import freechips.rocketchip.tile.{TileKey, RocketTileParams}
    import freechips.rocketchip.rocket.RocketCoreParams
    import freechips.rocketchip.subsystem.{SystemBusKey, SystemBusParams}
    import gemmini._
    import gemmini.Arithmetic.SIntArithmetic
    
    object HarnessParams {
      implicit val p: Parameters = new Config((site, here, up) => {
        case TileKey => RocketTileParams(core = RocketCoreParams())
        case SystemBusKey => SystemBusParams(beatBytes = 16, blockBytes = 64)
      })
    }
    class ExecHarness extends Module {
      import HarnessParams._
      val cfg = GemminiConfigs.defaultConfig
      val dim = cfg.meshRows * cfg.tileRows
      val io = IO(new Bundle {
        val cmd_valid = Input(Bool())
        val cmd_ready = Output(Bool())
        val funct = Input(UInt(7.W))
        val rs1 = Input(UInt(64.W))
        val rs2 = Input(UInt(64.W))
        val busy = Output(Bool())
        val completed = Output(Bool())
        val init_valid = Input(Bool())
        val init_addr = Input(UInt(14.W))
        val init_data = Input(UInt((dim*8).W))
        val wr_valid = Output(Vec(cfg.sp_banks, Bool()))
        val wr_addr = Output(Vec(cfg.sp_banks, UInt(14.W)))
        val wr_data = Output(Vec(cfg.sp_banks, UInt((dim*8).W)))
        val wr_mask = Output(Vec(cfg.sp_banks, UInt(dim.W)))
        val acc_valid = Output(Vec(cfg.acc_banks, Bool()))
        val acc_addr = Output(Vec(cfg.acc_banks, UInt(10.W)))
        val acc_data = Output(Vec(cfg.acc_banks, UInt((dim*32).W)))
        val acc_acc = Output(Vec(cfg.acc_banks, Bool()))
        val acc_mask = Output(Vec(cfg.acc_banks, UInt((dim*4).W)))
      })
      val ex = Module(new ExecuteController(64, 6, cfg))
      ex.io.cmd.valid := io.cmd_valid
      ex.io.cmd.bits := 0.U.asTypeOf(ex.io.cmd.bits)
      ex.io.cmd.bits.cmd.inst.funct := io.funct
      ex.io.cmd.bits.cmd.rs1 := io.rs1
      ex.io.cmd.bits.cmd.rs2 := io.rs2
      ex.io.cmd.bits.rob_id.valid := true.B
      io.cmd_ready := ex.io.cmd.ready
      io.busy := ex.io.busy
      io.completed := ex.io.completed.valid
      ex.io.counter.external_reset := false.B
      ex.io.im2col.req.ready := false.B
      ex.io.im2col.resp.valid := false.B
      ex.io.im2col.resp.bits := 0.U.asTypeOf(ex.io.im2col.resp.bits)
      for (b <- 0 until cfg.sp_banks) {
        val bank = Module(new ScratchpadBank(cfg.sp_bank_entries, cfg.sp_width, cfg.aligned_to, cfg.sp_singleported, false, false))
        bank.io.read.req <> ex.io.srams.read(b).req
        val q = Module(new Queue(new ScratchpadReadResp(cfg.sp_width), cfg.spad_read_delay, flow=false, pipe=true))
        q.io.enq <> bank.io.read.resp
        ex.io.srams.read(b).resp <> q.io.deq
        val initThisBank = io.init_valid && io.init_addr / cfg.sp_bank_entries.U === b.U
        bank.io.write.valid := initThisBank || ex.io.srams.write(b).valid
        bank.io.write.addr := Mux(initThisBank, io.init_addr, ex.io.srams.write(b).addr)
        bank.io.write.data := Mux(initThisBank, io.init_data.asUInt, ex.io.srams.write(b).data)
        bank.io.write.mask := Mux(initThisBank, Fill(dim, 1.U(1.W)), ex.io.srams.write(b).mask.asUInt).asTypeOf(bank.io.write.mask)
        ex.io.srams.write(b).ready := bank.io.write.ready && !initThisBank
        io.wr_valid(b) := ex.io.srams.write(b).fire
        io.wr_addr(b) := b.U * cfg.sp_bank_entries.U + ex.io.srams.write(b).addr
        io.wr_data(b) := ex.io.srams.write(b).data.asTypeOf(io.wr_data(b))
        io.wr_mask(b) := ex.io.srams.write(b).mask.asUInt
      }
      for (b <- 0 until cfg.acc_banks) {
        ex.io.acc.read_req(b).ready := false.B
        ex.io.acc.read_resp(b).valid := false.B
        ex.io.acc.read_resp(b).bits := 0.U.asTypeOf(ex.io.acc.read_resp(b).bits)
        ex.io.acc.write(b).ready := true.B
        io.acc_valid(b) := ex.io.acc.write(b).fire
        io.acc_addr(b) := b.U * cfg.acc_bank_entries.U + ex.io.acc.write(b).bits.addr
        io.acc_data(b) := ex.io.acc.write(b).bits.data.asTypeOf(io.acc_data(b))
        io.acc_acc(b) := ex.io.acc.write(b).bits.acc
        io.acc_mask(b) := ex.io.acc.write(b).bits.mask.asUInt
      }
    }
    object EmitExec extends App {
      _root_.circt.stage.ChiselStage.emitSystemVerilogFile(new ExecHarness, Array("--target-dir", "generated"), Array("--disable-all-randomization", "--strip-debug-info", "--lowering-options=disallowPackedArrays,disallowLocalVariables"))
    }
    val cy = file(sys.env("CHIPYARD_ROOT"))
    val gm = file(sys.env("GEMMINI_ROOT"))
    val settingsCommon = Seq(
      scalaVersion := "2.13.12",
      libraryDependencies ++= Seq("org.chipsalliance" %% "chisel" % "6.5.0", "org.scala-lang" % "scala-reflect" % "2.13.12", "com.lihaoyi" %% "sourcecode" % "0.3.1"),
      addCompilerPlugin("org.chipsalliance" % "chisel-plugin" % "6.5.0" cross CrossVersion.full),
      scalacOptions ++= Seq("-Ymacro-annotations", "-language:reflectiveCalls")
    )
    def sourceProject(n: String, src: File) = Project(n, file(n)).settings(settingsCommon).settings(Compile / unmanagedSourceDirectories := Seq(src))
    lazy val cde = sourceProject("cde", cy / "tools/cde/cde/src/chipsalliance/rocketchip")
    lazy val targetutils = sourceProject("targetutils", cy / "sims/firesim/sim/midas/targetutils/src/main/scala")
    lazy val hardfloat = sourceProject("hardfloat", cy / "generators/hardfloat/hardfloat/src/main/scala").dependsOn(targetutils)
    lazy val rocketMacros = sourceProject("rocketMacros", cy / "generators/rocket-chip/macros/src/main/scala")
    lazy val diplomacy = sourceProject("diplomacy", cy / "generators/diplomacy/diplomacy/src/diplomacy").dependsOn(cde)
    lazy val rocket = sourceProject("rocket", cy / "generators/rocket-chip/src/main/scala").dependsOn(hardfloat, rocketMacros, diplomacy, cde).settings(
     libraryDependencies ++= Seq("com.lihaoyi" %% "mainargs" % "0.5.0", "org.json4s" %% "json4s-jackson" % "4.0.5", "org.scala-graph" %% "graph-core" % "1.13.5", "org.apache.commons" % "commons-lang3" % "3.12.0", "org.apache.commons" % "commons-text" % "1.9"),
     Compile / unmanagedResourceDirectories := Seq(cy / "generators/rocket-chip/src/main/resources")
    )
    lazy val gemmini = sourceProject("gemmini", gm / "src/main/scala").dependsOn(rocket).settings(
     Compile / unmanagedSources / excludeFilter := "CustomCPUConfigs.scala" || "CustomSoCConfigs.scala"
    )
    lazy val root = Project("rtlcheck", file(".")).dependsOn(gemmini).settings(settingsCommon)
    exec_tb.sv (bench: fills A/B, issues the three commands, checks each accepted write)
    module exec_tb;
      reg clock=0;
      always #5 clock=~clock;
      reg reset=1;
      reg cmd_valid=0;
      wire cmd_ready;
      reg [6:0] funct=0;
      reg [63:0] rs1=0, rs2=0;
      reg init_valid=0;
      reg [13:0] init_addr=0;
      reg [127:0] init_data=0;
      wire busy,completed;
      wire wr_valid;
      wire [13:0] wr_addr;
      wire [127:0] wr_data;
      wire [15:0] wr_mask;
      ExecHarness dut(.clock(clock),.reset(reset),.io_cmd_valid(cmd_valid),.io_cmd_ready(cmd_ready),
        .io_funct(funct),.io_rs1(rs1),.io_rs2(rs2),.io_busy(busy),.io_completed(completed),
        .io_init_valid(init_valid),.io_init_addr(init_addr),.io_init_data(init_data),
        .io_wr_valid_0(wr_valid),.io_wr_addr_0(wr_addr),.io_wr_data_0(wr_data),.io_wr_mask_0(wr_mask));
      integer cstride=1, ws=0, ashift=0, act=0, address_only=0;
      integer writes=0, errors=0, cycles=0;
      integer memory [0:16383][0:15];
      always @(posedge clock) begin
        cycles <= cycles+1;
        if (!reset && wr_valid) begin
          writes <= writes+1;
          if (wr_addr !== (ws ? 512+writes*cstride : 512+(15-writes)*cstride)) begin
            $display("ADDRESS MISMATCH write=%0d got=%0d expected=%0d",writes,wr_addr,ws ? 512+writes*cstride : 512+(15-writes)*cstride);
            errors=errors+1;
          end
          if (writes < 16) $display("write row=%0d col0=%0d",wr_addr,$signed(wr_data[7:0]));
          for (integer j=0;j<16;j=j+1)
            if (wr_mask[j]) memory[wr_addr][j]=$signed(wr_data[8*j +: 8]);
        end
        if (cycles > 10000) $fatal(1,"timeout");
      end
      task command(input [6:0] f, input [63:0] a, input [63:0] b);
        begin
          @(negedge clock); cmd_valid=1;funct=f;rs1=a;rs2=b;
          do @(posedge clock); while(!cmd_ready);
          @(negedge clock); cmd_valid=0;
        end
      endtask
      function [63:0] matrix_arg(input [31:0] addr);
        matrix_arg={16'd16,16'd16,addr};
      endfunction
      initial begin
        if ($value$plusargs("CSTRIDE=%d",cstride)) begin end
        if ($value$plusargs("WS=%d",ws)) begin end
        if ($value$plusargs("SHIFT=%d",ashift)) begin end
        if ($value$plusargs("ACT=%d",act)) begin end
        if ($value$plusargs("ADDRESS_ONLY=%d",address_only)) begin end
        for(integer i=0;i<16384;i=i+1)
          for(integer j=0;j<16;j=j+1) memory[i][j]=-99;
        repeat(10) @(negedge clock);
        reset=0;
        // A is identity, in bank 0. B has constant row r+1, in bank 1.
        for (integer i=0;i<16;i=i+1) begin
          @(negedge clock);init_valid=1;init_addr=i;
          for(integer j=0;j<16;j=j+1) init_data[8*j +: 8]=(i==j);
        end
        for (integer i=0;i<16;i=i+1) begin
          @(negedge clock);init_valid=1;init_addr=4096+i;
          for(integer j=0;j<16;j=j+1) init_data[8*j +: 8]=i+1;
        end
        @(negedge clock);init_valid=0;
        command(0,64'h3f80000000010000 | (ws<<2) | (act<<3),(64'(cstride)<<48) | 64'(ashift));
        if(ws) begin
          command(6,matrix_arg(4096),matrix_arg(512));
          command(4,matrix_arg(0),matrix_arg(32'hffffffff));
        end else begin
          command(6,matrix_arg(32'hffffffff),matrix_arg(512));
          command(4,matrix_arg(0),matrix_arg(4096));
        end
        repeat(300) @(negedge clock);
        if(writes!=16) $fatal(1,"write count %0d expected 16",writes);
        for(integer i=0;i<16;i=i+1) begin
          integer expected;
          expected=i+1;
          if (!ws && ashift>0) expected=(expected>>ashift)+(((expected>>(ashift-1))&1) & (((expected&((1<<(ashift-1))-1))!=0) | ((expected>>ashift)&1)));
          for(integer j=0;j<16;j=j+1)
            if(!address_only && memory[512+cstride*i][j]!==expected) begin
              if(errors<8) $display("MISMATCH C[%0d][%0d] row=%0d got=%0d expected=%0d",i,j,512+cstride*i,memory[512+cstride*i][j],expected);
              errors=errors+1;
            end
        end
        $display("WS=%0d CSTRIDE=%0d SHIFT=%0d writes=%0d mismatches=%0d",ws,cstride,ashift,writes,errors);
        if(errors) $fatal(1,"matrix mismatch");
        $finish;
      end
    endmodule

    Tested on Gemmini master 8c3f992, unmodified, with its gemmini-rocc-tests (7c540b3a) and libgemmini (ea8f7ed7) submodules. It is still Gemmini HEAD as of 2026-10-10, so the Gemmini links point at current master. 7c540b3a is also the current head of gemmini-rocc-tests (branch dev). The libgemmini links use its newer master, 5b1254a7, as noted under Spec.

    • Chipyard e020744 supplied rocket-chip, hardfloat, diplomacy and cde.
    • Scala 2.13.12, Chisel 6.5.0, firtool 1.62.0, sbt 1.10.7, Temurin Java 17.0.20.1.
    • Verilator 5.053 devel (v5.052-319-g704ec556e) and Icarus 14.0 devel (s20260301-531-g633cc63a1).
    • Spike 1.1.1-dev at d6cfe9ef, RISC-V GCC 13.2.0, Ubuntu 24.04.

    Icarus does not produce the arithmetic values in this harness, so its runs check addresses only. The data comparison comes from Verilator. An independent re-run, from fresh checkouts, reproduced the result. The audit bench used:

    • three Verilator random-reset seeds;
    • physical rows 480-575 pre-filled with -99;
    • all four scratchpad banks watched (no other bank was written).

    This is simulation only. No FPGA, silicon or deployed system was checked.

    Duplicates. We searched all 404 issues and PRs in this repo (open and closed) for: stride, C_stride, c_addr_stride, output stationary / output-stationary, dataflow, config_ex, preload, w_address, output_counter, total_rows, ExecuteController, row order and reverse. We also searched gemmini-rocc-tests, libgemmini and chipyard for stride / output-stationary. Nothing matched. The closest are:

    Found by ChipLab's AI verification agents. Reproduced independently on Verilator from fresh checkouts, and reviewed by me before filing.

    — ChipLab · CLAB-2026-042

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

      • 0