fix(bitnet): retire prefetch_done on entry to IDLE - #2340
Merged
Conversation
weight_prefetch_ctrl raised prefetch_done in DONE_ST and returned to IDLE, but cleared the flag only inside the `if (start_prefetch)` guard. The clear is a non-blocking assignment, so it lands one cycle after the request is sampled -- and multilayer_sequencer tests prefetch_done in WAIT_PF, the same cycle it holds start_prefetch high. The next requester therefore read the previous transaction's completion and ran its layer against weights still being written into the weight BRAM. Emit the clear in the IDLE arm ahead of the guard, so prefetch_done is genuinely the one-cycle pulse the module doc-comment already described. No other signal, state or port changed. Guarded by prefetch_done_retired_in_idle_before_start_guard, which slices the IDLE case arm out of the emitted text and requires the clear to precede the guard. The assertion is anchored to that arm on purpose: the reset block also contains `prefetch_done <= 1'b0;`, so an unanchored contains check passes on the defective emitter. Closes #1985
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 15:48:14 UTC
Summary
Seal Status
|
gHashTag
enabled auto-merge (squash)
August 21, 2026 15:48
This was referenced Aug 21, 2026
Merged
gHashTag
pushed a commit
that referenced
this pull request
Aug 22, 2026
#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
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.
The defect was still live on master
#1985 reports itself as fixed. Its fixes landed on a branch that never opened a PR, so the
defect is on master verbatim. Found at
bootstrap/src/bitnet_buffers.rs:181-182:DONE_STraisesprefetch_doneand returns toIDLE; the only clear outside reset sitsinside the
start_prefetchguard.Why that is one cycle too late. The clear is a non-blocking assignment, so it takes
effect the cycle after the request is sampled. The consumer,
multilayer_sequencer, does:WAIT_PFtests the flag in the same cycle it holdsstart_prefetchhigh — exactly the onecycle where the controller is still in
IDLEand the stale1is still there.The fix
Emit the clear in the
IDLEarm ahead of the guard, so the flag is retired on entry toIDLEand is genuinely the one-cycle pulse the module doc-comment already claimed:No other signal, state or port changed. Scope:
bootstrap/src/bitnet_buffers.rs, itsintegration test, and one
docs/now/entry.Three bars
TRUE
rustc --test bootstrap/src/bitnet_buffers.rs— the module is dependency-free, so its realunit tests run standalone:
ALIVE — the guard is not vacuous, and the obvious version of it would have been
The reset block also contains
prefetch_done <= 1'b0;. Measured against the defectiveemitter:
An unanchored
containscheck passes on the bug. The new guard therefore slices theIDLEcase arm out of the emitted text first and asserts the clear precedes the guard.
BITING — planted mutant
Mutant: move the clear back inside the
start_prefetchbranch (i.e. restore master).The message prints the offending
IDLEarm, which is what proves the assertion read theright region rather than passing on the reset line. Reverting the mutant:
23 passed; 0 failed.RTL evidence (icarus, on the actually-emitted Verilog)
Two transactions, sampling
prefetch_donein the cycle the secondstart_prefetchisraised — the cycle
WAIT_PFlooks at it:sampled_done_t2done_riseswe_countdone_rises=2andwe_count=4are the controls: completion is still signalled exactly onceper transaction and all 2×2 words still reach the BRAM.
End-to-end, real
multilayer_sequencer+ realweight_prefetch_ctrl, two layers(re-run against the sequencer as changed by #2337):
overlap_cycleslayer_start_during_prefetchLayer 1 was starting compute while its own weights were still being written into the weight
BRAM. That is the overlap in the issue title.
The defect shipped under a green check
Master's own 22 unit tests all pass on the defective emitter (
22 passed; 0 failed).And they are not run in CI at all. No workflow invokes
cargo test -p t27c;corpus-ratchet.ymlrecords that the step was removed by #2292 after going red on master(1602 passed / 13 failed / 2 ignored). So the guards added here are proved locally and are
not executed by any required check. That gap is pre-existing and repo-wide — recorded here
rather than papered over, and deliberately not "fixed" in this PR, since the same comment
warns that re-adding it as a plain gate lands red and gets disabled.
Not fixed here, filed separately
#1985 reports a second defect:
multilayer_sequenceruses a level-triggered handshake andcannot distinguish "done already" from "done still". That is a different module
(
bootstrap/src/bitnet_pipeline.rs) and a different change, so it is not bundled here.The end-to-end measurement above shows this fix alone closes the observable overlap for that
consumer, but the handshake remains edge-insensitive by construction and is worth hardening
on its own. Filed as a follow-up rather than folded into this diff.
Notes
pre-commitcould not run: it callsscripts/tri check-now, which exits 1 withtri: t27c not foundbecause not27cbinary is built in this worktree (the wrapperprobes four paths, none present). That is a missing build artifact, not a policy
violation. The gate's substantive assertion was verified instead by running the CI script
itself, unmodified —
scripts/ci/now-sync-gate-diff.sh— which needs no binary:NOW sync gate passed: docs/now/2026-08-21-prefetch-done-stale-flag.md (UTC window: 2026-08-20 .. 2026-08-22)Closes #1985