10 / 20 · Lab
Lab: a skewed 2×2 array
Model one-edge forwarding delays and check every intermediate accumulator.
Lessons are free to read. Enroll to save your learning progress.
Make equal k values meet
Inject A(i,k) at the left boundary on logical edge k+i and B(k,j) at the top on k+j. Registered forwarding adds j edges horizontally and i vertically. Both operands therefore reach PE(i,j) on k+i+j. This staggered injection is input skew.
For A=[[1,2],[3,4]], B=[[5,6],[7,8]], clear finishes before E0. Boundary schedules are:
| Edge | Left row 0 | Left row 1 | Top col 0 | Top col 1 |
|---|---|---|---|---|
| E0 | 1(k0) | invalid | 5(k0) | invalid |
| E1 | 2(k1) | 3(k0) | 7(k1) | 6(k0) |
| E2 | invalid | 4(k1) | invalid | 8(k1) |
| E3 | invalid | invalid | invalid | invalid |
Save the following as schedule-lab.mjs and run node schedule-lab.mjs. It requires no packages and prints Schedule lab passed. Invalid tokens are null; valid tokens carry value and k. This single-job model omits job tags but checks k explicitly.
import assert from 'node:assert/strict';
const A=[[1,2],[3,4]],B=[[5,6],[7,8]],N=2,K=2;
const fresh=()=>({a:null,b:null,acc:0,count:0});
let grid=Array.from({length:N},()=>Array.from({length:N},fresh));
const expected=[[[5,0],[0,0]],[[19,6],[15,0]],
[[19,22],[43,18]],[[19,22],[43,50]]];
const token=(value,k)=>({value,k});
for(let t=0;t<K+2*N-2;t++) {
const next=grid.map(r=>r.map(p=>({...p})));
for(let i=0;i<N;i++) for(let j=0;j<N;j++) {
const ka=t-i,kb=t-j;
const a=j?grid[i][j-1].a:(ka>=0&&ka<K?token(A[i][ka],ka):null);
const b=i?grid[i-1][j].b:(kb>=0&&kb<K?token(B[kb][j],kb):null);
const p=next[i][j];p.a=a;p.b=b;
if(a&&b){assert.equal(a.k,b.k);assert.equal(a.k,p.count);
p.acc+=a.value*b.value;p.count++;}
}
grid=next;
assert.deepEqual(grid.map(r=>r.map(p=>p.acc)),expected[t]);
}
assert.deepEqual(grid.map(r=>r.map(p=>p.count)),[[2,2],[2,2]]);
console.log('Schedule lab passed');Each PE reads old state and writes next state. In-place iteration would incorrectly permit multi-hop forwarding within one edge.
After E0 only C(0,0)'s accumulator is five. E1 completes it at nineteen and begins its neighbors at six and fifteen. E2 completes those at twenty-two and forty-three, while the final PE has only eighteen. E3 adds thirty-two to produce fifty.
Change row injection from t−i to t. A tag assertion should detect misalignment. Completion requires four advance edges and every count equal to two. Initialization and serial output transfer are excluded. This executable model validates the stated schedule without claiming RTL or DDS execution.
Actual RTL: the registered 2×2 grid
The attached systolic_2x2.sv connects four instances from signed_mac_pe.sv, bounded to N=K=2. A moves right and B moves down through registered links carrying valid and k tags. The external boundary supplier performs skew; the testbench's step task drives the E0..E3 schedule directly. Clear finishes before E0 and no extra multiplier pipeline exists.
Save all three attachments together and run Icarus Verilog:
iverilog -g2012 -s tb_systolic_2x2 -o systolic.vvp signed_mac_pe.sv systolic_2x2.sv tb_systolic_2x2.sv
vvp systolic.vvpThe post-edge matrices are [[5,0],[0,0]], [[19,6],[15,0]], [[19,22],[43,18]] and [[19,22],[43,50]]. Done asserts only after E3. Two pauses give four advance edges and six physical compute edges. Checks also cover [[10,9],[−6,3]], complete state holding and reset mid-job. Expect RTL PASS:.
For an RTL modification exercise, replace a registered operand link with a combinational path. Predict the first tag-alignment or intermediate-sum failure, compare the log and restore the register. Extending this grid to N×N requires revisiting wiring, skew, widths, tags and completion together.
Try it yourself
After E2, what is PE(1,1)'s accumulator and remaining term? Why does it change at E3 despite invalid boundary inputs?
Read the explanation
The accumulator is 18 from 3×6, with 4×8=32 remaining. Those operands are in internal forwarding registers and arrive at E3; invalid boundaries do not instantly empty the array.