Skip to content

About

Interactive systolic array simulator: Rust simulator, web front-end (via WASM)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

96 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Systolic Array Matrix Multiplication Simulator

An interactive, educational playground designed to demystify how systolic arrays and pipelined processing elements perform high-throughput matrix multiplication.

πŸ”— Live demo: systolic-array-simulator.georgesung.com Β Β |Β Β  πŸ“ Companion blog post: Systolic Array Matmul

The project is split into two core layers:

  1. The Rust Core (Hardware Model): A cycle-accurate, deterministic software simulation of physical hardware-level behaviors (register latching, clock-edge synchronization, and Multiply-Accumulate logic).
  2. The Next.js Frontend (End-User App): A high-fidelity, interactive web application that compiles the Rust hardware simulator to WebAssembly (WASM), allowing users to stream data, step through clock cycles, inspect registers in real-time, and play with different systolic array dimensions.

A note on names: the crate is rust-hw-playground, the local directory is pipeline_add (a historical name from the very first pipelined-adder experiment), and the published repo is georgesung/systolic-array-simulator. They all refer to this same project.


πŸ“‚ Directory Structure

Below is an overview of the key directories and files comprising the repository, showing how the frontend, the Rust simulation core, the CLI interfaces, and the documentation are organized:

pipeline_add/
β”œβ”€β”€ Cargo.toml                  # Rust package configuration (CDYLib/RLib) and dependencies
β”œβ”€β”€ README.md                   # This comprehensive simulator guide and architecture overview
β”œβ”€β”€ AGENTS.md                   # Orientation guide for coding agents working in this repo
β”œβ”€β”€ .github/workflows/
β”‚   └── nextjs.yml              # CI: builds WASM + Next.js static export, deploys to GitHub Pages
β”œβ”€β”€ src/                        # πŸ¦€ Core Rust Hardware Simulation Engine
β”‚   β”œβ”€β”€ lib.rs                  # Module entrypoint + WASM panic hook (`init_panic_hook`)
β”‚   β”œβ”€β”€ processing_element.rs   # Weight-stationary processing element (MAC unit)
β”‚   β”œβ”€β”€ dot_product.rs          # 1D Pipelined array of PEs
β”‚   β”œβ”€β”€ systolic_array.rs       # 2D Systolic Array model
β”‚   β”œβ”€β”€ pipeline_add.rs         # Educational 2-stage pipelined integer adder
β”‚   β”œβ”€β”€ wasm_bindings.rs        # WebAssembly wrappers (DotProductSim, SystolicArray2DSim)
β”‚   └── bin/                    # Rust native CLI simulators for debugging
β”‚       β”œβ”€β”€ cli_dot_product.rs
β”‚       └── cli_systolic_array.rs
β”œβ”€β”€ pkg/                        # 🚧 Generated by `wasm-pack build` (git-ignored)
β”‚                               #    Consumed by the frontend as a local npm dependency
β”œβ”€β”€ web/                        # πŸ’» Next.js Frontend Application
β”‚   β”œβ”€β”€ package.json            # Node.js dependencies, scripts, and build metadata
β”‚   β”œβ”€β”€ next.config.ts          # Static export (`output: 'export'`) + async WASM webpack flag
β”‚   β”œβ”€β”€ public/CNAME            # Custom domain for GitHub Pages
β”‚   └── src/
β”‚       β”œβ”€β”€ app/                # Main React App Router (layout.tsx, page.tsx, globals.css)
β”‚       β”œβ”€β”€ components/         # Interactive simulation UI views
β”‚       β”‚   β”œβ”€β”€ Instructions.tsx            # Tab 1: Educational overview and orientation
β”‚       β”‚   β”œβ”€β”€ PESimulator.tsx             # Tab 2: Single PE MAC view
β”‚       β”‚   β”œβ”€β”€ Simulator.tsx               # Tab 3: 1D Dot Product simulator
β”‚       β”‚   β”œβ”€β”€ MatrixMultiplySimulator.tsx # Tab 4: 2D Matrix Multiply simulator
β”‚       β”‚   β”œβ”€β”€ TiledMatmulSimulator.tsx    # Tab 5: Tiled Matrix Multiply simulator
β”‚       β”‚   β”œβ”€β”€ SystolicGrid.tsx            # Shared PE-grid render used by Tabs 4 and 5
β”‚       β”‚   β”œβ”€β”€ PipelineVisualizer.tsx      # SVG datapath visualizer used by Tab 3
β”‚       β”‚   └── ui/                         # shadcn/ui primitives (button, card, input, ...)
β”‚       β”œβ”€β”€ hooks/              # Custom React Hooks interfacing with Rust WebAssembly
β”‚       β”‚   β”œβ”€β”€ usePipeline.ts              # Connects Tab 3 to DotProductSim
β”‚       β”‚   β”œβ”€β”€ useMatrixMultiply.ts        # Connects Tab 4 to SystolicArray2DSim
β”‚       β”‚   └── useTiledMatmul.ts           # Connects Tab 5 to SystolicArray2DSim (one pass per tile)
β”‚       └── lib/
β”‚           β”œβ”€β”€ utils.ts                    # Tailwind class-merge helper
β”‚           └── tones.ts                    # Shared color palettes for the grid visualizers
β”œβ”€β”€ docs/                       # πŸ“š Standalone MkDocs site (theory + architecture notes)
└── mkdocs.yml                  # MkDocs config β€” see the note under "Documentation" below

πŸ’» 1. Interactive Frontend (Next.js / React)

The user touchpoint is a modern, responsive web application located in ./web/. It uses Next.js 16 (App Router), React 19, TypeScript, and Tailwind CSS v4 with shadcn/ui components, turning hardware concepts into intuitive, interactive visual schematics.

Users can step through simulations cycle-by-cycle, or auto-play them at a fixed pace (1 s per cycle in the 1D tab, 1.5 s in the 2D tab, 0.7 s in the tiled tab). The whole app is a single client-rendered page; the five views are tabs, each deep-linkable via a URL hash:

Tab URL hash
Instructions #instructions (also #overview, or no hash)
Processing Element #pe
1D Dot Product #dot-product (also #dotproduct)
2D Matrix Multiply #matmul (also #matrix-multiply)
Tiled Matrix Multiply #tiled (also #tiled-matmul)

Progressive Learning Journey (The Tabs)

πŸ”¬ Tab 1: Instructions & Overview

  • Concept: Provides an educational overview and visual guide on how to interact with the simulators.
  • Content: Hand-authored React/JSX cards explaining what a systolic array is, what each simulator tab teaches, and the color-coding conventions used throughout the app, plus a "Start" button into the PE simulator.
  • Educational Takeaway: Serves as a foundational landing page, ensuring students understand the underlying hardware definitions and color-coding schemes before running the simulators.

πŸ“¦ Tab 2: Processing Element (PE) Simulator

  • Concept: Models the atomic Multiply-Accumulate (MAC) cell of a systolic array.
  • Inputs: Allows entering an internal stationary weight register and comma-separated lists of stream values for $X$ (Activations) and $Y$ (Accumulating partial sums).
  • Visual Representation: Renders the PE as a central box highlighting its internal stationary register, input arrows, and physical boundary registers that latch and delay outputs.
  • Educational Takeaway: Demonstrates why a value entering on cycle $N$ only affects downstream cells on cycle $N+1$ due to synchronous latching/delay.

πŸ“Š Tab 3: 1D Dot Product Simulator

  • Concept: Connects PEs into a single vertical column to calculate a vector dot product ($\vec{x} \cdot \vec{w}$).
  • Inputs: Customizable vector size $N$, batch size $M$ (how many input vectors stream through back-to-back), the weight vector, and the input matrix.
  • Visual Representation: Renders a vertical line of linked PEs, showing data elements actively shifting and computing step-by-step.
  • Educational Takeaway: Illustrates pipeline latencyβ€”the first dot-product result takes $N$ cycles to emerge from the bottom, but subsequent results cascade out sequentially every clock cycle thereafter.

🧱 Tab 4: 2D Matrix Multiply Simulator

  • Concept: Computes Weight-Stationary Matrix Multiplication ($C = A \times B$) using a 2D grid of PEs, with $B$ held stationary in the PE weight registers.
  • Inputs: Square size selector ($N \times N$, adjustable from $2 \times 2$ up to $8 \times 8$) and multi-matrix sequential batching (simultaneously feed $A_1, A_2, A_3$ through the array).
  • Visual Representation: Highlights the skewed input wavefront (where rows of Matrix A are delayed diagonally by $0, 1, 2, \dots$ cycles) so they intersect the correct stationary weight at the exact cycle their accumulating $Y$ sum arrives from the PE above.
  • Educational Takeaway: Clarifies spatial and temporal concurrency. Students see multiple independent multiplications and accumulations happening in parallel across different physical components in real-time.

🧩 Tab 5: Tiled Matrix Multiply Simulator

The most advanced tab, and the one that models a real accelerator end to end. It follows the TPU model: a fixed weight-stationary array, a weight FIFO, and an accumulator β€” deliberately not the CPU/BLIS cache-blocking model, because a systolic array's whole argument is that it gets its reuse spatially (as data propagates across PEs) rather than temporally (through a cache hierarchy).

  • Concept: Runs a matrix multiply that is larger than the hardware. The physical array is fixed (1Γ—1 up to 4Γ—4 PEs) while $B$ is chopped into tiles that are loaded into the array one at a time.
  • Inputs: $M$, $K$ and $N$ are set independently ($M$ up to 16, $K$/$N$ up to 8), plus the physical array size and a weight-FIFO toggle. $M$ is the important knob β€” it controls how many rows stream through one resident weight tile.
  • Loop order: N outer, K middle, M streamed innermost. This is the Goto/BLIS order: $B$ is read once, $A$ is re-read once per N-tile, and each $C$ cell is written once per K-tile.
  • Visual Representation:
    • A tile map showing $A$, $B$ and $C$ with tile boundaries drawn and the current tile window highlighted.
    • The shared PE grid running the current tile pass, with zero-padded PEs dimmed on edge tiles, and β€” during a weight load β€” the tile visibly shifting in one row per cycle from the top.
    • A weight FIFO panel staging the next tile while the current one computes.
    • A pass timeline: one bar per cycle, height = active MACs, colored by phase (weight load β†’ pipeline fill β†’ steady state β†’ pipeline drain).
    • A live accumulator for $C$: cells are dashed while partial and solid once every K-tile has landed.
  • Educational Takeaway: Every tile pass pays two fixed taxes regardless of how much work it does β€” shifting the weights in (one cycle per array row) and filling then draining the pipeline. The only way to make them cheap is to amortize them over more work. Raise $M$ and the steady-state plateau widens as utilization climbs (on a 2Γ—2 array with $K=N=4$: 36% at $M=2$, 63% at $M=6$, 82% at $M=16$). That is the whole argument for large batch sizes on an accelerator. Toggling the weight FIFO off exposes every load as a stall, which is why real hardware double-buffers them.
  • Note: This tab required no changes to the Rust core. It orchestrates repeated passes of the same SystolicArray2DSim from TypeScript, and models the weight-load timing in the scheduler β€” which is exactly how a fixed-size array is driven in real life.

πŸ¦€ 2. Hardware Simulation Core (Rust)

To guarantee true hardware fidelity, the underlying computation engine is written in Rust (src/) and acts as a cycle-accurate hardware model.

Strict Cycle-Accurate Components

  • ProcessingElement (src/processing_element.rs): A physical struct containing actual storage registers:
    • weight: Loaded beforehand (weight-stationary configuration register).
    • reg_x_out: Register holding activation $x$ passed to the right neighbor on the next tick.
    • reg_y_out: Register holding the accumulated $y$ value passed to the bottom neighbor on the next tick.
  • DotProduct1D (src/dot_product.rs): A 1D array of ProcessingElements.
  • SystolicArray2D (src/systolic_array.rs): A 2D grid of ProcessingElements.
  • PipelinedAdder (src/pipeline_add.rs): An auxiliary educational module representing a 2-stage pipelined integer adder. It is not exposed to the frontend.

Both array types are generic over their storage S, so the same logic backs a fixed-size stack array (new_static(), no allocation) and a runtime-sized Vec (new_dynamic(), used by the WASM bindings).

Simulating Simultaneous Clock Edges in Sequential Code

In actual hardware, all registers update simultaneously on a single clock edge. In sequential software, a naive loop would overwrite a register prematurely, causing a cascade of bad values within a single tick.

The 2D array solves this by iterating backward (from bottom-right to top-left). This allows reading neighbors' register values from the previous clock cycle before they are updated by the current tick:

// Iterating backwards simulates concurrent clock edge propagation
for r in (0..rows).rev() {
    for c in (0..cols).rev() {
        // Read input X from left PE's previous cycle register
        let x_in = if c == 0 { left_ins[r] } else { pes[r * cols + (c - 1)].reg_x_out() };

        // Read input Y from top PE's previous cycle register
        let y_in = if r == 0 { top_ins[c]  } else { pes[(r - 1) * cols + c].reg_y_out() };

        // Compute combinatorial logic & latch outputs in registers
        let idx = r * cols + c;
        pes[idx].tick(x_in, y_in);
    }
}

The 1D array achieves the same effect differently: it walks forward from top to bottom, but snapshots each PE's reg_y_out before ticking it, then feeds that saved previous-cycle value into the PE below. Both approaches guarantee that every PE in a tick observes only pre-clock-edge state.


πŸ”Œ 3. The WebAssembly Bridge (How They Connect)

The boundary between the user's browser (React) and the cycle-accurate compiled core (Rust) is crossed cleanly using WebAssembly and wasm-bindgen bindings.

       [ Next.js UI ] <--- Real-time Redraw of State --- [ React State ]
             |                                                 ^
             | (User ticks or Auto-Plays)                      | (get_state)
             v                                                 |
[ Custom React Hooks (usePipeline / useMatrixMultiply) ] ------+
             |
             v (WebAssembly Interface)
   [ Rust WASM Simulators (DotProductSim / SystolicArray2DSim) ]
  1. WASM Binding Compilation (src/wasm_bindings.rs): Exposes stateful Rust objects DotProductSim and SystolicArray2DSim directly to JavaScript.
  2. State Management via Hooks (web/src/hooks/): React hooks (usePipeline.ts, useMatrixMultiply.ts and useTiledMatmul.ts) instantiate the simulators and maintain UI synchronicity.
  3. The Cycle Loop:
    • When a user steps forward or toggles Auto-Play, the hook invokes the simulator's .tick() method.
    • The simulator executes one hardware-level cycle.
    • The hook immediately pulls a flat representation of all internal PE registers via .get_state() β€” a flat [W, X, Y, W, X, Y, ...] array in row-major PE order.
    • This updates the React state, prompting Tailwind/HTML elements to render updated numbers, flow arrows, and color highlights instantly on screen.

The generated pkg/ directory is wired into the frontend as a local file dependency ("rust-hw-playground": "file:../pkg" in web/package.json), so the WASM package must be built before the web app.


πŸš€ Quick Start: Running CLI Simulators

If you prefer to run simulations directly from the terminal, the Rust core includes interactive command-line interface simulators:

1. Run the 1D Pipelined Dot Product Simulator:

cargo run --bin cli_dot_product

2. Run the 2D Systolic Array Simulator:

cargo run --bin cli_systolic_array

πŸ› οΈ Developer Setup: Compiling and Running the Web App

Follow these steps to build the WebAssembly module and run the Next.js visual dashboard locally:

πŸ“¦ Prerequisites

  1. Install Rust and Cargo.
  2. Install wasm-pack for compiling Rust to WebAssembly:
    cargo install wasm-pack
  3. Install Node.js (v18+ recommended; CI uses v20).

1️⃣ Compile the Rust Core to WebAssembly (WASM)

In the root directory of the project, run:

wasm-pack build --target web

This compiles the Rust crate and outputs a web-ready package inside the ./pkg/ directory, complete with generated JavaScript/TypeScript bindings. Re-run this after any change to src/ β€” the frontend consumes the built artifact, not the Rust source.


2️⃣ Run the Next.js Frontend

Navigate to the web/ directory, install dependencies, and start the development server:

cd web
npm install
npm run dev

Open http://localhost:3000 in your browser to interact with the simulator dashboard.

Building for Production

To generate a static build of the website and serve it:

npm run build
npm run serve

next.config.ts sets output: 'export', so npm run build emits a fully static site into web/out/. It also enables webpack's asyncWebAssembly experiment, which is what allows the .wasm module to be imported.


🌐 Deployment

Every push to main triggers .github/workflows/nextjs.yml, which:

  1. Installs Rust + wasm-pack and runs wasm-pack build --target web.
  2. Installs the web/ dependencies and runs next build --webpack.
  3. Uploads web/out/ and deploys it to GitHub Pages.

The site is served at https://systolic-array-simulator.georgesung.com via the custom domain in web/public/CNAME.

Note that the workflow does not run cargo test β€” run the tests locally before pushing.


πŸ§ͺ Testing & Verification

The Rust hardware logic is tested to ensure correct behavior and compliance with theoretical models:

# Run all native unit tests (PE, 1D dot product, 2D systolic array, pipelined adder)
cargo test

# Run a specific educational pipeline-add test with verbose output
cargo test test_pipeline_add_educational -- --nocapture

The tests in src/wasm_bindings.rs are #[wasm_bindgen_test]s and are skipped by cargo test. Run them against Node with:

wasm-pack test --node

Highlights of the suite:

  • processing_element.rs β€” a fixed hand-verified case plus a 1000-iteration randomized MAC check.
  • systolic_array.rs β€” 2Γ—2 and 3Γ—3 hand-traced walkthroughs, plus a randomized test that compares the array's skewed-wavefront output against a naive triple-loop matmul for every size from 2Γ—2 to 10Γ—10.

πŸ“š Documentation

docs/ holds a standalone MkDocs site (theory of operation and architecture notes), configured by mkdocs.yml with the Material theme and MathJax:

pip install mkdocs-material   # or activate the local .venv
mkdocs serve

⚠️ These pages were last updated in June 2026, before the 2D matrix-multiply and single-PE tabs were built, so they describe only the 1D pipeline. They are also not built or published by CI. This README is the current source of truth for architecture.


πŸ“„ License

MIT β€” see LICENSE.

About

Interactive systolic array simulator: Rust simulator, web front-end (via WASM)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages