From 6fe86d030bfbdcbbe841dd2b909163a8b893c88d Mon Sep 17 00:00:00 2001 From: Dineth Hettiarachchi Date: Mon, 26 Oct 2026 12:40:15 +0530 Subject: [PATCH 1/2] feat: Phase 10 FPGA: UART wrapper, ECP5 synthesis, Yosys-compatible RTL - fpga/ecp5: fpga_top (the engine behind an 8N1 UART: CONFIG, LOAD, START, READ) and uart.sv. Simulated over the UART end to end (sim/run_fpga.sh): O bit-exact, DONE cycles and bytes equal the models. - synth.sh: Yosys synth_ecp5 with a latch assertion, then nextpnr-ecp5 (LFE5U-85F) for utilization and fmax. Result: 40,416 of 83,640 LUT4, 8,726 FF, 51 of 156 DSP, 0 BRAM; fmax estimate 11.5 MHz (target 25 MHz, not closed). - fpga.yml runs the flow in CI and writes the numbers to the job summary. - RTL changes for Yosys (no behaviour change; unit, e2e 200 seeds and golden tests rerun): packed LUT tables (generator updated), file-scope package imports, local copies of the LUTs before indexing, explicit sign extension in exp2_unit, and a split async reset in perf_counters. - Sim runners set CFG_CXXFLAGS_PCH_I=-include (the installed Verilator leaves it empty; see ARCHITECTURE section 15). - WASM engine rebuilt from the new RTL; Node test passes. - Docs: ARCHITECTURE section 15, README FPGA row, verification page, PLAN and CONTEXT. Not done: ULX3S pin constraints, a board run, timing closure at 25 MHz, block RAM for the memories. --- .github/workflows/fpga.yml | 47 ++++++ .gitignore | 1 + CONTEXT.md | 14 +- README.md | 2 +- docs/ARCHITECTURE.md | 46 ++++++ docs/PLAN.md | 2 +- fpga/ecp5/fpga_top.sv | 208 ++++++++++++++++++++++++++ fpga/ecp5/synth.sh | 50 +++++++ fpga/ecp5/uart.sv | 149 ++++++++++++++++++ rtl/attn_tile.sv | 3 +- rtl/exp2_unit.sv | 11 +- rtl/flash_ctrl.sv | 3 +- rtl/lut_pkg.sv | 84 +++++------ rtl/naive_ctrl.sv | 3 +- rtl/normalize.sv | 3 +- rtl/o_accumulator.sv | 3 +- rtl/online_softmax.sv | 3 +- rtl/perf_counters.sv | 10 +- rtl/recip_unit.sv | 9 +- rtl/scale_mask.sv | 3 +- rtl/tiny_attn_top.sv | 3 +- sim/golden/gen_luts.py | 13 +- sim/run_e2e.sh | 3 + sim/run_fpga.sh | 22 +++ sim/run_unit.sh | 3 + sim/test_fpga_uart.py | 137 +++++++++++++++++ web/public/tiny_attn.wasm | Bin 215648 -> 216175 bytes web/src/pages/docs/architecture.astro | 4 +- web/src/pages/docs/verification.astro | 10 +- 29 files changed, 777 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/fpga.yml create mode 100644 fpga/ecp5/fpga_top.sv create mode 100755 fpga/ecp5/synth.sh create mode 100644 fpga/ecp5/uart.sv create mode 100755 sim/run_fpga.sh create mode 100644 sim/test_fpga_uart.py diff --git a/.github/workflows/fpga.yml b/.github/workflows/fpga.yml new file mode 100644 index 0000000..6d4fa6e --- /dev/null +++ b/.github/workflows/fpga.yml @@ -0,0 +1,47 @@ +name: FPGA + +on: + push: + branches: [main] + paths: ['rtl/**', 'fpga/**', '.github/workflows/fpga.yml'] + pull_request: + paths: ['rtl/**', 'fpga/**', '.github/workflows/fpga.yml'] + workflow_dispatch: + +jobs: + ecp5: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install FPGA tools (YoWASP Yosys and nextpnr, ECP5) + run: bash .tools/fpga.sh + + # Synthesis fails on any inferred latch or failed check; nextpnr reports utilization and fmax. + - name: Synthesize and place-and-route fpga_top for ECP5 (LFE5U-85F) + run: bash fpga/ecp5/synth.sh + + - name: Write the job summary + if: always() + run: | + { + echo "## ECP5 (LFE5U-85F, CABGA381) synthesis" + echo "" + echo '```' + cat fpga/ecp5/build/stat.txt 2>/dev/null | grep -E "cells|LUT4|TRELLIS|MULT18|PFUMX|CCU2C|L6MUX" || echo "no statistics" + echo '```' + echo "" + echo "Utilization and fmax (nextpnr, no pin constraints, so fmax is an estimate):" + echo '```' + python3 -c "import json; r=json.load(open('fpga/ecp5/build/report.json')); print('fmax', r.get('fmax')); print('utilization', {k:v for k,v in r.get('utilization',{}).items() if k in ('TRELLIS_COMB','TRELLIS_FF','MULT18X18D','DP16KD')})" 2>/dev/null || echo "no report" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Keep the netlist out of artifacts + if: always() + run: rm -f fpga/ecp5/build/fpga_top.json diff --git a/.gitignore b/.gitignore index 664ef94..18812b1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ web/public/spike/ # Playwright output web/test-results/ web/playwright-report/ +fpga/ecp5/build/ diff --git a/CONTEXT.md b/CONTEXT.md index 86dcc10..98b2243 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -41,8 +41,18 @@ On a fresh machine delete the symlinks and run `.tools/setup.sh` and `.tools/ems - Phase 9 (docs, landing page, launch): built. Landing page with the animated FLASH schedule, six docs pages with live demos, `/og` social card, generated `measured.ts`, README rewritten for the series. Deployment: see the PR and ARCHITECTURE §14. -- Next: Phase 10 (FPGA synthesis, ECP5 for the ULX3S). Blocked here: no yosys / nextpnr / ecppack installed, and the disk - had under 1 GB free during Phase 9. +- Phase 10 (FPGA, ECP5 LFE5U-85F): partly done. UART wrapper (`fpga/ecp5/`), simulated over the UART end to end, synthesis + with no latches, utilization and an fmax estimate (11.5 MHz; not closed at 25 MHz). Not done: pin constraints for the + ULX3S, a board run, timing closure, block RAM for the memories (combinational reads map to LUT RAM). +- Next: the Phase 10 follow-ups above, or Phase 9's deployment (blocked: the production deploy was denied by the auto-mode classifier). + +## Gotchas found in Phase 10 +- Yosys rejects: unpacked array localparams, package imports in a module header, part-selects of package parameters, + width casts with expression widths, and compound async resets. All are fixed in rtl/ (see ARCHITECTURE §15). +- YoWASP tools only read and write the working directory: outputs go in `fpga/ecp5/build`, inside the project. +- The installed Verilator leaves `CFG_CXXFLAGS_PCH_I` empty, so a freshly generated model fails to link. `sim/run_fpga.sh` + passes `-include` through MAKEFLAGS. Other suites did not hit this. +- The wrapper's netlist is ~48 MB as JSON. `fpga.yml` deletes it after the run. ## Gotchas found in Phase 8 - The debug datapath wires are idle between tiles. The L2 view keeps the last busy clock's values, or the panel shows zeros. diff --git a/README.md b/README.md index 426510f..7a1ef6b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ optional overlap of the K/V loads with compute. | HBM traffic at N = 32 | Non-causal: FLASH 2432 B (4-row blocks) or 896 B (16-row blocks), NAIVE 12672 B. Causal: FLASH 1536 B or 768 B, NAIVE 7296 B. | | WebAssembly | The compiled module, on golden fixtures for each schedule, matches the same quantities. 83 KB gzipped. | | Web app | Browser tests of every page, the visualizer at phone width and in both colour schemes. | -| FPGA | Not yet synthesized (Phase 10). No timing or area numbers are claimed. | +| FPGA (ECP5 LFE5U-85F) | Synthesizes with no latches: 40,416 of 83,640 LUT4 positions, 8,726 flip-flops, 51 of 156 DSP blocks, no block RAM. fmax estimate 11.5 MHz, so timing is not closed at the ULX3S 25 MHz clock. The UART interface is verified in simulation; it has not run on a board. | Every row is a test in the repository, run by GitHub Actions on each pull request. The docs page [Verification](https://tiny-attention.vercel.app/docs/verification) lists each check and its numbers. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8b90b88..4e7dd83 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -443,3 +443,49 @@ Checks: `pnpm test:e2e` (12 tests, including all four views at 375 px and the li demos respond) and every page at 375 px without horizontal scroll. Tables scroll inside their own box on phones. - **Not in this phase:** FPGA numbers (Phase 10). The README and the Verification page say so. +## 15. Phase 10: FPGA synthesis (ECP5, ULX3S) + +**Target.** Lattice ECP5 LFE5U-85F (CABGA381, speed grade 6), the chip on the ULX3S board. Tools: Yosys (`synth_ecp5`) +and nextpnr-ecp5, both from YoWASP wheels in `.tools/fpga-venv` (`bash .tools/fpga.sh`). Run with +`bash fpga/ecp5/synth.sh`; `.github/workflows/fpga.yml` runs the same and writes the numbers to the job summary. + +**The wrapper.** `fpga/ecp5/fpga_top.sv` puts the engine behind an 8N1 UART (`fpga/ecp5/uart.sv`). A host sends +CONFIG, LOAD (one HBM word), START and READ commands and receives DONE (cycles, HBM bytes read and written) and WORD +replies; the exact byte layout is in the file header. The debug ports are left unconnected, so synthesis removes the +logic they alone needed. + +**Verified in simulation.** `bash sim/run_fpga.sh` drives the wrapper over its UART at 4 clocks per bit and checks, for +FLASH (causal and not) and NAIVE: the O words read back equal the golden model, and the DONE reply's cycle and byte +counts equal the closed-form models. It has not run on a board. + +**Synthesis (`fpga_top`, no pin constraints):** + +| Resource | Used | Available | Share | +|---|---|---|---| +| LUT4 positions (TRELLIS_COMB) | 40,416 | 83,640 | 48% | +| Flip-flops (TRELLIS_FF) | 8,726 | 83,640 | 10% | +| DSP (MULT18X18D) | 51 | 156 | 33% | +| Block RAM (DP16KD) | 0 | 208 | 0% | + +Yosys reports no inferred latches, and `check -assert` passes. fmax from nextpnr is **11.5 MHz**, against the board's +25 MHz clock, so **timing is not closed at 25 MHz**. The estimate has no pin constraints, so it is an estimate. Two +things would have to change before the engine runs at the board clock: the critical path (not yet analysed) and the +memories (below). + +**Memories are LUT RAM, not block RAM.** The HBM model, the K/V buffers and the staging buffer read combinationally, which +Yosys maps to distributed RAM (2,080 TRELLIS_DPR16X4). Block RAM needs a registered read, which adds a cycle to every DMA +word and changes the cycle model in `sim/golden/schedule.py` and the pinned baselines. That is a deliberate follow-up, +not a fix made here. + +**Changes made for Yosys.** These do not change behaviour; the unit and end-to-end suites were rerun on them: +- The LUT tables in `rtl/lut_pkg.sv` are packed vectors (the generator emits them), not unpacked arrays. Yosys does not + parse unpacked array localparams. +- Package imports are at file scope, not in the module header, with a Verilator `IMPORTSTAR` suppression saying why. +- Each LUT is copied into a local parameter before indexing: Yosys does not resolve a part-select of a package parameter. +- `exp2_unit` negates the shift with an explicit sign extension, not a width cast with an expression width. +- `perf_counters` resets with `!rst_n` in the asynchronous branch and `clr` in the synchronous one. A compound reset + does not infer a flip-flop in Yosys. + +**Not done.** The ULX3S pin constraints (the board is not here, so I did not write them from memory). Running the toy +sentence on the board. A 25 MHz timing closure. Block RAM for the memories. + diff --git a/docs/PLAN.md b/docs/PLAN.md index 411af74..ef4857a 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -463,7 +463,7 @@ The estimates assume one person part-time, at tiny-cpu's pace. - Deploy to `tiny-attention.vercel.app`. - **Exit:** the public URL is live; all CI is green. -### Phase 10: FPGA synthesis (½–1 week, can run alongside Phases 7–9) +### Phase 10: FPGA synthesis (½–1 week, can run alongside Phases 7–9) — PARTIAL (synthesis and simulated UART done; see ARCHITECTURE §15) - `fpga/ecp5` for the ULX3S: 32 INT8 MACs map onto DSP blocks, and the LUTs and SRAM go to BRAM. Host load and readback use UART. - `fpga.yml` posts LUT/FF/DSP/BRAM and fmax to the job summary; L4 uses fmax for "real time" estimates. - **Exit:** synthesis is clean with no latches; the numbers are in the README. With a board, the toy sentence runs over UART. diff --git a/fpga/ecp5/fpga_top.sv b/fpga/ecp5/fpga_top.sv new file mode 100644 index 0000000..6d52e87 --- /dev/null +++ b/fpga/ecp5/fpga_top.sv @@ -0,0 +1,208 @@ +// FPGA top: the TinyAttention engine behind a UART command interface (8N1, the host sets the baud rate). +// +// Commands (host -> device). Multi-byte fields are little-endian. +// 0x01 CONFIG nb, gg, flags, latency, scale[3] (7 bytes) flags: bit0 causal, bit1 naive, bit2 overlap +// 0x02 LOAD addr[2], word[4] (6 bytes) writes one HBM word; only while idle +// 0x03 START (0 bytes) runs the engine; the reply comes when it is done +// 0x04 READ addr[2] (2 bytes) reads one HBM word; reply is 4 bytes +// Replies (device -> host): +// 0x83 DONE cycles[4], hbm_rd_bytes[4], hbm_wr_bytes[4] (13 bytes including the code) +// 0x84 WORD word[4] (5 bytes including the code) +// Unknown command codes are ignored. Cycles, bytes and tiles follow tiny_attn_top's counters. +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header +import tiny_attn_pkg::*; + +module fpga_top #( + parameter int CLKS_PER_BIT = 217 // 25 MHz / 115200 baud +) ( + input logic clk, + input logic rst_n, + input logic uart_rx, + output logic uart_tx +); + + localparam logic [7:0] CMD_CONFIG = 8'h01; + localparam logic [7:0] CMD_LOAD = 8'h02; + localparam logic [7:0] CMD_START = 8'h03; + localparam logic [7:0] CMD_READ = 8'h04; + localparam logic [7:0] REP_DONE = 8'h83; + localparam logic [7:0] REP_WORD = 8'h84; + + // ---- UART + logic rx_valid; + logic [7:0] rx_byte; + uart_rx #(.CLKS_PER_BIT(CLKS_PER_BIT)) u_rx (.clk(clk), .rst_n(rst_n), .rx(uart_rx), .valid(rx_valid), .data(rx_byte)); + + logic tx_start, tx_busy; + logic [7:0] tx_byte; + uart_tx #(.CLKS_PER_BIT(CLKS_PER_BIT)) u_tx (.clk(clk), .rst_n(rst_n), .start(tx_start), .data(tx_byte), .busy(tx_busy), .tx(uart_tx)); + + // ---- command decoder + typedef enum logic [2:0] {S_CMD, S_PAY, S_RUN, S_REPLY} fstate_t; + fstate_t st; + logic [7:0] cmd; + logic [2:0] pay_len; // payload bytes expected + logic [2:0] pay_cnt; + logic [7:0] pbuf [7]; + logic [7:0] rbuf [13]; // reply buffer + logic [3:0] rlen; // reply length + logic [3:0] ridx; // next reply byte to send + + // ---- engine + logic [3:0] cfg_nb, cfg_gg; + logic cfg_causal, cfg_naive, cfg_overlap; + logic [6:0] cfg_latency; + logic [SCALE_W-1:0] cfg_scale; + logic eng_start, eng_busy, eng_done; + logic ld_en; + logic [HBM_AW-1:0] ld_addr, o_rd_addr; + logic [31:0] ld_word, o_rd_word; + logic [31:0] hbm_rd_bytes, hbm_wr_bytes, cycles, tiles, kv_loads; + logic settle; // counters are final one edge after done + + tiny_attn_top u_top ( + .clk(clk), .rst_n(rst_n), + .cfg_nb(cfg_nb), .cfg_gg(cfg_gg), .cfg_causal(cfg_causal), .cfg_naive(cfg_naive), + .cfg_overlap(cfg_overlap), .cfg_hbm_latency(cfg_latency), .cfg_scale(cfg_scale), + .start(eng_start), .busy(eng_busy), .done(eng_done), + .ld_en(ld_en), .ld_addr(ld_addr), .ld_word(ld_word), + .o_rd_addr(o_rd_addr), .o_rd_word(o_rd_word), + .hbm_rd_bytes(hbm_rd_bytes), .hbm_wr_bytes(hbm_wr_bytes), + .cycles(cycles), .tiles(tiles), .kv_loads(kv_loads), + .dbg_ctrl_state(), .dbg_tile_busy(), .dbg_cur_grp(), .dbg_srow(), + .dbg_s_tile(), .dbg_z_tile(), .dbg_p_tile(), .dbg_alpha(), .dbg_m(), .dbg_l(), .dbg_o(), + .dbg_mm1_w(), .dbg_mm1_a(), .dbg_mm1_psum(), .dbg_mm2_w(), .dbg_mm2_a(), .dbg_mm2_psum(), + .dbg_kbuf(), .dbg_vbuf(), .dbg_kv_sel(), .dbg_tile_done() + ); + + wire [HBM_AW-1:0] pay_addr = HBM_AW'({pbuf[1], pbuf[0]}); + wire [31:0] pay_word = {pbuf[5], pbuf[4], pbuf[3], pbuf[2]}; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + st <= S_CMD; + cmd <= '0; + pay_len <= '0; + pay_cnt <= '0; + rlen <= '0; + ridx <= '0; + cfg_nb <= 4'd1; + cfg_gg <= 4'd1; + cfg_causal <= 1'b0; + cfg_naive <= 1'b0; + cfg_overlap <= 1'b0; + cfg_latency <= '0; + cfg_scale <= '0; + eng_start <= 1'b0; + ld_en <= 1'b0; + ld_addr <= '0; + ld_word <= '0; + o_rd_addr <= '0; + settle <= 1'b0; + tx_start <= 1'b0; + tx_byte <= '0; + end else begin + eng_start <= 1'b0; + ld_en <= 1'b0; + tx_start <= 1'b0; + + // reply transmitter: send rbuf one byte at a time + if (ridx != rlen && !tx_busy && !tx_start) begin + tx_byte <= rbuf[ridx]; + tx_start <= 1'b1; + ridx <= ridx + 4'd1; + end + + case (st) + S_CMD: begin + if (rx_valid) begin + cmd <= rx_byte; + case (rx_byte) + CMD_CONFIG: begin pay_len <= 3'd7; pay_cnt <= '0; st <= S_PAY; end + CMD_LOAD: begin pay_len <= 3'd6; pay_cnt <= '0; st <= S_PAY; end + CMD_READ: begin pay_len <= 3'd2; pay_cnt <= '0; st <= S_PAY; end + CMD_START: begin + eng_start <= 1'b1; + st <= S_RUN; + end + default: st <= S_CMD; + endcase + end + end + + S_PAY: begin + if (rx_valid) begin + pbuf[pay_cnt] <= rx_byte; + pay_cnt <= pay_cnt + 3'd1; + if (pay_cnt == pay_len - 3'd1) begin + case (cmd) + CMD_CONFIG: begin + // pbuf: nb, gg, flags, latency, scale[3] (the last byte is in rx_byte) + cfg_nb <= pbuf[0][3:0]; + cfg_gg <= pbuf[1][3:0]; + cfg_causal <= pbuf[2][0]; + cfg_naive <= pbuf[2][1]; + cfg_overlap <= pbuf[2][2]; + cfg_latency <= pbuf[3][6:0]; + cfg_scale <= SCALE_W'({rx_byte, pbuf[5], pbuf[4]}); + st <= S_CMD; + end + CMD_LOAD: begin + ld_en <= 1'b1; + ld_addr <= pay_addr; + ld_word <= {rx_byte, pbuf[4], pbuf[3], pbuf[2]}; + st <= S_CMD; + end + CMD_READ: begin + o_rd_addr <= HBM_AW'({rx_byte, pbuf[0]}); + st <= S_REPLY; + // the READ reply is built one cycle later, once o_rd_word has settled + settle <= 1'b1; + end + default: st <= S_CMD; + endcase + end + end + end + + S_RUN: begin + if (eng_done) begin + settle <= 1'b1; + end + if (settle) begin + settle <= 1'b0; + rbuf[0] <= REP_DONE; + rbuf[1] <= cycles[7:0]; rbuf[2] <= cycles[15:8]; + rbuf[3] <= cycles[23:16]; rbuf[4] <= cycles[31:24]; + rbuf[5] <= hbm_rd_bytes[7:0]; rbuf[6] <= hbm_rd_bytes[15:8]; + rbuf[7] <= hbm_rd_bytes[23:16]; rbuf[8] <= hbm_rd_bytes[31:24]; + rbuf[9] <= hbm_wr_bytes[7:0]; rbuf[10] <= hbm_wr_bytes[15:8]; + rbuf[11] <= hbm_wr_bytes[23:16]; rbuf[12] <= hbm_wr_bytes[31:24]; + rlen <= 4'd13; + ridx <= '0; + st <= S_REPLY; + end + end + + S_REPLY: begin + if (cmd == CMD_READ && settle) begin + settle <= 1'b0; + rbuf[0] <= REP_WORD; + rbuf[1] <= o_rd_word[7:0]; + rbuf[2] <= o_rd_word[15:8]; + rbuf[3] <= o_rd_word[23:16]; + rbuf[4] <= o_rd_word[31:24]; + rlen <= 4'd5; + ridx <= '0; + end + // back to command mode once the reply has been sent + if (ridx == rlen && !tx_busy && !tx_start && !settle) st <= S_CMD; + end + + default: st <= S_CMD; + endcase + end + end + + // START is a one-cycle pulse; the engine is not busy when the host sends START. +endmodule diff --git a/fpga/ecp5/synth.sh b/fpga/ecp5/synth.sh new file mode 100755 index 0000000..50e0227 --- /dev/null +++ b/fpga/ecp5/synth.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Phase 10: synthesize fpga_top for the ULX3S (Lattice ECP5 LFE5U-85F, CABGA381) and report utilization and fmax. +# bash .tools/fpga.sh && bash fpga/ecp5/synth.sh +# Outputs go to fpga/ecp5/build/ (git-ignored): the netlist, the synthesis statistics, and the nextpnr report. +# Pin constraints are not included (no board here), so IO placement is left to nextpnr and fmax is an estimate. +set -euo pipefail +cd "$(dirname "$0")/../.." +Y=.tools/fpga-venv/bin/yowasp-yosys +N=.tools/fpga-venv/bin/yowasp-nextpnr-ecp5 +B=fpga/ecp5/build +FREQ=${FREQ:-25} +mkdir -p "$B" + +SRCS="rtl/lut_pkg.sv rtl/tiny_attn_pkg.sv $(ls rtl/*.sv | grep -v -e lut_pkg.sv -e tiny_attn_pkg.sv | tr '\n' ' ') fpga/ecp5/uart.sv fpga/ecp5/fpga_top.sv" + +# 1. synthesis. The latch check runs on the elaborated design, before technology mapping. +"$Y" -q -l "$B/yosys.log" -p "read_verilog -sv $SRCS; hierarchy -check -top fpga_top; proc; \ + select -assert-none t:\$dlatch t:\$dlatchsr t:\$_DLATCH_* ; \ + check -assert; \ + synth_ecp5 -top fpga_top -json $B/fpga_top.json; \ + tee -o $B/stat.txt stat" +echo "synthesis: ok (no latches, no unresolved checks)" + +# 2. place and route, with timing, against the ECP5 chip database +"$N" --85k --package CABGA381 --speed 6 --json "$B/fpga_top.json" --freq "$FREQ" \ + --report "$B/report.json" --timing-allow-fail -q > "$B/nextpnr.log" 2>&1 || true + +python3 - "$B" <<'PY' +import json, re, sys +b = sys.argv[1] +stat = open(f"{b}/stat.txt").read() +def count(name): + m = re.search(rf"^\s+(\d+)\s+{re.escape(name)}\b", stat, re.M) + return int(m.group(1)) if m else 0 +rep = json.load(open(f"{b}/report.json")) +fmax = None +for clk, v in rep.get("fmax", {}).items(): + fmax = (clk, v["achieved"], v["constraint"]) +util = rep.get("utilization", {}) +print("cells (synth): LUT4", count("LUT4"), " TRELLIS_FF", count("TRELLIS_FF"), + " MULT18X18D", count("MULT18X18D"), " TRELLIS_DPR16X4", count("TRELLIS_DPR16X4"), + " DP16KD", count("DP16KD"), " PFUMX", count("PFUMX"), " CCU2C", count("CCU2C")) +for k in ("TRELLIS_COMB", "TRELLIS_FF", "MULT18X18D", "DP16KD", "DCUA"): + if k in util: + print(f"nextpnr {k}: used {util[k]['used']} of {util[k]['available']}") +if fmax: + print(f"fmax: {fmax[1]:.1f} MHz achieved, constraint {fmax[2]:.1f} MHz (clock {fmax[0]})") +else: + print("fmax: not reported (see nextpnr.log)") +PY diff --git a/fpga/ecp5/uart.sv b/fpga/ecp5/uart.sv new file mode 100644 index 0000000..1218f26 --- /dev/null +++ b/fpga/ecp5/uart.sv @@ -0,0 +1,149 @@ +// 8N1 UART. CLKS_PER_BIT = clock / baud (217 for 25 MHz and 115200 baud; small values for simulation). +module uart_rx #( + parameter int CLKS_PER_BIT = 217 +) ( + input logic clk, + input logic rst_n, + input logic rx, + output logic valid, // one-cycle pulse: data is a received byte + output logic [7:0] data +); + typedef enum logic [1:0] {R_IDLE, R_START, R_DATA, R_STOP} rstate_t; + rstate_t st; + logic [1:0] sync; // input synchronizer + logic [15:0] cnt; + logic [2:0] bit_idx; + logic [7:0] shreg; + + wire rx_s = sync[1]; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + sync <= 2'b11; + st <= R_IDLE; + cnt <= '0; + bit_idx <= '0; + shreg <= '0; + valid <= 1'b0; + data <= '0; + end else begin + sync <= {sync[0], rx}; + valid <= 1'b0; + case (st) + R_IDLE: begin + if (!rx_s) begin + cnt <= '0; + st <= R_START; + end + end + R_START: begin // sample the middle of the start bit + if (cnt == 16'(CLKS_PER_BIT / 2)) begin + if (!rx_s) begin + cnt <= '0; + bit_idx <= '0; + st <= R_DATA; + end else begin + st <= R_IDLE; + end + end else begin + cnt <= cnt + 16'd1; + end + end + R_DATA: begin + if (cnt == 16'(CLKS_PER_BIT - 1)) begin + cnt <= '0; + shreg <= {rx_s, shreg[7:1]}; + if (bit_idx == 3'd7) st <= R_STOP; + bit_idx <= bit_idx + 3'd1; + end else begin + cnt <= cnt + 16'd1; + end + end + R_STOP: begin + if (cnt == 16'(CLKS_PER_BIT - 1)) begin + valid <= rx_s; + data <= shreg; + st <= R_IDLE; + end else begin + cnt <= cnt + 16'd1; + end + end + default: st <= R_IDLE; + endcase + end + end +endmodule + +module uart_tx #( + parameter int CLKS_PER_BIT = 217 +) ( + input logic clk, + input logic rst_n, + input logic start, // one-cycle pulse, only while not busy + input logic [7:0] data, + output logic busy, + output logic tx +); + typedef enum logic [1:0] {T_IDLE, T_START, T_DATA, T_STOP} tstate_t; + tstate_t st; + logic [15:0] cnt; + logic [2:0] bit_idx; + logic [7:0] shreg; + + assign busy = (st != T_IDLE); + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + st <= T_IDLE; + cnt <= '0; + bit_idx <= '0; + shreg <= '0; + tx <= 1'b1; + end else begin + case (st) + T_IDLE: begin + tx <= 1'b1; + if (start) begin + shreg <= data; + cnt <= '0; + tx <= 1'b0; + st <= T_START; + end + end + T_START: begin + if (cnt == 16'(CLKS_PER_BIT - 1)) begin + cnt <= '0; + bit_idx <= '0; + tx <= shreg[0]; + st <= T_DATA; + end else begin + cnt <= cnt + 16'd1; + end + end + T_DATA: begin + if (cnt == 16'(CLKS_PER_BIT - 1)) begin + cnt <= '0; + shreg <= {1'b0, shreg[7:1]}; + bit_idx <= bit_idx + 3'd1; + if (bit_idx == 3'd7) begin + tx <= 1'b1; + st <= T_STOP; + end else begin + tx <= shreg[1]; + end + end else begin + cnt <= cnt + 16'd1; + end + end + T_STOP: begin + if (cnt == 16'(CLKS_PER_BIT - 1)) begin + st <= T_IDLE; + end else begin + cnt <= cnt + 16'd1; + end + end + default: st <= T_IDLE; + endcase + end + end +endmodule diff --git a/rtl/attn_tile.sv b/rtl/attn_tile.sv index 7cff75b..bec0e51 100644 --- a/rtl/attn_tile.sv +++ b/rtl/attn_tile.sv @@ -1,3 +1,4 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // Tile core for one Q block of up to 32 rows (8 row groups of 4), driven by flash_ctrl or naive_ctrl. // // Operations (tile_op, see tiny_attn_pkg): @@ -13,8 +14,8 @@ // MM1 at c0+7..c0+10, P rows enter MM2 at c0+8..c0+11, PV rows leave at c0+15..c0+18, tile_done at c0+19. // tile_start to tile_done is 20 cycles (sim/test_attn_tile.py). /* verilator lint_off PINCONNECTEMPTY */ // mm_engine.busy is unused: tile_busy comes from the FSM +import tiny_attn_pkg::*; module attn_tile - import tiny_attn_pkg::*; ( input logic clk, input logic rst_n, diff --git a/rtl/exp2_unit.sv b/rtl/exp2_unit.sv index faa8baf..37efc9f 100644 --- a/rtl/exp2_unit.sv +++ b/rtl/exp2_unit.sv @@ -1,9 +1,10 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // 2^(x/256) for x <= 0 (Q9.8) -> UQ1.15. LUT256 on the fraction, right shift by the integer part, // truncating, exactly 0 once the shift reaches 16. Matches fixed.exp2_q (exp_round = False). // x > 0 is outside the contract; it saturates to 0xFFFF. +import tiny_attn_pkg::*; +import lut_pkg::*; module exp2_unit - import tiny_attn_pkg::*; - import lut_pkg::*; ( input logic signed [X_W-1:0] x, output logic [E_W-1:0] y @@ -12,12 +13,14 @@ module exp2_unit logic [7:0] f; logic signed [X_W-8:0] s; // -n, 0..256 (one bit wider than n) logic [E_W-1:0] lut; + // a local copy: Yosys does not resolve part-selects of package-scoped parameters + localparam logic [16*256-1:0] TAB = EXP2_LUT; always_comb begin n = x[X_W-1:8]; f = x[7:0]; - s = -(X_W-7)'(n); - lut = EXP2_LUT[f]; + s = -{n[X_W-9], n}; // sign-extend n by one bit, then negate + lut = TAB[f*16 +: 16]; if (x > 0) y = '1; else if (s >= 16) y = '0; else y = lut >> s[3:0]; diff --git a/rtl/flash_ctrl.sv b/rtl/flash_ctrl.sv index ade10de..4c66b2b 100644 --- a/rtl/flash_ctrl.sv +++ b/rtl/flash_ctrl.sv @@ -1,3 +1,4 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // FLASH-2 controller. Q blocks of G = cfg_qrows/4 row groups, outer loop. // Per block: clear state, load Q rows, then for each K/V tile kj up to the block's last tile: load K and V (DMA), // load Kᵀ/V into the engines (4 cycles), run every unmasked row group of the block through the tile core. @@ -10,8 +11,8 @@ // // The overlap-off state sequence is modelled cycle-exactly in sim/golden/schedule.py; the overlap-on numbers are pinned // baselines (sim/perf_baseline.json). +import tiny_attn_pkg::*; module flash_ctrl - import tiny_attn_pkg::*; ( input logic clk, input logic rst_n, diff --git a/rtl/lut_pkg.sv b/rtl/lut_pkg.sv index 6342bcb..207f925 100644 --- a/rtl/lut_pkg.sv +++ b/rtl/lut_pkg.sv @@ -2,49 +2,49 @@ /* verilator lint_off UNUSEDPARAM */ // each module uses one of the two tables package lut_pkg; // exp2: round(2^15 * 2^(f/256)), UQ1.15 - localparam logic [15:0] EXP2_LUT [0:255] = '{ - 16'h8000, 16'h8059, 16'h80b2, 16'h810b, 16'h8165, 16'h81bf, 16'h8219, 16'h8273, - 16'h82ce, 16'h8328, 16'h8383, 16'h83df, 16'h843a, 16'h8496, 16'h84f2, 16'h854e, - 16'h85ab, 16'h8608, 16'h8665, 16'h86c2, 16'h871f, 16'h877d, 16'h87db, 16'h883a, - 16'h8898, 16'h88f7, 16'h8956, 16'h89b5, 16'h8a15, 16'h8a75, 16'h8ad5, 16'h8b35, - 16'h8b96, 16'h8bf7, 16'h8c58, 16'h8cb9, 16'h8d1b, 16'h8d7d, 16'h8ddf, 16'h8e41, - 16'h8ea4, 16'h8f07, 16'h8f6b, 16'h8fce, 16'h9032, 16'h9096, 16'h90fa, 16'h915f, - 16'h91c4, 16'h9229, 16'h928e, 16'h92f4, 16'h935a, 16'h93c0, 16'h9427, 16'h948e, - 16'h94f5, 16'h955c, 16'h95c4, 16'h962c, 16'h9694, 16'h96fd, 16'h9765, 16'h97cf, - 16'h9838, 16'h98a2, 16'h990c, 16'h9976, 16'h99e0, 16'h9a4b, 16'h9ab6, 16'h9b22, - 16'h9b8d, 16'h9bf9, 16'h9c65, 16'h9cd2, 16'h9d3f, 16'h9dac, 16'h9e19, 16'h9e87, - 16'h9ef5, 16'h9f64, 16'h9fd2, 16'ha041, 16'ha0b0, 16'ha120, 16'ha190, 16'ha200, - 16'ha270, 16'ha2e1, 16'ha352, 16'ha3c3, 16'ha435, 16'ha4a7, 16'ha519, 16'ha58c, - 16'ha5ff, 16'ha672, 16'ha6e6, 16'ha759, 16'ha7ce, 16'ha842, 16'ha8b7, 16'ha92c, - 16'ha9a1, 16'haa17, 16'haa8d, 16'hab04, 16'hab7a, 16'habf1, 16'hac69, 16'hace0, - 16'had58, 16'hadd1, 16'hae49, 16'haec2, 16'haf3b, 16'hafb5, 16'hb02f, 16'hb0a9, - 16'hb124, 16'hb19f, 16'hb21a, 16'hb296, 16'hb312, 16'hb38e, 16'hb40b, 16'hb488, - 16'hb505, 16'hb583, 16'hb601, 16'hb67f, 16'hb6fe, 16'hb77d, 16'hb7fc, 16'hb87c, - 16'hb8fc, 16'hb97c, 16'hb9fd, 16'hba7e, 16'hbaff, 16'hbb81, 16'hbc03, 16'hbc86, - 16'hbd09, 16'hbd8c, 16'hbe0f, 16'hbe93, 16'hbf18, 16'hbf9c, 16'hc021, 16'hc0a7, - 16'hc12c, 16'hc1b2, 16'hc239, 16'hc2c0, 16'hc347, 16'hc3ce, 16'hc456, 16'hc4df, - 16'hc567, 16'hc5f0, 16'hc67a, 16'hc703, 16'hc78d, 16'hc818, 16'hc8a3, 16'hc92e, - 16'hc9ba, 16'hca46, 16'hcad2, 16'hcb5f, 16'hcbec, 16'hcc7a, 16'hcd08, 16'hcd96, - 16'hce25, 16'hceb4, 16'hcf43, 16'hcfd3, 16'hd063, 16'hd0f4, 16'hd185, 16'hd216, - 16'hd2a8, 16'hd33a, 16'hd3cd, 16'hd460, 16'hd4f3, 16'hd587, 16'hd61b, 16'hd6b0, - 16'hd745, 16'hd7da, 16'hd870, 16'hd906, 16'hd99d, 16'hda34, 16'hdacc, 16'hdb63, - 16'hdbfc, 16'hdc94, 16'hdd2e, 16'hddc7, 16'hde61, 16'hdefb, 16'hdf96, 16'he031, - 16'he0cd, 16'he169, 16'he205, 16'he2a2, 16'he340, 16'he3dd, 16'he47b, 16'he51a, - 16'he5b9, 16'he658, 16'he6f8, 16'he799, 16'he839, 16'he8db, 16'he97c, 16'hea1e, - 16'heac1, 16'heb64, 16'hec07, 16'hecab, 16'hed4f, 16'hedf4, 16'hee99, 16'hef3f, - 16'hefe5, 16'hf08b, 16'hf132, 16'hf1da, 16'hf281, 16'hf32a, 16'hf3d3, 16'hf47c, - 16'hf525, 16'hf5d0, 16'hf67a, 16'hf725, 16'hf7d1, 16'hf87d, 16'hf929, 16'hf9d6, - 16'hfa84, 16'hfb32, 16'hfbe0, 16'hfc8f, 16'hfd3e, 16'hfdee, 16'hfe9e, 16'hff4f + localparam logic [4095:0] EXP2_LUT = { + 16'hff4f, 16'hfe9e, 16'hfdee, 16'hfd3e, 16'hfc8f, 16'hfbe0, 16'hfb32, 16'hfa84, + 16'hf9d6, 16'hf929, 16'hf87d, 16'hf7d1, 16'hf725, 16'hf67a, 16'hf5d0, 16'hf525, + 16'hf47c, 16'hf3d3, 16'hf32a, 16'hf281, 16'hf1da, 16'hf132, 16'hf08b, 16'hefe5, + 16'hef3f, 16'hee99, 16'hedf4, 16'hed4f, 16'hecab, 16'hec07, 16'heb64, 16'heac1, + 16'hea1e, 16'he97c, 16'he8db, 16'he839, 16'he799, 16'he6f8, 16'he658, 16'he5b9, + 16'he51a, 16'he47b, 16'he3dd, 16'he340, 16'he2a2, 16'he205, 16'he169, 16'he0cd, + 16'he031, 16'hdf96, 16'hdefb, 16'hde61, 16'hddc7, 16'hdd2e, 16'hdc94, 16'hdbfc, + 16'hdb63, 16'hdacc, 16'hda34, 16'hd99d, 16'hd906, 16'hd870, 16'hd7da, 16'hd745, + 16'hd6b0, 16'hd61b, 16'hd587, 16'hd4f3, 16'hd460, 16'hd3cd, 16'hd33a, 16'hd2a8, + 16'hd216, 16'hd185, 16'hd0f4, 16'hd063, 16'hcfd3, 16'hcf43, 16'hceb4, 16'hce25, + 16'hcd96, 16'hcd08, 16'hcc7a, 16'hcbec, 16'hcb5f, 16'hcad2, 16'hca46, 16'hc9ba, + 16'hc92e, 16'hc8a3, 16'hc818, 16'hc78d, 16'hc703, 16'hc67a, 16'hc5f0, 16'hc567, + 16'hc4df, 16'hc456, 16'hc3ce, 16'hc347, 16'hc2c0, 16'hc239, 16'hc1b2, 16'hc12c, + 16'hc0a7, 16'hc021, 16'hbf9c, 16'hbf18, 16'hbe93, 16'hbe0f, 16'hbd8c, 16'hbd09, + 16'hbc86, 16'hbc03, 16'hbb81, 16'hbaff, 16'hba7e, 16'hb9fd, 16'hb97c, 16'hb8fc, + 16'hb87c, 16'hb7fc, 16'hb77d, 16'hb6fe, 16'hb67f, 16'hb601, 16'hb583, 16'hb505, + 16'hb488, 16'hb40b, 16'hb38e, 16'hb312, 16'hb296, 16'hb21a, 16'hb19f, 16'hb124, + 16'hb0a9, 16'hb02f, 16'hafb5, 16'haf3b, 16'haec2, 16'hae49, 16'hadd1, 16'had58, + 16'hace0, 16'hac69, 16'habf1, 16'hab7a, 16'hab04, 16'haa8d, 16'haa17, 16'ha9a1, + 16'ha92c, 16'ha8b7, 16'ha842, 16'ha7ce, 16'ha759, 16'ha6e6, 16'ha672, 16'ha5ff, + 16'ha58c, 16'ha519, 16'ha4a7, 16'ha435, 16'ha3c3, 16'ha352, 16'ha2e1, 16'ha270, + 16'ha200, 16'ha190, 16'ha120, 16'ha0b0, 16'ha041, 16'h9fd2, 16'h9f64, 16'h9ef5, + 16'h9e87, 16'h9e19, 16'h9dac, 16'h9d3f, 16'h9cd2, 16'h9c65, 16'h9bf9, 16'h9b8d, + 16'h9b22, 16'h9ab6, 16'h9a4b, 16'h99e0, 16'h9976, 16'h990c, 16'h98a2, 16'h9838, + 16'h97cf, 16'h9765, 16'h96fd, 16'h9694, 16'h962c, 16'h95c4, 16'h955c, 16'h94f5, + 16'h948e, 16'h9427, 16'h93c0, 16'h935a, 16'h92f4, 16'h928e, 16'h9229, 16'h91c4, + 16'h915f, 16'h90fa, 16'h9096, 16'h9032, 16'h8fce, 16'h8f6b, 16'h8f07, 16'h8ea4, + 16'h8e41, 16'h8ddf, 16'h8d7d, 16'h8d1b, 16'h8cb9, 16'h8c58, 16'h8bf7, 16'h8b96, + 16'h8b35, 16'h8ad5, 16'h8a75, 16'h8a15, 16'h89b5, 16'h8956, 16'h88f7, 16'h8898, + 16'h883a, 16'h87db, 16'h877d, 16'h871f, 16'h86c2, 16'h8665, 16'h8608, 16'h85ab, + 16'h854e, 16'h84f2, 16'h8496, 16'h843a, 16'h83df, 16'h8383, 16'h8328, 16'h82ce, + 16'h8273, 16'h8219, 16'h81bf, 16'h8165, 16'h810b, 16'h80b2, 16'h8059, 16'h8000 }; // reciprocal seed: round(2^16 / (1 + (i + 0.5) / 64)), UQ0.16 - localparam logic [15:0] RECIP_LUT [0:63] = '{ - 16'hfe04, 16'hfa23, 16'hf660, 16'hf2ba, 16'hef2f, 16'hebbe, 16'he866, 16'he526, - 16'he1fc, 16'hdee9, 16'hdbeb, 16'hd902, 16'hd62c, 16'hd368, 16'hd0b7, 16'hce17, - 16'hcb87, 16'hc908, 16'hc698, 16'hc437, 16'hc1e5, 16'hbfa0, 16'hbd69, 16'hbb3f, - 16'hb921, 16'hb710, 16'hb50a, 16'hb30f, 16'hb120, 16'haf3b, 16'had60, 16'hab8f, - 16'ha9c8, 16'ha80b, 16'ha656, 16'ha4aa, 16'ha306, 16'ha16b, 16'h9fd8, 16'h9e4d, - 16'h9cc9, 16'h9b4c, 16'h99d7, 16'h9869, 16'h9701, 16'h95a0, 16'h9446, 16'h92f1, - 16'h91a3, 16'h905a, 16'h8f17, 16'h8dda, 16'h8ca3, 16'h8b70, 16'h8a43, 16'h891b, - 16'h87f8, 16'h86d9, 16'h85bf, 16'h84aa, 16'h8399, 16'h828d, 16'h8185, 16'h8081 + localparam logic [1023:0] RECIP_LUT = { + 16'h8081, 16'h8185, 16'h828d, 16'h8399, 16'h84aa, 16'h85bf, 16'h86d9, 16'h87f8, + 16'h891b, 16'h8a43, 16'h8b70, 16'h8ca3, 16'h8dda, 16'h8f17, 16'h905a, 16'h91a3, + 16'h92f1, 16'h9446, 16'h95a0, 16'h9701, 16'h9869, 16'h99d7, 16'h9b4c, 16'h9cc9, + 16'h9e4d, 16'h9fd8, 16'ha16b, 16'ha306, 16'ha4aa, 16'ha656, 16'ha80b, 16'ha9c8, + 16'hab8f, 16'had60, 16'haf3b, 16'hb120, 16'hb30f, 16'hb50a, 16'hb710, 16'hb921, + 16'hbb3f, 16'hbd69, 16'hbfa0, 16'hc1e5, 16'hc437, 16'hc698, 16'hc908, 16'hcb87, + 16'hce17, 16'hd0b7, 16'hd368, 16'hd62c, 16'hd902, 16'hdbeb, 16'hdee9, 16'he1fc, + 16'he526, 16'he866, 16'hebbe, 16'hef2f, 16'hf2ba, 16'hf660, 16'hfa23, 16'hfe04 }; endpackage diff --git a/rtl/naive_ctrl.sv b/rtl/naive_ctrl.sv index 24c2f6f..e442afc 100644 --- a/rtl/naive_ctrl.sv +++ b/rtl/naive_ctrl.sv @@ -1,3 +1,4 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // NAIVE (3-pass safe softmax, FlashAttention paper Algorithm 0) controller. Blocks are 4 rows (one row group). // Per block qi, the four passes run in order over the block's K/V tiles kj = 0 .. last (last = qi when causal): // pass 1 load K tile, weight load, OP_NZ: Z tile -> staging -> DMA write to Z[qi][kj] @@ -8,8 +9,8 @@ // order produces the same values and the same traffic totals as fixed.naive (which runs pass by pass over all blocks). // // No K/V overlap in NAIVE (Phase 5 applies it to FLASH only). Buffer 0 is used for every K/V load. +import tiny_attn_pkg::*; module naive_ctrl - import tiny_attn_pkg::*; ( input logic clk, input logic rst_n, diff --git a/rtl/normalize.sv b/rtl/normalize.sv index 68d927e..f65558d 100644 --- a/rtl/normalize.sv +++ b/rtl/normalize.sv @@ -1,7 +1,8 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // out = clip_int16( (o * r + 2^(7+k)) >>> (8+k) ): o / l as Q8.8 in V-LSB units, round to nearest, where // 1/l ~= r / 2^(16+k) comes from recip_unit. One element (combinational). Matches fixed.normalize. +import tiny_attn_pkg::*; module normalize - import tiny_attn_pkg::*; ( input logic signed [O_W-1:0] o, input logic [R_W-1:0] r, diff --git a/rtl/o_accumulator.sv b/rtl/o_accumulator.sv index 6c71d23..96cbf09 100644 --- a/rtl/o_accumulator.sv +++ b/rtl/o_accumulator.sv @@ -1,8 +1,9 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // O update for ONE row (combinational), 4 columns. Matches fixed.o_update: // o_new = ((o_old * alpha) >>> 15) + (pv <<< 4) truncating (floor) shift // pv is one row of MM2 = P.V for the current tile (signed, PV_W bits). +import tiny_attn_pkg::*; module o_accumulator - import tiny_attn_pkg::*; ( input logic [4*O_W-1:0] o_old, input logic [E_W-1:0] alpha, diff --git a/rtl/online_softmax.sv b/rtl/online_softmax.sv index b1c4278..8e0829c 100644 --- a/rtl/online_softmax.sv +++ b/rtl/online_softmax.sv @@ -1,10 +1,11 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // One online-softmax step for ONE row of a 4-wide S tile (combinational). Matches fixed.online_softmax_step. // m_new = max(m_old, rowmax(z)); alpha = exp2(m_old - m_new) // P_j = masked ? 0 : round_to_nearest(exp2(z_j - m_new) / 2^7) (UQ1.15 -> UQ1.8) // l_new = ((l_old * alpha) >> 15) + (sum(P) << 4) // A masked z (0x8000) never wins the max and contributes P = 0. +import tiny_attn_pkg::*; module online_softmax - import tiny_attn_pkg::*; ( input logic [4*Z_W-1:0] z, // 4 logits, element j in bits [j*Z_W +: Z_W] input logic signed [Z_W-1:0] m_old, diff --git a/rtl/perf_counters.sv b/rtl/perf_counters.sv index aae3e8b..729e999 100644 --- a/rtl/perf_counters.sv +++ b/rtl/perf_counters.sv @@ -11,9 +11,13 @@ module perf_counters ( output logic [31:0] kv_loads ); always_ff @(posedge clk or negedge rst_n) begin - if (!rst_n || clr) begin - cycles <= '0; - tiles <= '0; + if (!rst_n) begin + cycles <= '0; + tiles <= '0; + kv_loads <= '0; + end else if (clr) begin + cycles <= '0; + tiles <= '0; kv_loads <= '0; end else begin if (busy) cycles <= cycles + 32'd1; diff --git a/rtl/recip_unit.sv b/rtl/recip_unit.sv index 71af59d..ca5cc68 100644 --- a/rtl/recip_unit.sv +++ b/rtl/recip_unit.sv @@ -1,10 +1,11 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // 1/l for l >= 1 (unsigned, L_W bits). 1/l ~= r / 2^(16 + k): r is UQ1.16 (0x8000..0x10000), k = floor(log2 l). // Normalize to a UQ1.15 mantissa, LUT64 seed on its top fraction bits, one Newton-Raphson step. // Matches fixed.recip (nr_steps = 1). Combinational; the Phase 4 controller registers it as needed. // l == 0 is outside the contract and returns r = 0, k = 0. +import tiny_attn_pkg::*; +import lut_pkg::*; module recip_unit - import tiny_attn_pkg::*; - import lut_pkg::*; ( input logic [L_W-1:0] l, output logic [R_W-1:0] r, @@ -12,6 +13,8 @@ module recip_unit ); logic [15:0] m16; // mantissa in [1,2) as UQ1.15 logic [15:0] seed; // UQ0.16 + // a local copy: Yosys does not resolve part-selects of package-scoped parameters + localparam logic [16*64-1:0] TAB = RECIP_LUT; logic [31:0] mr; // m16 * seed logic [17:0] t; // M*r ~ 1.0 as UQ1.16 logic [17:0] e; // 2 - M*r as UQ1.16 @@ -23,7 +26,7 @@ module recip_unit if (l[i]) k = K_W'(i); end m16 = 16'((l << (L_W - 1 - int'(k))) >> (L_W - 16)); // normalize: MSB to bit L_W-1, keep top 16 - seed = RECIP_LUT[m16[14:9]]; + seed = TAB[m16[14:9]*16 +: 16]; mr = m16 * seed; t = 18'(mr >> 15); e = 18'h20000 - t; diff --git a/rtl/scale_mask.sv b/rtl/scale_mask.sv index d5aa145..f99e7f7 100644 --- a/rtl/scale_mask.sv +++ b/rtl/scale_mask.sv @@ -1,7 +1,8 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // S (int17) -> Z (Q8.8): Z = sat((S * cfg_scale) >>> (SF - 8)) clipped to +-32767; a masked entry becomes 0x8000. // Truncating (floor) shift, as in fixed.scale_mask. +import tiny_attn_pkg::*; module scale_mask - import tiny_attn_pkg::*; ( input logic signed [S_W-1:0] s, input logic [SCALE_W-1:0] scale, diff --git a/rtl/tiny_attn_top.sv b/rtl/tiny_attn_top.sv index 5d54174..5437afd 100644 --- a/rtl/tiny_attn_top.sv +++ b/rtl/tiny_attn_top.sv @@ -1,10 +1,11 @@ +/* verilator lint_off IMPORTSTAR */ // package import at file scope: Yosys rejects it in the module header // TinyAttention engine: FLASH-2 or NAIVE schedule over an off-chip (HBM) model, optionally with K/V overlap. // // Host protocol: write Q, K, V rows into HBM with the load port (one word per row: lane c in bits [c*8 +: 8]). // Set the config, pulse start, wait for done, read O back (two words per row, lanes 0-1 then 2-3, int16). // Load and readback only while idle. Counters are cleared by start and hold their values after done. +import tiny_attn_pkg::*; module tiny_attn_top - import tiny_attn_pkg::*; ( input logic clk, input logic rst_n, diff --git a/sim/golden/gen_luts.py b/sim/golden/gen_luts.py index 0027938..e0f74a1 100644 --- a/sim/golden/gen_luts.py +++ b/sim/golden/gen_luts.py @@ -1,7 +1,7 @@ """Single source of truth for the exp2 and reciprocal ROMs. fixed.py imports exp2_lut()/recip_lut() from here; running this file writes the same values to -rtl/lut_pkg.sv as SystemVerilog localparam arrays (no $readmemh, so it works in Verilator, Yosys and +rtl/lut_pkg.sv as packed SystemVerilog localparams (no $readmemh, so it works in Verilator, Yosys and WASM without any file access). test_golden.py fails if the checked-in file drifts. python sim/golden/gen_luts.py @@ -29,11 +29,14 @@ def recip_lut() -> list[int]: def _array(name: str, values: list[int]) -> str: + """Packed constant: entry i is bits [16*i +: 16]. Unpacked array localparams do not parse in Yosys.""" + n = len(values) + tokens = [f"16'h{v:04x}" for v in reversed(values)] # the first entry goes in the low bits rows = [] - for i in range(0, len(values), 8): - rows.append(" " + ", ".join(f"16'h{v:04x}" for v in values[i:i + 8])) + for i in range(0, n, 8): + rows.append(" " + ", ".join(tokens[i:i + 8])) body = ",\n".join(rows) - return f" localparam logic [15:0] {name} [0:{len(values) - 1}] = '{{\n{body}\n }};\n" + return f" localparam logic [{16 * n - 1}:0] {name} = {{\n{body}\n }};\n" def render_pkg() -> str: @@ -55,4 +58,4 @@ def write_all() -> None: if __name__ == "__main__": write_all() - print(f"wrote {PKG_PATH} (EXP2_LUT[256], RECIP_LUT[64])") + print(f"wrote {PKG_PATH} (EXP2_LUT[256], RECIP_LUT[64], packed)") diff --git a/sim/run_e2e.sh b/sim/run_e2e.sh index 053a9af..5342026 100755 --- a/sim/run_e2e.sh +++ b/sim/run_e2e.sh @@ -3,6 +3,9 @@ # SEEDS=200 by default (about 200 x 16 runs). Usage: bash sim/run_e2e.sh (after `source scripts/env.sh`) set -euo pipefail cd "$(dirname "$0")" +# The installed Verilator leaves CFG_CXXFLAGS_PCH_I empty, so some generated models fail to link their precompiled header. +# -include is the GCC flag that rule needs (see sim/run_fpga.sh). +export MAKEFLAGS="CFG_CXXFLAGS_PCH_I=-include" R=$PWD/../rtl SRCS="$R/lut_pkg.sv $R/tiny_attn_pkg.sv $(ls $R/*.sv | grep -v -e lut_pkg.sv -e tiny_attn_pkg.sv | tr '\n' ' ')" rm -f results.xml diff --git a/sim/run_fpga.sh b/sim/run_fpga.sh new file mode 100755 index 0000000..a6817b0 --- /dev/null +++ b/sim/run_fpga.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Phase 10 simulation: the FPGA top driven over its UART (fpga/ecp5/fpga_top.sv), at 4 clocks per bit to keep it quick. +# Usage: bash sim/run_fpga.sh (after `source scripts/env.sh`) +set -euo pipefail +cd "$(dirname "$0")" +R=$PWD/../rtl +F=$PWD/../fpga/ecp5 +SRCS="$R/lut_pkg.sv $R/tiny_attn_pkg.sv $(ls $R/*.sv | grep -v -e lut_pkg.sv -e tiny_attn_pkg.sv | tr '\n' ' ') $F/uart.sv $F/fpga_top.sv" +rm -f results.xml +# The installed Verilator leaves CFG_CXXFLAGS_PCH_I empty, so its precompiled-header rule passes a file name to the +# compiler. -include is the GCC flag the rule needs. +MAKEFLAGS="CFG_CXXFLAGS_PCH_I=-include" make MODULE=test_fpga_uart TOPLEVEL=fpga_top VERILOG_SOURCES="$SRCS" \ + WAVES=0 SIM_BUILD=sim_build/fpga EXTRA_ARGS="-GCLKS_PER_BIT=4" > sim_build_fpga.log 2>&1 || true +python3 - <<'PY' +import xml.etree.ElementTree as ET +r = ET.parse("results.xml").getroot() +bad = [c.get("name") for c in r.iter("testcase") if c.find("failure") is not None or c.find("error") is not None] +n = len(list(r.iter("testcase"))) +print(f"fpga uart: {n - len(bad)}/{n} passed" + (f" FAILED: {bad}" if bad else "")) +raise SystemExit(1 if bad else 0) +PY +rm -rf sim_build/fpga sim_build_fpga.log diff --git a/sim/run_unit.sh b/sim/run_unit.sh index 4e3f53a..8e16dc2 100755 --- a/sim/run_unit.sh +++ b/sim/run_unit.sh @@ -3,6 +3,9 @@ # disk is small). Usage: bash sim/run_unit.sh (after `source scripts/env.sh`). End-to-end: sim/run_e2e.sh. set -euo pipefail cd "$(dirname "$0")" +# The installed Verilator leaves CFG_CXXFLAGS_PCH_I empty, so some generated models fail to link their precompiled header. +# -include is the GCC flag that rule needs (see sim/run_fpga.sh). +export MAKEFLAGS="CFG_CXXFLAGS_PCH_I=-include" R=$PWD/../rtl ARITH="$R/lut_pkg.sv $R/tiny_attn_pkg.sv $R/pe.sv $R/systolic_array.sv $R/mm_engine.sv $R/row_max.sv $R/scale_mask.sv $R/exp2_unit.sv $R/recip_unit.sv $R/online_softmax.sv $R/o_accumulator.sv $R/normalize.sv" TILE="$ARITH $R/attn_tile.sv" diff --git a/sim/test_fpga_uart.py b/sim/test_fpga_uart.py new file mode 100644 index 0000000..da8d790 --- /dev/null +++ b/sim/test_fpga_uart.py @@ -0,0 +1,137 @@ +"""The FPGA top (fpga/ecp5/fpga_top.sv) driven over its UART, the way a host would drive the board. + +A host model sends commands on uart_rx and reads replies on uart_tx at CLKS_PER_BIT clocks per bit (set with +EXTRA_ARGS="-GCLKS_PER_BIT=4" in the run script; the board uses 217 for 25 MHz and 115200 baud). Checked: + * the O words read back over UART equal the golden model, bit for bit + * the DONE reply's cycle, HBM read and write byte counts equal the closed-form models (FLASH: schedule.py, traffic.py) +""" + +import random + +import cocotb +from cocotb.triggers import RisingEdge, Timer + +from tbutil import fixed, pack, reset, start_clock, unpack # sets up the golden-model import path +from schedule import cycles as sched_cycles +from traffic import traffic + +Q_BASE, K_BASE, V_BASE, O_BASE = 0, 32, 64, 96 +CMD_CONFIG, CMD_LOAD, CMD_START, CMD_READ = 0x01, 0x02, 0x03, 0x04 +REP_DONE, REP_WORD = 0x83, 0x84 + + +class Host: + """UART host: bit-banged at CPB clocks per bit, LSB first, 8N1.""" + + def __init__(self, dut, cpb: int): + self.dut = dut + self.cpb = cpb + + async def _hold(self, level: int, cycles: int): + for _ in range(cycles): + self.dut.uart_rx.value = level + await RisingEdge(self.dut.clk) + await Timer(1, "ns") + + async def send(self, data: bytes): + for b in data: + await self._hold(0, self.cpb) # start + for i in range(8): + await self._hold((b >> i) & 1, self.cpb) + await self._hold(1, self.cpb) # stop + + async def recv(self, n: int, timeout: int = 2_000_000) -> bytes: + """n bytes from uart_tx: find each start edge, sample every data bit at its middle.""" + out = bytearray() + for _ in range(n): + waited = 0 + while int(self.dut.uart_tx.value) == 1: + await self._advance(1) + waited += 1 + if waited > timeout: + raise AssertionError("no reply from the device") + # now at the first cycle of the start bit; data bit k is centred at (k + 1.5) bit periods + await self._advance(self.cpb + self.cpb // 2) + byte = 0 + for k in range(8): + byte |= int(self.dut.uart_tx.value) << k + await self._advance(self.cpb) + out.append(byte) # we are now in the middle of the stop bit + return bytes(out) + + async def _advance(self, cycles: int): + for _ in range(cycles): + await RisingEdge(self.dut.clk) + await Timer(1, "ns") + + async def config(self, nb, gg, causal, naive, overlap, latency, scale): + flags = (1 if causal else 0) | (2 if naive else 0) | (4 if overlap else 0) + await self.send(bytes([CMD_CONFIG, nb, gg, flags, latency]) + scale.to_bytes(3, "little")) + + async def load(self, addr, word): + await self.send(bytes([CMD_LOAD]) + addr.to_bytes(2, "little") + (word & 0xFFFFFFFF).to_bytes(4, "little")) + + async def start(self): + await self.send(bytes([CMD_START])) + rep = await self.recv(13) + assert rep[0] == REP_DONE, f"bad reply code {rep[0]:#x}" + vals = [int.from_bytes(rep[1 + 4 * i: 5 + 4 * i], "little") for i in range(3)] + return dict(cycles=vals[0], rd=vals[1], wr=vals[2]) + + async def read(self, addr): + await self.send(bytes([CMD_READ]) + addr.to_bytes(2, "little")) + rep = await self.recv(5) + assert rep[0] == REP_WORD, f"bad reply code {rep[0]:#x}" + return int.from_bytes(rep[1:5], "little") + + +async def run_case(dut, cpb, rng, n, causal, naive, qrows): + q = [[rng.gauss(0, 2.5) for _ in range(4)] for _ in range(n)] + k = [[rng.gauss(0, 2.5) for _ in range(4)] for _ in range(n)] + v = [[rng.gauss(0, 1.0) for _ in range(4)] for _ in range(n)] + qz = fixed.quantize(q, k, v) + + host = Host(dut, cpb) + await host.config(n // 4, (4 if naive else qrows) // 4, causal, naive, False, 10, qz.cfg_scale) + for i in range(n): + await host.load(Q_BASE + i, pack(qz.q[i], 8)) + for i in range(n): + await host.load(K_BASE + i, pack(qz.k[i], 8)) + for i in range(n): + await host.load(V_BASE + i, pack(qz.v[i], 8)) + + rep = await host.start() + qblock = 4 if naive else qrows + t = traffic(n, causal, "NAIVE" if naive else "FLASH", qblock=qblock) + ctx = f"{'NAIVE' if naive else 'FLASH'} N={n} causal={causal} qrows={qblock}" + assert rep["rd"] == t["rd"], f"{ctx}: read bytes {rep['rd']} != {t['rd']}" + assert rep["wr"] == t["wr"], f"{ctx}: write bytes {rep['wr']} != {t['wr']}" + if not naive: + assert rep["cycles"] == sched_cycles(n, causal, qblock, 10), f"{ctx}: cycles {rep['cycles']}" + + got = [] + for i in range(n): + lo = await host.read(O_BASE + 2 * i) + hi = await host.read(O_BASE + 2 * i + 1) + got.append(unpack(lo, 16, 2) + unpack(hi, 16, 2)) + want = (fixed.naive(qz, n, causal) if naive else fixed.flash(qz, n, causal, qblock=qblock)).out.tolist() + assert got == want, f"{ctx}: O over UART differs from the golden model" + + +@cocotb.test() +async def uart_host_flow(dut): + """Configure, load, run and read back over the UART: FLASH and NAIVE, causal and not.""" + import os + + cpb = int(os.environ.get("CLKS_PER_BIT", "4")) + await start_clock(dut) + dut.uart_rx.value = 1 + await reset(dut) + rng = random.Random(71) + cases = [ + dict(n=8, causal=False, naive=False, qrows=4), + dict(n=8, causal=True, naive=False, qrows=8), + dict(n=8, causal=True, naive=True, qrows=4), + ] + for c in cases: + await run_case(dut, cpb, rng, **c) diff --git a/web/public/tiny_attn.wasm b/web/public/tiny_attn.wasm index 6f65f01fed070cd77820c9593c4177265a635f22..6de09417fd17a3e3005183b58cdb988db43bffcc 100755 GIT binary patch delta 61863 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 delta 61273 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/perf_baseline.json.

- The design has not been synthesized for an FPGA yet (Phase 10). The numbers on this site come from simulation. + The design synthesizes for an ECP5 FPGA (LFE5U-85F): 40,416 of 83,640 LUT4 positions, 51 of 156 DSP blocks, no block + RAM. The timing estimate is 11.5 MHz, below the 25 MHz board clock, so timing is not closed yet. The numbers on the rest + of this site come from simulation. See ARCHITECTURE section 15.

diff --git a/web/src/pages/docs/verification.astro b/web/src/pages/docs/verification.astro index b3698d6..c22620f 100644 --- a/web/src/pages/docs/verification.astro +++ b/web/src/pages/docs/verification.astro @@ -97,9 +97,15 @@ const seq = MEASURED.study.sequence; +

FPGA

+

+ The engine behind a UART synthesizes for an ECP5 (LFE5U-85F) with no latches. The UART protocol is simulated: the + results read back over the serial line match the golden model, and the cycle and byte counts match the models + (bash sim/run_fpga.sh). Synthesis runs in CI (fpga.yml). +

Not verified yet

    -
  • Synthesis for an FPGA. The timing and area numbers are not measured yet (Phase 10).
  • -
  • Real hardware. Everything here runs in simulation or in WebAssembly.
  • +
  • Timing at the board clock. The fmax estimate is 11.5 MHz against 25 MHz, and the estimate has no pin constraints.
  • +
  • Real hardware. The board has not been used. Everything else runs in simulation or in WebAssembly.
From b4236e69900ce98ff32b51358a299d278f2f1dee Mon Sep 17 00:00:00 2001 From: Dineth Hettiarachchi Date: Mon, 26 Oct 2026 12:48:08 +0530 Subject: [PATCH 2/2] fix: track .tools/fpga.sh so the FPGA CI job can install the tools The .gitignore excluded it (only setup.sh and emsdk.sh were allowed), so the ecp5 job failed with 'No such file'. --- .gitignore | 1 + .tools/fpga.sh | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100755 .tools/fpga.sh diff --git a/.gitignore b/.gitignore index 18812b1..a135386 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ web/public/spike/ .tools/* !.tools/setup.sh !.tools/emsdk.sh +!.tools/fpga.sh # OS / editor cruft .DS_Store diff --git a/.tools/fpga.sh b/.tools/fpga.sh new file mode 100755 index 0000000..c966832 --- /dev/null +++ b/.tools/fpga.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# FPGA tools for Phase 10: Yosys and nextpnr for ECP5, as YoWASP wheels (WebAssembly builds, no system packages). +# Installs into .tools/fpga-venv. Usage: bash .tools/fpga.sh +set -euo pipefail +cd "$(dirname "$0")" +if [ ! -x fpga-venv/bin/yowasp-yosys ] || [ ! -x fpga-venv/bin/yowasp-nextpnr-ecp5 ]; then + python3 -m venv fpga-venv + fpga-venv/bin/pip install -q yowasp-yosys yowasp-nextpnr-ecp5 +fi +echo FPGA_TOOLS_DONE