The demo below shows a 3×3 systolic array computing the product C=A×B. Rows of matrix A enter from the left, columns of B enter from the top — each staggered by one clock tick so they meet at exactly the right cell at the right moment. Press Step to advance one tick, or Run to let it play automatically.
<!-- Controls: step through ticks or run automatically. -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="tick-label">Tick: <span id="tick-display">0</span></span>
</div>
<div class="matrices-row">
<div class="mat-block">
<div class="mat-title">Matrix A</div>
<div class="mat-grid" id="mat-a"></div>
</div>
<div class="mat-block">
<div class="mat-title">Matrix B</div>
<div class="mat-grid" id="mat-b"></div>
</div>
<div class="mat-block">
<div class="mat-title">Result C</div>
<div class="mat-grid" id="mat-c"></div>
</div>
</div>
<div class="array-section">
<div class="array-label">Systolic Array (3×3 cells)</div>
<div class="systolic-grid" id="systolic-grid"></div>
</div>
<div class="status-bar" id="status-bar">Press Step or Run to start the systolic march.</div>
/* Systolic array demo styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a2535; background: #f4f7fa; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; padding: .6rem .8rem; background: #fff; border-bottom: 1px solid #dde3ea; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #2c5282; background: #2c5282; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #2c5282; }
button:disabled { opacity: .4; cursor: default; }
.tick-label { font-size: .85rem; color: #555; margin-left: auto; }
/* Input/output matrix display. */
.matrices-row { display: flex; gap: .8rem; padding: .7rem .8rem .4rem; flex-wrap: wrap; }
.mat-block { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.mat-title { font-size: .75rem; font-weight: 700; letter-spacing: .04em; color: #2c5282; text-transform: uppercase; }
.mat-grid { display: grid; grid-template-columns: repeat(3, 36px); gap: 3px; }
.mat-cell { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font: 600 13px ui-monospace, monospace; border-radius: 5px; background: #e8eef6; border: 1px solid #c5d2e0; color: #1a2535; transition: background .25s; }
.mat-cell.active { background: #bee3f8; border-color: #4299e1; }
/* Systolic grid cell styles. */
.array-section { padding: .4rem .8rem .6rem; }
.array-label { font-size: .75rem; font-weight: 700; letter-spacing: .04em; color: #2c5282; text-transform: uppercase; margin-bottom: .5rem; }
.systolic-grid { display: grid; grid-template-columns: repeat(3, 72px); gap: 6px; }
.s-cell { width: 72px; height: 72px; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 8px; background: #fff; border: 2px solid #c5d2e0; transition: all .2s; position: relative; }
.s-cell.active { border-color: #4299e1; background: #ebf8ff; }
.s-cell.done { border-color: #48bb78; background: #f0fff4; }
.s-cell-acc { font: 700 15px ui-monospace, monospace; color: #2c5282; }
.s-cell-inputs { font: 500 10px ui-monospace, monospace; color: #718096; margin-top: 2px; }
.s-cell-label { position: absolute; top: 3px; left: 5px; font: 600 9px system-ui; color: #a0aec0; }
/* Status bar styles. */
.status-bar { padding: .5rem .8rem; font-size: .88rem; font-weight: 600; color: #2c5282; min-height: 1.8em; background: #fff; border-top: 1px solid #dde3ea; }
.status-bar.done { color: #276749; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Input matrices A and B (3x3). C = A*B is computed systolically.
const A = [[1,2,3],[4,5,6],[7,8,9]];
const B = [[9,8,7],[6,5,4],[3,2,1]];
// Precompute the expected result for verification and display.
// C[i][j] = sum_k A[i][k]*B[k][j]
const C_expected = (() => {
const C = [[0,0,0],[0,0,0],[0,0,0]];
for(let i=0;i<3;i++) for(let j=0;j<3;j++) for(let k=0;k<3;k++) C[i][j]+=A[i][k]*B[k][j];
return C;
})();
// Systolic array state: accumulator per cell, done flag per cell.
// In a 3x3 systolic array for matrix multiply:
// A rows enter left-to-right, staggered by row index.
// B columns enter top-to-bottom, staggered by col index.
// Cell (i,j) accumulates A[i][k]*B[k][j] when diagonal wave k reaches it.
// Wave k arrives at cell (i,j) at tick = k + i + j.
const N = 3;
const MAX_TICK = 3*(N-1) + N; // Total ticks needed: first data arrives at tick 0, last at tick 2*(N-1)+N-1.
let tick = 0;
let running = false;
let timer = null;
// Accumulators and completion flags for each cell.
const acc = Array.from({length:N},()=>Array(N).fill(0));
const done = Array.from({length:N},()=>Array(N).fill(false));
function getAInput(i, k) {
// A[i][k-i] enters row i at tick k (staggered by i ticks).
return (k >= i && k - i < N) ? A[i][k - i] : null;
}
function getBInput(j, k) {
// B[k-j][j] enters col j at tick k (staggered by j ticks).
return (k >= j && k - j < N) ? B[k - j][j] : null;
}
function renderMatrices(activeA, activeB) {
// Render all three matrices, highlighting active input elements.
['a','b','c'].forEach(id => {
const el = document.getElementById('mat-' + id);
if(!el) return;
el.innerHTML = '';
const mat = id === 'a' ? A : id === 'b' ? B : C_expected;
for(let i=0;i<3;i++) for(let j=0;j<3;j++){
const d = document.createElement('div');
d.className = 'mat-cell';
if(id === 'a' && activeA && activeA[i] === j) d.classList.add('active');
if(id === 'b' && activeB && activeB[j] === i) d.classList.add('active');
if(id === 'c') { const v = acc[i][j]; d.textContent = done[i][j] ? v : (v > 0 ? v : ''); if(done[i][j]) d.classList.add('active'); }
else d.textContent = mat[i][j];
el.appendChild(d);
}
});
}
function renderGrid(waveK) {
// Render the systolic grid, marking active and completed cells.
const grid = document.getElementById('systolic-grid');
grid.innerHTML = '';
for(let i=0;i<N;i++) for(let j=0;j<N;j++){
const cell = document.createElement('div');
cell.className = 's-cell';
const lbl = document.createElement('div');
lbl.className = 's-cell-label';
lbl.textContent = 'C' + (i+1) + (j+1);
cell.appendChild(lbl);
const aVal = getAInput(i, waveK - j);
const bVal = getBInput(j, waveK - i);
const contributing = (aVal !== null && bVal !== null);
const accDiv = document.createElement('div');
accDiv.className = 's-cell-acc';
accDiv.textContent = acc[i][j];
const inpDiv = document.createElement('div');
inpDiv.className = 's-cell-inputs';
if(contributing){
cell.classList.add('active');
inpDiv.textContent = 'a=' + aVal + ' b=' + bVal;
} else if(done[i][j]){
cell.classList.add('done');
inpDiv.textContent = '= ' + C_expected[i][j];
}
cell.appendChild(accDiv);
cell.appendChild(inpDiv);
grid.appendChild(cell);
}
}
function step() {
if(tick >= MAX_TICK) return;
// Advance one tick: apply diagonal wave, update accumulators.
// diagonal wave index = tick; wave k hits cell (i,j) when i+j+k == tick => k = tick-i-j
const waveK = tick;
const activeA = {};
const activeB = {};
for(let i=0;i<N;i++) for(let j=0;j<N;j++){
const k = waveK - i - j;
if(k >= 0 && k < N){
const aVal = A[i][k];
const bVal = B[k][j];
acc[i][j] += aVal * bVal;
activeA[i] = k;
activeB[j] = k;
}
// Mark cell done when its accumulator has reached the expected value.
if(acc[i][j] === C_expected[i][j] && tick >= i + j + N - 1) done[i][j] = true;
}
tick++;
document.getElementById('tick-display').textContent = tick;
renderMatrices(activeA, activeB);
renderGrid(waveK);
updateStatus(waveK);
if(tick >= MAX_TICK){
stopRun();
document.getElementById('status-bar').textContent = 'Done! Every cell has computed its share of C = A × B.';
document.getElementById('status-bar').className = 'status-bar done';
}
}
function updateStatus(waveK) {
const bar = document.getElementById('status-bar');
bar.className = 'status-bar';
bar.textContent = i18nFmt('Tick %tick% — diagonal wave %wave% passing through the array.', { tick: tick, wave: waveK });
}
function startRun() {
if(running) return;
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
timer = setInterval(()=>{ step(); if(tick >= MAX_TICK) stopRun(); }, 600);
}
function stopRun() {
running = false;
clearInterval(timer);
timer = null;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
function reset() {
stopRun();
tick = 0;
for(let i=0;i<N;i++) for(let j=0;j<N;j++){ acc[i][j]=0; done[i][j]=false; }
document.getElementById('tick-display').textContent = 0;
renderMatrices(null, null);
renderGrid(-1);
const bar = document.getElementById('status-bar');
bar.className = 'status-bar';
bar.textContent = 'Press Step or Run to start the systolic march.';
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', ()=>{ running ? stopRun() : startRun(); });
document.getElementById('btn-reset').addEventListener('click', reset);
// Initialize display on load.
renderMatrices(null, null);
renderGrid(-1);
<!-- Controls: step through ticks or run automatically. -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="tick-label">Tick: <span id="tick-display">0</span></span>
</div>
<div class="matrices-row">
<div class="mat-block">
<div class="mat-title">Matrix A</div>
<div class="mat-grid" id="mat-a"></div>
</div>
<div class="mat-block">
<div class="mat-title">Matrix B</div>
<div class="mat-grid" id="mat-b"></div>
</div>
<div class="mat-block">
<div class="mat-title">Result C</div>
<div class="mat-grid" id="mat-c"></div>
</div>
</div>
<div class="array-section">
<div class="array-label">Systolic Array (3×3 cells)</div>
<div class="systolic-grid" id="systolic-grid"></div>
</div>
<div class="status-bar" id="status-bar">Press Step or Run to start the systolic march.</div>
/* Systolic array demo styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a2535; background: #f4f7fa; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; padding: .6rem .8rem; background: #fff; border-bottom: 1px solid #dde3ea; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #2c5282; background: #2c5282; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #2c5282; }
button:disabled { opacity: .4; cursor: default; }
.tick-label { font-size: .85rem; color: #555; margin-left: auto; }
/* Input/output matrix display. */
.matrices-row { display: flex; gap: .8rem; padding: .7rem .8rem .4rem; flex-wrap: wrap; }
.mat-block { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.mat-title { font-size: .75rem; font-weight: 700; letter-spacing: .04em; color: #2c5282; text-transform: uppercase; }
.mat-grid { display: grid; grid-template-columns: repeat(3, 36px); gap: 3px; }
.mat-cell { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font: 600 13px ui-monospace, monospace; border-radius: 5px; background: #e8eef6; border: 1px solid #c5d2e0; color: #1a2535; transition: background .25s; }
.mat-cell.active { background: #bee3f8; border-color: #4299e1; }
/* Systolic grid cell styles. */
.array-section { padding: .4rem .8rem .6rem; }
.array-label { font-size: .75rem; font-weight: 700; letter-spacing: .04em; color: #2c5282; text-transform: uppercase; margin-bottom: .5rem; }
.systolic-grid { display: grid; grid-template-columns: repeat(3, 72px); gap: 6px; }
.s-cell { width: 72px; height: 72px; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 8px; background: #fff; border: 2px solid #c5d2e0; transition: all .2s; position: relative; }
.s-cell.active { border-color: #4299e1; background: #ebf8ff; }
.s-cell.done { border-color: #48bb78; background: #f0fff4; }
.s-cell-acc { font: 700 15px ui-monospace, monospace; color: #2c5282; }
.s-cell-inputs { font: 500 10px ui-monospace, monospace; color: #718096; margin-top: 2px; }
.s-cell-label { position: absolute; top: 3px; left: 5px; font: 600 9px system-ui; color: #a0aec0; }
/* Status bar styles. */
.status-bar { padding: .5rem .8rem; font-size: .88rem; font-weight: 600; color: #2c5282; min-height: 1.8em; background: #fff; border-top: 1px solid #dde3ea; }
.status-bar.done { color: #276749; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Input matrices A and B (3x3). C = A*B is computed systolically.
const A = [[1,2,3],[4,5,6],[7,8,9]];
const B = [[9,8,7],[6,5,4],[3,2,1]];
// Precompute the expected result for verification and display.
// C[i][j] = sum_k A[i][k]*B[k][j]
const C_expected = (() => {
const C = [[0,0,0],[0,0,0],[0,0,0]];
for(let i=0;i<3;i++) for(let j=0;j<3;j++) for(let k=0;k<3;k++) C[i][j]+=A[i][k]*B[k][j];
return C;
})();
// Systolic array state: accumulator per cell, done flag per cell.
// In a 3x3 systolic array for matrix multiply:
// A rows enter left-to-right, staggered by row index.
// B columns enter top-to-bottom, staggered by col index.
// Cell (i,j) accumulates A[i][k]*B[k][j] when diagonal wave k reaches it.
// Wave k arrives at cell (i,j) at tick = k + i + j.
const N = 3;
const MAX_TICK = 3*(N-1) + N; // Total ticks needed: first data arrives at tick 0, last at tick 2*(N-1)+N-1.
let tick = 0;
let running = false;
let timer = null;
// Accumulators and completion flags for each cell.
const acc = Array.from({length:N},()=>Array(N).fill(0));
const done = Array.from({length:N},()=>Array(N).fill(false));
function getAInput(i, k) {
// A[i][k-i] enters row i at tick k (staggered by i ticks).
return (k >= i && k - i < N) ? A[i][k - i] : null;
}
function getBInput(j, k) {
// B[k-j][j] enters col j at tick k (staggered by j ticks).
return (k >= j && k - j < N) ? B[k - j][j] : null;
}
function renderMatrices(activeA, activeB) {
// Render all three matrices, highlighting active input elements.
['a','b','c'].forEach(id => {
const el = document.getElementById('mat-' + id);
if(!el) return;
el.innerHTML = '';
const mat = id === 'a' ? A : id === 'b' ? B : C_expected;
for(let i=0;i<3;i++) for(let j=0;j<3;j++){
const d = document.createElement('div');
d.className = 'mat-cell';
if(id === 'a' && activeA && activeA[i] === j) d.classList.add('active');
if(id === 'b' && activeB && activeB[j] === i) d.classList.add('active');
if(id === 'c') { const v = acc[i][j]; d.textContent = done[i][j] ? v : (v > 0 ? v : ''); if(done[i][j]) d.classList.add('active'); }
else d.textContent = mat[i][j];
el.appendChild(d);
}
});
}
function renderGrid(waveK) {
// Render the systolic grid, marking active and completed cells.
const grid = document.getElementById('systolic-grid');
grid.innerHTML = '';
for(let i=0;i<N;i++) for(let j=0;j<N;j++){
const cell = document.createElement('div');
cell.className = 's-cell';
const lbl = document.createElement('div');
lbl.className = 's-cell-label';
lbl.textContent = 'C' + (i+1) + (j+1);
cell.appendChild(lbl);
const aVal = getAInput(i, waveK - j);
const bVal = getBInput(j, waveK - i);
const contributing = (aVal !== null && bVal !== null);
const accDiv = document.createElement('div');
accDiv.className = 's-cell-acc';
accDiv.textContent = acc[i][j];
const inpDiv = document.createElement('div');
inpDiv.className = 's-cell-inputs';
if(contributing){
cell.classList.add('active');
inpDiv.textContent = 'a=' + aVal + ' b=' + bVal;
} else if(done[i][j]){
cell.classList.add('done');
inpDiv.textContent = '= ' + C_expected[i][j];
}
cell.appendChild(accDiv);
cell.appendChild(inpDiv);
grid.appendChild(cell);
}
}
function step() {
if(tick >= MAX_TICK) return;
// Advance one tick: apply diagonal wave, update accumulators.
// diagonal wave index = tick; wave k hits cell (i,j) when i+j+k == tick => k = tick-i-j
const waveK = tick;
const activeA = {};
const activeB = {};
for(let i=0;i<N;i++) for(let j=0;j<N;j++){
const k = waveK - i - j;
if(k >= 0 && k < N){
const aVal = A[i][k];
const bVal = B[k][j];
acc[i][j] += aVal * bVal;
activeA[i] = k;
activeB[j] = k;
}
// Mark cell done when its accumulator has reached the expected value.
if(acc[i][j] === C_expected[i][j] && tick >= i + j + N - 1) done[i][j] = true;
}
tick++;
document.getElementById('tick-display').textContent = tick;
renderMatrices(activeA, activeB);
renderGrid(waveK);
updateStatus(waveK);
if(tick >= MAX_TICK){
stopRun();
document.getElementById('status-bar').textContent = 'Done! Every cell has computed its share of C = A × B.';
document.getElementById('status-bar').className = 'status-bar done';
}
}
function updateStatus(waveK) {
const bar = document.getElementById('status-bar');
bar.className = 'status-bar';
bar.textContent = i18nFmt('Tick %tick% — diagonal wave %wave% passing through the array.', { tick: tick, wave: waveK });
}
function startRun() {
if(running) return;
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
timer = setInterval(()=>{ step(); if(tick >= MAX_TICK) stopRun(); }, 600);
}
function stopRun() {
running = false;
clearInterval(timer);
timer = null;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
function reset() {
stopRun();
tick = 0;
for(let i=0;i<N;i++) for(let j=0;j<N;j++){ acc[i][j]=0; done[i][j]=false; }
document.getElementById('tick-display').textContent = 0;
renderMatrices(null, null);
renderGrid(-1);
const bar = document.getElementById('status-bar');
bar.className = 'status-bar';
bar.textContent = 'Press Step or Run to start the systolic march.';
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', ()=>{ running ? stopRun() : startRun(); });
document.getElementById('btn-reset').addEventListener('click', reset);
// Initialize display on load.
renderMatrices(null, null);
renderGrid(-1);
Notice that no cell ever fetches data from far away: each only talks to its immediate neighbors. This locality is the key to why systolic arrays scale so well in hardware — wires stay short, power stays low, and thousands of cells can run in parallel without a traffic jam.
Comments
Loading comments...