fix(bitnet): pair the DMA local write address with its own beat (Closes #2003) - #2345
Merged
Merged
Conversation
#2003) READ_DATA raised local_we and post-incremented local_addr in the same non-blocking group, so both landed on the same edge: beat 0's data was written to address 1 and slot 0 was never written at all. Drive local_addr from a new beat_index counter so address, data and enable are registered from one stage and describe the same beat. beat_index is cleared at reset and re-armed in the IDLE start branch. WRITE_DATA is untouched -- there local_addr is a read pointer and its post-increment is correct. Differential icarus run, both emitters in one testbench: defective writes to addresses 1,2,3,4 leaving mem[0] unwritten; fixed writes to 0,1,2,3. Control (AXI write-channel beat count) unchanged at 3 in both.
gHashTag
enabled auto-merge (squash)
August 21, 2026 16:13
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
PR DashboardGenerated at: 2026-08-21 16:13:31 UTC
Summary
Seal Status
|
This was referenced Aug 21, 2026
Open
gHashTag
added a commit
that referenced
this pull request
Aug 22, 2026
…ss fix PR #2345 cited a differential simulation as its primary evidence and did not commit the testbench, so the claim was true but unreproducible from the tree. sim/tb_bitnet_dma_write_address.v elaborates the pre-fix and post-fix renderings of dma_controller in one simulation and drives them from identical stimulus, following the sim/tb_bitnet_request_overflow.v precedent from #2351. The published numbers reproduce: old writes 1..4 and never writes address 0, new writes 0..3. Reporting only -- vvp exits 0 on PASS and on FAIL, and no workflow runs it. Wiring sim/ into CI is out of scope here and unchanged. Refs #2348 Refs #2003
gHashTag
added a commit
that referenced
this pull request
Aug 22, 2026
…ss fix (#2379) PR #2345 cited a differential simulation as its primary evidence and did not commit the testbench, so the claim was true but unreproducible from the tree. sim/tb_bitnet_dma_write_address.v elaborates the pre-fix and post-fix renderings of dma_controller in one simulation and drives them from identical stimulus, following the sim/tb_bitnet_request_overflow.v precedent from #2351. The published numbers reproduce: old writes 1..4 and never writes address 0, new writes 0..3. Reporting only -- vvp exits 0 on PASS and on FAIL, and no workflow runs it. Wiring sim/ into CI is out of scope here and unchanged. Refs #2348 Refs #2003
gHashTag
pushed a commit
that referenced
this pull request
Aug 22, 2026
) #2006 defaults `local_we` low ahead of the case. The pre-fix and post-fix renderings are observationally IDENTICAL: every reachable path already drove the strobe, and the states that never mention it are never entered with it high, because READ_DATA's only exit is DONE_ST. A harness that passes by finding no difference proves nothing, so this one is three-way: A = pre-#2006 (PR #2344 base), B = #2006 (PR #2344 head), C = #2006 + #2003 (PR #2345 head). B and C are consecutive revisions -- #2344's head rendering is byte-identical to #2345's base -- so one comparator sees all three. A vs B must be identical; B vs C must differ. Same comparator, same stimulus, same run, same 293-bit vector of every output port. Putting B in the C slot makes the run fail rather than certify its null result. Measured: A vs B 0 mismatching cycles, B vs C 266, over 373 cycles and seven phases. Deleting READ_DATA's `end else local_we <= 1'b0;` from both renderings makes A emit 22 local writes against B's 18 -- latent today, a real backstop the moment an arm stops clearing the strobe. Reporting, not a gate: `vvp` exits 0 on FAIL as well as PASS. Refs #2348
gHashTag
added a commit
that referenced
this pull request
Aug 22, 2026
) (#2381) * test(sim): differential harness for the zero-count layer_sequencer hang (Refs #1977) `layer_sequencer` never left RUN when asked for zero work: both terminators are `index == count-1` against an unsigned port, and the bare literal 1 widens each subtraction to 32 bits, so a zero count borrows to 32'hFFFFFFFF while the index zero-extends. Elaborates the PR #2337 pre-fix and post-fix renderings in one simulation. Reproduces the published numbers: 200,000 cycles with no `done` and `neuron_id` reaching exactly 50,000, plus six non-zero controls that are identical on every output every cycle. Reporting, not a gate: `vvp` exits 0 on FAIL as well as PASS. Refs #2348 * test(sim): differential harness for the stale prefetch_done level (Refs #1985) `weight_prefetch_ctrl` documented `prefetch_done` as a one-cycle pulse but cleared it only inside the start guard, leaving it asserted for the whole idle gap. A requester sampling it in the cycle it raises `start_prefetch` reads the previous transaction's completion. Elaborates the PR #2340 pre-fix and post-fix renderings in one simulation. Reproduces the published numbers: t2 sampled_done OLD=1 / NEW=0, done_rises 2/2, we_count 4/4. A second case varies the idle gap to separate level from pulse: old's high-time grows with the gap, new's does not. Reporting, not a gate: `vvp` exits 0 on FAIL as well as PASS. Refs #2348 * test(sim): three-way harness for the latent local_we default (Refs #2006) #2006 defaults `local_we` low ahead of the case. The pre-fix and post-fix renderings are observationally IDENTICAL: every reachable path already drove the strobe, and the states that never mention it are never entered with it high, because READ_DATA's only exit is DONE_ST. A harness that passes by finding no difference proves nothing, so this one is three-way: A = pre-#2006 (PR #2344 base), B = #2006 (PR #2344 head), C = #2006 + #2003 (PR #2345 head). B and C are consecutive revisions -- #2344's head rendering is byte-identical to #2345's base -- so one comparator sees all three. A vs B must be identical; B vs C must differ. Same comparator, same stimulus, same run, same 293-bit vector of every output port. Putting B in the C slot makes the run fail rather than certify its null result. Measured: A vs B 0 mismatching cycles, B vs C 266, over 373 cycles and seven phases. Deleting READ_DATA's `end else local_we <= 1'b0;` from both renderings makes A emit 22 local writes against B's 18 -- latent today, a real backstop the moment an arm stops clearing the strobe. Reporting, not a gate: `vvp` exits 0 on FAIL as well as PASS. Refs #2348 * docs(sim): document the three new harnesses and the standalone emit recipe (Refs #2348) Adds a README section per harness in the style #2379 established, plus the shared emit recipe: every BitNet emitter compiles standalone under `rustc` with a four-line driver, no cargo and no target directory, because the only `use` in any of them is `use super::*` inside `#[cfg(test)]`. Records the base/head shas each harness was rendered from, and the three instrument faults found while reproducing the published claims -- a posedge-sampled observer trailing the design by a cycle, an observer racing the stimulus that drove `start_prefetch`, and `first_chunk`/`last_chunk` having no reset in either rendering. Each was a fault in the instrument; every published number reproduced once the instrument was corrected. Refs #1977, #1985, #2006 --------- Co-authored-by: Claude <claude@anthropic.com>
This was referenced Aug 22, 2026
gHashTag
added a commit
that referenced
this pull request
Aug 22, 2026
…teral count (#2385) dma_local_addr_autoincrement_both_paths counted one literal across the whole module and required two. #2345 replaced the read path's post-increment with local_addr <= beat_index -- the #2003 fix -- so the count fell to one and the test has been red on master since. The emitter is correct; the test was stale. A global count cannot say which path a match came from, which is what let the read path lose its advance while the assertion still saw one match. Lowering the threshold to >= 1 would go green and re-open the same blind spot. Now sliced per state arm. Invisible until now because cargo test stops at the first failing target and this is the 42nd of 73 (#2382). Closes #2384
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.
Closes #2003
The defect is live on master verbatim
#2003 reports itself as fixed. Its fixes landed on a branch that never merged (no PR, in any state), so the defect is live. Confirmed against freshly-fetched
origin/master(cb1f0d4eb9805d7e3b947b13cb53b299d50c969c) —bootstrap/src/bitnet_dma.rs:195-196:Both are non-blocking, so both land on the same edge: the cycle the strobe goes high, the address has already advanced. Beat 0 is captured while
local_addris 0, but the memory seeswe=1withaddr=1. Every beat is written one slot high and address 0 is never written at all.The change
Add a
beat_indexcounter.READ_DATAdriveslocal_addr <= beat_index;and advancesbeat_index <= beat_index + 12'd1;, so address, data and enable are all registered from the same stage and describe the same beat.beat_indexis cleared at reset and re-armed in theIDLEstart branch.WRITE_DATAis deliberately untouched — therelocal_addris a read pointer and its post-increment is correct.Differential simulation — old and new emitter in one testbench
4-beat read, identical well-behaved AXI slave driving both DUTs:
Slot 0 goes from never-written to correctly written; the control (AXI write-channel beat count on the untouched
direction=1path) is unchanged in both.Mutant evidence
Guard 1 —
read_data_pairs_address_with_data_and_enable. Mutant: revert the READ_DATA arm to master's shape. Fails with:The message prints the offending arm — that is what shows the assertion read the right text.
Here an unanchored guard would have been WRONG, not merely vacuous — measured.
WRITE_DATAlegitimately keeps its ownlocal_addr <= local_addr + 12'd1;. A naive!v.contains("local_addr + 12'd1")therefore fails on the CORRECT emitter:The guard slices the
READ_DATAarm out first, so it can be satisfied — or broken — only by the state it is about.Guard 2 —
beat_index_rearmed_in_idle_not_only_at_reset, planted separately. One mutant per guard, not one per file. Mutant: delete only theIDLEre-arm. Fails with:Guard 1 still passes under this mutant, so neither guard masks the other. Note the reset block still contains
beat_index <= 12'd0;under this mutant — so the guard is provably not satisfied by the reset line, which is the trap that had to be repaired hours after landing elsewhere in this campaign.Reverting every mutant: 19 passed, 0 failed. The emitted Verilog parses under
iverilog -g2005.Pre-existing gap, recorded not papered over
These tests are not executed by CI. No workflow runs
cargo test -p t27c;corpus-ratchet.ymlrecords that the step was removed by #2292 after going red on master. Run withrustc --teston the module, which is self-contained.Adjacent problems seen and deliberately NOT fixed here
arlen/awlenstill hardwired to8'hFF;READ_ADDRstill advances onif (m_axi_arready)with noarvalid;READ_DATAstill exits onm_axi_rlast || bytes_remaining <= 32'd8. Deriving the burst length requires a multi-burst FSM (the local memory is 12-bit addressed = 4096 beats, i.e. up to 16 maximal bursts), which is a redesign, not this diff.wstrb, so alengththat is not a multiple of 8 over-reads to the next 8-byte boundary.DONE_STis entered without ever waiting onm_axi_bvalidon the write path (assign m_axi_bready = 1'b1;andbvalidis never sampled), sodonecan rise before the slave has accepted the write response.Not fixed here, and not filed as fixed. Nothing was weakened: no
|| true, nocontinue-on-error, no existing assertion relaxed, no case dropped.