[Hardware][Chisel] Fix TensorStoreNarrowVME high-half stutter / AXI W-stability violation on mid-burst WREADY de-assert#41
Open
dreamqin68 wants to merge 2 commits into
Conversation
…ndshake TensorStoreNarrowVME reset its sub-block select register `tag` to 0 on `tag === (numMemBlock-1).U` without gating on `io.vme_wr.data.fire`. On a WREADY-low cycle the store FSM holds sWriteData (and thus WVALID), so the ungated wrap rolled `tag` back to the low half while the high-half beat was still unaccepted. This changed WDATA under `WVALID && !WREADY` (an AXI4 write-data channel stability violation) and re-accepted the low half, shifting the accepted write stream by one 64-bit beat. Gate both the wrap and the increment on `io.vme_wr.data.fire`, matching how the sibling `set` and `raddr_cur` pointers in the same block already advance. On the no-stall path the tag sequence is unchanged; under back-pressure the high-half beat now stays presented and stable until it is accepted. This is the store-side analogue of the LoadUop FSM fix in 164f3e0. Signed-off-by: dreamqin68 <huanwu768@gmail.com>
Add a chiseltest (treadle backend) that stores two 128-bit elements and de-asserts io.vme_wr.data.ready (WREADY) for three cycles on the high-half beat, then checks the AXI4 write-data channel stability invariant (WDATA must hold while WVALID && !WREADY) and that the low half is not accepted twice. The test fails on the pre-fix RTL (the ungated tag wrap changes WDATA under back-pressure and re-accepts the low half) and passes with the tag-wrap fix in this PR. Runs under `sbt test` with the treadle backend, no external tooling. Signed-off-by: dreamqin68 <huanwu768@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TensorStoreNarrowVMEadvances its sub-block select registertagback to0ontag === (numMemBlock - 1).Uwithout gating that wrap on the write handshakeio.vme_wr.data.fire. When the AXI slave legally de-assertsWREADYat the high-half beat of a 128-bit output element, the engine rollstagback to the low half while the high-half beat is still unaccepted. This has two consequences:WDATAwhileWVALIDis held high and the beat has not been accepted — an AXI4 write-data channel stability violation (per the AMBA AXI handshake-process rule, ARM IHI 0022 §A3.2.1: once a source assertsVALIDit must keepVALIDasserted and hold the payload —WDATA/WSTRB/WLAST— stable until theREADYhandshake completes).AWburst committed to its last slot is displaced off the end. When the burst is packed with real data that displaced beat is a real block (silent data loss); the test below proves the beat-sequence shift itself but does not exhibit a real-data drop (see the scope note).The bug only fires when
WREADYde-asserts at the high-half (last sub-block) beat; a stall at the low-half beat is benign. It is contingent on the AXI slave / interconnect de-assertingWREADYmid-burst, which AXI4 explicitly permits, so designs whose memory subsystem happens to holdWREADYhigh across a burst never observe it — but it is a latent correctness bug, not a protocol-legal behavior.Scope of the "corruption" claim: the test below proves the module-level effect — the accepted-beat sequence shifts by one 64-bit beat and re-accepts the low half — which is a genuine AXI-protocol violation on the write channel. The end-to-end numerical impact on a stored tensor (which DRAM bytes end up wrong, and whether a tail block is dropped) additionally depends on the
AW-committed burst length and the memory subsystem's tail handling, and is not reproduced here. So "corruption" here means a proven write-stream desynchronization, not a demonstrated end-to-end wrong-value workload.Root cause
In
hardware/chisel/src/main/scala/core/TensorStoreNarrowVME.scala:io.vme_wr.data.bits.data := mdata(tag)andio.vme_wr.data.valid := state === sWriteData. On aWREADY-low cycle the FSM stays insWriteData(its transition out is guarded byio.vme_wr.data.ready), soWVALIDstays high whiletagwraps underneath it.The inconsistency is visible inside the same block: the sibling pointers for the same write stream are already handshake-gated —
Only
tag's wrap is left ungated.The fix
Gate both the wrap and the increment on
io.vme_wr.data.fire, matching howsetandraddr_curalready advance:On entry (
sWriteCmd)tagresets to 0; on each accepted beat it wraps if at the last sub-block, else increments. During aWREADYstall there is no fire, sotagholds and the high-half beat stays presented and stable.Why this is behavior-preserving off the stall path
The one place the FSM leaves
sWriteDatabetween sub-block groups is thesReadMemre-read cycle. A maintainer will reasonably ask whattagdoes across thesWriteData -> sReadMem -> sWriteDataround-trip, since the original code resettagunconditionally at the wrap moment while this patch folds the reset into the fire branch. Tracing the FSM (sWriteDatablock andis(sReadMem){ state := sWriteData }):sReadMemis entered only onio.vme_wr.data.ready(= fire, sinceWVALIDis high insWriteData) withtag === (numMemBlock - 1).Uandxcnt =/= xlen. So the high-half beat has already been accepted before the FSM moves tosReadMem..elsewhen(io.vme_wr.data.fire)branch and computesMux(tag === last, 0.U, ...)=0.U— identical to the original unconditional reset for the fire case.sReadMemcycle itself,state === sWriteCmdis false andio.vme_wr.data.valid(state === sWriteData) is false, sofireis false; neither branch executes andtagholds0.sWriteData,tag = 0selects the low half of the next element.So on the no-stall path the patched
tagsequence is bit-for-bit identical to the original; the only behavioral change is under back-pressure (high-half beat not yet fired), wheretagnow holds instead of wrapping early. Empirically the no-stall accepted-beat sequence (the four real beatsLOW0, HIGH0, LOW1, HIGH1followed by twoZEROpad beats), which crosses thesReadMemround-trip between elements 0 and 1, is identical before and after the patch.Verification
This PR adds a chiseltest regression test,
hardware/chisel/src/test/scala/unittest/TensorStoreNarrowVMETest.scala, that reproduces the defect directly on the Chisel module — treadle backend, no Verilator or external tooling, runs undersbt test:It stores two 128-bit elements, holds
WREADYlow for three cycles on the high-half beat, and asserts (P1)WDATAis stable whileWVALID && !WREADYand (P2) the low half is not accepted twice. On the pre-fix RTL it fails both properties; with this fix it passes.Concretely, P2 pins the accepted-beat sequence under the stall to the following (this is the contrast the assertion encodes, not console output — chiseltest reports only pass/fail).
LOWk/HIGHkare element k's two 64-bit halves — bytes0x10..0x1ffor element 0,0x40..0x4ffor element 1 — andZEROis a burst pad beat read past the two loaded elements:Pre-fix,
LOW0is duplicated and every later beat shifts one slot right:HIGH1is still accepted, and what falls off the burst tail here is aZEROpad beat, so this stimulus does not drop real data (the two pad beats absorb the shift; a burst packed with real data would drop a real block — see the scope note).numMemBlock = 2here is derived, not a literal:(tensorWidth = blockOut = 16) * (tensorElemBits = outBits = 8) / (memBlockBits = memParams.dataBits = 64) = 2(TensorUtil.scala:62, storetensorType = "out") — a 128-bit element streamed as two 64-bit sub-blocks {low, high}. The fix gates the wrap onfireand is correct for anynumMemBlock.Precedent (same defect class, load side)
The same class of defect — a DRAM-transfer pointer advancing while the data handshake is de-asserted — was fixed once before on the load path, in
164f3e0d("LoadUop FSM bug fix", 2020): "The LoadUop FSM incorrectly advances the address of the next uop to read from DRAM when the DRAM data valid bit is deasserted", fixed by folding the address advance into the handshake-guarded section — the same shape as this store-side fix. The store-sidetagwrap was not covered then and is still ungated.Related / out of scope (not part of this PR)
masterbranch. Themasterbranch (not the defaultmain) still ships the pre-refactor monolithicTensorStore.scala(256 lines), whosetagwrap at lines 182-186 is the identical ungatedwhen(state === sWriteCmd || tag === (numMemBlock - 1).U) { tag := 0.U }.elsewhen(io.vme_wr.data.fire()) { tag := tag + 1.U }. Onmain,TensorStore.scalais only a 62-line dispatcher with notaglogic (the datapath was split intoTensorStoreNarrowVME/TensorStoreWideVME), so amaingrep ofTensorStore.scalawill not show the bug — it lives inTensorStoreNarrowVME.scala, which this PR fixes. Ifmasteris still maintained, the same one-line handshake-gating applies there.TensorLoadSimple.scaladead-code look-alike. It has a structurally similar ungatedtagwrap on the read side (state === sIdle || state === sReadCmd || tag === (numMemBlock-1).U -> tag := 0), but it is dead code:TensorLoad.scalaselects it only underval forceSimpleTensorLoad = false, a hardcoded flag, so no shipped config instantiates it. Not touched by this PR.TensorLoadNarrowVME/TensorLoadWideVME) has no ungated sub-block-select register analogous to the store'stag: the onlytagthere is the VME transaction id (io.vmeData.bits.tag/io.vmeCmd.bits.tag), not a wrapping sub-block counter, and every scratchpad write is gated on the read-data handshake —vmeDataFirePipe = vmeDataValidPipe & vmeDataReadyPipe(TensorLoadWideVME.scala:100) with the SRAM write enable driven byShiftRegister(vmeDataFirePipe, ...)(:229). So the ungated-pointer class here cannot arise on the read side; this is verifiable by reading the code, no witness required.