Skip to content

simx: model the issue lock, per-slot dispatch queues and port-0 flush register as the RTL builds them - #423

Merged
tinebp merged 9 commits into
masterfrom
simx-parity-issue-lock
Oct 3, 2026
Merged

tinebp merged 9 commits into
masterfrom
simx-parity-issue-lock

Conversation

@tinebp

@tinebp tinebp commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Brings every model_parity case within the 5% default tolerance. No tolerance is widened and no case is marked known_issue.

Every change makes SimX copy an RTL structure that was checked against the RTL source and against debug traces from both models.

Case Before After
sgemv +6.38% +0.83%
wgmma-fedp2k-rs +6.21% +0.92%
wgmma-dxa-mcast −7.31% +3.32%

Positive means SimX is slower than the RTL.

This branch carries 7 commits; the last one is the subject of this PR. The 6 before it are earlier parity work from local master that was never pushed, and the last commit builds on them.

Changes

Issue lock and dispatch queues

  • Issue lock: a warp inside a locked micro-op sequence (WGMMA, RTU TRACE, OM export) now blocks every other warp of its issue slot, whatever unit they target. The lock reopens when the sequence's last micro-op issues.
  • Dispatch queues: each issue slot has its own queue, and the lowest slot is served first. The issue credit returns when the op leaves its queue.

Memory

  • Data-cache port 0: requests on the first port take one more register, because that port also carries cache-flush injection. That sets which request of a same-bank pair is served first, and so the eviction order under cache thrash (sgemv).

Smaller timing fixes

  • The near-full flag reaches issue through two registers.
  • The micro-op sequencer adds a one-cycle start bubble. It is timed from each warp's issue history, so release and debug builds now give identical cycle counts.
  • Reduced-width ALU/FPU get their extra input and output registers.
  • Multiply/divide latency is 3.
  • TCU admission stalls while results back up.
  • The fflags/frm CSR fields are scoreboarded like registers.
  • Results cross one more register before commit.

FPU latency coupling (VX_config.toml)

  • DPI is a fast simulation stand-in for STD. It now takes STD's FDIV/FSQRT latencies from the same toml helpers: 17/17, or 32/32 with the D extension, instead of its own 15/10.
  • Changing STD's latency now changes DPI's too, and SimX (STD) and rtlsim (DPI) resolve to the same values.
  • DSP and FPNEW keep their own latencies.

Perf baselines

  • Regenerated with pytest ci -m perf_gate --update-baselines.
  • The latency change moves only kernels that divide or take square roots, all by less than 0.2%.
  • The other moves were already present before this change: upstream RTL commits merged since the last recording, and catalog config changes. wgmma-dxa (−13.9%) and wgmma-dxa-mcast (+3.8%) were already outside the 2% tolerance.

Test plan (32-bit)

  • pytest ci -m model_parity: 31/31. Worst case fp16 at −4.51%; that margin is only partly explained.
  • pytest ci -m perf_gate: 49/49 against the regenerated baselines.
  • All 23 SimX CI categories: 261/261.
  • Release and debug SimX builds give identical cycles on wgmma-fedp2k-rs and draw3d.

🤖 Generated with Claude Code

tinebp and others added 9 commits September 30, 2026 10:04
…h and DXA drain with the RTL

The perf_gate and model_parity jobs timed out (run 36547574183). Since
#416 the ALU/LSU/FPU block counts follow ISSUE_WIDTH, so every
ISSUE_WIDTH=4 gate case elaborates a four-block model several times the
size, and a hosted runner cannot build it within the job. Those cases now
pin one block per unit; their cycles return to the pre-#416 values
(wgmma-dxa-mcast nt4/nt16 reproduce them exactly).

Two build costs amplified it:
- rtlsim passed -O2 through CXXFLAGS, which lands after Verilator's
  per-class knobs and also optimizes the slow-path files. The level now
  goes through OPT_FAST/OPT_GLOBAL: wgmma-dxa 598 -> 224 CPU-min, same
  cycles.
- blackbox built the driver with the caller's CONFIGS, then the run
  rebuilt it with the app's resolved CONFIGS. A runtime-<driver> target in
  each suite's common.mk builds it once, with the CONFIGS the run uses.

SimX timing, both mismatches found on the DXA parity cases:
- The dispatcher stepped through the issue-slot batches in fixed rotation,
  spending a cycle on every empty slot. VX_lane_dispatch grants the lowest
  batch holding an instruction and rests on the last when none does.
- The DXA scatter drain (K-major, Flat, BlockMajor) gathered a word of
  elements per beat. VX_dxa_smem_wr writes one element per beat; DXA
  lmem_writes on wgmma-dxa now equal the RTL's.

SimX against recorded RTL cycles, all 31 model_parity cases (RTL
unchanged): wgmma-dxa 24.9% -> 5.5%, wgmma-sp-dxa 17.8% -> 5.2%,
wgmma-dxa-mcast 9.8% -> 0.9%, sgemm2-fp16 4.5% -> 0.1%, wgmma-fedp2k-rs
4.0% -> 0.7%; every other case unchanged. The multicast case drops its
known_issue and 10% tolerance, the sparse case its 10% tolerance; the two
remaining known_issue reasons now state the measured gap. The wgmma-dxa
residual is C-tile store drain at CTA turnover.

Not run here: the full rtlsim model_parity and perf_gate gates. perf
baselines are stale (fingerprint) and need --update-baselines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the RTL does

SGEMM on two cores (model_parity sgemm-mc) ran 10.5% slower in SimX than in
the RTL. The kernel's loop is a dependent eight-FMA chain with thirteen loads,
so it is bound by the FPU's two tags. Three SimX timing paths diverged from the
RTL:

- Issue: VX_scoreboard drops a warp from arbitration while its target FU's
  dispatch queue is going-full. SimX only de-prioritized such warps and let
  them all through when every ready warp targeted a congested FU. Excess FPU
  ops (7 issued and not yet in the FPU, against the RTL's 4) filled the two
  shared operand collectors and blocked loads and ALU ops behind them. The
  warps fell into lockstep and their load writebacks committed in bursts of
  17-40 beats that starved the lower-priority FPU. The gate is now hard. The
  suppress-mask arbiter overloads lose their last caller and are removed.

- FPU: VX_fpu_unit holds each tag until the result fires into the commit
  arbiter (no buffer between them at full bandwidth). SimX freed tags on a
  timer, buffered extra results and added two cycles to every latency. The
  response port now has one slot per tag, so taking a result off it frees the
  tag. At full bandwidth the commit arbiter reads the port directly. With
  partial bandwidth the lane-gather stage does. The latencies are the
  configured unit latencies.

- Crossbar: TxCrossBar ignored its arbiter type and always granted the lowest
  input. Round-robin outputs now rotate from the last accepted input, reset to
  the last input, as VX_stream_xbar / VX_rr_arbiter do. The cache crossbars
  (round-robin) change; local memory (priority) does not.

SimX against the recorded RTL cycles, all 31 model_parity cases: seven were
over 5%, three are now. sgemm-mc 10.49% -> 2.06%, sgemmx 3.93 -> 0.53,
rt_raycast 6.27 -> 2.83, fp16 6.28 -> 3.32, fp16-mc 4.07 -> 1.47,
wgmma-sp-dxa 5.15 -> 4.21. Three got worse, all from the issue gate:
sgemv 7.23 -> 9.59, wgmma-dxa 5.46 -> 8.53, mxfp8 0.82 -> 3.36. The gate
matches the RTL, so these expose other SimX timing errors it was offsetting.
They are not localized yet.

Catalog: sgemmx and fp16 return to the 5% default and wgmma-sp-dxa loses its
known_issue. sgemv and copy-mcast keep their existing 10% tolerance, and the
wgmma-dxa reason states its measured gap.

Verified: SimX-only sweep of all 31 model_parity cases against recorded RTL
cycles (the RTL is unchanged). The final channel-based FPU wiring reproduces
the swept cycles exactly on sgemm-mc, sgemv, fp16 and wgmma-dxa. sgemmx moves
2 cycles because the event wheel's push_back does not preserve FIFO order
(fixed separately). FP-heavy apps pass in both FPU gather modes. Not run: the
full pytest model_parity suite with its rtlsim legs. perf_gate runs on rtlsim
only and cannot move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RTL memory scheduler takes a response-buffer entry when the core request
is accepted into its queue and frees it at the last response, so loads waiting
in the queue and loads in flight share one pool of LSU_PENDING_SIZE. SimX
allocated the pending entry only when the request left req_queue, so up to
four more reads could sit in the LSU: traces of parity-sgemv show 12 loads
inside the SimX LSU against the RTL's 8, and each load waited correspondingly
longer (matrix-row loads 59 cycles from LSU entry vs RTL 50).

Reads (loads and AMOs) now count against the pool on entry and turn the
reservation into their tag on issue; stores and fences are unaffected. Load
latency from LSU entry now tracks the RTL (48.4 vs 50.0 and 34.1 vs 31.5 for
the two sgemv streams), and parity-sgemv drops from 9.6% to 0.65%, so its 10%
tolerance goes back to the 5% default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e escapes

Remove the last known_issue marker (model_parity-wgmma-dxa) and every per-case
parity tolerance override (copy-mcast at 10%, the redundant 5% on sgemm-mc),
so each model_parity case asserts cycle agreement within the 5% default and a
case over the threshold fails CI instead of being reported as an expected
failure. The catalog now carries no known_issue anywhere.

At the committed SimX model this turns two cases red: wgmma-dxa (SimX 8.5%
slower than the RTL) and copy-mcast (6.8%). The pending SimX tile-buffer and
LMEM DMA port model brings wgmma-dxa to 2.4%; copy-mcast needs the SimX
per-stage latency rework.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lds them

wgmma-dxa ran 8.5% slower in SimX than the RTL. The four warps of a
warp-group interleaved their C-tile stores one instruction at a time, so the
stores evicted each other's lines and data-cache bank 0 fetched every C line
two or three times. In the RTL the warps leave the MMA staggered and store
back to back.

The stagger comes from the TCU's shared B buffer. It holds one bank row,
keyed by the lowest block with a WGMMA compute uop, and a block whose
(desc_b, step_k, step_n) differs waits; each A buffer holds one k-stripe.
SimX planned every A and B line of the WGMMA up front and held all blocks
until all were resident, so the blocks ran in lockstep. The tile buffers now
refill per key with the RTL's request counts, one read in flight per buffer
and fixed-priority arbitration, and a setup uop drops them at the end of its
cycle.

That alone moved wgmma-dxa only to 8.2%: a refill took 4 cycles in SimX
against 1 in the RTL. SimX routed TCU reads and DXA writes through the LSU's
per-word bank crossbar, behind the LSU ports and with the TCU ahead of the
DXA. Local memory now has the RTL's row-wide DMA port: DXA first, then TCU,
one access per cycle, priority over the LSU on the banks it touches, and
read data the cycle after the request.

SimX vs RTL cycles (model_parity, all 31 cases swept against recorded RTL
cycles): wgmma-dxa 8.53% -> 2.43%, wgmma-dxa-mcast 2.26% -> 0.20%,
wgmma-sp-dxa 4.21% -> 0.08%, wgmma-int8 0.15% -> 0.32%, wgmma-fedp2k-rs
0.19% -> 0.30%; every other case bit-identical. All 47 functional dxa and
tensor_wg SimX cases pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
copy-mcast ran 6.8% slower in SimX. Measured per opcode, every gap was in
the ops that hold their warp until they resolve: a branch took 18 cycles
from schedule to the warp's next schedule against the RTL's 17, SPLIT and
WSYNC 20 against 17, JOIN 20 against 18. The RTL releases such a warp from
the unit that resolves it, through one register (a join through one more,
for the divergence-stack pop); SimX released it only when the result left
the functional unit. The ALU and SFU now send the release to the core on a
one-cycle channel, and a TMC that disables every thread retires its warp the
same way.

Releasing on time exposed three places where SimX ran faster than the RTL,
compared per event on vecadd:
- a warp freed by TMC was reused for the next CTA in the same cycle; the
  RTL picks from a registered active set, registers the pick, and the new
  warp is schedulable the cycle after;
- integer multiply/divide had the plain ALU's latency; the multiplier
  pipeline is two stages longer;
- a dependent instruction dispatched 4 cycles after its producer committed,
  against 7 in the RTL, while ALU and SFU results reached commit 2 cycles
  late. The scoreboard now clears a destination two cycles after commit, and
  the ALU and SFU result latencies lose the matching two cycles.

SimX vs RTL cycles, all 31 model_parity cases (on top of the previous
commit): copy-mcast 6.83% -> 2.22%, softmax 3.44% -> 0.04%, copy 4.06% ->
0.35%, sgemm2-dxa 2.83% -> 0.81%, vecadd 2.94% -> 1.54%; 30 of 31 within 5%.

Known regression: parity-sgemm-mc goes from 2.24% to 11.85% (SimX slower)
and fails the 5% gate. Most of it follows the scoreboard release delay
(5.11% without it). On a reduced run FMADD dispatch-to-commit averages 49
cycles in SimX against 15 in the RTL: SimX dispatches the FMA chain early and
it queues in front of the FPU and at commit, where the RTL holds it in the
instruction buffer. Under investigation; raycast (4.53%), draw3d (4.73%) and
sparse-fp16 (4.89%) are close to the limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… register as the RTL builds them

Brings every model_parity case within the 5% default. The three cases
over it: sgemv 6.38% -> 0.83%, wgmma-fedp2k-rs 6.21% -> 0.92%,
wgmma-dxa-mcast -7.31% -> 3.32%. Each change models an RTL structure
confirmed in the RTL source and against debug traces from both models.

Issue and dispatch:
- Issue lock: a warp inside a locked uop sequence (WGMMA, RTU TRACE, OM
  export) blocks every other warp of its issue slot, whatever the unit.
  The lock reopens when the last uop issues. SimX used to lock only the
  target unit and released the lock later, when the unit accepted the
  last uop.
- Dispatch queues: one per issue slot, with the lowest slot served
  first. A slot's issue credit returns when its op leaves the queue
  (new ReleaseOut channel). Dispatchers bind straight to the unit
  inputs. SimX used to share one queue per unit and return the credit
  when the unit accepted the op. The queue order sets how CTAs split
  across warps in wgmma-fedp2k-rs.
- The near-full flag reaches issue through two registers, and a refused
  operand-to-dispatch hand-off counts as that unit's stall.
- The uop sequencer offers a macro's first uop one cycle after the macro
  reaches the buffer front, timed from each warp's issue history rather
  than instruction uuids. Release builds zero the uuids, so a uuid-based
  check timed differently in release and debug. TCU and pack-load macros
  no longer stall fetch.

Units and memory:
- Reduced-width ALU/FPU: one extra input register and one extra
  lane-gather output register.
- Multiply/divide latency is 3; one result per cycle is kept.
- TCU admission holds while a completing result finds an earlier one
  still waiting for commit.
- The first data-cache port also carries cache-flush injection, so its
  requests take one more register. Same-bank pairs are therefore served
  port 1 first, which sets the eviction order under the cache thrash in
  sgemv.

Commit and scoreboard:
- The fflags/frm CSR fields are scoreboarded like registers.
- A result crosses one more register before the commit arbiter.
- Scoreboard release delay goes from 2 cycles to 1.

FPU latency coupling (VX_config.toml):
- The DPI FPU is a fast simulation stand-in for STD, so it now takes
  STD's FDIV/FSQRT latencies from the same toml helpers (17/17, or 32/32
  with D) instead of its own 15/10. Changing STD's latency therefore
  changes DPI's too. DSP and FPNEW keep their own values.
- SimX (STD) and rtlsim (DPI) now resolve to the same latencies.

Perf baselines, regenerated with `pytest -m perf_gate --update-baselines`:
- The latency change moves only kernels that divide or take square
  roots, and by little: raycast +0.17%, stencil3d +0.18%, draw3d
  +0.09%, softmax -0.01%, rt_raycast -0.14%.
- The other moves were already present before this change: upstream
  RTL commits merged after the last recording, and catalog configs
  changed without a re-record. wgmma-dxa (-13.9%) and wgmma-dxa-mcast
  (+3.8%) were outside the 2% tolerance, so perf_gate was already
  failing on them.

Verified on a 32-bit build:
- model_parity: 31/31 within 5%, worst fp16 -4.51%. The fp16 margin is
  only partly explained.
- perf_gate: 49/49 against the new baselines.
- All 23 SimX CI test categories pass: 261/261.
- Release and debug SimX builds give identical cycle counts on wgmma and
  draw3d.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…put change

GitHub cannot condition a cron schedule on the repository. Both gates
therefore started a run every night just to find out whether master had
moved. On a quiet night that left an empty run behind. And because the
check keyed on master's SHA, a docs-only or SimX-only commit triggered a
full multi-hour sweep.

ASIC gate (hosted runners, no host to keep state on) now arms and
disarms itself:
- The workflow stays disabled between qualifying pushes, so its
  schedule does not fire.
- ci.yml's plan job enables it on a push to master that touches hw/,
  the configuration TOMLs, VERSION, the gate's scripts, catalog or
  baselines, or the workflow itself. An indeterminate diff also arms
  it.
- The scheduled run disables the workflow as its first step, so a push
  that lands mid-sweep re-arms the next night. A build error re-enables
  it to retry. A pass or a regression is a verdict, and its run is the
  record.
- The SHA-marker cache and the skipped-night republish are gone.

FPGA gate (self-hosted Vivado runner) loses its schedule:
- A nightly cron on the runner's host runs ci/fpga_gate_dispatch.sh. It
  dispatches the workflow only when a synthesis input changed since the
  last gated SHA (the workflow's existing state file), the runner is up,
  and no gate run is queued or in progress.
- The workflow drops its skip check, its republish step and the force
  input. A dispatched run always has work.

synth_report.py loses --note: only the removed republish steps used it.

Host setup still needed: install the crontab line from the script's
header, and authenticate gh with a token that can dispatch workflows.

Not run: these workflows only execute on GitHub. The dispatcher passed
bash -n but has not been exercised end to end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tinebp
tinebp merged commit 5eec172 into master Oct 3, 2026
2 checks passed
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