fix(bitnet): default local_we low before the DMA case (Closes #2006) - #2344
Merged
Conversation
gHashTag
enabled auto-merge (squash)
August 21, 2026 16:05
Contributor
PR DashboardGenerated at: 2026-08-21 16:06:37 UTC
Summary
Seal Status
|
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
The dma_controller emitter raised the local write strobe in READ_DATA and cleared it only in READ_DATA's else branch and in DONE_ST, leaving no default for states that never drive it. Emit `end else begin` / `local_we <= 1'b0;` / `case (state)` so the strobe is a one-cycle pulse rather than a level. Behaviourally latent on today's single-burst FSM -- measured, not assumed: a differential icarus run of the master and fixed emitters in one testbench shows identical observables. It becomes load-bearing once #1970 derives the burst loop and READ_ADDR is re-entered between bursts. Case arms are unchanged.
gHashTag
force-pushed
the
fix/dma-local-we-default-2006
branch
from
August 21, 2026 16:10
030f66d to
4f07aa8
Compare
Contributor
PR DashboardGenerated at: 2026-08-21 16:10:23 UTC
Summary
Seal Status
|
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
This was referenced Aug 21, 2026
Merged
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 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 #2006
The defect was live on master verbatim
#2006 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(ee494b990b795267ce1c955d6009819b57ac55e8) before writing anything —bootstrap/src/bitnet_dma.rs:165:No default for
local_we. The only clears are:193(end else local_we <= 1'b0;, insideREAD_DATA) and:218(DONE_ST).The change
Emit
end else begin/local_we <= 1'b0;/case (state), closing withendcase+end. Any state that does not explicitly drive the strobe now leaves it low. The case arms are byte-identical — no port, state, or other signal changed.Honest scope: this fix is behaviourally latent today, and that was measured
A differential Icarus simulation running the master emitter and the fixed emitter side by side in one testbench, over a 4-beat read with a well-behaved AXI slave:
Identical. The reason is structural:
local_weis only ever raised inREAD_DATA, and the sole exit fromREAD_DATAisDONE_ST, which clears it. There is no path back toREAD_ADDRin the single-burst FSM, so the missing default is unreachable today. TheREAD_ADDR-between-bursts window named in the issue only opens once #1970 derives the burst loop.Claiming a behavioural win here would have been false, so it is not claimed. This lands the defensive shape first so the multi-burst change cannot silently reintroduce a stuck strobe.
Mutant evidence
Guard 1 —
local_we_defaults_low_before_the_case. Mutant: delete only the emitted default line (begin/end stay balanced). Fails with:The message prints the offending span — that is what shows the assertion read the right text. Reverting: 17 passed, 0 failed.
The anchor is load-bearing, and that was measured. Adding a naive
v.contains("local_we <= 1'b0;")to the defective master emitter:The naive check passes on the defective emitter because
DONE_STalready emits that exact string with single spacing (and the reset block emits a padded variant). An unanchored guard here would have been vacuous — the same trap that had to be repaired hours after landing elsewhere in this campaign.Guard 2 —
always_block_begin_end_balanced, planted separately. One mutant per guard, not one per file. Mutant: delete the closingend. Fails with:Guard 1 still passes under this mutant, so neither guard masks the other.
The emitted Verilog parses:
iverilog -g2005accepts the fixed module.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. They were run withrustc --teston the module, which is self-contained. That gap is pre-existing and out of scope here.Adjacent problems seen and deliberately NOT fixed here
:189-190registerlocal_we <= 1'b1andlocal_addr <= local_addr + 12'd1in the same non-blocking group, so beat 0's data lands at address 1 and slot 0 is never written. Visible in the trace above (mem[0] = ffffffffffffffffon both columns). Filed separately, fixed in its own PR.arlen/awlenhardwired to8'hFF, andREAD_DATAexits onm_axi_rlast || bytes_remaining <= 32'd8.Nothing was weakened: no
|| true, nocontinue-on-error, no existing assertion relaxed, no case dropped.