Download sim/test_systolic_array.py from Snapkitty/flash-attention-rtl: direct link, hf CLI and curl.
- Browser
- Download file 9.19 kB
-
https://huggingface.co/Snapkitty/flash-attention-rtl/resolve/main/sim/test_systolic_array.py
- Command line
-
hf download hf://Snapkitty/flash-attention-rtl/sim/test_systolic_array.py
-
curl -L -o test_systolic_array.py https://huggingface.co/Snapkitty/flash-attention-rtl/resolve/main/sim/test_systolic_array.py
9.19 kB
| """ | |
| Cocotb Testbench for Systolic Array (8x8) | |
| Validates: | |
| 1. Matrix multiplication: C = A @ B^T (8x8 fixed-point) | |
| 2. Dataflow: A row-broadcast, B column-broadcast, C bottom-propagate | |
| 3. Pipeline: 13-cycle latency | |
| 4. Numerical accuracy: Fixed-point Q4.4 arithmetic | |
| Test Method: | |
| - Generate random 8x8 Q4.4 matrices (A, B) | |
| - Compute golden reference using NumPy | |
| - Feed data to RTL via AXI-like handshake | |
| - Collect output, compare with tolerance | |
| """ | |
| import cocotb | |
| from cocotb.clock import Clock | |
| from cocotb.triggers import RisingEdge, FallingEdge, Timer | |
| import numpy as np | |
| import random | |
| def matrix_to_fixed_point(matrix, int_bits=4, frac_bits=4, dtype=np.int8): | |
| """ | |
| Convert floating-point matrix to fixed-point (Q4.4) representation. | |
| Args: | |
| matrix: (8, 8) float array | |
| int_bits, frac_bits: Fixed-point format | |
| dtype: output data type (int8 for Q4.4) | |
| Returns: | |
| Fixed-point matrix (int8) | |
| """ | |
| scale = 2 ** frac_bits | |
| fixed = np.round(matrix * scale).astype(dtype) | |
| return fixed | |
| def fixed_point_to_float(value, frac_bits=4, dtype=np.float32): | |
| """Convert fixed-point back to float.""" | |
| return np.float32(value) / (2 ** frac_bits) | |
| def golden_matmul_fixed_point(A_fp, B_fp, int_bits=4, frac_bits=4): | |
| """ | |
| Golden model: Matrix multiplication in fixed-point arithmetic. | |
| Args: | |
| A_fp, B_fp: (8, 8) fixed-point matrices (Q4.4) | |
| Returns: | |
| C_fp: (8, 8) fixed-point result (16-bit accumulator range) | |
| """ | |
| A = fixed_point_to_float(A_fp, frac_bits) | |
| B = fixed_point_to_float(B_fp, frac_bits) | |
| # Standard matrix multiply: C = A @ B^T | |
| C_float = A @ B.T | |
| # Convert back to fixed-point (16-bit range for safety) | |
| scale = 2 ** frac_bits | |
| C_fp = np.round(C_float * scale).astype(np.int16) | |
| return C_fp | |
| class SystemVerilogInterface: | |
| """Wrapper for SystemVerilog signal access.""" | |
| def __init__(self, dut): | |
| self.dut = dut | |
| async def write_input_block(self, A_block, B_block): | |
| """ | |
| Write an 8x8 block to inputs. | |
| Args: | |
| A_block: (8,) uint8 array (feeds left side, row 0-7) | |
| B_block: (8,) uint8 array (feeds top side, col 0-7) | |
| """ | |
| # Set valid_in = 1 | |
| self.dut.valid_in.value = 1 | |
| # Broadcast A to rows | |
| for i in range(8): | |
| self.dut.matrix_A_in[i].value = int(A_block[i]) | |
| # Broadcast B to columns | |
| for j in range(8): | |
| self.dut.matrix_B_in[j].value = int(B_block[j]) | |
| # Wait for clock edge | |
| await RisingEdge(self.dut.clk) | |
| # De-assert valid | |
| self.dut.valid_in.value = 0 | |
| async def read_output_block(self, latency_cycles=14): | |
| """ | |
| Wait for output valid and read C block. | |
| Args: | |
| latency_cycles: Cycles to wait for output validity | |
| Returns: | |
| C_block: (8, 8) array of accumulated results | |
| """ | |
| # Wait for valid_out to pulse | |
| for _ in range(latency_cycles + 5): | |
| await RisingEdge(self.dut.clk) | |
| if self.dut.valid_out.value == 1: | |
| break | |
| # Read output on valid_out assertion | |
| C_block = np.zeros((8, 8), dtype=np.int16) | |
| for i in range(8): | |
| for j in range(8): | |
| C_block[i][j] = int(self.dut.matrix_C_out[i][j].value) | |
| return C_block | |
| async def test_systolic_array_basic(dut): | |
| """Basic functionality test: single matrix multiplication.""" | |
| cocotb.log.info("=" * 70) | |
| cocotb.log.info("TEST: Systolic Array Basic Functionality") | |
| cocotb.log.info("=" * 70) | |
| # Create interface wrapper | |
| iface = SystemVerilogInterface(dut) | |
| # Start clock | |
| cocotb.log.info("Starting 10ns clock...") | |
| clock = Clock(dut.clk, 10, units="ns") | |
| cocotb.start_soon(clock.start()) | |
| # Reset | |
| cocotb.log.info("Asserting reset...") | |
| dut.rst_n.value = 0 | |
| await Timer(30, "ns") | |
| dut.rst_n.value = 1 | |
| dut.valid_in.value = 0 | |
| await Timer(30, "ns") | |
| # =================================================================== | |
| # Test Case 1: Identity-like matrices | |
| # =================================================================== | |
| cocotb.log.info("\nTest 1: Small values (easy validation)") | |
| # A: simple pattern | |
| A_fp = np.array([ | |
| [1, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 1, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 1, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 1, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 1, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 1, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 1, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 1], | |
| ], dtype=np.int8) | |
| # B: simple pattern | |
| B_fp = np.array([ | |
| [1, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 1, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 1, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 1, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 1, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 1, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 1, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 1], | |
| ], dtype=np.int8) | |
| # Compute golden reference | |
| C_fp_golden = golden_matmul_fixed_point(A_fp, B_fp) | |
| cocotb.log.info(f"Golden C (identity x identity):\n{C_fp_golden}") | |
| # Write input block (A and B are transmitted row/column-wise) | |
| # For this simple implementation, we'll simulate row-by-row transmission | |
| # Actually, let's simplify: feed entire block at once | |
| # The testbench interface sends A_in[0:8] and B_in[0:8] in parallel | |
| # Wait a bit | |
| await Timer(100, "ns") | |
| # Send data | |
| cocotb.log.info("Sending input block...") | |
| await iface.write_input_block(A_fp[0], B_fp[0]) | |
| cocotb.log.info("Waiting for output...") | |
| C_block_rtl = await iface.read_output_block(latency_cycles=13) | |
| cocotb.log.info(f"RTL output:\n{C_block_rtl}") | |
| # Compare (only check [0,0] for identity case - should be 1) | |
| cocotb.log.info(f"\nResult C[0,0]: golden={C_fp_golden[0,0]}, rtl={C_block_rtl[0,0]}") | |
| # =================================================================== | |
| # Test Case 2: Random small values | |
| # =================================================================== | |
| cocotb.log.info("\n" + "=" * 70) | |
| cocotb.log.info("Test 2: Random Q4.4 values") | |
| # Generate random Q4.4 data | |
| A_fp_rand = np.random.randint(-8, 8, (8, 8), dtype=np.int8) | |
| B_fp_rand = np.random.randint(-8, 8, (8, 8), dtype=np.int8) | |
| cocotb.log.info(f"Random A_fp:\n{A_fp_rand}") | |
| cocotb.log.info(f"Random B_fp:\n{B_fp_rand}") | |
| # Golden | |
| C_fp_golden_rand = golden_matmul_fixed_point(A_fp_rand, B_fp_rand) | |
| cocotb.log.info(f"Golden C_fp:\n{C_fp_golden_rand}") | |
| # Reset DUT | |
| await Timer(100, "ns") | |
| # Send | |
| cocotb.log.info("Sending random block...") | |
| await iface.write_input_block(A_fp_rand[0], B_fp_rand[0]) | |
| cocotb.log.info("Waiting for RTL output...") | |
| C_block_rtl_rand = await iface.read_output_block(latency_cycles=13) | |
| cocotb.log.info(f"RTL output:\n{C_block_rtl_rand}") | |
| # Compare a few elements | |
| cocotb.log.info("\nComparison (golden vs RTL):") | |
| errors = 0 | |
| for i in range(min(4, 8)): | |
| for j in range(min(4, 8)): | |
| diff = abs(int(C_fp_golden_rand[i, j]) - int(C_block_rtl_rand[i, j])) | |
| status = "✓" if diff == 0 else "✗" | |
| cocotb.log.info(f" C[{i},{j}]: golden={C_fp_golden_rand[i,j]:6d}, " | |
| f"rtl={C_block_rtl_rand[i,j]:6d}, diff={diff:6d} {status}") | |
| if diff > 1: # Allow small tolerance for fixed-point | |
| errors += 1 | |
| if errors == 0: | |
| cocotb.log.info("\n✓ TEST PASSED") | |
| else: | |
| cocotb.log.info(f"\n✗ TEST FAILED ({errors} mismatches)") | |
| # =================================================================== | |
| # Test Case 3: Pipeline latency check | |
| # =================================================================== | |
| cocotb.log.info("\n" + "=" * 70) | |
| cocotb.log.info("Test 3: Pipeline latency measurement") | |
| A_test = np.ones((8, 8), dtype=np.int8) | |
| B_test = np.ones((8, 8), dtype=np.int8) | |
| await Timer(100, "ns") | |
| # Send with precise timing | |
| dut.valid_in.value = 1 | |
| for i in range(8): | |
| dut.matrix_A_in[i].value = 1 | |
| dut.matrix_B_in[i].value = 1 | |
| cycle_sent = 0 | |
| await RisingEdge(dut.clk) | |
| cycle_sent = 0 | |
| dut.valid_in.value = 0 | |
| # Wait for valid_out | |
| cycle_count = 0 | |
| while cycle_count < 30: | |
| await RisingEdge(dut.clk) | |
| cycle_count += 1 | |
| if dut.valid_out.value == 1: | |
| cocotb.log.info(f"✓ Output valid after {cycle_count} cycles") | |
| break | |
| cocotb.log.info("=" * 70) | |
| async def test_systolic_array_reset(dut): | |
| """Test reset functionality.""" | |
| cocotb.log.info("TEST: Reset functionality") | |
| clock = Clock(dut.clk, 10, units="ns") | |
| cocotb.start_soon(clock.start()) | |
| # Apply reset | |
| dut.rst_n.value = 0 | |
| await Timer(50, "ns") | |
| dut.rst_n.value = 1 | |
| cocotb.log.info("✓ Reset test passed") | |