Targets Vortex release v3.0.
Every Vortex core counts how many cycles its issue stage spends blocked on the
scoreboard. Run any kernel with --perf=1 and look at the stalls: line:
PERF: stalls: fetch=0%, ibuf=0%, scrb=36%, opds=3%, alu=0%, lsu=0%, sfu=0%, fpu=0%
scrb=36% says that in more than a third of all cycles the next instruction of
every ready warp was waiting for a register still owned by an earlier,
in-flight instruction. It does not say what it was waiting for. A load that
missed in the data cache, a 4-cycle fmadd chain and a slow div in the SFU
call for different fixes: more warps, a prefetch, a shorter FPU pipeline,
a different instruction schedule. In this assignment you add the missing answer.
What you build. Every scoreboard-stall cycle is charged to the functional unit (ALU, FPU, LSU or SFU) that produces the register the stalled instruction is waiting on. The rule, applied once per cycle per issue slice:
- Among the warps of the slice that are blocked on the scoreboard this cycle, take the lowest-index one.
- Of its operands, checked in the order
rd, rs1, rs2, rs3, take the first one that is busy. - Charge the cycle to the unit of the instruction that owns that register.
If no register is busy (e.g. only an
fcsrfield is), charge the ALU.
So the four per-unit counts always add up to the existing total
VX_CSR_MPM_STALL_SCRB; they partition it.
Where the counters live. Performance counters are machine performance
monitoring (MPM) CSRs, grouped in classes (--perf=1 selects class 1 = CORE).
The host reads them over the DCR bus with a 5-bit index from
VX_CSR_MPM_BASE = 0xB00 (see the address math in
VX_dcr_data.sv
and the range check in
vx_device_mpm_query()),
so a class has at most 32 counters: 0xB00–0xB1F (low halves) and
0xB80–0xB9F (high halves). In the CORE class only three slots are
free: 0xB18 and 0xB19 (commented out as reserved: VX_CSR_MPM_MEM_READS/WRITES
in VX_types.toml)
and 0xB1F. Four units, three slots: you store FPU, LSU and SFU and derive
ALU as the remainder STALL_SCRB - (fpu + lsu + sfu).
Output format. The runtime prints one new line after stalls:, per core
(only when there is more than one core) and in total. Percentages are of the
scoreboard total, not of cycles:
PERF: scoreboard stalls=291525 (alu=11%, fpu=79%, lsu=10%, sfu=0%)
Clone the v3.0 release and build it out of tree. If you fetched the v3.0
tag before 2026-10-07, refresh it first with git fetch --tags --force.
git clone --branch v3.0 --recursive https://github.com/vortexgpgpu/vortex.git
cd vortex
mkdir build32 && cd build32
../configure --xlen=32 --tooldir=$HOME/tools
./ci/toolchain_install.sh --all # prebuilt toolchain, first time only
make -sAll commands below run from build32/. After any edit to a *.toml file or a
Makefile, run ../configure again (same arguments) and then make -s;
configure is what regenerates build32/sw/VX_types.h and
build32/hw/VX_types.vh from the toml.
Check the baseline before you change anything:
./ci/blackbox.sh --driver=simx --cores=1 --app=sgemm --perf=1No handout patch is needed for this assignment.
Read these first; the SimX scoreboard already knows the answer, you only have to consume it.
sim/simx/scoreboard.cpp:in_use(trace)is true when any destination or source register oftrace(or anfcsrfield) is reserved.get_uses(trace)(L82) returns onereg_use_tper busy register, destination first, thenrs1..rs3. Each entry carries the owner'sfu_type(scoreboard.h).Core::issue()loops over issue slices (iw) and, inside, over the slice's warps (w, lowest index first). Whenscoreboard_->in_use(uop_trace)is true it already callsget_uses()for the debug trace and setsany_scrb_blocked = true(L618). At the end of the slice loop (L723) it incrementsperf_stats_.scrb_stallsonce.
In VX_types.toml, section [csr_mpm_core], replace the four
# reserved: VX_CSR_MPM_MEM_READS/WRITES comment lines with your three counters:
# PERF: scoreboard stall attribution (producer unit of the blocking operand)
# The ALU share is not stored: it is STALL_SCRB minus the counters below.
VX_CSR_MPM_SCRB_FPU = 0xB18 # scoreboard stalls waiting on an FPU result
VX_CSR_MPM_SCRB_FPU_H = 0xB98
VX_CSR_MPM_SCRB_LSU = 0xB19 # scoreboard stalls waiting on an LSU result
VX_CSR_MPM_SCRB_LSU_H = 0xB99
VX_CSR_MPM_SCRB_SFU = 0xB1F # scoreboard stalls waiting on an SFU result
VX_CSR_MPM_SCRB_SFU_H = 0xB9FRegenerate the headers and check them:
../configure --xlen=32 --tooldir=$HOME/tools
grep SCRB_ sw/VX_types.h hw/VX_types.vhIn sim/simx/core.h,
next to scrb_stalls:
uint64_t scrb_stalls = 0;
uint64_t scrb_fpu = 0;
uint64_t scrb_lsu = 0;
uint64_t scrb_sfu = 0;This is the core of the assignment. In sim/simx/core.cpp, inside the slice
loop for (uint32_t iw = 0; ...):
-
Next to
bool any_scrb_blocked = false;declare the unit the current cycle is charged to, defaulting to the ALU:FUType scrb_fu_type = FUType::ALU; -
In the
if (scoreboard_->in_use(uop_trace))branch, replaceany_scrb_blocked = true;so that only the first blocked warp of the slice sets the unit, and it takes thefu_typeof the first entry ofuses. Guard against an emptyusesvector:in_use()can be true when only anfcsrfield is busy, andget_uses()then returns nothing. -
Extend the
// track scoreboard stallsblock at the end of the slice loop so that, besides++perf_stats_.scrb_stalls, it incrementsscrb_fpu,scrb_lsuorscrb_sfuaccording toscrb_fu_type. ALU (and TCU, when the tensor core is enabled) increment nothing: they are the remainder.
In CsrUnit::get_csr(),
case VX_DCR_MPM_CLASS_CORE, after the VX_CSR_MPM_STALL_SCRB line:
CSR_READ_64(VX_CSR_MPM_SCRB_FPU, core_perf.scrb_fpu);
CSR_READ_64(VX_CSR_MPM_SCRB_LSU, core_perf.scrb_lsu);
CSR_READ_64(VX_CSR_MPM_SCRB_SFU, core_perf.scrb_sfu);All in sw/runtime/common/perf.cpp:
-
struct CoreCounters(L124): addscrb_fpu,scrb_lsu,scrb_sfu(alluint64_t,= 0) afterstall_scrb. -
After
print_metric_list()add a printer. It computes the ALU remainder and prints through the existing helpers (perf_print_core()adds thecore<N>:prefix,perf_print()does not):static void print_scrb_attribution(FILE *stream, int core_id, const CoreCounters &c) { const uint64_t claimed = c.scrb_fpu + c.scrb_lsu + c.scrb_sfu; const uint64_t scrb_alu = (c.stall_scrb > claimed) ? (c.stall_scrb - claimed) : 0; const char *fmt = "scoreboard stalls=%" PRIu64 " (alu=%d%%, fpu=%d%%, lsu=%d%%, sfu=%d%%)"; // TODO: compute the four percentages of c.stall_scrb with calc_percent() // and print with perf_print_core() if core_id >= 0, else perf_print(). }
-
In
case VX_DCR_MPM_CLASS_COREofvx_device_dump_perf(), query the three counters right afterVX_CSR_MPM_STALL_SCRB(L313):CHECK_ERR(vx_device_mpm_query(hdevice, mpm_class, VX_CSR_MPM_SCRB_FPU, core_id, &c.scrb_fpu), { return err; });
(and the same for LSU and SFU).
-
Call
print_scrb_attribution(stream, core_id, c);right after the per-coreprint_metric_list(stream, "stalls", core_id, stall_metrics);(L365). -
Accumulate the totals next to
tot.stall_scrb += c.stall_scrb;(L402), and callprint_scrb_attribution(stream, -1, tot);after the globalprint_metric_list(stream, "stalls", -1, global_stalls);(L450).
../configure --xlen=32 --tooldir=$HOME/tools
make -s
./ci/blackbox.sh --driver=simx --cores=1 --app=demo --perf=1
./ci/blackbox.sh --driver=simx --cores=1 --app=vecadd --perf=1
./ci/blackbox.sh --driver=simx --cores=1 --app=sgemm --perf=1
./ci/blackbox.sh --driver=simx --cores=4 --app=sgemm --perf=1 # per-core lines + total--perf=1 adds -DPERF_ENABLE, and a new --cores value is a new
configuration, so the first run of each combination rebuilds the SimX driver
(a minute or two). Check that:
- every run prints
PASSED!; - the four percentages add up to 100 (±1 for rounding);
scoreboard stalls=is the existingSTALL_SCRBtotal: on 1 core it is aboutscrb%×cycles(thescrb=field of thestalls:line, at issue width 1);demo(integer) has no FPU share,sgemmis FPU-dominated.
Part B implements the same counters, rule and output in the RTL. Steps 1 (toml), 4–5 (runtime) of Part A are shared: do not repeat them. Your SimX build from Part A is the reference.
SimX keeps a pointer to the producing instruction for every busy register. The
RTL scoreboard
(hw/rtl/core/VX_scoreboard.sv)
keeps one bit per register and nothing else:
| Signal | Line | Meaning |
|---|---|---|
g_scoreboard[w] |
L100 | one generate iteration per warp of the issue slice |
inuse_regs |
L101 | busy bit per architectural register (int + float) |
stg_opds, stg_opd_mask[i] |
L111, L125 | the staged instruction's {rs3, rs2, rs1, rd} and a one-hot register mask per operand |
inuse_regs_n |= stg_opd_mask[0] |
L149 | reserve rd when the staged instruction fires with wb |
operands_ready[w] |
L232 | registered readiness; folds data hazards and FU-queue congestion |
perf_stall_per_cycle |
L74 | |(stg_valid_in & ~operands_ready), counted into perf_stalls |
So you need a new per-warp array that remembers, for each reserved register,
the ex_type of the instruction that reserved it (EX_ALU, EX_LSU, EX_SFU,
EX_FPU in
VX_gpu_pkg.sv).
In hw/rtl/VX_gpu_pkg.sv, issue_perf_t
(L1309),
next to scb_stalls:
logic [NUM_EX_UNITS-1:0][PERF_CTR_BITS-1:0] scb_stall_units;-
Add an output under the existing
ifdef PERF_ENABLEin the port list:output reg [NUM_EX_UNITS-1:0][PERF_CTR_BITS-1:0] perf_stall_units,
-
Inside
g_scoreboard, afterassign operands_ready[w] = operands_ready_r;, add aPERF_ENABLEblock that- declares
reg [NUM_REGS-1:0][EX_BITS-1:0] inuse_units;and writesinuse_units[staging_if[w].data.rd] <= staging_if[w].data.ex_typeon the same condition that reservesrd(staging_fire && staging_if[w].data.wb). No reset is needed: an entry is only read while its register is busy, and it was written when the register was reserved; - computes, combinationally, the unit of the first busy operand in
rd, rs1, rs2, rs3order. A loop that walks the operands fromNUM_OPDS-1down to0and overwrites the result whenever|(inuse_regs & stg_opd_mask[i])is set ends with the lowest busy index. Default toEX_ALU; - exports it as
stall_units[w](awire [PER_ISSUE_WARPS-1:0][EX_BITS-1:0]declared at slice level).
Use
inuse_regs, notwb_inuse_regs.operands_ready_ris one cycle behind, so in the writeback cycle the warp is still counted as stalled whilewb_inuse_regsalready shows the register free; with the released view the last stall cycle of every dependency would fall to the ALU. - declares
-
At slice level, replace
perf_stall_per_cycleby a maskstg_valid_in & ~operands_readyand its OR, select the lowest-index stalled warp with aVX_priority_encoder(data_in= the mask, useindex_out), and add one counter per unit:for (genvar e = 0; e < NUM_EX_UNITS; ++e) begin : g_perf_stall_units always @(posedge clk) begin if (reset) begin perf_stall_units[e] <= '0; end else begin perf_stall_units[e] <= perf_stall_units[e] + PERF_CTR_BITS'(perf_stall_per_cycle && perf_stall_unit == EX_BITS'(e)); end end end
-
VX_issue_slice.sv, scoreboard instance: connect.perf_stall_units (issue_perf.scb_stall_units). -
VX_issue.sv, inside the existingg_issue_perf_units_usesloop, sum across issue slices like the neighbouring counters:`PERF_COUNTER_ADD (issue_perf, per_issue_perf, scb_stall_units[i], PERF_CTR_BITS, `VX_CFG_ISSUE_WIDTH, (`VX_CFG_ISSUE_WIDTH > 2))
-
VX_core.svneeds nothing:pipeline_perf.issueis wired as a whole struct.
In VX_csr_data.sv,
VX_DCR_MPM_CLASS_CORE case, next to STALL_SCRB:
`CSR_READ_64(`VX_CSR_MPM_SCRB_LSU, dcr_data_w, pipeline_perf.issue.scb_stall_units[EX_LSU]);
`CSR_READ_64(`VX_CSR_MPM_SCRB_SFU, dcr_data_w, pipeline_perf.issue.scb_stall_units[EX_SFU]);Put the FPU read inside the existing `ifdef VX_CFG_EXT_F_ENABLE block next
to STALL_FPU (L421):
EX_FPU is defined as EX_SFU + VX_CFG_EXT_F_ENABLED, so without F it
aliases the SFU.
Run the four commands of Part A Step 6 with --driver=rtlsim. Build cost: every
distinct configuration (--perf=1, each --cores value) is a full Verilator
build, 10–15 min on an idle 4-core machine and longer on a shared one. You can
build ahead while you read:
CONFIGS="-DVX_CFG_NUM_CORES=4 -DPERF_ENABLE" make -C tests/regression/demo runtime-rtlsimCross-check against Part A:
instrs=must match SimX exactly for every app and core count;- the top unit must agree: LSU for
demo, FPU forsgemm(invecaddthe ALU and LSU shares are within a few points of each other and may swap); - totals agree within ~15% on 1 core; the RTL ALU share is 4–15 points higher
in every run except 4-core
demo, where both are 1–2% (see Question 5).
Default app sizes, default v3.0 configuration (4 warps × 4 threads, issue
width 1, no L2), --perf=1. All runs pass. stalls is the
scoreboard stalls= total (summed over cores), cycles and instrs are the
PERF: instrs=..., cycles=... line, and scrb% is the existing
stalls: scrb= field (scoreboard stall cycles as a share of all cycles).
Default sizes: demo -n64 processes 64 ints per hardware thread, so its
problem grows with the core count (4 cores do 4× the 1-core work:
9,776 = 4 × 2,444 instructions); vecadd -n64 (64 floats) and sgemm
(64×64) are fixed-size, so their instruction counts barely change.
| app | cores | SimX stalls | SimX alu/fpu/lsu/sfu % | rtlsim stalls | rtlsim alu/fpu/lsu/sfu % | SimX / rtlsim cycles | instrs (both) |
|---|---|---|---|---|---|---|---|
| demo | 1 | 2,158 | 34 / 0 / 65 / 0 | 1,833 | 43 / 0 / 57 / 0 | 6,529 / 6,511 | 2,444 |
| vecadd | 1 | 422 | 38 / 22 / 39 / 1 | 463 | 42 / 20 / 36 / 2 | 1,270 / 1,216 | 400 |
| sgemm | 1 | 291,525 | 11 / 79 / 10 / 0 | 297,573 | 20 / 71 / 10 / 0 | 802,800 / 797,014 | 336,912 |
| demo | 4 | 105,531 | 2 / 0 / 98 / 0 | 193,966 | 1 / 0 / 99 / 0 | 31,878 / 49,048 | 9,776 |
| vecadd | 4 | 496 | 25 / 19 / 52 / 4 | 568 | 32 / 16 / 45 / 6 | 498 / 448 | 448 |
| sgemm | 4 | 331,651 | 10 / 70 / 19 / 0 | 368,374 | 25 / 59 / 15 / 0 | 206,224 / 204,728 | 336,960 |
scrb% (SimX / rtlsim): demo 33 / 28, vecadd 33 / 38, sgemm 36 / 37 on 1 core;
demo 83 / 99, vecadd 25 / 33, sgemm 40 / 45 on 4 cores. On 4 cores the
per-core lines of demo and sgemm are within 1–2 points of the totals
(vecadd, with only 113–150 stalls per core, varies by up to 4 points),
e.g. SimX sgemm:
PERF: core0: scoreboard stalls=86483 (alu=11%, fpu=70%, lsu=19%, sfu=0%)
PERF: core1: scoreboard stalls=81013 (alu=10%, fpu=70%, lsu=20%, sfu=0%)
...
PERF: scoreboard stalls=331651 (alu=10%, fpu=70%, lsu=19%, sfu=0%)
The table is also in expected/a1/scoreboard_attribution.csv.
The 4-core demo cycle gap (31,878 vs 49,048) is a memory-system difference
between the two v3.0 models that exists without your change; the attribution
(98–99% LSU) is the same on both.
Reproduce (from build32/):
for app in demo vecadd sgemm; do
for c in 1 4; do
./ci/blackbox.sh --driver=simx --cores=$c --app=$app --perf=1 | tee simx_${app}_c$c.log
./ci/blackbox.sh --driver=rtlsim --cores=$c --app=$app --perf=1 | tee rtlsim_${app}_c$c.log
done
done
grep -H "scoreboard stalls\|PERF: instrs" *_c?.logdemo(TYPE=int, a loop over 64 elements per thread) andvecadd(TYPE=float, one element per thread, no loop) both computedst = src0 + src1as load, load, add, store. Explain the ALU share ofdemoand the FPU share that appears invecaddfrom that instruction sequence: which instruction waits on which producer?sgemmis FPU-dominated on one core, but its LSU share nearly doubles on four cores (10% → 19% in SimX). What changed in the memory system? The FPU share drops from 79% to 70% although the total FPU work is unchanged (sameinstrs): compute the FPU-attributed stall cycles in both runs (share × total) and explain what actually grew.- On four cores
demospends 83–99% of its cycles stalled, nearly all on the LSU. Would adding warps help? Would a data-cache prefetch (Assignment 2) help? Argue from the numbers. - Why do SimX and rtlsim cycles differ (e.g. sgemm 802,800 vs 797,014) when the instruction counts are identical?
- The RTL ALU share is higher than SimX in every run except 4-core
demo(where both are 1–2%), e.g. sgemm 20% vs 11% on 1 core. Look at howoperands_readyis computed inVX_scoreboard.svand at thefu_full_seen_path in SimXCore::issue(): which stall cycles does the RTL count that SimX does not, and where does your rule put them? - You charge a cycle to the lowest-index stalled warp only. Propose a rule that is fairer to other warps. What would it cost in counters, and why does the 5-bit MPM index make that hard?
- With
VX_CFG_EXT_TCU_ENABLE, where do stalls waiting on the tensor core land in your output? What would you need to report them separately?
