Add write skid buffer for same-register read-modify-write - #330
Conversation
Under token discipline, an operation that reads and writes the same register within one ctrl step (an accumulator, or #281's `NOT $0 -> $0, SOUTH` under backpressure) deadlocked: the write was rejected while the operand token was unconsumed, and the step could not complete until the write was delivered. Each bank now has a one-entry write skid buffer (one write port -> at most one blocked write). A write is accepted whenever the skid is free (outport_wr_rdy = ~skid_valid, pure registered state, so the producer's rdy never combinationally depends on any consumer's readiness and never collides with a commit on the single write port): - It lands directly if that cannot disturb an in-flight read (target token-free and not read by the current step), or at the boundary of the reading step (the write's token atomically replaces the consumed one; set wins over clear in the token update). - Otherwise it parks in the skid and commits once its target is no longer read by an open step: at that step's completion pulse, or immediately if the target is not being read and holds no token. The stability invariant — a register being read holds its value until the step completes — also fixes the partial-multicast corruption from issue #281 (a speculative register write landing while another fan-out leg is backpressured, followed by re-execution on the changed value), which the previous direct-write-if-token-free behavior still allowed. Tests: - test_reg_cluster_write_skid_buffer: three back-to-back writes with a stalled consumer flow through park/commit in order. - test_tile_same_register_accumulate: single-ctrl-word INC accumulator through one register with a stalled tile outport (the #281 shape); deadlocks without the skid buffer (verified), completes with it. - The cluster test harnesses now always emulate the tile's per-step done-tracking to drive inport_ctrl_proceed, since commit timing is tied to step completion.
2c43ee9 to
b672077
Compare
yyan7223
left a comment
There was a problem hiding this comment.
The overall skid buffer control logic looks fine. But I need more elaboration to 4 signals below to help me better understand the details:
- s.wr_target_token
- s.skid_target_token
- s.wr_target_read
- s.skid_target_read
Per review: wr_target_token / skid_target_token read ambiguously (the "target token" could be mistaken for the value being written), and wr_target_read / skid_target_read look like read enables. Renamed to wr_target_holds_token / skid_target_holds_token (the destination register still holds its old, unconsumed token) and wr_target_being_read / skid_target_being_read (hazard flags: the current ctrl step is reading that destination), with the declaration comments expanded accordingly and a short definition of "ctrl step" added to the module docstring.
|
@yyan7223 Elaboration on the 4 signals (they were renamed in a recent push, exactly to address this — mapping below):
Two clarifications that tie these together:
Branch is also synced with |
The first version made outport_wr_rdy = ~skid_valid, i.e. it accepted any write while the skid was free and parked whatever could not land. That silently changed flow control for kernels that never do same-register RMW: a write whose target still held an unconsumed token used to backpressure its producer, but was now accepted into the skid, letting the producer run ahead. Two FIR kernels (CgraRTL_fir_2x2_loop_counter_test and CgraVerifAssert_test, both 2x2 fir *_return) computed wrong results as a result -- 0x413 instead of 0x8a7 -- which CI caught after this branch was synced with master. Verified by bisection: reverting only the acceptance condition (not the read-hazard parking rule) restores them, and both tests pass on master. outport_wr_rdy is now the pre-existing condition -- land directly when the target holds no unconsumed token and is not being read this step -- OR-ed with the one case that condition cannot express: a write whose target is being read by its own ctrl step, which deadlocks if backpressured and corrupts the in-flight read if it lands mid-step. Only that case uses the skid. Backpressure for every other flow is bit-for-bit the pre-existing behavior. All terms remain registered state or ctrl-word fields, so the producer's rdy still never combinationally depends on a consumer's readiness, and a direct write still cannot collide with a skid commit on the single write port.
|
Fixed the two CI failures ( Root cause. The first version set How it was isolated.
Fix. The skid is now used only for the case that has no other solution — a write whose target is being read by its own ctrl step (backpressuring it deadlocks; landing it mid-step corrupts the in-flight read): s.outport_wr_rdy @= (~s.wr_target_holds_token & ~s.wr_target_being_read) | \
(s.wr_target_being_read & ~s.skid_valid)The first term is the pre-existing acceptance condition, so backpressure for every flow that already worked is bit-for-bit unchanged; only same-register RMW takes the skid path. All terms are still registered state or ctrl-word fields, so Verification: 23/23 locally — both previously-failing tests, the full FIR 2x2 set, |
Follow-up to #322 (based on its branch; retarget to
masterafter #322 merges). Removes #322's known limitation and addresses the register half of #281/#286.Problem
Under #322's token discipline, an operation that reads and writes the same register within one ctrl step deadlocked: the write was rejected while the operand's token was unconsumed, and the step could not complete until the write was delivered. This pattern is real — the mapper emits it (
NOT $0 -> $0, SOUTHin the histogram kernel from #281), and it is the natural encoding of an accumulator.Design
Each bank gains a one-entry write skid buffer (one write port ⇒ at most one blocked write, so one entry per bank suffices; the cluster/tile interfaces are unchanged).
outport_wr_rdy = ~skid_valid— a write is accepted whenever the skid is free. Pure registered state: the producer'srdynever combinationally depends on any consumer's readiness (deliberately not onskid_commit, which derives fromctrl_proceedand would close a loop through the FU's rdy chain), and a direct write can never collide with a commit on the single write port.ctrl_proceedpulse — the new token atomically replaces the consumed one; set wins over clear). Otherwise it parks in the skid and commits once its target is no longer read by an open step.With the write-through path, a same-register accumulator runs at full rate (one iteration per step, no bubble).
Tests
test_reg_cluster_write_skid_buffer: three back-to-back writes to one register with a stalled consumer flow through park→commit→park; all three delivered in order.test_tile_same_register_accumulate: single-ctrl-wordINCaccumulator through one register that also fans out to a stalled tile outport — the [P0] Simulator and rtl comparison (latency gap) debug #281 shape end-to-end at tile level. Deadlocks at max-cycles against the Prevent overwriting unconsumed tokens in register banks #322 base (verified as a negative control); completes with the skid. The unarmed first read seeds the accumulator with the register default, so the outport observes 1, 2, 3.ctrl_proceed), since commit timing is tied to step completion; historical expectations are preserved, with stalled-sink tests observing one leading unarmed read.CgraRTL_test×3, FIR terminate, streaming ×2, vector global reduce, migration ×3): 10/10 pass locally.Relation to #286
The xbar half of #281 (
send_rdy_vectorignoringsend_accepted) is separate and remains open in #286 — this PR handles the register-file half (stability + RMW liveness).