Skip to content

Memory-level parallelism (MLP), phases 1 and 2 - #436

Open
tinebp wants to merge 126 commits into
masterfrom
feature/mlp-phase2
Open

tinebp wants to merge 126 commits into
masterfrom
feature/mlp-phase2

Conversation

@tinebp

@tinebp tinebp commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds memory-level parallelism (MLP) to the memory system, in two phases:

  • Phase 1: more requests in flight at every level of the memory system.
  • Phase 2: cheaper fences, read merging, and closer agreement between SimX and the RTL.

The design is documented in docs/designs/mlp_optimizations.md.

Perf vs master 50cb174 (rtlsim perf_gate, XLEN 32, CI toolchain LLVM v3.0.1):

  • Geomean is −3.36% cycles over all 49 cases.
  • Retired instruction counts are identical in all 49 cases.

What changes

Phase 1: requests in flight (afd4331 and earlier)

  • MSHR line table, so a cache can hold more misses in flight.
  • Hashed bank select, so power-of-two strides spread across banks.
  • Back-to-back fills into the bank pipeline.
  • Write-allocate without fetching the line at a write-back LLC.
  • Flush:
    • one broadcast write, with per-port flush tokens;
    • only the L1s flushed between launches when the LLC is the coherence point.
  • Fences:
    • stop at the point of coherence;
    • IO fences skip the cache walk.
  • Each LSU block has its own memory path.
  • Each TCU block has its own metadata port and its own TCU_LD AGU.
  • AFU: one AXI port per GPU memory port, with many transactions in flight.
  • The command processor's DMA goes through the shared LLC.
  • Fetch width is decoupled from issue width (VX_CFG_FETCH_WIDTH).
  • Core, socket and cluster counts no longer have to be powers of two.
  • Redesigned DRAM model (independent channels, refresh, configured from data-sheet values).
  • Hardware profiles: a100, h100, v100, rtx1080, tinygpu.
  • RV64 portability fixes across sw/tests.
  • Zero-warnings build.
  • SimX:
    • matches the RTL's buffer structure, arbiters and registered ports at each pipeline boundary;
    • follows the RTL in KMU, CTA retirement, barrier and collector timing.

Phase 2: fences, merging, parity (347c914..c08c245)

  • Memory fences:
    • split into acquire and release;
    • .aq/.rl AMOs are ordered;
    • .aq/.rl fences are dropped at a coherent dcache.
  • Dcache read merge:
    • a warp's identical loads are served with one bank access;
    • port 0 now joins the merge.
  • Same-line icache fetches from a core's fetch slices are merged.
  • Lazy-fetch:
    • a fully written sector is refetched without a memory read;
    • fixed a dcache deadlock between AMO write-back and fills.
  • A draining write-through chain stays pending; bank paths retimed.
  • Kernel args are staged in clean slots, without a cache flush.
  • TCU output is fp32 only, with bit-exact TFR numerics in SimX, plus a tcu_unit test.
  • SimX:
    • models the miss path's buffers, LSU ingress, SFU result buffers and the lmem crossbar as the RTL does;
    • keeps the LLC across a launch reset.

Since merging master (058d0e7..40ec5f4)

  • Merges origin/master 50cb174, which brings in Rv64 fixes #427, dispatch fix on single-packet inst #428, MUFU + Zfhmin #432 (MUFU and Zfhmin) and RTU performance counters + perf-counter fixes #435 (RTU perf counters).
  • Merge fixes:
    • removes a stray MUFU instance from VX_sfu_unit;
    • restores the RTU perf-counter read in VX_csr_mpm (lint failed with PERF_ENABLE and the RTU on).
  • SimX RVC fix: an instruction that straddles two fetch words keeps its low half until decode accepts it. Before this, a full decode latch made it decode the wrong half-word and hang. It was seen at RV64 with C on.
  • CI: the model_parity and perf_gate cells are split into shards of about an hour each.
  • perf_gate baselines re-recorded after the merge. Cycle and instruction counts are unchanged; only the config hashes moved.

Validation

  • Final gate on Phase 1 + Phase 2:
    • lint, x32 and x64: 40/40 clean;
    • regression: simx 30, simx xlen64 30, rtlsim 32;
    • cache: simx 27, rtlsim 36;
    • unittest: 10;
    • model_parity: 47 passed. The worst cases are sgemmx at 3.40% and sgemv at 3.38%; the tolerance is 5%.
    • perf_gate: 49 cases recorded.
  • After the merge:
    • perf_gate: 49/49 cases have the same cycles and instruction counts as before the merge.
    • perf-enabled lint: 7/7.
    • model_parity for raytracing and MUFU: 4/4.
    • RVC 64-bit regression passes on SimX.
    • perf_gate with RVC on by default: 49/49 cases complete, with no hangs.
  • FPGA gate (U55C, 300 MHz target), at c08c245:
    • core: 291.5 MHz;
    • top: 267.9 MHz, against a master baseline built at a 250 MHz target.
  • ASIC gate (yosys), at c08c245 plus the catalog fix: all 12 blocks pass; top runs at 618 MHz against a 621 MHz baseline, with +0.5% area.
  • ASIC gate at 40ec5f4, on the blocks the merge touched: all pass.
    • core: 586.6 MHz (baseline 586.7), cell area −0.06%.
    • mufu: 1039.5 MHz, identical to the baseline.
    • rtu: 904.5 MHz (baseline 903.4), cell area +0.01%.
  • FPGA gate: not rerun since the merge. The RTL the merge changed is mostly off in the default configuration (MUFU, Zfhmin, PERF/RTU counters). The always-on part is a few LUTs, away from the core's critical path.

Known items

  • perf_gate cases slower than master: six cases are slower by 0.24–0.57%. The worst is tensor:perf_gate-sgemm2-fp16 at +0.57%. The rest are below 0.4%: copy-mcast, vecadd-nt4, mxfp8, wgmma-fp16-ss and om-nt16.
  • sgemm2-fp16: it is sensitive to interleaving. With read merge on and off, the final tree differs by only 180 cycles.
  • tensor:perf_gate-fp16-mc-nt16: it is −8.6% vs master but +6.3% vs Phase 1. Port 0 joining the read merge changes how CTAs split across the two cores, and this shape is bound by the slower core.
  • Master's own nightly FPGA gate has failed since Oct 17 on the cache build (301.8 → 280.3 MHz). This branch keeps its own cache RTL, which the merge did not change.

🤖 Generated with Claude Code

tinebp and others added 30 commits October 6, 2026 13:09
LinkedList::push_back inserted before the current tail, because insert()
places a node ahead of its position: pushing 1, 2, 3 produced 2, 3, 1. The
timing wheel and the zero-delay event list are LinkedLists, so two packets
sent into one SimChannel in the same cycle with the same delay arrived
reversed. Models almost never send twice per channel per cycle, which kept
it hidden; a two-wide fetch prototype exposed it as a younger instruction
issuing ahead of its producer.

push_back now appends at end(). The header also includes <cstddef> so it
compiles on its own. tests/unittest/linked_list checks the order and fails
against the old header.

SimX legs of all 31 model_parity cases: identical retired instructions;
cycles move by at most 0.7%, except rt_raycast +1.7%, toward rtlsim
(gap 2.4% -> 0.8% against a current rtlsim run). rtlsim and the perf gate
do not use this code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On RV64 an index computed in uint32_t wraps at 32 bits, so LLVM cannot
widen it into a 64-bit induction variable or fold it into a pointer
increment, and re-emits zero-extend + shift + add for every access
(sgemm's unrolled body: 90 instructions on RV64 vs 35 on RV32). Indices
that feed pointer arithmetic are now size_t, which on RV32 is the same
type as uint32_t, so RV32 kernels compile to identical code; vm_stress
uses int, where size_t moved the loop exit onto a divergent per-thread
pointer compare.

RV64 correctness:
- vx_dxa.h: keep the wgather result (lane 0 carries the shared-memory
  address) at register width instead of truncating it to 32 bits.
- vx_graphics.h: compute the OM aperture address in uintptr_t and pass it
  to the export at register width; on RV32 the old expression was a
  64-bit sum whose dead carry cost an instruction per fragment.
- vx_intrinsics.h, vx_tensor.h, packld_box: pack-load strides and ldm
  steps are size_t; the address unit reads the full register.
- vx_serial.S: keep sp 16-byte aligned across the callback (frames 32/64
  bytes on RV32/RV64); select the XLEN path with __riscv_xlen, as
  LOAD_IMMEDIATE64 in src/common.h now does.
- vx_start.S: __dso_handle is pointer-sized on RV64.
- vx_syscalls.c: use newlib's prototypes for _lseek/_read/_write/_sbrk.
- dotproduct2: don't pass uninitialized upper bytes through the shuffle.

Kernels whose RV64 cycles got worse with a wider index are left as they
were: sgemv, demo, probe, fence, dogfood, dotproduct, softmax, vm_test,
mpi_dotproduct, mpi_put_dotproduct, sgemm_tcu, sgemm_tcu_mx,
sgemm_tcu_sp_mx and sgemm_tcu_wg. In most of them LLVM's loop strength
reduction moves the loop exit onto a per-thread pointer compare, which
needs predication every iteration.

RV64 simx cycles: sgemm -55%, mstress -28%, sgemm_tcu_wg_mx -22%,
sgemm2_dxa -21%, stencil3d -20%, sgemm_v1 -18%, sgemm2_v1 -16%,
dxa_copy_mcast -16%, sgemm2 -12%, sgemm2_dxa_mcast -12%, jacobi -10%,
sgemm2_tcu -6%; no measured kernel is slower (mpi_sgemm, mpi_blocked_sgemm
and the graphics pipeline kernels print no cycle counts and were checked
for correctness only). On RV32 only the OM export path and vx_serial's frame
change; the
affected graphics perf-gate baselines are regenerated with
--update-baselines (om -1.3%/-1.9%, draw3d -0.4% cycles on rtlsim).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On RV64 the trace lost the upper half of both warp-uniform pointers. SimX
narrowed the scene base to 32 bits, so a scene above 4 GiB was read from the
wrong address and every ray missed. SimX and the RTL both carried the payload
pointer as 32 bits and wrote it back through the single VX_RT_PAYLOAD_PTR_LO
slot, so a callback shader got a truncated payload pointer. vx_rt_wtrace took
uint32_t pointers and narrowed the gathered config, and every tests/raytracing
kernel masked its device addresses to 32 bits, which kept all of it hidden:
device buffers normally sit below 4 GiB.

- The high word goes in slot VX_RT_PAYLOAD_PTR_LO + 1, which already sat
  unused next to the low word. It is not a new VX_types define: the perf-gate
  fingerprint hashes VX_types, so a define would mark every perf baseline stale
  without moving a cycle.
- SimX keeps the scene base at the device address width and the payload at
  full width, and writes the high word on RV64.
- RTL widens payload_ptr to XLEN across the RTU bus, the arm staging RAM and
  the slot descriptor. On RV64 the record write-back sends one more word (the
  high half) before the hit attribute; STATUS stays last. At XLEN=32 the logic
  is unchanged.
- vx_raytrace.h takes uintptr_t pointers, keeps the gathered config at register
  width, and adds vx_rt_get_payload(), which reads both words on RV64.
- The tests/raytracing kernels pass full-width pointers. On a 64-bit device
  rt_smoke_chs places its scene and payload above 4 GiB; it fails before this
  change (miss, payload never written) and passes after.
- ci/testcases/raytracing.yaml adds rt_suite-x64, the suite on RV64 simx.
- hw/unittest gfx and rtu_core tops follow the wider arm word.

RV32: all 34 ray-tracing kernels compile to identical code; the three
rt_raycast perf_gate cases reproduce their baseline cycles and instruction
counts exactly, the rtlsim suite and model_parity pass, and every perf_gate
config hash is unchanged.

RV64: the simx suite passes 34/34, and rtlsim passes 31/32 plus
rt_smoke_chs above 4 GiB. rt_raycast fails pixel comparison on RV64 rtlsim
identically before and after this change (210/768 pixels); that is a separate,
pre-existing RTL defect, so rt_suite-x64 stays simx-only until it is fixed.
The extra RV64 write-back word moves RV64 rtlsim rt_raycast cycles by 0% to
+5.5% depending on shape (NT4 +5.1%, NT16 0.0%, 2-core +0.7%, 8x8 +5.5%)
against two added cycles per record; RV64 rtlsim has no perf gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On RV64 with the D extension an FP register is 64 bits, and a single-precision
value in it must have its upper word all ones; anything else reads back as the
canonical NaN. The core-side RTU unit zero-extended every slot read, so the
hit t/u/v that vx_rt_wait (GETWF) lands in f0..f2 arrived as NaN, and the
shading that used them went wrong on every hit. rt_raycast failed pixel
comparison on RV64 rtlsim (210/768 pixels, maxdiff 172) while simx passed:
SimX already NaN-boxes GETWF, as the LSU does for flw and the TCU for its
results. The result path now NaN-boxes when the destination is an FP register,
the same test the LSU uses; GP reads and RV32 are unchanged.

rt_suite-x64 now also runs on rtlsim.

RV64 rtlsim: rt_raycast passes with 0/768 mismatched pixels at NT4, NT16,
2-core and 8x8, and its retired instruction count (46136 at NT4) now matches
simx. The ray-tracing CI category passes at xlen 64 on simx and rtlsim, and
at xlen 32 on rtlsim (suite, model_parity, perf_gate unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange

Setup converts the screen-space depth plane Z = A'x + B'y + C' to Q7.24 with
a plain float-to-int cast. C' is the plane at the screen origin, so a steep
plane on a triangle away from the origin puts it past Q7.24's +-128 range even
though the depth at every covered pixel is in range. That conversion is
undefined: RISC-V fcvt saturates (wrong depth on the device), x86 cvttss2si
returns INT_MIN. gfx_pipeline_kernel failed on primbuf[41] at both xlen 32 and
64 with dev C' = INT_MAX against ref C' = INT_MIN.

Late-Z and the raster early-Z (SimX and RTL) all evaluate the plane mod 2^32,
so a term stored modulo 2^32 gives the exact depth at the covered pixels.
PlaneToFixed keeps the plain cast for in-range terms, wraps an out-of-range
term through int64, and maps a non-finite or absurdly steep term (past 2^39,
which would overflow int64) to 0 so every input is defined on host and device.

gfx_setup_kernel, gfx_binsort_kernel and gfx_pipeline_kernel compare the device
front end byte for byte against the host reference but were in no CI category;
they now run in graphics at xlen 32 and 64.

gfx_pipeline_kernel passes at xlen 32 and 64 (failed at both before). graphics
simx 38/38 at xlen 32 and 64, vulkan simx 13/13 at xlen 32 and 64, graphics
rtlsim 56/56 at xlen 32 including every perf_gate case unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…detour

vx_printf and vx_vprintf carried no format attribute, so a kernel could pass a
64-bit value, a pointer or a size_t under a %d and the compiler said nothing;
on RV64 that reads the wrong varargs slot. They are now declared
format(printf, ...), so -Wall -Werror rejects a mismatched call. tinyprintf
already implements the l/ll/z/j/t/h modifiers and %p the checker asks for.
Every current caller (vx_spawn.c, tests/kernel/*, tests/regression/printf)
is clean at xlen 32 and 64; a deliberately mismatched call is rejected at both.

vx_putint copied its int argument into a float before itoa, so any value past
2^24 printed rounded (16777217 printed as 16777216). It now stays an int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…W_LOAD_B

The SW_LOAD_B cooperative load still computed its indices in uint32_t, the
pattern the RV64 index-portability change replaced in the SW_LOAD_A loop next
to it; it now uses size_t (the same type as uint32_t on RV32). RV64 simx at the
dxa sgemm_tcu_wg_dxa-1 config, 8 warps x 32 threads: SW_LOAD_A + SW_LOAD_B
28832 vs 29141 cycles (-1.1%), SW_LOAD_B alone 23167 vs 23160 (flat).

With both loads in software no DXA transfer is issued, is_dxa_warp goes unused,
and -Werror rejected the build at xlen 32 and 64; it is now [[maybe_unused]],
like the descriptor ids above it.

RV32 code is identical with SW_LOAD_B, and the default (DXA) build CI runs is
identical at xlen 32 and 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m_tcu

These five kernels kept uint32_t indices in cbd9cbe because widening them
made RV64 slower: LLVM's LoopTermFold moved each loop's uniform exit test onto
a per-thread pointer, which needs divergence predication every iteration.
llvm_vortex a81a5957f406 keeps a uniform exit uniform on targets with branch
divergence, and with it the size_t form is the faster one on RV64 simx:
demo -20.4%, fence -20.6%, dogfood -14.6%, softmax -10.7%, sgemm_tcu -8.6%
cycles. RV32 code is identical (size_t is uint32_t there).

This depends on that compiler change: with the current toolchain the same
kernels are 1.4-3.8% slower on RV64 (demo +1.8%, fence +1.4%, dogfood +1.5%,
softmax +3.8%, sgemm_tcu +2.1%). It must land after the toolchain prebuilt
carrying a81a5957f406, together with the perf baselines that compiler moves.

The other kernels left narrow in cbd9cbe stay so: sgemv (+5.0%),
dotproduct (+1.5%), sgemm_tcu_mx (+3.7%), sgemm_tcu_sp_mx (+1.8%) and
sgemm_tcu_wg (+1.3%) are still slower widened with the new compiler, and
vm_test is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the branch up to master 7fe1936 so it can be proposed as one pull
request. Conflicts:
- vx_dxa.h: master's new __VX_DXA_ISSUE1/2 macros pinned the gathered
  operands in uint32_t registers, which re-truncates the DXA shared-memory
  address on RV64; the macros are kept and widened to size_t.
- rt_smoke_ahs_geom: master's capture-buffer rename over the full-width
  scene/payload pointers.
- ci/baselines/perf/graphics.json: master's file; the entries this branch
  moves are re-recorded with --update-baselines in a later commit.

The merged tree equals the rv64-portability-fixes rebase that was validated
on master, plus the five widened kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code merged from master after the RTU pointer widening narrowed the scene
again on RV64:
- vx_rt_wtrace pinned the gathered trace config in a uint32_t x31 register,
  truncating the scene base it had just gathered at full width;
- the SimX ray log stored scene_root in 32 bits, so a snapshot of a scene
  above 4 GiB read the wrong memory, and rt_replay replayed it against that
  address;
- rt_smoke_ahs_multi masked its scene pointer to the low word.

The ray-log record keeps its 112-byte size (scene_root takes one reserved
word); the format version goes from 1 to 2 since the field offsets change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile (profiles/<name>.json) names a complete machine: per-core knobs,
chip-level knobs, the platform around it, the measurements it is calibrated
against, and the sources of every value. gen_config.py layers it between the
VX_config.toml defaults and explicit -D flags, validates each knob against
the TOML (unknown knobs, enum values, bool/int types, required sources) and
folds it into CONFIGS with --merge-configs, one -D per macro so -Werror
builds never see a redefinition. Profiles inherit with "extends".

config.mk does the merge once, so every flow that includes it -- simulators,
runtimes, kernels, tests and the synthesis Makefiles -- builds the same
machine without per-flow changes. configure --profile sets a build
directory's default and bakes it into the generated headers and the config
stamp; blackbox.sh --profile selects one per run. Without a profile the
generated flags and headers are byte-identical to before.

Adds profiles/rtx_5080.json (GB203 at base clock) with the knobs the model
has today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Warnings had become non-fatal again: a June RTU change re-added Verilator
-Wno-fatal to rtlsim after it had been removed, the AFU simulators copied it,
and several C/C++ trees (OpenCL/HIP host code, unit tests, kernel library)
never had -Werror. Warnings then accumulated unseen.

Every C/C++ build (simx, rtlsim, xrtsim, opaesim, avedsim, runtime,
kernel library, hw/sw unit tests, OpenCL/HIP/Vulkan apps) is now -Werror and
Verilator warnings are fatal, with the code fixed rather than silenced:
- RTL: VX_find_first, VX_onehot_encoder (MODEL1) and VX_pipe_buffer rewritten
  loop-free so Verilator -O0 does not report UNOPTFLAT (each proven equivalent
  to the original with an sv2v+yosys miter, with negative controls); width
  fixes in the tex bus arbiter, RTU scheduler config, PLRU replacement, the
  no-F CSR data path and the TFR MX scale; unused cluster/AFU ports removed or
  tied off; the early-Z y-pitch add widened explicitly (WIDTHEXPAND), and the
  RTU scheduler's issue block reads the memory grant instead of mem_fire,
  which is derived from the block's own mem_issue output (UNOPTFLAT).
- Host code: 21 OpenCL apps, HIP, Vulkan, the kernel library and the DXA/FMA
  testbenches; -Wno-maybe-uninitialized/-Wno-array-bounds dropped; MAKEFLAGS
  cleared around verilator --build (jobserver warning).
- vx_spawn.c printed uint32_t with %d, which the vx_printf format attribute
  rejects under GCC's newlib (uint32_t is unsigned long there).
- The riscv-tests benchmark patch cast the unsuffixed COUT address straight to
  a pointer (-Wint-to-pointer-cast on RV64), and the riscv-tests stamp did not
  depend on the patch, so an edited patch was never re-applied.

Remaining exceptions compile code that is not ours: the yosys gate-level
netlist sims, the cln28hpc vendor macro testbench, and the FireSim driver,
whose upstream bridge and platform objects share our CXXFLAGS.

Verified: from-clean build of every group at XLEN 32 and 64 with 0 warnings;
all 252 CI Verilator commands verilate with 0 warnings; the CI matrix cells
run so far (perf_gate, amo, cache, config1 on rtlsim/simx) build warning-free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare -D<ENUM>_<value> override was recognised by splitting the flag at its
last underscore, so a value that itself contains one (e.g.
-DVX_CFG_PLATFORM_MEMORY_TYPE_HBM2_A100) never matched its enum: the generator
silently kept the default, and the profile merge treated the flag and the
profile's entry as different knobs and passed both. The enum is now matched
by name prefix and the rest checked against its declared values, in both
places. Output is byte-identical to before for all 247 CONFIGS strings in
ci/testcases at both XLENs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU_DEV_CAPS encoded threads, warps, socket/cluster sizes and clusters as
3-bit log2 fields, so a machine that is not a power of two (48 warps, 84 or
108 cores) reported the wrong shape to the runtime. The spare bits [63:42] now
carry exact warps (7 bits), sockets per cluster (8) and clusters (7), produced
identically by the RTL CP regfile and the C++ CP model. The decoder prefers
them and falls back to the log2 fields only when they read zero, so devices
built before this change keep reporting correctly.

Verified: every simx/rtlsim test in the matrix so far decodes these caps
(NUM_WARPS/NUM_CORES drive the kernel launch), and the A100-shaped 108-core
SimX run reports 108 cores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With 4 KB pages a working set beyond the TLB reach thrashes: sgemm at N=512
on a VM-enabled RTX 5080-shaped machine ran 18x slower (1% L1 TLB hit rate,
the memory port blocked behind 2 requests parked per in-flight page). GPU
drivers back allocations with 2 MB pages. VORTEX_VM_MEGAPAGE_HEAP=1 maps the
whole heap once with superpages at its identity VA, which the page walker and
the L2 TLB's megapage array already support, so a buffer costs no TLB entry
per 4 KB (in that study VM then cost 1-4% instead of 18x). Per-buffer access flags
are not enforced in this mode and it cannot be combined with
VORTEX_RANDOMIZE_VA; the default mapping is unchanged. Adds vm-megapage-heap
(sgemm, simx + rtlsim).

Verified: vm-megapage-heap passes on simx (XLEN 64); the rtlsim variant runs
in the CI matrix vm cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cache::Config stored the input count and the pipeline latency in uint8_t, so
any machine whose L2 sees more than 255 inputs (an 84-132 core chip: 16 L1
requests per socket) or whose L2 latency exceeds 255 cycles failed to build
with a narrowing error. The RTL caches take both as unbounded parameters;
the model now matches.

Verified: the 84-core rtx5080 profile builds and runs vecadd on simx; the
a100/h100/v100/rtx1080 profiles build (1,728 L2 inputs on a100).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With local memory disabled (e.g. the tinygpu profile) LMEM_BLK_BASE was never
read, and Verilator's UNUSEDPARAM warning, now fatal, stopped the rtlsim
build. No CI configuration disables LMEM, so the zero-warnings sweep did not
reach it. The window parameters and their static check now sit under the
same VX_CFG_LMEM_ENABLE guard as their only use.

Verified: 1 core, 2 warps x 2 threads, LMEM disabled builds warning-free on
rtlsim and passes sgemm -n16 (15,240 instructions, as on simx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vx_device_max_occupancy_grid sizes a 1D block to a whole core and rounds the
grid up, so grid x block exceeds the problem size whenever a core has more
threads than there are points. These four kernels indexed with the global
id unguarded, so the surplus threads read and wrote past their buffers. On a
core with 1,024 threads vecadd -n256 overwrote its kernel-argument block and
later warps faulted on a float loaded as the destination pointer
("misaligned memory access", every app on the 20-132 core profiles). Each
kernel now returns when the index is past num_points, the size the host
already passes, as any kernel on a rounded-up grid must.

The other tests that launch through the helper were audited: they guard the
index, use a grid-stride loop, or size the problem as an exact multiple of
the block.

Verified: the four pass on simx at the default config and at 32 warps x 32
threads, including sizes that do not divide the block (vecadd -n64/-n100/
-n256/-n16384, relu -n64/-n100, dropout -n1000/-n1024, minimal -n1); the
unguarded vecadd reproduces the fault at 32x32 -n256. The guard adds
instructions, so the perf-gate baselines are re-recorded after this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LLC AMO commit engine queues each result in a 4-entry writeback queue
that drains through the bank's core-request slot, last in the input
priority. Its sizing assumed commit_busy serializes AMOs, but commit_busy
only gates core requests: a burst of fills for different lines, each line's
chain ending in one AMO, replays an AMO per fill while the fill/replay stream
holds the input slot, so nothing drains and the fifth result overflows the
queue (amo:zacas:rtlsim, exposed by the per-channel DRAM model's tighter fill
bursts; latent on master).

- The engine counts the AMOs still able to reach the queue (S0, S0..stC, the
  compute stage) and flags when the queue cannot take one more; the bank
  admits a fill only while it can. A fill is admitted only between replay
  chains and a chain carries at most one AMO, so replays never wait
  mid-chain, and the drain still fires between chains as before. The flag
  is registered (the sum grows by at most one a cycle, so it tests one
  entry early).
- req_input_defer missed the cycle a missing AMO finalizes at S1 (the S0
  pulse has passed, the MSHR probe does not see the entry yet), letting a
  same-line request link behind it. A registered S1 allocation term closes
  it, in both the LLC and passthrough guards, and the MSHR asserts that no
  request links behind a pending AMO at the LLC.
- chain_stall now paces any AMO behind a busy compute stage, not only a
  same-line one: a core AMO admitted while a chain's head leaves through the
  forward port could otherwise be overwritten by the chain's storing AMO.
- A flush no longer starts while AMO results are still queued
  (bank_empty now includes commit_busy).

SimX models no writeback queue; cycles change only when a fill would
overflow it.

Verified: amo and cache cells on rtlsim, XLEN 32 and 64, all pass (zacas
included).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able type

The shared Ramulator wrapper (sim/common/dram_sim) capped every simulator's
memory far below the configured device:
- one injection queue for all channels accepted one request per DRAM cycle
  in total, and one response was drained per core cycle device-wide, so the
  whole memory delivered at most about one line per cycle;
- each 64 B line was split into four 16 B requests, four bursts' worth of
  command bandwidth for one line;
- Ramulator's AllBank refresh manager iterates over a "rank" level, which
  HBM2 and GDDR6 do not have, so refresh never fired;
- the DRAM clock was a raw -DMEM_CLOCK_RATIO define (default 1, the core
  clock), unrelated to the configured platform.

DramSim now models one independent controller per platform memory bank, in
simx, rtlsim, opaesim (which also gets per-bank queues) and xrtsim alike: the
caller passes the bank as the channel and the address within it; a channel
accepts one transaction per DRAM cycle and drains its own responses; a
transaction is one burst (64 B HBM2, 32 B GDDR6) with the A100 HBM interleave
or GPGPU-Sim's RTX 3070 GDDR layout and IPOLY bank hash; refresh is issued per
channel. VX_config.toml selects it:
  VX_CFG_PLATFORM_MEMORY_TYPE        HBM2 | HBM2_A100 | GDDR6
  VX_CFG_PLATFORM_MEMORY_CLOCK_RATE  DRAM clock (MHz); with PLATFORM_CLOCK_RATE
                                     it sets the DRAM cycles per core cycle
  VX_CFG_PLATFORM_MEMORY_LATENCY     fixed read latency (core cycles)
  VX_CFG_PLATFORM_MEMORY_WINDOW      requests a channel holds in flight
The refresh manager and Ramulator's GDDR6 device (shipped but not built
upstream) are C++20 like Ramulator, so they live in a companion library,
third_party/ramulator_ext -> libramulator-vortex.so, built by
third_party/Makefile and part of the CI third-party cache key.

tests/unittest/dram_sim checks the properties the old model broke: bandwidth
scales with the channel count (40 channels = 40x one), the fixed latency adds
exactly, the window bounds requests in flight, a two-burst block answers once,
refresh stalls reads (fails with the AllBank manager), and the A100 and RTX
5080 shapes land on their targets: A100 open-row/row-conflict reads take
216/257 core cycles (GPGPU-Sim SM80: ~215/~255) and stream 952 of 1,373
B/cycle; GDDR6 at the 5080 shape streams 393 of its 418 B/cycle peak (the
card's 960 GB/s at the 2,295 MHz base clock).

Every rtlsim/simx cycle count moves; the perf baselines are re-recorded in
the same series.

Verified: tests/unittest/dram_sim passes (12 checks); every simulator builds
with the new model at XLEN 32 and 64; the CI matrix reruns on the series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the named machines next to rtx5080 (renamed from rtx_5080), each with a
source for every value and sizes rounded to the powers of two the caches
accept:
- tinygpu: 1 core, 2 warps x 2 threads, shared memory at its default size,
  default ISA (so it runs the shipped RV32IMAF/RV64IMAFD runtime); the FPGA
  bring-up and fast simulation machine;
- rtx1080 (GTX 1080), v100, a100, h100 and rtx5080 as SimX-scale study
  machines, now including their DRAM: HBM2 for V100, A100 HBM timing, HBM3
  bandwidth on the HBM2 model for H100, GDDR5X/GDDR7 bandwidth on the GDDR6
  model for the GeForce parts.
Documents profiles and the DRAM keys in docs/simulation.md, configure
--profile in docs/install_vortex.md and the profile layer in the build
configuration design.

Verified: every profile builds and passes vecadd on simx (XLEN 32); tinygpu
passes sgemm on rtlsim at XLEN 32 and 64 with the same instruction count as
simx, and sgemm2 (-t2) and dotproduct, which go through shared memory, on
simx and rtlsim at XLEN 32 and 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
regression.sh fell back to $HOME/tools/gem5 when GEM5_HOME was unset, but
ci/gem5_install.sh installs gem5 under $TOOLDIR/gem5, so the gem5 cell failed
in any build configured with another --tooldir ("gem5.opt not found"). CI
sets GEM5_HOME explicitly and is unaffected. The fallback now follows
TOOLDIR, defaulting to the configure-time value.

Verified: the gem5 cell passes at XLEN 64 in a build configured with a
non-default --tooldir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with `pytest ci -m perf_gate --update-baselines` (XLEN 32,
rtlsim) on this series, with the toolchain the CI will download (llvm-vortex
a81a5957f406). All 49 entries move, because the per-channel DRAM model
changes every rtlsim cycle count:
- dxa/wgmma cases: 13-20% fewer cycles (independent channels, no shared
  injection queue);
- most core, graphics and tensor cases: within a few percent;
- vecadd: +21% instructions and +24%/+32% cycles (nt4/nt16) from the global
  index guard the kernel now needs on a rounded-up grid (one argument load
  and a branch in a kernel of ~two dozen instructions);
- nine entries change instruction counts, from the toolchain and the test
  kernel changes in this series.

Verified: the 49 perf_gate cases pass on rtlsim while recording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernels write a*b +/- b, which clang contracts into fmadd/fmsub/fnmadd/
fnmsub (single rounding), but the host references computed the same
expressions unfused (a*b rounded first). Near a = 1, a*b - b cancels, and
the reference's rounding of a*b becomes hundreds of ulp of the small
result, so fmsub failed whenever the configuration's element count put a
sample near that point (4 cores x 8 warps x 8 threads with an L2: up to
512 ulp). The references now use fmaf, matching the instructions the
kernels exercise; the kernels are unchanged.

Verified: dogfood passes 22/22 on simx at that configuration (failing
before) and at the default, and on rtlsim at that configuration; the
kernel still emits fmadd.s x2, fmsub.s, fnmadd.s x2 and fnmsub.s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VX_tlb_cam and VX_mmu each defined an identical vpn_mask function. In the
multi-core VM configurations (2-4 cores with an L2/L3) Verilator reported
VARHIDDEN for it, which the zero-warnings policy makes fatal, so the
vm:tlb-concurrent, vm:mc-l2-amo and vm:mc-l3 rtlsim builds failed. The
function now lives once in VX_tlb_pkg as tlb_vpn_mask, next to
tlb_perm_ok, and both modules call it.

Verified: the vm cell on rtlsim passes 23/23 at XLEN 32 and 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DRAM keys added with the model redesign mixed device settings with
wrapper internals: a DRAM clock, a request window, an A100-specific type,
per-type timing tables in DRAM cycles, a forced 256-bit channel and custom
address maps. Studying another GPU meant re-deriving all of them.

A channel is now described the way a data sheet describes it:
VX_CFG_PLATFORM_MEMORY_TYPE (HBM2, HBM3, GDDR6), _DATA_RATE (MT/s per pin,
default twice the core clock), _NUM_BANKS (channels), _LATENCY (fixed
PHY/controller/interconnect cycles) and _QUEUE_SIZE (requests in flight).
- One Ramulator memory system per channel: independent controllers, any
  channel count.
- JEDEC organization with 32 B bursts (HBM: BL4 on 64-bit pseudo-channels;
  GDDR6: x16), array timings in nanoseconds converted by Ramulator at the
  data rate, bus timings in clocks. HBM array timing is JEDEC HBM2 as
  GPGPU-Sim's QV100 configuration lists it (tRCD/tCL/tRP 14, tRAS 33, tRC
  47 ns); GDDR6 is Ramulator's own x16 preset (the GPGPU-Sim RTX 3070 values
  used before were half of real GDDR6).
- Address map: four bursts per row visit, then the next pseudo-channel,
  bank group and bank, with the bank permuted by the folded row, so streams
  a power of two apart do not share banks; Ramulator's MOP4CLXOR mapper
  XORs with column bits instead and sent such streams to the same banks.
- No refresh: GPU controllers hide refresh with per-bank refresh, for which
  Ramulator has no manager (its HBM REFsb also lacks the tRFCsb busy time);
  all-bank refresh cost every channel ~9% of its time and added 8.5 cycles
  to sgemm's mean memory latency. ChannelRefresh is removed; ramulator_ext
  only builds GDDR6.cpp.

The default machine clocks the DRAM with the core: 14/26 cycles idle
(open row / row conflict; master 13/27), one 64 B block per cycle at peak.
SimX sgemm -n128 mean memory latency at NT=NW=4/8/16: 17.9/18.4/18.5 cycles
(master 16.4/17.6/19.0). Profiles: v100 HBM2 1752 MT/s, a100 HBM2 3038 and
h100 HBM3 6545 (32 channels carrying the cards' 40-channel bandwidth),
rtx1080 GDDR6 10000, rtx5080 GDDR6 15000 with LATENCY 339 (407 less the 68
cycles JEDEC GDDR6 adds to a row conflict; derived, not re-measured).
Streams reach 93-98% of peak. The dram_sim unit test checks idle latency,
channel scaling, peak efficiency, strided streams and the queue bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SimX picked the next warp greedy-then-oldest, with ages reset whenever a
warp stopped requesting and ties going to the lowest warp id; the RTL's
scoreboard arbiter (VX_scoreboard out_arb, round-robin, STICKY) keeps the
current warp while it is ready and then passes to the next one in index
order. On sgemm_tcu fp16 (NT=8) RTL warps run in lockstep, loading and
waiting on memory together, while SimX warps drift apart (17/17/16/14
blocks per warp against 16 each) and overlap their memory waits. The gap
grew with memory latency and reached 6.9% (tensor:model_parity-fp16, 5%
tolerance) once the DRAM clock followed the core clock.

SimX now uses a greedy round-robin arbiter matching VX_rr_arbiter with
STICKY=1. model_parity-fp16: simx 39747 vs rtlsim 39221 cycles (+1.3%, was
-6.9%); at 2000 MT/s +1.75% (was -3.8%); sgemm parity unchanged within 0.3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bank pipeline is a timed FIFO whose delay is the configured latency,
created with the default capacity of two, so a bank accepted only two
requests per LATENCY cycles: 0.5 per cycle at the stock L2 latency of 4,
far less with large caches. The RTL bank (PIPELINE_STAGES = LATENCY)
accepts one request per cycle at any depth. Size the FIFO to the latency
(at least two).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with pytest ci -m perf_gate --update-baselines (49/49) after the
DRAM model's data-sheet configuration: the default DRAM is now clocked with
the core instead of at 1 GHz, with JEDEC timing, 32 B bursts and permuted
bank interleave, so memory-bound cases moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tinebp and others added 30 commits October 24, 2026 04:18
Regenerated with pytest ci -m perf_gate --update-baselines on fc0a35b,
where the Phase 1 port moved cycle counts. perf_gate passes exactly
against them (49/49) on c5ed76a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AFU memory port count was a per-board constant (PLATFORM_MERGED_
MEMORY_INTERFACE), so it did not follow the GPU configuration and the
U280 left channels unmapped. The port count now follows the GPU's memory
ports, rounded up to a power of two; the build reads it with
afu_mem_ports.sh and generates the HBM/DDR connectivity from it.

VX_mem_to_axi without INTERLEAVE truncated the bank index away, sending
every bank to the first 1/N of memory; bank i now serves the i-th
contiguous 1/N, and xrtsim aborts on an address outside its port's range.
Interleaved mode is unchanged.

VX_mm_axi_arb (MULTI_OUT) held a W behind the AW grant register and could
deadlock with a slave that takes W before AW. W now follows AW order
through a 4-entry queue and moves in the AW cycle, which doubles
single-beat write throughput. VX_afu_wr_gate and the drain counter keep
reset quiesce from splitting a burst, including one whose W leads its AW.

xrtsim xrt CI 23/23; afu_reset_seq, mm_axi_arb (old arbiter fails 5/13),
cp_llc_port, cp_axi_path and cp_core pass. rtlsim and SimX do not build
the changed arbiter mode, so perf_gate and model_parity are unaffected.
Not yet run: Vivado timing of the combinational W select, and the U200
DDR address base on a board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SimX ran the two-core L2 sgemm shape and the multi-slice fetch shapes
several percent off the RTL because it folded buffers and registers on
the miss path into bare channel delays. Each is now modeled as the RTL
builds it:

- The first dcache and icache port also carries the flush injection,
  whose arbiter buffers two requests and registers them. A registered
  two-entry TxBuffer stands in for it, replacing the per-port delay
  knobs on the coalescer and the lane adapter.
- A memory response reaches its bank through the port's response queue,
  when there is one, and a response network registered at every level.
  Caches now say whether they have that queue (mem_rsp_queue) or a
  registered boundary that stands for it (mem_rsp_slice), and the flush
  taps carry what the bank arbiter's response delay does not.
- The socket's L1 memory arbiter is a priority arbiter, icache first,
  registered in both directions.
- The LSU's result crosses one register into the lane gather, so its
  commit queue holds two entries, and its commit-side elasticity is the
  response buffer plus the result arbiter (four entries, not six).
- With two or more operand collectors, their round-robin merge
  registers its output.
- The lmem switch's dcache response buffer holds one entry when the
  coalescer or a lane adapter feeds it combinationally.
- A private dcache's arbiter can keep port 0 out of read merging
  (merge_port0), which is how the RTL tagged that port.

CI: core:parity-sgemm-fw4 (four fetch slices on a four-port icache);
parity-sgemm-fw2 tightens to 3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under lazy fetch a write miss allocates its sector without reading
memory and leaves it partial. A later load refetches the sector from
memory and merges the stored bytes over it. When stores have already
covered every byte, nothing from that read survives the merge, yet it
still cost a DRAM round trip. That cost is why the GPU profiles keep
L2_LAZY_FETCH off.

A refetch root (a partial-miss read, or a parked chain head) whose
sector's dirty mask is full now loops back through the lazy queue as a
data-less refetch instead of going to memory. It does so only while the
queue has the room a new request is admitted with; otherwise it reads
memory as before. The RTL (VX_cache_bank) and SimX change together. In
SimX a refetch fill now learns whether its sector is still resident at
its tag lookup, the tick after it enters, as in the RTL.

lazy_fetch -f stores whole lines, or lines with one word per sector
left unwritten, loads them back, and compares the two runs' memory
reads: every fully written sector must save exactly one read. A chained
mode loads while the stores' allocation is still pending, and a mixed
mode interleaves whole and partial lines. It needs the memory counters
(--perf).

CI: cache:lazy_fetch-3 (lazy-fetch dcache) and cache:lazy_fetch-4
(write-back lazy-fetch L2 shared by four cores), on simx and rtlsim;
core:parity-lazy-fetch-full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SimX ran ahead of, or behind, the RTL at several control points. Each
now follows the RTL's registers:

- The KMU's lane slice registers a CTA into the core, so the KMU sends
  it without a delay of its own. The dispatcher returns the lane credit
  as the CTA is admitted, and the lane buffer offers the freed entry the
  next cycle.
- A CTA's last warp exit reaches the slot table registered and updates
  it by read-modify-write. The slot frees in the fourth cycle, and the
  write holds admission off for that cycle. Before, it freed at once.
- A transaction-barrier completion crosses the completion queue and a
  register before it applies (two cycles, not one).
- Fetch picks warps by an ibuffer-full flag registered at the end of the
  previous cycle.
- FIFO replacement: the pointer names the victim even when another way
  is free, and it advances on every installed fill, sector refills
  included.
- With several operand collectors, the scoreboard's skid buffer feeds
  them through a demux. Only its head is offered, to its own collector,
  and an entry reaches the head the cycle after the one ahead leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every launch writes its argument block into device memory through the
CP DMA. While the last-level cache may hold lines the DMA would miss or
later overwrite, each such write first appends a full cache flush. A
run of short launches therefore paid one full flush per launch for its
arguments alone.

The args pool now records, per free slot, the flush epoch from which
the last-level cache holds none of the slot's lines. A slot a kernel
read becomes clean again after the next full flush. Thirty-two slots
are allocated when the device opens, before any kernel has run, so they
start clean. args_slot_acquire prefers a clean slot, and a MEM_WRITE
into one needs no flush ahead of it. Oversized blocks and slots a
kernel has read since the last full flush keep the flush.

A dcache is also the point of coherence between kernels when it is the
only dcache and every core of the one socket shares it. Before, only a
single-core device qualified. The RTL (VX_gpu_pkg) and SimX now accept
the socket-wide case, so L1-only flush tokens there are answered without
a walk.

VX_graphics gains a simulation-only assertion that each cluster flush
round finds its flush kind latched by a DCR write before it takes the
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full-bandwidth ALU or FPU (as many blocks as issue slots and lanes as
the SIMD width) has no lane-dispatch buffer: the RTL unit takes the op
straight off the dispatch queue, in the cycle the queue releases it.
SimX gave it one more register on the way in. Such a unit now holds only
the queue's head and is created after its dispatcher, so it ticks after
it. It returns the issue credit itself as it accepts the op, and a
resolved branch releases its warp through the result register and then
the branch register.

The SFU, like the LSU, carries its result buffer and response switch in
its output channel, so its commit queue holds two entries and its
commit hop is one register. A committed result is written back from the
commit arbiter's output register, and issue reads a registered
readiness, so the scoreboard releases a register two cycles after
commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every memory fence sent a full L1 flush token, which both drains earlier
writes and invalidates the cache. The fences the RISC-V mappings emit
usually need only half of that. A load-acquire's fence r,rw needs only
later reads to see other harts' writes (invalidate). A store-release's
fence rw,w needs only earlier writes to be visible (drain).

Decode now derives the kind from the pred/succ sets. Acquire applies
when the successor set reads, release when the predecessor set writes
or the successor set does not read. A device access in the predecessor
set, or fence.i, does both. The token carries the kind in its attributes
(acquire: io with byte enables set; release: noalloc). VX_cache_init:

- An acquire walks above the point of coherence and forwards only when
  the cache below may also hold stale lines (ACQ_FORWARD: an L2 that is
  not the shared LLC).
- A release only quiesces the banks and forwards a release token behind
  the earlier writes.
- Kinds visible together combine to the weakest one that serves them
  all.

Atomics with .aq/.rl were treated as sequentially consistent, which
nothing enforced. A new uop expander (VX_uop_amo; LsuUopGen in SimX)
places a release fence before an atomic with .rl and an acquire fence
after one with .aq. The RTL and SimX change together.

fence_mp -m selects how the flag is published and observed: 0 full
fences, 1 acquire/release fences, 2 amoswap.rl / amoor.aq.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VX_mem_unit's passthru lanes shifted the LSU tag right by one and
cleared its MSB, while lane 0, which also takes the flush injection,
kept its select bit at the LSB. One request's tag was therefore encoded
differently on port 0 than on the other ports, and the dcache's
same-tag read merge never paired port 0 with another port. The tag now
passes unchanged; its clear LSB sits where the flush lane's select bit
does. SimX drops the merge_port0 knob that modeled the old behavior.

SimX's LSU-to-memory path also follows the RTL more closely:

- The lmem switch splits combinationally. The LSU ticks first and hands
  a request over with no delay, and the switch routes it into its output
  buffers in that cycle. The LSU checks room only in the buffers the
  beat addresses (req_ready), instead of a one-entry ingress.
- The switch's response arbiter has a one-entry output buffer, which
  the coalescer or a multi-channel lane adapter fills combinationally.
- A coalesced batch crosses the coalescer's output register and then the
  lane adapter's request buffer. The adapter registers its responses.
- The dcache's MSHR occupancy is a registered count: a slot freed this
  cycle admits a request only from the next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dcache that is itself the point of coherence commits every atomic in
the one cache all harts use, so the fences around an .aq/.rl atomic
order nothing more there. The uop sequencer and SimX decode now leave
such atomics unexpanded.

DBG_MEM_BANK0_WDELAY is a debug-only knob. A write-through cache holds
each write at the head of bank 0's memory queue for 2048 cycles, so
writes leaving through the other banks overtake it unless a release
orders them. Without a delay, no shape exposes a release fence that
fails to drain. fence_mp places its data in even 64-byte lines and its
flag in an odd one, so on a two-bank dcache they leave through different
banks, and it checks the data newest first.

SimX models the knob as the RTL builds it: the delay holds the head of
bank 0's in-order memory queue, so everything queued behind the write
waits too. Every bank memory send goes through send_mem_req(), which
keeps a request from leaving before the one ahead of it, and a launch
reset clears the queue's next free departure cycle. A later
read-miss fill of the same line therefore cannot overtake the write and
install the old word. Without the knob, timing is unchanged.

SimX LLC atomics: a store-bearing atomic writes its result back into the
line through the core-request slot two cycles after its commit, and
holds new core requests off until then. A token flush takes two more
cycles after its last write-back before the bank reports done. Both
follow the RTL bank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A debug build widens the RTL UUID to 44 bits and turns on assertions
and traces; none of that may move timing. A debug-only tag width did
reach the dcache's read merge and changed cycle counts, and no check
noticed. A blackbox case run with --debug can now set
release_parity: true. The runner then reruns it built release at xlen
32 and requires identical instructions and cycles, reading the debug
run's counts from its --log file. ci lint requires a --debug blackbox
case and rejects combining it with a check.

debug:sgemm_tcu-2 carries it, reshaped (8 threads, 8 warps, a 4 KB
dcache, an L2) so the data cache is under pressure and its two channels'
same-word reads merge.

SimX: without a registered tree level, the cache's port crossbar is
combinational. Each bank is now offered one request per cycle, and the
ports are dequeued only when the bank takes it, so a later same-word
read can still merge with a request waiting at its port. An offer still
pending the next cycle is withdrawn and the ports re-arbitrate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tensor core accumulates in fp32 (int32 for integer inputs) and has
no other output format. Narrow fmt_d paths (fp16, bf16, fp8, bf8 and
tf32 out, rounded at every step) existed only in SimX and in the test
references, and modeled a datapath that does not exist. They are
removed. vx_tensor.h rejects another output type at compile time, and
VX_tcu_core and SimX assert on an instruction that encodes one.
tensor:sgemm_tcu-4 and -6 now produce fp32.

SimX on the TFR backend computed dot products as discrete fp32
multiply-adds, which round apart from the datapath's single windowed
rounding on subnormal rows and near-cancellations. It now uses the
TfrFedp model: products aligned to the largest exponent in a 25-bit
window with a sticky bit, summed as integers, rounded once. The model
moves from hw/unittest/tcu_fedp to sw/common/fedp.h and gains MX (E8M0)
and NVFP4 (UE4M3) scales and fp4 lanes. Other backends keep the
discrete fp32 order of the DPI datapath.

RTL TFR fixes found by checking the datapath bit for bit:
- MXFP4 E8M0 scales were added into the 6-bit term alignment field, so
  scales far from one (such as 2^-34) wrapped. They now join the lane
  exponent.
- An MX fp8 lane scaled below the exponent range wrapped. The exponent
  sum is now one bit wider and signed, and such a lane only adds to the
  sticky bit.
- NaN scales (E8M0 0xFF, UE4M3 0x7F) now make their block NaN.
- TCU_EXP_BITS is 10 under MX.

CI: unittest:hw-tcu-unit runs VX_tcu_unit end to end (dispatch, uops,
operand routing, commit) at 0 ULP against TfrFedp on TFR and against
the discrete reference on DPI. Directed vectors cover a subnormal fp16
row and near-cancellation. tcu_fedp gains MX/NVFP4 suites with directed
scale corners. tcu_unit's format IDs now match VX_tcu_pkg, elements are
packed per register, and a packet is dispatched exactly once.

The WGMMA design doc gains fmt_d, the
MX/NVFP4 scale semantics and TFR rounding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fence_mp-5 (-m1, release/acquire fences) and fence_mp-6 (-m2, .rl/.aq
atomics) run two cores with an L2 under DBG_MEM_BANK0_WDELAY, on simx
and rtlsim. Bank 0's writes are held back, and the flag, in a bank-1
line, overtakes the data in bank-0 lines unless the release waits for
them. Without the delay, no shape would fail a release that does not
drain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With FETCH_WIDTH > 1 each fetch slice has its own icache port. Slices
of one core often fetch different words of the same line in the same
cycle (sgemm's three-line loop does almost every cycle), and each took
its own bank access, serialized behind the others.

READ_MERGE_LINE extends read merging from same-word to same-line reads.
Plain reads of one line from a merge group in the same cycle share one
bank access. The request carries every group port's {tag, word select}
above the merged-port mask, the bank returns one word per slot, and the
response crossbar hands each port its own word and tag. Only read-only
caches may use it (static assert). The icache enables it through
VX_CFG_ICACHE_READ_MERGE, default 1, which changes nothing at the
default FETCH_WIDTH of 1. SimX merges by line in the port arbiter. The
merged ports' tags ride the leader's request through the bank and back
on its response, as in the RTL tag, and the response multicast hands
each port its own.

Under line merging the bank reads the whole line: decoding every slot's
word select would sit on the data array's enable path.

CI: config1:icache_merge-1 (sgemm, four fetch slices) and
icache_merge-2 (raycast, four fetch slices on 16 icache banks), on simx
and rtlsim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a write-through cache the MSHR's pending test left out write entries,
because their data had already gone to memory. But a chained store
reaches the array only when it replays behind the fill. Once the
chain's read head had replayed, a same-word load arriving before the
store's replay found the line resident and hit, returning the value
from before the thread's own store. Writes now count as pending on
either policy, so the load links behind the store. SimX already chained
such a hit.

Several compare cones move off the bank's critical paths. Cycles do
not change, and each carries a runtime assertion against the original
combinational result:
- VX_cache_amo registers its lane hits in every configuration and takes
  its coalesce keys a cycle early.
- The dirty-byte mask is read one stage ahead of commit from a
  dual-port RAM, with write forwarding (MASK_SYNC, deferred-commit banks
  only), so commit takes it from a register and a large mask fits block
  RAM.
- The MSHR takes its allocate match a cycle early against the request
  entering S0. The order-hazard probes drop the replay-dequeue
  exclusion, which the bank's link hold makes redundant, taking the S0
  address compare out of the data array's write-enable cone.

raw_order issues load, store, store, store, delay, load per warp and
sweeps the delay across the chain's drain, then checks that every load
returned its own store. CI: cache:raw_order-wt (write-through L1 under
an L2, simx and rtlsim).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffing per-core issue order against the RTL on sgemm n64 with two
cores and an L2 found three structures SimX did not model:

- The LSU's lane-dispatch skid buffer, the scheduler's request queue and
  its pending table all register their full flags, so a slot freed in
  one cycle takes a new request in the next. SimX freed the entry, sent
  the next request and ingested a new trace in one tick. The LSU now
  admits against room sampled at the start of the tick, and its inputs
  are registered-ready.
- The WCTL and CSR units each hold two results in their own buffer
  ahead of the SFU's shared response switch, which holds two more. SimX
  let one six-entry output channel absorb them, so up to ten SFU ops sat
  between dispatch and commit, where the RTL holds eight. A WCTL or CSR
  op is now admitted only while fewer than two of that unit's results
  wait beyond the switch's entries. Results reach commit after
  different latencies, so each is tracked at its arrival cycle, in the
  order commit pops them.
- The local-memory request crossbar registers its output toward the
  banks. With it, a shared-memory load's minimum latency matches the
  RTL's 14 cycles.

CI: core:parity-sgemm-mc64 (n64, two cores, L2, two operand collectors
per issue slice, 3% tolerance) runs at +1.72% against the RTL. parity-sgemmx moves from -0.62% to -3.40%, still within its
5%: the registered LSU input is RTL-faithful, so another SimX error on
that local-memory kernel had been offsetting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a lazy-fetch dcache with a deferred commit stage (latency 3), a
queued AMO result drains through the core-request slot. Like a core
request, it waited for the lazy queue's pipeline reservation. Meanwhile
a full AMO write-back queue held off the fills that drain the lazy
queue, and the bank deadlocked. With the pipe empty, only the write-back
itself can push, so it now needs only a free slot (lazyq_wb_block).

The lazy queue is now twice the pipe depth, so a few queued entries
leave admission and the refetch loopback open. SimX sizes its loopback
the same way.

CI: cache:lazy_fetch-5 (latency-3 lazy-fetch dcache with atomics, simx
and rtlsim).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The platform tops pass unpacked per-port arrays to VX_afu_wrap, which
Vivado accepts only from SystemVerilog sources. The elaboration check,
the per-block DUT project, the XRT kernel packager and the AVED IP
packager now set vortex_afu.v's file type accordingly. The DUT `top`
build also resolves AFU_MEM_PORTS with afu_mem_ports.sh, as the XRT
flow does.

The unused `unittest` DUT, which had no top module, leaves the Xilinx
and Altera synthesis catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A launch after CP DMA writes into the last-level cache drained the
whole model before its reset, including the transfer's line fills. The
hardware does not reset between a transfer and the launch that follows
it. The last-level cache, the memory below it and the CP port are now
built under a LaunchKeepScope and survive reset_launch(), so the kernel
meets fills still in flight. The crossbar's 64-input bound, which its
per-output round-robin state never needed, is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with pytest ci -m perf_gate --update-baselines on the
CI toolchain (LLVM v3.0.1) after the MLP series. Every case's config
hash moves with the new defaults; retired instruction counts are
unchanged. Cycles move with the miss-path, fence, lazy-fetch and
tensor-core changes in the preceding commits: the 49 cases are 3.4%
faster than master at the geometric mean, led by sgemv-nt16 (-44%),
fp16-mc-nt16 (-8.6%) and stencil3d-nt16 (-3.1%). The same tree passed
model_parity and the regression suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/designs/mlp_optimizations.md describes the memory-level
parallelism features of the cache, memory, DMA and LSU paths: what
each one fixes, how it works, the cycles it saves on the measured
shapes, and the VX_CFG knobs that control it. Each feature has a
diagram under docs/assets/img/mlp_*.svg. The index links it under the
memory-system designs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The yosys top DUT synthesizes VX_afu_wrap, but its include path also
pulled in hw/rtl/afu/xrt, so sv2v read the XRT platform top too. That
top now spells out one AXI master per GPU memory port through
AFU_MEM_PORTS, a count only the Xilinx flows resolve from the
elaborated configuration, so asic_gate's top build stopped at source
conversion on the undefined macro. The platform top is not part of
the synthesized design: drop its directory from the include path.

asic_gate -b top: 618.3 MHz against the 620.9 MHz baseline, cell area
+0.5%, PASS. The other eleven asic_gate DUTs pass on the same tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in master's Rv64 fixes (#427), the single-packet dispatch fix
(#428), MUFU + Zfhmin (#432) and the RTU performance counters with
the perf-counter fixes (#435).

Conflicts keep the branch's structure where it reorganized the code:
- Cache bank and MSHR: the branch already carries #427's AMO write-back
  queue and commit-busy checks; its back-to-back fill path (no
  fill_inflight stall) and lazy-fetch queue stay.
- SFU: master's MUFU PE moves into VX_sfu_block, next to the RTU PE,
  and SimX's SFU dispatches MUFU ops per block through send_out.
- Host performance-counter reads: the branch's VX_csr_mpm already
  serves every counter master's VX_csr_data block reads.
- AFU/xrtsim memory ports, device caps (socket size), hardware
  profiles, changelog and command-processor docs: the branch's.
- SimX memory and local-memory perf counters now run from device
  reset, as master fixed them and as the RTL counts.
- The regression list gains mufu and zfhmin; the Xilinx DUT catalog
  gains mufu.

The perf baselines keep the branch's recorded values; they are
re-recorded on the merged tree by perf_gate --update-baselines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each cell's two shards ran about 2h25 on the CI runners, close to the
tests job's timeout and the longest jobs of a run. Their cases are long
rtlsim runs, so these two cells get a 60-minute shard budget instead of
the default 150 minutes: both now plan four shards, balanced on the
recorded runtimes (model_parity 54-58 min, perf_gate 50 min each).
The shard count still comes from the planner and reaches pytest
through --shard, so every shard derives the same split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RVC decompressor joined a 32-bit instruction that straddles a word
from the buffered low half and the refetched high half, and cleared the
buffer as it did. When the decode latch was full the response stayed and
was offered again, but the cleared buffer sent the retry down the fresh
fetch path, which read the wrong half-word: at XLEN 64, raycast with RVC
decoded a c.srli in place of an fmul.s, jumped to a wild PC and spun
until the CI cell's 3-hour timeout. The RTL decompressor holds its state
under backpressure. The buffer is now freed when the instruction enters
the decode latch, so a retry sees the same state.

Decode backpressure while a response waits arises on this branch once
every warp's ibuffer is full and ready warps still fetch.

rvc and simx: 5/5 at XLEN 64 and 32; raycast -w 64 -h 64 with RVC at
XLEN 64 passes with master's instruction count (5,676,110). Non-RVC
SimX cycles are unchanged (sgemm 796,238, raycast 21,106,995).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merging master auto-merged its VX_mufu_unit instance into VX_sfu_unit,
but on this branch the SFU's processing elements live in VX_sfu_block,
which already instantiates MUFU. VX_sfu_unit has none of the PE
interfaces the instance names, so any build with
VX_CFG_EXT_MUFU_ENABLE failed to elaborate: parity-mufu on rtlsim and
the mufu app in the rtlsim regression could not build. VX_sfu_unit is
back to its pre-merge form, and the mufu cases now run when
VX_sfu_block changes.

mufu on simx, rtlsim and xrtsim: 9/9. parity-mufu 0.01%, -nt8 0.14%,
-nt16 0.15%. mufu and zfhmin regression apps pass on rtlsim. Builds
without MUFU are unchanged: the removed block was entirely under
VX_CFG_EXT_MUFU_ENABLE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with pytest ci -m perf_gate --update-baselines on the
merged tree (CI toolchain, LLVM v3.0.1), in four shards whose records
were combined case by case. Every case's config hash changes with the
configuration keys master added; retired instructions and cycles are
identical to the pre-merge baselines in all 49 cases. Against master's
baselines the 47 comparable cases are 3.5% faster at the geometric mean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The master merge brought in the RTU perf counters (#435), but the RTU
class case in VX_csr_mpm was lost while resolving the branch's MPM
rework. With PERF_ENABLE and the RTU on, sysmem_perf.rtu/rtcache went
unread and lint failed (UNUSEDSIGNAL), which broke model_parity-rt_raycast.
SimX and the runtime already read this class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proposals are working documents that are never committed, so
docs/proposals/ has no tracked files and the rule sent every agent to
write untracked drafts into its own worktree, where they scattered. Where
contributors keep drafts is their own workflow, not a repo rule. The
documentation map keeps docs/designs/ for accepted designs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VX_afu_wrap took its AXI memory ports as SystemVerilog unpacked arrays,
which plain Verilog cannot connect, so the platform tops (vortex_afu.v)
were read as SystemVerilog. The Vitis kernel packager rejects an
SystemVerilog top: it then refuses every SV interface with struct ports in
the design, and no XRT bitstream could be packaged.

Each AXI field now crosses into VX_afu_wrap as one flat vector with port i
in slice i, and the wrapper splits it back into the per-port arrays its
logic uses. The platform tops and the xrtsim shim are plain Verilog again,
and the file-type overrides in the Vivado flows are gone. The port count
still follows C_M_AXI_MEM_NUM_BANKS, so VX_afu_wrap builds standalone as
the synthesis `top` DUT as before.

Verified: xrtsim CI xrt cases 24/24; U55C configs on xrtsim (4c socket 4
+ AMO + VM; 2c + TCU fp16 sparse; 1c IW4 + TCU sparse + WGMMA + DXA)
pass vecadd, sgemm, amo, vm_test, sgemm_tcu(_sp), sgemm_tcu_wg_sp(_dxa)
and dxa_copy; Vivado elaboration check and vortex_afu.xo packaging pass
for the 4-core config (they failed before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant