Skip to content

fix(bitnet): default local_we low before the DMA case (Closes #2006) - #2344

Merged
gHashTag merged 1 commit into
masterfrom
fix/dma-local-we-default-2006
Aug 21, 2026
Merged

fix(bitnet): default local_we low before the DMA case (Closes #2006)#2344
gHashTag merged 1 commit into
masterfrom
fix/dma-local-we-default-2006

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

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:

    s.push_str("        end else case (state)\n");

No default for local_we. The only clears are :193 (end else local_we <= 1'b0;, inside READ_DATA) and :218 (DONE_ST).

The change

Emit end else begin / local_we <= 1'b0; / case (state), closing with endcase + 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:

--- OLD emitter (origin/master) ---        --- NEW emitter (fixed) ---
  local writes = 4                          local writes = 4
    write[0] addr=1 data=d0d0...0000          write[0] addr=1 data=d0d0...0000
    write[3] addr=4 data=d0d0...0003          write[3] addr=4 data=d0d0...0003
  mem[0] = ffffffffffffffff                 mem[0] = ffffffffffffffff
  local_we high outside READ_DATA = 1       local_we high outside READ_DATA = 1
CONTROL: old AXI w-beats = 3   new AXI w-beats = 3

Identical. The reason is structural: local_we is only ever raised in READ_DATA, and the sole exit from READ_DATA is DONE_ST, which clears it. There is no path back to READ_ADDR in the single-burst FSM, so the missing default is unreachable today. The READ_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:

`local_we` must be defaulted low between the reset block and `case (state)`, so the
strobe is a one-cycle pulse instead of a level that persists through states which never
drive it (READ_ADDR between bursts, IDLE, WRITE_ADDR, WRITE_DATA). Text found between
`end else` and `case (state)` was:
"end else begin\n            "
test result: FAILED. 16 passed; 1 failed

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:

test tests::naive_unanchored_guard_ON_DEFECTIVE_EMITTER ... ok
test tests::anchored_guard_ON_DEFECTIVE_EMITTER ... FAILED

The naive check passes on the defective emitter because DONE_ST already 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 closing end. Fails with:

unbalanced begin/end in emitted module: 15 `begin` vs 14 `end` (excluding `endcase`/`endmodule`)
test result: FAILED. 16 passed; 1 failed

Guard 1 still passes under this mutant, so neither guard masks the other.

The emitted Verilog parses: iverilog -g2005 accepts 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.yml records that the step was removed by #2292 after going red on master. They were run with rustc --test on the module, which is self-contained. That gap is pre-existing and out of scope here.

Adjacent problems seen and deliberately NOT fixed here

Nothing was weakened: no || true, no continue-on-error, no existing assertion relaxed, no case dropped.

@gHashTag
gHashTag enabled auto-merge (squash) August 21, 2026 16:05
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-21 16:06:37 UTC

Summary

Status Count
Total Open PRs 4
PRs with Failing Checks 2
PRs with All Checks Green 2
READY 1
FAILING 2
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=65f033d04125 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
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
gHashTag force-pushed the fix/dma-local-we-default-2006 branch from 030f66d to 4f07aa8 Compare August 21, 2026 16:10
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-21 16:10:23 UTC

Summary

Status Count
Total Open PRs 4
PRs with Failing Checks 2
PRs with All Checks Green 2
READY 1
FAILING 2
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=65f033d04125 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@gHashTag
gHashTag merged commit cb1f0d4 into master Aug 21, 2026
24 of 27 checks passed
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>
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.

Wave Loop 581 — the DMA closes: a write strobe was a level, not a pulse

2 participants