Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Basys 3 Dataflow & Energy-Efficiency Study

A self-contained, board-portable hardware project that takes the core dataflow idea of a large research accelerator and reduces it to the smallest honest thing that still behaves the same way, so it can run on a cheap FPGA board and be measured with a real power meter. The goal is not to reproduce every feature of the original design. The goal is to isolate and study one question, on real hardware, cleanly:

How does data move through a weight-stationary, output-stationary, time-multiplexed MAC array, and how much energy does that movement cost per useful operation?

This README explains the whole project in plain language — what it is, why it is built the way it is, how each part works, how to run it in simulation, how to build it for the board, and how to turn the board into a reproducible energy-measurement platform. For a signal-by-signal, cycle-accurate walkthrough of every SystemVerilog file, read the companion document ARCHITECTURE.md; this README is the readable tour, that one is the deep reference.


1. Where this comes from and why it exists

This design is a deliberately simplified distillation of the dense compute core of a much larger accelerator originally built for a Xilinx UltraScale+ part. The parent design is a full system: a macro-instruction dispatcher, a circuit-switched network-on-chip, on-chip weight staging with ping-pong buffers, a compute-in-memory digital twin, block-floating-point arithmetic, adaptive weight precision, drift refresh, and a sparse ternary matrix engine. All of that is powerful, and all of it is out of scope here.

The reason for stripping it down is focus. Those features, valuable as they are, make it hard to see the one thing this project cares about: the cost of moving data. A modern accelerator spends a large fraction of its energy not on arithmetic but on shuttling weights and activations into and out of the compute fabric, and on the bubbles between compute bursts where the array sits idle waiting for its next operands. To study that honestly you want the simplest possible design that still has the same movement structure as the big one, running on a board cheap enough to instrument with a meter. That is the Digilent Basys 3 (Artix-7 XC7A35T), and that is this project.

What was kept from the parent is exactly the set of ideas that define the movement behavior: weight-stationary processing elements, activation broadcast into a physical grid, keeping partial sums local to the grid, reusing a small physical kernel over a larger logical problem by time-multiplexing, an output-stationary accumulator, and a lean streaming style with no heavyweight bus protocol. What was removed is everything specific to the parent's research novelties. In their place, the arithmetic is a plain signed 8-bit integer multiply-accumulate. This is a general dataflow accelerator, not a specialized one, and that generality is the point: the conclusions transfer.

The one thing this project adds that the parent never needed is measurement machinery: a way to generate a dense, repeatable workload entirely on-chip with no host feeding it, a knob to dial the compute rate up and down without touching the clock, a set of counters that record exactly how the array spent every cycle, and a serial link to stream those counters to a host that is simultaneously watching a power meter.


2. What the design actually computes

At its heart the engine computes a matrix–vector product. You give it a reduction depth, it produces a result vector of sixteen numbers, and each of those numbers is the sum of many multiply-accumulate operations over a shared set of inputs. What makes it interesting is not the math itself — it is a dot product — but the fact that the reduction is tiled in three nested ways, and each level of tiling corresponds to a different kind of data movement.

The innermost level is the beat. Each beat presents eight activations at once, one to each row of the physical array, and every processing element in a row sees the same activation. This is the broadcast: a single activation value fans out to eight multipliers simultaneously, which is where activation reuse comes from. A run streams some number of beats — call it K — and each processing element accumulates its own running sum across those K beats inside its own register. That accumulation across beats is the temporal reduction.

The next level is the row. Within one beat, the eight activations feed eight rows of the array, and after the K beats have all streamed through, the eight partial sums in a column have to be added together to form one output. That addition across rows is the spatial reduction, and it happens with a small combinational adder tree. Crucially, both the temporal and the spatial reductions finish inside the physical array. Only the finished column results ever leave it. This locality is a deliberate architectural property inherited from the parent, and it is what keeps the interconnect simple.

The outermost level is the tile. The physical array is only eight rows by eight columns, but the logical problem is larger. To cover it, the same physical array is reused several times in sequence, once per logical tile. There are four tiles in the default configuration, arranged as a two-by-two grid. Two of them extend the reduction dimension (their results are added together), and two of them produce different output columns (their results sit side by side in the final vector). Every time the array moves from one tile to the next, it must reload its stationary weights, and during that reload no useful arithmetic happens. That reload is the movement tax at the center of this study.

The full computation, then, is: for each of the sixteen output columns, sum the products of the right activations and weights across all beats, all rows, and both row-tiles. That triple sum is precisely what the testbenches check the hardware against, value for value.


3. Why the tiling structure is the whole story

If you built a physical array large enough to hold the entire logical problem at once, you would spend a great deal of silicon and you would still have to deliver operands to it. Instead, this design builds one small array and reuses it in time. That trade — area for time — is the essence of a time-multiplexed accelerator, and it comes with a very specific cost: every time you switch the array to a new tile, you must stream a fresh set of weights into it, and the array idles for those cycles.

For the default geometry, loading the weights of one tile takes eight cycles, during which zero multiply-accumulate operations occur. If the reduction depth K is small, those eight idle cycles are a large fraction of the tile's total time, and the array's utilization is poor. If K is large, the eight idle cycles are amortized over hundreds of productive cycles, and utilization approaches its ceiling. This single relationship — utilization rising as K grows — is the fundamental efficiency curve of the whole architecture, and it is the same curve that governs the much larger parent design. Reproducing it on a cheap board, and being able to measure it in both cycles and joules, is the reason this project exists.

Everything else in the design serves this measurement. The on-chip operand generator exists so the array can be kept genuinely busy without a host. The performance counters exist so you can see exactly how many cycles went to real compute versus weight reloads versus pipeline drain. The throttle knob exists so you can vary the compute rate while holding everything else fixed, which is what makes the energy-per-operation extractable from a coarse total-board power reading. And the serial telemetry exists so a host can pair a rate with a wattage.


4. How the hardware is organized

The design is a clean hierarchy of eight SystemVerilog modules. Reading from the compute core outward:

The processing element is the atom of computation. It holds one weight in a register that does not change while activations stream past it — this is what "weight-stationary" means — and it performs a pipelined multiply-and-accumulate, keeping its running sum in an internal accumulator that is intended to map to a single hardware DSP block. When the surrounding logic asks, it snapshots that accumulator into a stable output. Sixty-four of these, one per DSP, form the array.

The group arranges those sixty-four processing elements into an eight-by-eight grid, wires the activation broadcast so each row shares an input, handles loading a whole row of weights at a time, and contains the combinational adder tree that performs the spatial reduction across rows. It enforces the locality rule: partial sums never escape; only the eight finished column outputs leave.

The array is the brain. It is a single module that plays four roles at once. First, it is the sequencer — a state machine that walks through the tiles in order, and for each tile loads the weights, streams the beats, drains the pipeline, snapshots the result, and folds it into the accumulator. Second, it is the accumulator bank — a small register file that holds the sixteen output values as they are built up tile by tile, loading a strip fresh on the first row-tile and accumulating onto it for later ones. Third, it is the operand generator — instead of reading weights and activations from memory, it computes them on the fly from a deterministic hash of their address, which is what lets the board run forever with no host and lets the testbench reproduce every value exactly. Fourth, it is the counter block — seven free-running counters that record how many multiply- accumulates were issued, how many cycles were busy, how many beats streamed, how many cycles went to weight loading and to pipeline stalls, how many weight bytes moved, and how many complete passes finished.

Around this compute core sit the board-facing modules. The board top connects the engine to the physical pins: the clock, the switches, the buttons, the LEDs, the seven-segment display, and the serial line. It synthesizes a clean reset, debounces the buttons, maps the switches to the measurement knobs, and implements a free-running mode that keeps the engine issuing passes back-to-back with no host involvement. A telemetry module periodically snapshots the seven counters and frames them into a fixed binary record, which a small UART transmitter serializes out the serial pin. A seven-segment driver shows a live readout of any chosen counter so you can confirm the board is working at a glance, without a host attached at all.

The full stack, from pins down to the DSP, is: board top contains the array and the telemetry and the display; the array contains the group; the group contains the sixty-four processing elements. The complete signal-level description of each of these, including pipeline timing and state tables, is in ARCHITECTURE.md.


5. The self-generating, self-checking workload

A subtle but important design decision is that the design stores no operands anywhere. There is no weight memory, no activation buffer, no block RAM holding data. Instead, every weight and every activation is a pure mathematical function of its address, computed by a small hash the moment it is needed. This has three consequences that matter a great deal.

First, it makes the board completely autonomous. Program the bitstream, flip a switch, and the array runs a dense, high-activity workload indefinitely with nothing plugged in but power. That is exactly what you want for a steady-state power measurement: a stable, repeatable load you can leave running while you read a meter.

Second, it makes the whole design self-checking. Because the hardware and the testbench compute their operands with the same hash function, the testbench can predict the exact result the hardware should produce — down to the last bit — with no external reference vectors. Every level of the design, from a single processing element up to the full engine, is verified against this golden prediction.

Third, it keeps the arithmetic units cleanly attributable in the power measurement. The hash uses only shifts and exclusive-ORs, never a multiply, so it consumes none of the FPGA's dedicated multiplier blocks. That means every one of those blocks in the finished design belongs to the MAC array and nothing else, so when the power meter moves, it is the compute fabric moving, not some incidental operand-generation logic.


6. The measurement knobs on the board

The board's switches and buttons are laid out as an instrument panel for the energy study.

The most important control is the throttle, mapped to the low switches. It inserts idle cycles between activation beats, which dilutes the rate at which multiply-accumulates are issued without changing the clock frequency. This is the key to a clean energy measurement: by sweeping the throttle you trace how power changes as compute rate changes, while everything else — clock tree, I/O, static leakage — stays fixed. The slope of that line is the energy per operation, and the intercept is the fixed overhead you are not charged for per operation.

A free-run switch tells the engine to re-issue a pass automatically the moment it finishes the last one, so the board sustains a continuous load. A telemetry-enable switch turns on the periodic serial reporting of the counters. Two small groups of switches select which counter, and which slice of it, appears on the seven-segment display. The buttons provide reset, a single manual pass, a counter clear that marks the start of a timed measurement window, and a force-a-report-now trigger. The LEDs show, at a glance, whether the engine is busy, whether free-run and telemetry are on, a heartbeat blink, and a live count of completed passes. The full control map is documented in measure/POWER_METER_GUIDE.md.


7. Repository layout

basys3_dataflow/
├── ARCHITECTURE.md              deep, signal-level reference for every module
├── README.md                   this document
├── constraints/
│   └── basys3_dataflow.xdc      Basys 3 pin + timing constraints (XC7A35T)
├── rtl/
│   ├── df_pkg.sv               widths, geometry, operand hash, address builders
│   ├── df_mac_pe.sv            one weight-stationary fused MAC (one DSP)
│   ├── df_group.sv             8x8 PE grid + spatial column reduction
│   ├── df_array.sv             sequencer + accumulator bank + operands + counters
│   ├── uart_tx.sv              8N1 serial transmitter
│   ├── df_telemetry.sv         frames the 7 counters into a serial record
│   ├── seg7_hex.sv             4-digit hex display driver
│   └── df_basys3_top.sv        board wrapper: pins, controls, free-run, LEDs
├── tb/
│   ├── tb_df_mac_pe.sv         PE correctness
│   ├── tb_df_group.sv          group / column-reduction correctness
│   ├── tb_df_array.sv          full engine vs golden, throttled and full-rate
│   ├── tb_df_dataflow.sv       efficiency sweep -> reports/dataflow_sweep.csv
│   ├── tb_df_basys3_top.sv     board-top smoke test
│   └── tb_df_top_activity.sv   non-terminating activity harness for power runs
├── scripts/
│   ├── add_sources_basys3.tcl  create/open the Basys 3 Vivado project
│   ├── run_tb_df_*.tcl         per-testbench simulation launchers
│   ├── report_dataflow.tcl     run the efficiency sweep bench
│   ├── build_basys3.tcl        synth -> impl -> bitstream + gating reports
│   ├── gen_activity_saif.tcl   dump switching activity for power estimation
│   ├── report_power_saif.tcl   annotate the routed design with that activity
│   └── power_sweep.tcl         estimated power vs throttle curve
└── measure/
    ├── POWER_METER_GUIDE.md    the physical measurement procedure
    ├── parse_uart_perf.py      read the serial telemetry, print MAC rate
    └── compute_efficiency.py   fit power vs rate -> energy per MAC, GOPS/W

8. Verifying the design in simulation

The project follows a simulation-first discipline: nothing is synthesized until the behavior is proven correct in simulation. The testbenches are meant to be run bottom-up, so that a failure is localized to the smallest possible module. First the processing element, then the group, then the full engine, then the board top. Each testbench builds its own expected results from the same operand hash the hardware uses, and prints a clear pass or fail message.

From the Vivado Tcl console, run the launchers in order:

source .../basys3_dataflow/scripts/run_tb_df_mac_pe.tcl
source .../basys3_dataflow/scripts/run_tb_df_group.tcl
source .../basys3_dataflow/scripts/run_tb_df_array.tcl
source .../basys3_dataflow/scripts/run_tb_df_basys3_top.tcl

The engine-level testbench is the most important one: it runs several passes at different reduction depths, both at full rate and with the throttle engaged, and confirms that the sixteen outputs match the golden triple-sum in every case. It also proves that engaging the throttle changes only the timing of a pass and never its numeric result, which is what lets you use the throttle freely during power sweeps without worrying about correctness.

A separate characterization bench sweeps the reduction depth and the throttle across many operating points, measures the real cycle counts the hardware produces, derives the efficiency metrics for each point, and writes them to a CSV. Running it produces the cycle-level half of the efficiency story — the throughput and utilization numbers that you later pair with measured watts:

source .../basys3_dataflow/scripts/report_dataflow.tcl

9. Building the bitstream for the board

Once simulation is clean, the build scripts create a Basys 3 project targeting the Artix-7 part, add all the sources and the pin constraints, and run synthesis and implementation through to a bitstream. Along the way they generate the gating reports — resource utilization, timing, design-rule checks, methodology, and a first-pass power estimate — that tell you whether the design closed cleanly.

source .../basys3_dataflow/scripts/add_sources_basys3.tcl
source .../basys3_dataflow/scripts/build_basys3.tcl

The default geometry uses sixty-four of the part's ninety DSP blocks and a modest amount of logic, so it fits comfortably with headroom. The pin assignments follow the standard Digilent Basys 3 master constraints; verify them against your specific board revision before programming hardware.


10. Measuring energy on real hardware

This is the part the whole project is built around, and it is described in full in the power-meter guide. The short version is that a stock Basys 3 only lets you measure total board power — the FPGA plus its regulators, oscillator, and I/O — and not the compute fabric in isolation. Two techniques recover the number you actually want anyway.

The first and simplest is the idle-versus-active difference. Measure the board's power with the engine idle, then again with it running at full rate, and take the difference. That difference is the dynamic power the dataflow engine adds, and dividing the measured compute rate by it gives an energy efficiency figure. It is quick but slightly overstates the engine's cost, because the difference still includes some fixed clock-tree activity.

The second and better technique is the throttle sweep with a straight-line fit. Total board power behaves like a fixed baseline plus an amount proportional to the compute rate. Because the throttle changes the compute rate while holding the clock frequency constant, you can measure the board power at several throttle settings, plot power against the measured rate, and fit a line. The slope of that line is the dynamic energy per multiply- accumulate — the honest efficiency figure — and the intercept is the fixed overhead you are not charged for per operation. This method is robust: it cancels the meter's absolute offset and all the fixed board power you could not otherwise separate out, so it is the number to trust and to report.

To carry it out, program the board, put a meter on the five-volt supply, enable free-run and telemetry, and step through the throttle settings. On the host, one script reads the serial telemetry and reports the live compute rate, and a second script takes the table of throttle settings, measured watts, and measured rates and performs the fit, printing the energy per operation, the fixed overhead, and the efficiency in operations per watt. The Vivado side of the flow provides an estimated version of the same curve — a switching-activity-based power estimate at each throttle setting — as a cross-check, but the meter is always the number of record.


11. What the counters tell you

The seven performance counters turn every efficiency figure into a simple ratio, computed from how the counts change over a measurement window. The ratio of multiply-accumulates issued to the theoretical peak over the same cycles gives utilization — the fraction of the array's capacity you actually used. The rate of multiply-accumulates over wall-clock time gives throughput. The ratio of multiply-accumulates to weight bytes moved gives weight reuse, which equals the reduction depth and tells you directly how well the weight-reload cost amortized. The ratio of multiply-accumulates to activation bytes gives activation reuse, which equals the array width and reflects the broadcast efficiency.

The two most diagnostic counters are the weight-load cycles and the stall cycles. Their share of the busy cycles tells you exactly where the movement overhead went — how much time the array spent reloading weights, and how much it spent draining its pipeline between compute bursts. Those two fractions are what explain the utilization number at any given reduction depth, and watching them shrink as the depth grows is watching the amortization happen in real numbers. When you combine throughput with measured watts, you get operations per watt; when you combine it with the energy-per-operation from the linear fit, you get the clean dynamic efficiency that is the headline result.


12. Honest limitations and where to take it next

This is a study platform, not a product, and its simplifications are deliberate. The arithmetic is plain integer rather than the parent's block-floating-point, which is fine for a movement-and-energy study but is not what you would ship for a neural network that needs wide dynamic range. The accumulator width bounds how deep a reduction can run before it would risk overflowing, though that bound is generous. The activation stream is regenerated for each output-column tile rather than being held in a buffer and truly reused, which is an intentional choice that shows up honestly in the reuse metric. The accumulator bank is a small register file, which is the right choice at sixteen outputs but would need to become a block-RAM structure if the design were extended to produce many output vectors at once. And the board pin assignments should always be checked against your specific board before programming.

The most valuable extension, and the one that would most change the efficiency story, is the same one that matters in the parent design: amortizing the weight reload over more than one activation vector by adding output batching, so the eight idle reload cycles are paid once for many results instead of once per result. Beyond that, one could widen the array to a second physical group to use more of the available multipliers, add a real clock generator to sweep frequency as a second energy knob, or replace the hash-based operand source with a small loader so genuine matrices can be streamed in. Each of these is a natural next step from the current structure, and each is discussed in the extensions section of ARCHITECTURE.md.


13. A note on discipline and status

The project inherits a strict engineering discipline from its parent: a single clock domain, synchronous reset, no inferred latches, no magic numbers outside the central package, simulation before synthesis, and formal-style assertions checking every handshake and timing contract in simulation. The intent is that the design closes with clean design-rule and methodology reports, and that the first thing anyone does with it is run the testbenches to a pass before trusting any hardware result.

The Vivado power numbers are estimates; the physical meter is the ground truth, and you should resist the temptation to pre-fill an expected efficiency figure — it is precisely what the measurement is for. The RTL, testbenches, build scripts, and host-side measurement tools are all written and internally consistent, and the acceptance gate is the first clean pass of the simulation flow described above. Read ARCHITECTURE.md when you want to know exactly what any signal does on any cycle; read measure/POWER_METER_GUIDE.md when you are standing at the bench with a meter in your hand.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages