Download rtl/src/systolic_array.sv from Snapkitty/flash-attention-rtl: direct link, hf CLI and curl.
- Browser
- Download file 6.98 kB
-
https://huggingface.co/Snapkitty/flash-attention-rtl/resolve/main/rtl/src/systolic_array.sv
- Command line
-
hf download hf://Snapkitty/flash-attention-rtl/rtl/src/systolic_array.sv
-
curl -L -o systolic_array.sv https://huggingface.co/Snapkitty/flash-attention-rtl/resolve/main/rtl/src/systolic_array.sv
6.98 kB
| /** | |
| * 8x8 Systolic Array for Matrix Multiplication | |
| * | |
| * Computes matrix product: C = A @ B^T | |
| * - A: 8×8 matrix (Q tile, broadcast row-wise from left) | |
| * - B: 8×8 matrix (K^T tile, broadcast column-wise from top) | |
| * - C: 8×8 output matrix (partial products accumulated bottom-wise) | |
| * | |
| * Dataflow (Systolic): | |
| * - Input A enters from left, shifts right across rows | |
| * - Input B enters from top, shifts down across columns | |
| * - Partial sums propagate downward | |
| * | |
| * Pipeline Stages: | |
| * - Each PE has 3-stage internal pipeline | |
| * - Systolic latency: 3 + 8 = 11 cycles (first output valid after 11 cycles) | |
| * | |
| * Data Format: | |
| * - All data: 8-bit signed fixed-point (Q4.4) | |
| * - Accumulators: 16-bit signed (to handle multiplication overflow) | |
| * | |
| * Example data flow (first output): | |
| * Cycle 1: A[0,0], B[0,0] enter top-left PE | |
| * Cycles 2-3: Pipelined processing in PE | |
| * Cycle 4-10: Data propagates to neighbors | |
| * Cycle 11: First valid output at C[0,0] | |
| */ | |
| module systolic_array #( | |
| parameter DATA_WIDTH = 8, // Input/output data width (Q4.4) | |
| parameter ACC_WIDTH = 16, // Accumulator width | |
| parameter PE_ROWS = 8, // Number of rows | |
| parameter PE_COLS = 8 // Number of columns | |
| ) ( | |
| input logic clk, | |
| input logic rst_n, | |
| // Control | |
| input logic valid_in, | |
| output logic valid_out, | |
| // Input: A matrix data (from left side, feeds all rows) | |
| // Array shape: [PE_ROWS][1] - one column (left edge) | |
| input logic [PE_ROWS-1:0][DATA_WIDTH-1:0] matrix_A_in, | |
| // Input: B matrix data (from top side, feeds all columns) | |
| // Array shape: [1][PE_COLS] - one row (top edge) | |
| input logic [PE_COLS-1:0][DATA_WIDTH-1:0] matrix_B_in, | |
| // Output: C matrix data (from bottom side, all accumulators) | |
| // Array shape: [PE_ROWS][PE_COLS] | |
| output logic [PE_ROWS-1:0][PE_COLS-1:0][ACC_WIDTH-1:0] matrix_C_out | |
| ); | |
| // ===================================================================== | |
| // Internal Signals: Systolic Mesh Connections | |
| // ===================================================================== | |
| // A dataflow: row-wise propagation (left to right) | |
| // shape: [PE_ROWS][PE_COLS+1] - extra column for input | |
| logic [PE_ROWS-1:0][PE_COLS:0][DATA_WIDTH-1:0] A_mesh; | |
| // B dataflow: column-wise propagation (top to bottom) | |
| // shape: [PE_ROWS+1][PE_COLS] - extra row for input | |
| logic [PE_ROWS:0][PE_COLS-1:0][DATA_WIDTH-1:0] B_mesh; | |
| // C dataflow: column-wise propagation (top to bottom) | |
| // shape: [PE_ROWS+1][PE_COLS] | |
| logic [PE_ROWS:0][PE_COLS-1:0][ACC_WIDTH-1:0] C_mesh; | |
| // Valid signals: one per PE (for pipeline control) | |
| logic [PE_ROWS-1:0][PE_COLS-1:0] pe_valid_in; | |
| logic [PE_ROWS-1:0][PE_COLS-1:0] pe_valid_out; | |
| // ===================================================================== | |
| // Input Boundary Conditions | |
| // ===================================================================== | |
| // A enters from left: A_mesh[:][0] = matrix_A_in[:] | |
| always_comb begin | |
| for (int i = 0; i < PE_ROWS; i++) begin | |
| A_mesh[i][0] = matrix_A_in[i]; | |
| end | |
| end | |
| // B enters from top: B_mesh[0][:] = matrix_B_in[:] | |
| always_comb begin | |
| for (int j = 0; j < PE_COLS; j++) begin | |
| B_mesh[0][j] = matrix_B_in[j]; | |
| end | |
| end | |
| // C enters as zeros (partial sums start from zero) | |
| always_comb begin | |
| for (int j = 0; j < PE_COLS; j++) begin | |
| C_mesh[0][j] = {ACC_WIDTH{1'b0}}; | |
| end | |
| end | |
| // Valid signal broadcast to top-left corner | |
| always_comb begin | |
| pe_valid_in[0][0] = valid_in; | |
| for (int i = 1; i < PE_ROWS; i++) begin | |
| for (int j = 0; j < PE_COLS; j++) begin | |
| pe_valid_in[i][j] = 1'b0; // Only top-left gets valid signal | |
| end | |
| end | |
| end | |
| // ===================================================================== | |
| // Systolic Mesh: Generate and Connect PE Array | |
| // ===================================================================== | |
| generate | |
| for (genvar i = 0; i < PE_ROWS; i++) begin : gen_rows | |
| for (genvar j = 0; j < PE_COLS; j++) begin : gen_cols | |
| processing_element #( | |
| .DATA_WIDTH(DATA_WIDTH), | |
| .ACC_WIDTH(ACC_WIDTH), | |
| .INIT_VALUE({ACC_WIDTH{1'b0}}) | |
| ) pe_inst ( | |
| .clk(clk), | |
| .rst_n(rst_n), | |
| // Input | |
| .valid_in(pe_valid_in[i][j]), | |
| .matrix_A_in(A_mesh[i][j]), // From left | |
| .matrix_B_in(B_mesh[i][j]), // From top | |
| .matrix_C_in(C_mesh[i][j]), // From top | |
| // Output | |
| .valid_out(pe_valid_out[i][j]), | |
| .matrix_A_out(A_mesh[i][j+1]), // To right | |
| .matrix_C_out(C_mesh[i+1][j]) // To bottom | |
| ); | |
| // Extract output: C_out[i][j] | |
| assign matrix_C_out[i][j] = C_mesh[i+1][j]; | |
| end | |
| end | |
| endgenerate | |
| // ===================================================================== | |
| // Output Valid Signal | |
| // ===================================================================== | |
| // The output is valid when data exits bottom-right corner PE | |
| // Due to 3-stage PE pipeline, this occurs 3 cycles after bottom-right PE | |
| // processes data. For an 8x8 array, diagonal propagation takes: | |
| // 3 (PE internal) + 7 (diagonal to bottom-right) + 3 (output from PE) = 13 cycles | |
| // Simpler approach: use a valid propagation counter | |
| // valid_out pulses when first block of data reaches output | |
| logic [3:0] valid_counter; // Count cycles (0-15 sufficient for 13-cycle latency) | |
| logic output_valid_q; | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) begin | |
| valid_counter <= 4'b0; | |
| output_valid_q <= 1'b0; | |
| end else begin | |
| // Count up to 13 (11 for propagation + 2 for pipeline sync) | |
| if (valid_in && valid_counter == 4'b0) begin | |
| valid_counter <= 4'd1; | |
| end else if (valid_counter > 4'b0 && valid_counter < 4'd13) begin | |
| valid_counter <= valid_counter + 1'b1; | |
| end else if (valid_counter == 4'd13) begin | |
| output_valid_q <= 1'b1; | |
| valid_counter <= 4'b0; // Reset for next block | |
| end else begin | |
| output_valid_q <= 1'b0; | |
| end | |
| end | |
| end | |
| assign valid_out = output_valid_q; | |
| endmodule : systolic_array | |