# Project export: Diffusion Accelerator

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Modern AI accelerator hardware is not well optimized for diffusion models. This protoype address that issue.
- Devpost: https://devpost.com/software/diffusion-accelerator
- GitHub: https://github.com/danelzhan/diffusion_accelerator
- Video: https://www.youtube.com/embed/5uIZajG5ZFo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — danelzhan (4 commits)

## Devpost submission (written by the team)

### Inspiration

and

### What it does

Stable diffusion on GPUs can be slow because a lot of time gets wasted moving data around before the actual compute even starts. My project is based on the paper SD-Acc: Accelerating Stable Diffusion through Phase-aware Sampling and Hardware Co-Optimizations, which proposes a cleaner hardware approach for this bottleneck. I built a RTL prototype of that idea: instead of filling a huge im2col buffer for convolution, I generate SRAM addresses on the fly and feed the data straight into a shared systolic array. The goal is less buffer copying, less switching overhead between conv and attention layers, and way fewer wasted cycles. How I built it I wrote the whole thing in SystemVerilog from scratch. The core is a 4x4 INT8 systolic array, plus an address-centric datapath that decides which SRAM locations to read depending on whether its running convolution or matrix multiplication. For conv layers, the address generator replaces the im2col buffer by calculating the sliding-window reads directly. For matmul/attention layers, the same systolic array is reused with a different control path. So the hardware doesnt need seperate compute blocks for each layer type. I verified the design with Verilator and compared all 256 output values against a golden model. Then I built a visualizer that replays the simulation trace cycle by cycle, so the speedup comes from the actual RTL behavior, not just a estimate. Challenges I ran into The systolic array timing was annoying. I had a off-by-one bug in the drain phase that made the outputs look almost right, but not fully correct. Comparing every output against the golden model was what finally caught it. Cognichip also helped a lot with debugging from the testbench results. When the waveform was hard to read, it helped me narrow down where the RTL behavior stopped matching the expected output, which made the debugging way faster. Accomplishments that I'm proud of The RTL actually works. I got 256/256 outputs correct against the C++ model, verified through Verilator.

### What's next

Right now the prototype runs on a 8x8 feature map, which proves the idea but is still small. The next step is adding tiling so bigger Stable Diffusion layers can fit through the SRAM and systolic array. I also want to scale the array to 16x16 or 32x32 and implement the phase-aware sampling part from SD-Acc, where redundant diffusion steps can be skipped. That would stack with the hardware speedup.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 732 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (31 of 31)

```
command_results/output_tooluse_9j63ZtWSiXrDgKLQYgmy0a.txt
command_results/output_tooluse_9W4BQEurUTNp2Rr7mxwGxl.txt
command_results/output_tooluse_cr5Bna2by7ufstR0A444kh.txt
command_results/output_tooluse_cYf2SisOWYRpDhfHAPOdq5.txt
conv_addr_gen.sv
conv_controller.sv
DEPS.yml
generate_trace.py
matmul_controller.sv
pe.sv
PERFORMANCE_STATS.md
sim_visualizer.html
simulation_results/sim_2026-06-21T09-12-15-882Z/dumpfile.fst
simulation_results/sim_2026-06-21T09-12-15-882Z/eda_results.json
simulation_results/sim_2026-06-21T09-45-32-461Z/dumpfile.fst
simulation_results/sim_2026-06-21T09-45-32-461Z/eda_results.json
simulation_results/sim_2026-06-21T09-46-58-248Z/dumpfile.fst
simulation_results/sim_2026-06-21T09-46-58-248Z/eda_results.json
simulation_results/sim_2026-06-21T15-49-32-493Z/dumpfile.fst
simulation_results/sim_2026-06-21T15-49-32-493Z/eda_results.json
simulation_results/sim_2026-06-21T15-54-08-805Z/dumpfile.fst
simulation_results/sim_2026-06-21T15-54-08-805Z/eda_results.json
sram_model.sv
summary.tex
systolic_array_4x4.sv
tb_conv_addr_gen.sv
tb_conv2d.sv
tb_matmul.sv
tb_top_accel.sv
top_accel.sv
trace_data.js
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- visulaizer
- top level unit for the entire accelerator and its testbench
- combinational block for computing sram addresses for convolution operations plus its logic controller and testbenches
- sram model and the controler for matrix multiplication path, plus verification test bench
- initial PE module and array using them for start of matrix multiplication data path

## Key source files (fetched from GitHub, selected and truncated for size)

### PERFORMANCE_STATS.md

```markdown
# SD-Acc: Address-Centric Accelerator — Performance Statistics

> All simulation numbers are measured from actual RTL simulation.  
> Stable Diffusion projections use the same formulas, scaled to real layer sizes.

---

## What Does "Faster" Actually Mean Here?

Modern neural network accelerators are **memory-bandwidth-bound**, not compute-bound.
The systolic array can multiply numbers very fast — the bottleneck is feeding it data.

The traditional approach (im2col) solves this by pre-copying input data into a
temporary buffer shaped for matrix multiplication.  This prototype eliminates that
buffer entirely, computing addresses on-the-fly instead.

Fewer bytes moved = less time waiting for memory = faster inference.

---

## 1. Simulation-Measured Results  (8×8×4 conv layer, our prototype)

```
Layer: 8×8 feature map, 4 input channels, 4 output channels, 3×3 kernel
─────────────────────────────────────────────────────────────────────────
                          IM2COL (baseline)    ADDRESS-CENTRIC (ours)
─────────────────────────────────────────────────────────────────────────
Intermediate buffer         2,304 bytes            0 bytes       ◄ KEY
Input SRAM reads            2,304                  1,936
Weight SRAM reads           9,216                    288
─────────────────────────────────────────────────────────────────────────
TOTAL SRAM reads           11,520                  2,224
─────────────────────────────────────────────────────────────────────────
Reduction in total reads                             5.2×
Weight read reduction                               32.0×
Intermediate buffer saved                        2,304 bytes (100%)
─────────────────────────────────────────────────────────────────────────
Output values verified         256 / 256 ✓   (zero mismatches)
```

**Why weight reads drop 32×:**  
Im2col re-reads weights once per output pixel (64 pixels × 36 steps = 2,304 weight reads
per output channel, × 4 channels = 9,216).  Our design preloads weights into local
registers once per output-channel tile (288 reads total) and reuses them across all
64 pixels — a classic weight-stationary optimization made practical by the fixed-stride
SRAM layout.

**Why input reads drop ~16%:**  
368 of 2,304 kernel-pixel queries land on zero-padding.  Im2col writes explicit zeros
into the buffer anyway.  Our conv_addr_gen detects out-of-bounds positions and injects
zeros without touching the SRAM at all — saving those 368 reads entirely.

---

## 2. Projected to a Real Stable Diffusion U-Net Layer

Typical ResBlock conv in SD 1.x (512×512 image, first downsampling block):

```
Layer: 64×64 feature map, C_in=320, C_out=320, 3×3 kernel, stride=1, pad=1
──────────────────────────────────────────────────────────────────────────────
                               IM2COL              ADDRESS-CENTRIC
──────────────────────────────────────────────────────────────────────────────
Intermediate buffer (INT8)   11.3 MB                  0 bytes
Intermediate 
[truncated — 4562 more characters]
```

### DEPS.yml

```yaml
bench_conv_addr_gen:
  deps:
    - conv_addr_gen.sv
    - tb_conv_addr_gen.sv
  top: tb_conv_addr_gen

bench_matmul:
  deps:
    - pe.sv
    - systolic_array_4x4.sv
    - sram_model.sv
    - matmul_controller.sv
    - tb_matmul.sv
  top: tb_matmul

bench_conv2d:
  deps:
    - pe.sv
    - systolic_array_4x4.sv
    - sram_model.sv
    - conv_addr_gen.sv
    - conv_controller.sv
    - tb_conv2d.sv
  top: tb_conv2d

bench_top_accel:
  deps:
    - pe.sv
    - systolic_array_4x4.sv
    - sram_model.sv
    - conv_addr_gen.sv
    - matmul_controller.sv
    - conv_controller.sv
    - top_accel.sv
    - tb_top_accel.sv
  top: tb_top_accel

```

### generate_trace.py

```python
"""
generate_trace.py — Extract cycle-by-cycle events from the conv_controller FSM.

Implements the exact same FSM as conv_controller.sv (verified against
Verilator simulation: 3297 cycles, 1936 input reads, 288 weight reads,
256/256 outputs correct).

Also models the im2col + GEMM baseline for comparison.

Outputs trace_data.js — a JavaScript file containing both event streams
embedded as constants, ready to load in the visualizer.
"""

import json, math

# ── Layer parameters (matching our simulation) ────────────────────────────────
H_IN = W_IN = H_OUT = W_OUT = 8
C_IN = C_OUT = 4
STRIDE = PADDING = 1
K_STEPS = C_IN * 9          # 36
K_STEPS_MAX = 72             # fixed-stride weight SRAM layout
TOTAL_PIXELS = H_OUT * W_OUT # 64

# ── FSM state constants (matching conv_controller.sv) ─────────────────────────
IDLE, PRELOAD, CLEAR, FEED, DRAIN, WRITE, DONE = 0, 1, 2, 3, 4, 5, 6
STATE_NAMES = ['IDLE','PRELOAD','CLEAR','FEED','DRAIN','WRITE','DONE']


def is_valid(oh, ow, ic, kr, kc):
    """Mirror conv_addr_gen.sv bounds check."""
    ih = oh * STRIDE + kr - PADDING
    iw = ow * STRIDE + kc - PADDING
    return 0 <= ih < H_IN and 0 <= iw < W_IN


def simulate_accel():
    """
    Exact cycle-by-cycle simulation of conv_controller.sv FSM.
    Returns list of event dicts, one per cycle.
    """
    events = []
    state = IDLE

    pre_j = pre_k = 0
    oh = ow = oc_base = 0
    step = ic = kr = kc = 0
    drain = write_cnt = 0

    # Run until DONE (plus one extra cycle to record DONE state)
    for _ in range(20000):
        ev = {
            'cycle':      len(events),
            'state':      STATE_NAMES[state],
            'oh': oh, 'ow': ow, 'oc': oc_base,
            'step':       step,
            'input_re':   0,
            'weight_re':  0,
            'output_we':  0,
            'sa_valid':   0,
            'addr_valid': 0,
            'padding':    0,
        }

        # ── State outputs ──────────────────────────────────────────────
        if state == PRELOAD:
            ev['weight_re'] = 1

        elif state == CLEAR:
            ev['sa_valid'] = 0   # sa_clear=1 this cycle
            # Present addr for step 0  (ic=0, kr=0, kc=0)
            v = is_valid(oh, ow, 0, 0, 0)
            ev['addr_valid'] = int(v)
            ev['input_re'] = int(v)
            ev['padding'] = int(not v)

        elif state == FEED:
            ev['sa_valid'] = 1
            # The data being FED this cycle came from the previous cycle's SRAM read.
            # addr_valid here tracks what is currently being fed to the array
            # (for PE animation) — that's the CURRENT step's validity.
            cur_valid = is_valid(oh, ow, ic, kr, kc)
            ev['addr_valid'] = int(cur_valid)
            ev['padding']    = int(not cur_valid)

            # input_re: is the SRAM being read THIS cycle?
            # The RTL presents the NEXT step's address (ag_query_valid = step < K-1).
            if step < K_STEPS - 1:
                # Compute next (ic, kr, kc) without mutating state
                n_kc = kc + 1
                n_kr, n_ic = kr, ic
                if n_kc > 2:
                    n_kc = 0; n_kr = kr + 1
                    if n_kr > 2:
                        n_kr = 0; n_ic = ic + 1
                next_valid = is_valid(oh, ow, n_ic, n_kr, n_kc)
                ev['input_re'] = int(next_valid)
            else:
                ev['input_re'] = 0   # last step: ag_query_valid=0, no SRAM read

        elif state == DRAIN:
            pass  # sa_valid=0, nothing

        elif state == WRITE:
            ev['output_we'] = 1

        # ── State transitions ──────────────────────────────────────────
        if state == IDLE:
            state = PRELOAD

        elif state == PRELOAD:
            if pre_k == K_STEPS_MAX - 1:
                pre_k = 0
                if pre_j == 3:
                    pre_j = 0
                    state = CLEAR
                    step = ic = kr = kc = 0
                else:
                    pre_j += 1
            else:
                pre_k += 1

        elif state == CLEAR:
            state = FEED
            step = ic = kr = kc = 0

        elif state == FEED:
            if step == K_STEPS - 1:
                state = DRAIN
                drain = 0
            else:
                step += 1
                # Advance (ic, kr, kc) counters
                kc += 1
                if kc > 2:
                    kc = 0; kr += 1
                    if kr > 2:
                        kr = 0; ic += 1

        elif state == DRAIN:
            drain += 1
            if drain == 6:
                state = WRITE
                write_cnt = 0

        elif state == WRITE:
            write_cnt += 1
            if write_cnt == 4:
                write_cnt = 0
                # Advance pixel / oc_tile
                if ow < W_OUT - 1:
                    ow += 1; state = CLEAR
                elif oh < H_OUT - 1:
                    ow = 0; oh += 1; state = CLEAR
                elif oc_base + 4 < C_OUT:
                    ow = oh = 0; oc_base += 4
                    pre_j = pre_k = 0; state = PRELOAD
                else:
                    state = DONE
                if state == CLEAR:
                    step = ic = kr = kc = 0

        elif state == DONE:
            events.append(ev)
            break

        events.append(ev)

    return events


def simulate_im2col():
    """
    Cycle-by-cycle model of im2col + output-stationary GEMM baseline.
    Phases:
      FILL  (2304 cycles): write each of H_out*W_out*C_in*9 activations to buffer
      GEMM  (9216 cycles): for each of H_out*W_out*C_out output values,
                           accumulate over K=36 weight+buffer reads
      WRITE  (256 cycles): write results to output SRAM
    """
    events = []

    FILL_CYCLES  = H_OUT * W_OUT * K_STEPS          # 2304
    GEMM_CYCLES  = H_OUT * W_OUT * C_OUT * K_STEPS  # 9216
    WRITE_CYCLES = H_OUT * W_OUT * C_OUT             # 256

    total =
[truncated — 6584 more characters]
```

### sim_visualizer.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SD-Acc vs GPU — RTL Simulation Replay</title>
<script src="trace_data.js"></script>
<style>
:root{--bg:#080c18;--panel:#0d1426;--border:#1e2d50;--gpu:#ff5733;--acc:#00cfff;--good:#00ff88;--warn:#ffcc00;--dim:#3a4a70;--text:#dde6ff;--sub:#7a8ab0}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:'Courier New',monospace;padding:16px;min-height:100vh}
h1{text-align:center;font-size:1.4rem;letter-spacing:.1em;color:#fff;margin-bottom:4px}
.subtitle{text-align:center;font-size:.72rem;color:var(--sub);margin-bottom:12px}
.badge{display:inline-block;background:#0f2040;border:1px solid var(--acc);border-radius:4px;padding:2px 8px;font-size:.65rem;color:var(--acc);margin-left:8px}
/* Tabs */
.tabs{display:flex;gap:8px;justify-content:center;margin-bottom:16px}
.tab-btn{background:var(--panel);border:1px solid var(--border);color:var(--sub);padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:.82rem;transition:all .2s}
.tab-btn:hover{border-color:var(--acc);color:var(--text)}
.tab-btn.active{border-color:var(--acc);color:var(--acc);background:#0a1830}
.tab-section{display:none}
.tab-section.active{display:block}
/* Controls */
.controls{display:flex;align-items:center;gap:12px;justify-content:center;margin-bottom:16px;flex-wrap:wrap}
button{background:var(--panel);border:1px solid var(--border);color:var(--text);padding:6px 16px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:.85rem;transition:border-color .2s}
button:hover{border-color:var(--acc)}
button.play-btn{border-color:var(--acc);color:var(--acc)}
.speed-wrap{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--sub)}
input[type=range]{accent-color:var(--acc);width:100px}
.cyc-disp{font-size:.82rem;color:var(--warn);min-width:160px;text-align:center}
/* Race */
.race{display:grid;grid-template-columns:80px 1fr 120px;gap:6px;align-items:center;margin-bottom:18px}
.race-label{font-size:.72rem;text-align:right;padding-right:8px}
.race-label.gpu{color:var(--gpu)}
.race-label.acc{color:var(--acc)}
.track{background:#0d1020;border-radius:3px;height:22px;position:relative;overflow:hidden;border:1px solid var(--border)}
.fill{height:100%;border-radius:3px;transition:width .05s linear;position:relative}
.fill.gpu{background:linear-gradient(90deg,#7a1a08,var(--gpu))}
.fill.acc{background:linear-gradient(90deg,#004466,var(--acc))}
.fill-label{position:absolute;right:6px;top:50%;transform:translateY(-50%);font-size:.68rem;white-space:nowrap;text-shadow:0 0 6px #000}
.race-stat{font-size:.72rem;text-align:center}
.done-badge{display:inline-block;background:#001a0d;border:1px solid var(--good);color:var(--good);border-radius:3px;padding:1px 7px;font-size:.68rem}
/* Panels */
.panels{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
@media(max-width:700px){.panels{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:14px}
.panel-title{font-size:.9rem;font-weight:bold;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.panel-title.gpu{color:var(--gpu)}
.panel-title.acc{color:var(--acc)}
.sec{font-size:.65rem;color:var(--sub);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px;margin-top:10px}
.canvas-wrap{position:relative;margin-bottom:6px}
canvas{display:block;border-radius:3px;border:1px solid var(--border)}
.buf-meter{background:#0d1020;border:1px solid #3a2000;border-radius:4px;height:36px;position:relative;overflow:hidden;margin-bottom:4px}
.buf-fill{height:100%;background:linear-gradient(90deg,#7a2800,var(--gpu));transition:width .05s;border-radius:4px}
.buf-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.72rem;color:#fff;text-shadow:0 0 8px #000}
.no-buf{background:#00150a;border:1px solid var(--good);border-radius:4px;height:36px;display:flex;align-items:center;justify-content:center;font-size:.78rem;color:var(--good);margin-bottom:4px}
.phase-box{display:inline-block;padding:3px 10px;border-radius:3px;font-size:.75rem;font-weight:bold;border:1px solid;min-width:90px;text-align:center}
.phase-IDLE{color:var(--dim);border-color:var(--dim)}
.phase-PRELOAD,.phase-FILL{color:var(--warn);border-color:var(--warn)}
.phase-CLEAR{color:#80c0ff;border-color:#80c0ff}
.phase-FEED,.phase-GEMM{color:var(--acc);border-color:var(--acc)}
.phase-DRAIN{color:#8888ff;border-color:#8888ff}
.phase-WRITE,.phase-OUTPUT{color:var(--good);border-color:var(--good)}
.phase-DONE{color:var(--good);border-color:var(--good)}
.phase-STALL{color:#ff4422;border-color:#ff4422}
.phase-SWITCH{color:var(--good);border-color:var(--good)}
.stat-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:8px}
.stat-card{background:#0a0e1c;border:1px solid var(--border);border-radius:4px;padding:6px 8px}
.stat-val{font-size:1.1rem;font-weight:bold}
.stat-lbl{font-size:.62rem;color:var(--sub);margin-top:1px}
.stat-val.gpu{color:var(--gpu)}
.stat-val.acc{color:var(--acc)}
.stat-val.good{color:var(--good)}
.insights{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}
@media(max-width:900px){.insights{grid-template-columns:repeat(2,1fr)}}
.insight{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}
.insight-val{font-size:1.5rem;font-weight:bold;color:var(--acc)}
.insight-lbl{font-size:.65rem;color:var(--sub);margin-top:3px}
/* Multi-layer timeline */
.tl-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.tl-label{font-size:.72rem;width:80px;text-align:right;flex-shrink:0;padding-right:6px}
.tl-canvas-wrap{flex:1;position:relative}
.tl-stat{font-size:.7rem;min-width:130px;text-align:center}
.stall-badge{display:inline-block;background:#2a0000;border:1px solid #ff3300;border-radius:3px;padding:1px 8p
[truncated — 27463 more characters]
```