From 69946aa748f530fb46f9776a7d663456066e6dfc Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 18 Jun 2026 05:39:33 +0000 Subject: [PATCH 1/6] :bug: [masku] Drain ALU operands during vid.v vid.v does not consume an ALU operand, but the lanes can still drive ALU data into the mask-unit operand path. With the operand never acknowledged, the producing lane stalls indefinitely and the mask unit deadlocks. Acknowledge the ALU operand unconditionally while the issued instruction is VID so the operand path drains. Refs #448 --- hardware/src/masku/masku_operands.sv | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hardware/src/masku/masku_operands.sv b/hardware/src/masku/masku_operands.sv index 2788652c7..f355a1979 100644 --- a/hardware/src/masku/masku_operands.sv +++ b/hardware/src/masku/masku_operands.sv @@ -249,7 +249,13 @@ module masku_operands import ara_pkg::*; import rvv_pkg::*; #( for (int lane = 0; lane < NrLanes; lane++) begin // Acknowledge alu operand for (int operand_fu = 0; operand_fu < NrMaskFUnits; operand_fu++) begin - masku_operand_ready_o[lane][2 + operand_fu] = (masku_fu_e'(operand_fu) == masku_fu_i) && masku_operand_alu_ready_i[lane]; + // VID does not consume an ALU operand, but the lanes can still push ALU + // data into the operand path. If it is never acknowledged, the producing + // lane stalls forever and the mask unit deadlocks. Drain it during VID. + if (vinsn_issue_i.op == VID) + masku_operand_ready_o[lane][2 + operand_fu] = 1'b1; + else + masku_operand_ready_o[lane][2 + operand_fu] = (masku_fu_e'(operand_fu) == masku_fu_i) && masku_operand_alu_ready_i[lane]; end // Acknowledge vd operands masku_operand_ready_o[lane][1] = masku_operand_vd_lane_ready[lane]; From b6823e8c7736e9c9fb42aacb684d29ff8fb114dd Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 18 Jun 2026 07:18:14 +0000 Subject: [PATCH 2/6] :white_check_mark: [apps] Add vid_drain_deadlock differential reproducer Self-checking reproducer for the mask-unit ALU-operand drain bug (#448): vid.v followed by a mask compare and a masked merge. Verified on the Verilated model (4 lanes, VLEN=4096): * with the masku_operands.sv fix: PASS (masked merge writes 9.0 to elem 2) * without the fix: FAIL (merge dropped, elem 2 stays 7) The drain bug manifests here as a wrong masked-merge result; under different timing it can also deadlock. Refs #448 --- apps/vid_drain_deadlock/main.c | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 apps/vid_drain_deadlock/main.c diff --git a/apps/vid_drain_deadlock/main.c b/apps/vid_drain_deadlock/main.c new file mode 100644 index 000000000..972047495 --- /dev/null +++ b/apps/vid_drain_deadlock/main.c @@ -0,0 +1,64 @@ +// Copyright 2026 ETH Zurich and University of Bologna. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Differential reproducer for issue #448 (mask unit ALU-operand drain on vid.v). +// +// vid.v does not consume an ALU operand, but lanes can still drive ALU data into +// the mask-unit operand path. If it is never acknowledged, the stale operand +// state corrupts the following mask flow. With the fix in masku_operands.sv +// (drain ALU operands during VID) the masked merge is correct; without it the +// merge under v0 is dropped (observed here as a wrong element-2 result; the same +// drain bug can also deadlock under different timing). +// +// Mirrors the reported sequence (vid.v -> mask compare -> masked merge) and +// self-checks: vfmerge must write 9.0f (0x41100000) into masked element 2. + +#include + +#ifdef SPIKE +#include "util.h" +#include +#elif defined ARA_LINUX +#include +#else +#include "printf.h" +#endif + +static volatile uint32_t mask_out[1]; +static volatile uint32_t out[4]; + +int main() { + uint64_t vl; + float f = 9.0f; + + asm volatile("vsetivli %0, 4, e32, m1, ta, ma" : "=r"(vl)); + asm volatile("vmv.v.i v8, 7"); // stand-in for a vle32.v load + asm volatile("vid.v v9"); // v9 = {0,1,2,3} + asm volatile("vmseq.vi v0, v9, 2"); // mask: element 2 set + asm volatile("vsm.v v0, (%0)" ::"r"(mask_out) : "memory"); + asm volatile("vfmerge.vfm v8, v8, %0, v0" ::"f"(f)); // merge under mask + asm volatile("vse32.v v8, (%0)" ::"r"(out) : "memory"); + + // Masked merge must write 9.0f (0x41100000) into element 2; others stay 7. + uint32_t expected = 0x41100000u; // 9.0f + if (out[2] == expected && out[0] == 7 && out[1] == 7 && out[3] == 7) { + printf("vid_drain_deadlock: PASS (out[2]=0x%x)\n", out[2]); + return 0; + } + printf("vid_drain_deadlock: FAIL (out={%u,%u,0x%x,%u}, expected out[2]=0x%x)\n", + out[0], out[1], out[2], out[3], expected); + return 1; +} From 4efbebf88636bd663f2251cd4541c533a08003f8 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 18 Jun 2026 09:21:21 +0000 Subject: [PATCH 3/6] :white_check_mark: [apps] Add vid_m4_hang reproducer (#437 closed by #448) The vid.v hang reported in #437 (widening multiply followed by vid.v at LMUL=4) shares the mask-unit ALU-operand-drain path fixed for #448. Confirmed on the Verilated model (4 lanes, VLEN=4096): * without the masku_operands.sv #448 fix: hangs (testbench timeout) * with the fix: completes (vid_m4=1) Refs #437 #448 --- apps/vid_m4_hang/main.c | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 apps/vid_m4_hang/main.c diff --git a/apps/vid_m4_hang/main.c b/apps/vid_m4_hang/main.c new file mode 100644 index 000000000..89d054ee0 --- /dev/null +++ b/apps/vid_m4_hang/main.c @@ -0,0 +1,44 @@ +// Copyright 2026 ETH Zurich and University of Bologna. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Probe for issue #437 (vid.v hang). Mirrors the reported sequence: a widening +// multiply followed by vid.v at LMUL=4 hangs a buggy Ara. Reaching the print +// means no deadlock. This shares the mask-unit VID path with #448. + +#include + +#ifdef SPIKE +#include "util.h" +#include +#elif defined ARA_LINUX +#include +#else +#include "printf.h" +#endif + +int main() { + uint64_t vl, res; + + asm volatile("vsetvli %0, zero, e16, m4, ta, ma" : "=r"(vl)); + asm volatile("vmv.v.i v0, 2"); + asm volatile("vwmul.vx v8, v0, %0" ::"r"(0L)); // widening -> v8 group (e32, m8) + asm volatile("vid.v v0"); // <- reported hang point + asm volatile("vadd.vi v0, v0, 1"); + asm volatile("vmv.x.s %0, v0" : "=r"(res)); // v0 = {0,1,2,...}+1 -> res = 1 + + printf("vid_m4=%d\n", (int)res); + return 0; +} From 2ce6c2cc976b3dea5f52858e3b9cea8d4779e033 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 18 Jun 2026 08:21:24 +0000 Subject: [PATCH 4/6] :bug: [masku] Trim vcpop/vfirst operand to the active vl vcpop.m and vfirst.m fed the full NrLanes*DataWidth mask word to the popcount/lzc units without masking tail bits beyond vl. With vl < VLMAX the agnostic tail bits were counted/scanned, so vcpop returned the slice width instead of vl and vfirst could report a 1 past the vector end. Zero operand bits at index >= (issue_cnt_q + in_ready_cnt_q*delta_elm_q), which reconstructs the valid element count for the current word. Verified with a Spike-vs-Ara differential (vcpop_vl app, 4 lanes, VLEN=4096): * before: Ara vcpop = {16,16,16,16,16}, Spike = {1,2,4,8,16} (diverge) * after: Ara vcpop = {1,2,4,8,16} == Spike (match) Refs #446 --- apps/vcpop_vl/main.c | 51 +++++++++++++++++++++++++++++++++++++ hardware/src/masku/masku.sv | 6 +++++ 2 files changed, 57 insertions(+) create mode 100644 apps/vcpop_vl/main.c diff --git a/apps/vcpop_vl/main.c b/apps/vcpop_vl/main.c new file mode 100644 index 000000000..e7ed53ae5 --- /dev/null +++ b/apps/vcpop_vl/main.c @@ -0,0 +1,51 @@ +// Copyright 2026 ETH Zurich and University of Bologna. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Spike-vs-Ara differential probe for issue #446 (vcpop.m ignores VL). +// +// vmset.m sets the active (vl) mask bits; the tail beyond vl is mask-agnostic. +// vcpop.m must count only the first vl bits. The bug counts tail bits too, so +// Ara reports more than vl while Spike reports exactly vl. Each line prints the +// computed population count for a given vl; diff the Spike and Ara outputs. + +#include + +#ifdef SPIKE +#include "util.h" +#include +#elif defined ARA_LINUX +#include +#else +#include "printf.h" +#endif + +#define PROBE(VLNUM) \ + do { \ + uint64_t vl, cnt; \ + asm volatile("vsetivli %0, " #VLNUM ", e32, m1, ta, ma" : "=r"(vl)); \ + asm volatile("vmset.m v0"); \ + asm volatile("vcpop.m %0, v0" : "=r"(cnt)); \ + printf("vl=%d vcpop=%d\n", (int)vl, (int)cnt); \ + } while (0) + +int main() { + PROBE(1); + PROBE(2); + PROBE(4); + PROBE(8); + PROBE(16); + return 0; +} diff --git a/hardware/src/masku/masku.sv b/hardware/src/masku/masku.sv index d2fdccdc2..c50eaa9b4 100644 --- a/hardware/src/masku/masku.sv +++ b/hardware/src/masku/masku.sv @@ -858,6 +858,12 @@ module masku import ara_pkg::*; import rvv_pkg::*; #( // VCPOP, VFIRST: mask the current slice and feed the popc or lzc unit [VCPOP:VFIRST] : begin vcpop_operand = (!vinsn_issue.vm) ? masku_operand_alu_seq & masku_operand_m_seq : masku_operand_alu_seq; + // #446: do not count/scan mask bits past the valid element range. + // issue_cnt_q + in_ready_cnt_q*delta_elm_q reconstructs the element + // count at the start of the current word, i.e. the last valid index. + for (int unsigned i = 0; i < NrLanes*DataWidth; i++) + if (i >= (vlen_t'(issue_cnt_q) + (vlen_t'(in_ready_cnt_q) * vlen_t'(delta_elm_q)))) + vcpop_operand[i] = 1'b0; end default:; endcase From d32583d6351d0a891567c8fa1ec657726d0e12fc Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Thu, 18 Jun 2026 17:12:41 +0000 Subject: [PATCH 5/6] :bug: [masku] Tag last selected vcompress element as terminator vcompress drives two completion markers through per-element FIFOs: vcompress_last_idx (index FIFO -> masku completion) and is_last_req (request FIFO -> lane VRGATHER FSM return-to-IDLE). Both were set on the last *input* element (vrgat_cnt == vl-1), but the FIFOs are only pushed for *selected* elements. When the trailing input elements are unselected, neither marker is ever pushed, so the masku never retires and the lane FSM never leaves REQUESTING (blocking the next vcompress) -> hang. Tag the last *selected* element instead: a selected element is the last output iff no mask bit is set in (vrgat_cnt, vl). The whole mask is in masku_operand_alu_seq when vl fits in one word, so look ahead there and set both markers on that element's FIFO entry. For vl beyond one word keep the legacy behavior. Root-caused via FST waveform analysis (the masku scan completes and the index FIFO drains, but vcompress_issue_end never asserts; separately the lane FSM stays in REQUESTING for want of is_last_req). Verified with a Spike-vs-Ara differential sweep (vcompress_sweep, 4 lanes, VLEN=4096): trailing-unselected, last-selected, all-selected, all-unselected, multi-word vl=20, and back-to-back ops all match Spike; before the fix the trailing-unselected case hangs. Refs #450 --- apps/vcompress_sweep/main.c | 81 +++++++++++++++++++++++++++++++++++++ hardware/src/masku/masku.sv | 19 +++++++++ 2 files changed, 100 insertions(+) create mode 100644 apps/vcompress_sweep/main.c diff --git a/apps/vcompress_sweep/main.c b/apps/vcompress_sweep/main.c new file mode 100644 index 000000000..73f1f0878 --- /dev/null +++ b/apps/vcompress_sweep/main.c @@ -0,0 +1,81 @@ +// Copyright 2026 ETH Zurich and University of Bologna. +// SPDX-License-Identifier: Apache-2.0 +// +// Self-checking regression test for issue #450 (vcompress hang). Covers the +// reported trailing-unselected case plus last-selected, all-selected, a +// multi-word vl, and the all-unselected (compress-to-0) edge. Built from vector +// ops only. Expected values verified against Spike. Returns nonzero on mismatch; +// a hang is caught by the testbench timeout. + +#include +#ifdef SPIKE +#include "util.h" +#include +#elif defined ARA_LINUX +#include +#else +#include "printf.h" +#endif + +static volatile uint32_t out[24]; + +static int check4(const char *tag, int e0, int e1, int e2, int e3) { + if (out[0] == (uint32_t)e0 && out[1] == (uint32_t)e1 && + out[2] == (uint32_t)e2 && out[3] == (uint32_t)e3) + return 0; + printf("%s FAIL: {%d,%d,%d,%d}\n", tag, (int)out[0], (int)out[1], (int)out[2], (int)out[3]); + return 1; +} + +int main() { + uint64_t vl; + int err = 0; + + // Case A: even selected -> last element unselected (the reported bug). {0,2,4,6} + asm volatile("vsetivli %0, 8, e32, m1, ta, ma" : "=r"(vl)); + asm volatile("vid.v v2"); + asm volatile("vand.vi v3, v2, 1"); + asm volatile("vmseq.vi v6, v3, 0"); + asm volatile("vcompress.vm v4, v2, v6"); + asm volatile("vse32.v v4, (%0)" ::"r"(out) : "memory"); + err += check4("A", 0, 2, 4, 6); + + // Case B: odd selected -> last element selected (regression guard). {1,3,5,7} + asm volatile("vid.v v2"); + asm volatile("vand.vi v3, v2, 1"); + asm volatile("vmsne.vi v6, v3, 0"); + asm volatile("vcompress.vm v4, v2, v6"); + asm volatile("vse32.v v4, (%0)" ::"r"(out) : "memory"); + err += check4("B", 1, 3, 5, 7); + + // Case C: all selected -> identity {0,1,2,3} + asm volatile("vid.v v2"); + asm volatile("vmset.m v6"); + asm volatile("vcompress.vm v4, v2, v6"); + asm volatile("vse32.v v4, (%0)" ::"r"(out) : "memory"); + err += check4("C", 0, 1, 2, 3); + + // Case E: multi-word vl=20, even selected -> {0,2,...,18}; check out[0,1,9] + asm volatile("vsetvli %0, %1, e32, m1, ta, ma" : "=r"(vl) : "r"((uint64_t)20)); + asm volatile("vid.v v2"); + asm volatile("vand.vi v3, v2, 1"); + asm volatile("vmseq.vi v6, v3, 0"); + asm volatile("vcompress.vm v4, v2, v6"); + asm volatile("vse32.v v4, (%0)" ::"r"(out) : "memory"); + if (!(out[0] == 0 && out[1] == 2 && out[9] == 18)) { + printf("E FAIL: out[0]=%d out[1]=%d out[9]=%d\n", (int)out[0], (int)out[1], (int)out[9]); + err++; + } + + // Case D: none selected (vl=8) -> compress to length 0; must complete. + asm volatile("vsetivli %0, 8, e32, m1, ta, ma" : "=r"(vl)); + asm volatile("vid.v v2"); + asm volatile("vmclr.m v6"); + asm volatile("vcompress.vm v4, v2, v6"); + + if (err == 0) + printf("vcompress_sweep: PASS\n"); + else + printf("vcompress_sweep: FAIL (%d)\n", err); + return err; +} diff --git a/hardware/src/masku/masku.sv b/hardware/src/masku/masku.sv index c50eaa9b4..f1101d4ad 100644 --- a/hardware/src/masku/masku.sv +++ b/hardware/src/masku/masku.sv @@ -1142,6 +1142,25 @@ module masku import ara_pkg::*; import rvv_pkg::*; #( vrgat_req_fifo_push = 1'b1; // Increase the number of elements to write vcompress_cnt_d = vcompress_cnt_q + 1; + // #450: tag the LAST selected element's FIFO entry as the terminator. + // The terminator carries both vcompress_last_idx (so the masku + // completes) and is_last_req (so the lane VRGATHER FSM returns to + // IDLE). Previously these only rode the last *input* element, which + // is never pushed when trailing elements are unselected -> hang. + // A selected element is the last one iff no mask bit is set in + // (vrgat_cnt, vl). The whole mask is in masku_operand_alu_seq when + // vl fits in one word; for larger vl keep the legacy behavior. + if (vinsn_issue.vl <= NrLanes*DataWidth) begin + automatic logic more_after; + more_after = 1'b0; + for (int unsigned i = 0; i < NrLanes*DataWidth; i++) + if ((i > vrgat_cnt_q) && (i < vinsn_issue.vl)) + more_after |= masku_operand_alu_seq[i]; + if (!more_after) begin + vcompress_last_idx_d = 1'b1; + vrgat_req_is_last_req_d = 1'b1; + end + end end end end From aee901c70f9eb9be644da556dc4a2ddd0de48661 Mon Sep 17 00:00:00 2001 From: Saurav Singh Date: Sat, 20 Jun 2026 09:36:13 +0000 Subject: [PATCH 6/6] :memo: [changelog] Document pr/mask-unit-fixes --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 542eb153d..bb76d5475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + + - Add `vid_drain_deadlock` app: differential reproducer for the mask-unit VID drain bug (#448) + - Add `vid_m4_hang` app: regression reproducer for the vid.v LMUL=4 hang (#437, closed by the #448 fix) + - Add `vcpop_vl` app: Spike-vs-Ara differential probe for the vcpop VL-trim bug (#446) + - Add `vcompress_sweep` app: self-checking regression test for the vcompress completion hang (#450) + ### Fixed + - Trim `vcpop.m`/`vfirst.m` operand to the active element range, fixing wrong counts when vl < VLMAX (#446) + - Tag the last selected vcompress element as the terminator (last_idx/is_last_req), fixing a hang when trailing elements are unselected (#450) + - Drain ALU operands in the mask unit during `vid.v` to avoid a lane deadlock (#448, also closes #437) - Fix dump vtrace script for vsetvli instructions without x0 (ideal dispatcher) - Fix Pathfinder and FFT performance - Stall Ara and wait for ara_idle upon CSR write/read