first commit
This commit is contained in:
109
README.md
Normal file
109
README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# FPGA Resource Usage Benchmarking for Verilog Solutions
|
||||
|
||||
This repository contains the code for benchmarking FPGA resource usage for Verilog solutions generated by different LLMs.
|
||||
|
||||
## Simulation and Synthesis Tools
|
||||
The simulation tool for functional correctness tests and the synthesis tool for obtaining resource usage are both based on **Vivado**. Please install **Vivado** in advance to run the framework. If you wish to use other tools, modify the relevant Python scripts accordingly.
|
||||
|
||||
Some dependencies are listed in `requirements.txt`, which you can install using:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Benchmark Dataset (`problems.json`)
|
||||
The `problems.json` file contains our benchmark dataset, formatted as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"Combinational Logic": [
|
||||
{
|
||||
"module": "parity_8bit",
|
||||
"Problem": "Implement a Verilog module that computes the parity of an 8-bit input vector. The output should be 1 if the number of '1's in the input is odd, and 0 otherwise.",
|
||||
"Module header": "module parity_8bit (\n input [7:0] in,\n output out\n);",
|
||||
"Testbench": "`timescale 1ns / 1ps\n\nmodule parity_8bit_tb; ..."
|
||||
}
|
||||
],
|
||||
"Finite State Machines": []
|
||||
}
|
||||
```
|
||||
|
||||
You can use this dataset to generate solutions and run functional correctness checks for any LLMs you want to evaluate.
|
||||
|
||||
## Experimental Results (`solutions.json` Format)
|
||||
The `solutions` directory contains our experimental results, formatted as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"gpt-3.5-turbo": {
|
||||
"Combinational Logic": [
|
||||
{
|
||||
"module": "parity_8bit",
|
||||
"solutions": [
|
||||
{
|
||||
"solution": "module parity_8bit (input [7:0] in, output out); assign out = in[0] ^ in[1] ^ in[2] ^ in[3] ^ in[4] ^ in[5] ^ in[6] ^ in[7]; endmodule",
|
||||
"pass": "true",
|
||||
"resource usage": {
|
||||
"optimized": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
},
|
||||
"primitives": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Finite State Machines": [
|
||||
{
|
||||
"module": "fsm_3state",
|
||||
"solutions": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"gpt-4o":{}
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Run Instructions
|
||||
To quickly run the benchmarking process, copy `solutions.json` from the `solutions` directory to the same directory as `setup.py`, then execute:
|
||||
|
||||
```sh
|
||||
python setup.py -model gpt-4o 5 your_openai_api_key -functional_correctness -resource_usage
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Generate 5 solutions for each problem using `gpt-4o`.
|
||||
2. Run the functional correctness check.
|
||||
3. Obtain the resource usage report for LUT usage.
|
||||
|
||||
The standard script currently supports OpenAI's GPT models. If you want to test other LLMs, please modify `generate_solutions.py` accordingly.
|
||||
|
||||
## Running Functional and Resource Usage Tests on Custom Solutions
|
||||
You can also run the functional test and resource usage analysis on your own solutions. Ensure that your `solutions.json` follows the format above and place it in the same directory as `setup.py`, then execute:
|
||||
|
||||
```sh
|
||||
python setup.py -functional_correctness -resource_usage
|
||||
```
|
||||
|
||||
## Running Individual Tests
|
||||
To run the **functional correctness check** alone:
|
||||
```sh
|
||||
python setup.py -functional_correctness
|
||||
```
|
||||
|
||||
To run **resource usage analysis** alone:
|
||||
```sh
|
||||
python setup.py -resource_usage
|
||||
```
|
||||
|
||||
|
||||
|
||||
45
evaluate/count_pass.py
Normal file
45
evaluate/count_pass.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import pandas as pd
|
||||
from collections import defaultdict
|
||||
|
||||
# Load the JSON file
|
||||
file_path = "solutions.json" # Adjust this path based on your local directory
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Initialize a dictionary to store the structured results
|
||||
structured_results = defaultdict(lambda: defaultdict(lambda: {"total": 0, "pass": 0, "syntax_error": 0, "functional_error": 0}))
|
||||
|
||||
# Process the data to count various results per LLM and type
|
||||
for llm, categories in data.items():
|
||||
for category, modules in categories.items():
|
||||
for module in modules:
|
||||
for solution in module.get("solutions", []):
|
||||
structured_results[category][llm]["total"] += 1
|
||||
|
||||
pass_info = solution.get("pass", "")
|
||||
if pass_info == "true":
|
||||
structured_results[category][llm]["pass"] += 1
|
||||
elif "Detected error while running simulation" in pass_info:
|
||||
structured_results[category][llm]["syntax_error"] += 1
|
||||
|
||||
# Functional error count
|
||||
structured_results[category][llm]["functional_error"] = (
|
||||
structured_results[category][llm]["total"]
|
||||
- structured_results[category][llm]["syntax_error"]
|
||||
- structured_results[category][llm]["pass"]
|
||||
)
|
||||
|
||||
# Create a DataFrame from the structured results
|
||||
df_restructured = pd.DataFrame.from_dict(
|
||||
{category: {llm: f"{counts['pass']} | {counts['functional_error']} | {counts['syntax_error']}" for llm, counts in llms.items()}
|
||||
for category, llms in structured_results.items()},
|
||||
orient="index"
|
||||
)
|
||||
|
||||
# Save to a CSV file
|
||||
csv_output_path = "solution_pass_analysis.csv" # Adjust the path as needed
|
||||
df_restructured.to_csv(csv_output_path)
|
||||
|
||||
print(f"CSV file saved at: {csv_output_path}")
|
||||
# print(df_restructured)
|
||||
32
evaluate/count_resource.py
Normal file
32
evaluate/count_resource.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
import pandas as pd
|
||||
from collections import defaultdict
|
||||
|
||||
# Load the JSON file
|
||||
file_path = "solutions.json"
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Initialize a dictionary to store the minimal LUT usage for each module and LLM
|
||||
lut_results = defaultdict(lambda: defaultdict(lambda: float("inf")))
|
||||
|
||||
# Process the data to extract the minimum LUT usage per module per LLM
|
||||
for llm, categories in data.items():
|
||||
for category, modules in categories.items():
|
||||
for module_data in modules:
|
||||
module_name = module_data["module"].replace("_", " ") # Replace underscores with spaces
|
||||
for solution in module_data.get("solutions", []):
|
||||
if "resource usage" in solution and "optimized" in solution["resource usage"]:
|
||||
lut_count = solution["resource usage"]["optimized"].get("LUT", float("inf"))
|
||||
# Store the minimum LUT usage
|
||||
lut_results[module_name][llm] = min(lut_results[module_name][llm], lut_count)
|
||||
|
||||
# Convert the dictionary into a DataFrame
|
||||
df_lut = pd.DataFrame.from_dict(lut_results, orient="index")
|
||||
|
||||
# Save to a CSV file
|
||||
csv_output_path = "solution_resource_analysis.csv"
|
||||
df_lut.to_csv(csv_output_path)
|
||||
|
||||
# Print the CSV file path
|
||||
print(f"CSV file saved at: {csv_output_path}")
|
||||
136
evaluate/plot_pass.py
Normal file
136
evaluate/plot_pass.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import json
|
||||
import matplotlib.pyplot as plt
|
||||
import re
|
||||
import seaborn as sns
|
||||
import pandas as pd
|
||||
|
||||
# --- Utility Functions ---
|
||||
|
||||
def compute_module_pass(solution_list, k):
|
||||
"""
|
||||
Check the first k solutions for a module.
|
||||
Return 1 if at least one of them has a "pass" value (after stripping and lowercasing) equal to "true",
|
||||
otherwise return 0.
|
||||
"""
|
||||
for sol in solution_list[:k]:
|
||||
if sol.get("pass", "").strip().lower() == "true":
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def compute_pass_at_k_for_modules(modules, k):
|
||||
"""
|
||||
Given a list of modules (each module is expected to have a "solutions" list),
|
||||
compute the fraction of modules that pass@k.
|
||||
"""
|
||||
total = len(modules)
|
||||
if total == 0:
|
||||
return 0
|
||||
passed = sum(compute_module_pass(mod["solutions"], k) for mod in modules)
|
||||
return passed / total
|
||||
|
||||
def compute_overall_pass_at_k(llm_data, ks):
|
||||
"""
|
||||
Given one LLM's data (a dict mapping category names to lists of modules),
|
||||
compute the overall pass@k (over all modules in all categories).
|
||||
Returns a dictionary mapping each k to the pass@k value.
|
||||
"""
|
||||
all_modules = []
|
||||
for cat, modules in llm_data.items():
|
||||
all_modules.extend(modules)
|
||||
overall = {}
|
||||
for k in ks:
|
||||
overall[k] = compute_pass_at_k_for_modules(all_modules, k)
|
||||
return overall
|
||||
|
||||
def compute_category_pass_at_k(llm_data, ks):
|
||||
"""
|
||||
For each category (type) in one LLM, compute pass@k.
|
||||
Returns a dictionary mapping category names to a dictionary of k -> pass@k.
|
||||
"""
|
||||
cat_results = {}
|
||||
for cat, modules in llm_data.items():
|
||||
k_dict = {}
|
||||
for k in ks:
|
||||
k_dict[k] = compute_pass_at_k_for_modules(modules, k)
|
||||
cat_results[cat] = k_dict
|
||||
return cat_results
|
||||
|
||||
# --- Main processing and plotting ---
|
||||
|
||||
# Choose the k values you want to evaluate pass@k for:
|
||||
ks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
|
||||
# Load the JSON file.
|
||||
input_json_file = "solutions.json" # adjust filename if necessary
|
||||
with open(input_json_file, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# We'll store our computed pass@k results per LLM in a dictionary.
|
||||
llm_results = {}
|
||||
for llm, llm_data in data.items():
|
||||
overall = compute_overall_pass_at_k(llm_data, ks)
|
||||
categories = compute_category_pass_at_k(llm_data, ks)
|
||||
llm_results[llm] = {
|
||||
"overall": overall,
|
||||
"categories": categories
|
||||
}
|
||||
|
||||
# --- Plot Overall Pass@k for each LLM ---
|
||||
plt.figure(figsize=(10, 6))
|
||||
for llm, res in llm_results.items():
|
||||
plt.plot(ks, [res["overall"][k] for k in ks], marker='o', label=llm)
|
||||
|
||||
# plt.xticks(ks) # Ensure all values from 1 to 15 are shown
|
||||
# plt.xlabel("k", fontsize=14)
|
||||
# plt.ylabel("Overall Pass@k", fontsize=14)
|
||||
# plt.title("Overall Pass@k across k for each LLM", fontsize=16) # Larger title
|
||||
# plt.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Legend outside the plot
|
||||
# plt.grid(True)
|
||||
# plt.tight_layout()
|
||||
# plt.savefig("./figures/overall_pass_at_k.png")
|
||||
# plt.show()
|
||||
|
||||
|
||||
# --- Plot Per-Category Pass@k for all LLMs, one figure per k ---
|
||||
# First, determine the union of all categories across LLMs.
|
||||
# Prepare data for heatmap
|
||||
category_pass_k = {}
|
||||
for llm, res in llm_results.items():
|
||||
for cat, kdict in res["categories"].items():
|
||||
if cat not in category_pass_k:
|
||||
category_pass_k[cat] = {}
|
||||
category_pass_k[cat][llm] = kdict[15] # Using Pass@15
|
||||
|
||||
# Convert to DataFrame
|
||||
df_heatmap = pd.DataFrame.from_dict(category_pass_k).T
|
||||
|
||||
|
||||
for k in ks:
|
||||
# Convert to DataFrame
|
||||
df_heatmap = pd.DataFrame.from_dict(category_pass_k).T
|
||||
|
||||
# Plot heatmap
|
||||
plt.figure(figsize=(10, 6))
|
||||
sns.heatmap(df_heatmap, annot=True, cmap="Blues", linewidths=0.5, fmt=".2f")
|
||||
|
||||
plt.title("Pass@15 Heatmap for Each LLM Across Categories", fontsize=16, fontweight="bold")
|
||||
plt.xlabel("LLM", fontsize=14, fontweight="bold")
|
||||
plt.ylabel("Category", fontsize=14, fontweight="bold")
|
||||
|
||||
plt.xticks(rotation=45, ha="right", fontsize=12)
|
||||
plt.yticks(fontsize=12)
|
||||
|
||||
plt.tight_layout()
|
||||
heatmap_path = f"./figures/per_category_pass_k{k}_heatmap.png"
|
||||
plt.savefig(heatmap_path)
|
||||
|
||||
# --- (Optional) Print the computed results ---
|
||||
print("Overall Pass@k per LLM:")
|
||||
for llm, res in llm_results.items():
|
||||
print(f"{llm}: {res['overall']}")
|
||||
|
||||
print("\nPer-Category Pass@k per LLM:")
|
||||
for llm, res in llm_results.items():
|
||||
print(f"{llm}:")
|
||||
for cat, kdict in res["categories"].items():
|
||||
print(f" {cat}: {kdict}")
|
||||
13
evaluate/solution_pass_analysis.csv
Normal file
13
evaluate/solution_pass_analysis.csv
Normal file
@@ -0,0 +1,13 @@
|
||||
,gpt-3.5-turbo,gpt-4,gpt-4o,gpt-o1-mini,llama3.1-405B,qwen-max,qwen-plus,qwen2.5-coder-32B-instruct,codestral
|
||||
Combinational Logic,112 | 5 | 3,117 | 3 | 0,120 | 0 | 0,118 | 1 | 1,115 | 2 | 3,117 | 2 | 1,109 | 1 | 10,112 | 2 | 6,120 | 0 | 0
|
||||
Finite State Machines,23 | 15 | 22,32 | 22 | 6,31 | 24 | 5,39 | 18 | 3,31 | 24 | 5,34 | 26 | 0,27 | 23 | 10,39 | 10 | 11,36 | 6 | 18
|
||||
Mathematical Functions,13 | 19 | 43,6 | 39 | 30,36 | 10 | 29,46 | 24 | 5,7 | 6 | 62,26 | 27 | 22,20 | 26 | 29,5 | 8 | 62,0 | 3 | 72
|
||||
Basic Arithmetic Operations,37 | 2 | 36,63 | 8 | 4,66 | 9 | 0,68 | 4 | 3,43 | 2 | 30,38 | 22 | 15,27 | 13 | 35,54 | 6 | 15,62 | 13 | 0
|
||||
Bitwise and Logical Operations,35 | 0 | 25,55 | 0 | 5,58 | 2 | 0,59 | 0 | 1,52 | 0 | 8,47 | 0 | 13,33 | 11 | 16,36 | 0 | 24,55 | 0 | 5
|
||||
Pipelining,0 | 59 | 16,11 | 54 | 10,26 | 49 | 0,15 | 38 | 22,7 | 38 | 30,15 | 32 | 28,16 | 26 | 33,21 | 31 | 23,6 | 56 | 13
|
||||
Polynomial Evaluation,19 | 3 | 53,69 | 0 | 6,74 | 1 | 0,68 | 5 | 2,58 | 6 | 11,55 | 2 | 18,28 | 5 | 42,65 | 7 | 3,69 | 6 | 0
|
||||
Machine Learning,31 | 3 | 41,60 | 8 | 7,60 | 13 | 2,73 | 1 | 1,45 | 28 | 2,63 | 12 | 0,61 | 12 | 2,57 | 2 | 16,64 | 8 | 3
|
||||
Financial Computing,9 | 23 | 28,21 | 22 | 17,29 | 13 | 18,20 | 20 | 20,11 | 21 | 28,28 | 15 | 17,15 | 12 | 33,16 | 7 | 37,17 | 23 | 20
|
||||
Encryption,30 | 0 | 15,30 | 2 | 13,25 | 20 | 0,30 | 0 | 15,26 | 0 | 19,25 | 9 | 11,30 | 1 | 14,30 | 0 | 15,30 | 0 | 15
|
||||
Physics,45 | 3 | 12,57 | 0 | 3,53 | 4 | 3,54 | 5 | 1,41 | 11 | 8,49 | 7 | 4,40 | 17 | 3,38 | 15 | 7,55 | 2 | 3
|
||||
Climate,8 | 15 | 37,21 | 30 | 9,41 | 11 | 8,41 | 15 | 4,24 | 23 | 13,38 | 19 | 3,19 | 31 | 10,32 | 14 | 14,28 | 19 | 13
|
||||
|
57
evaluate/solution_resource_analysis.csv
Normal file
57
evaluate/solution_resource_analysis.csv
Normal file
@@ -0,0 +1,57 @@
|
||||
,gpt-3.5-turbo,gpt-4,gpt-4o,gpt-o1-mini,llama3.1-405B,qwen-max,qwen-plus,qwen2.5-coder-32B-instruct,codestral
|
||||
parity 8bit,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0
|
||||
mux4to1,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0
|
||||
majority,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0
|
||||
bin to gray,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0
|
||||
eq comparator,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0
|
||||
decoder 2to4,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0
|
||||
seven segment decoder,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0,4.0
|
||||
priority encoder,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0
|
||||
fsm 3state,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
traffic light,1.0,1.0,2.0,0.0,0.0,2.0,3.0,2.0,inf
|
||||
elevator controller,3.0,3.0,2.0,2.0,2.0,2.0,2.0,2.0,2.0
|
||||
vending machine,1.0,1.0,1.0,1.0,2.0,1.0,1.0,2.0,1.0
|
||||
int sqrt,inf,inf,68.0,177.0,inf,64.0,229.0,173.0,inf
|
||||
fibonacci,inf,56.0,1.0,56.0,56.0,56.0,inf,inf,inf
|
||||
mod exp,inf,inf,4466.0,4669.0,inf,1911.0,1678.0,inf,inf
|
||||
power,inf,79.0,74.0,93.0,inf,93.0,93.0,93.0,inf
|
||||
log2 int,inf,inf,inf,10.0,20.0,inf,inf,12.0,inf
|
||||
add 8bit,8.0,8.0,8.0,8.0,8.0,8.0,8.0,8.0,8.0
|
||||
mult 4bit,16.0,16.0,16.0,16.0,16.0,16.0,16.0,16.0,16.0
|
||||
abs diff,12.0,12.0,14.0,12.0,12.0,inf,12.0,12.0,12.0
|
||||
modulo op,82.0,82.0,82.0,82.0,111.0,inf,inf,inf,inf
|
||||
subtract 8bit,8.0,8.0,8.0,8.0,inf,inf,inf,8.0,8.0
|
||||
bitwise ops,16.0,16.0,16.0,16.0,16.0,16.0,16.0,16.0,16.0
|
||||
left shift,10.0,10.0,10.0,10.0,10.0,12.0,12.0,10.0,10.0
|
||||
bitwise not,8.0,8.0,8.0,8.0,8.0,8.0,8.0,8.0,8.0
|
||||
rotate left,inf,12.0,12.0,12.0,12.0,12.0,inf,12.0,12.0
|
||||
pipelined adder,inf,0.0,16.0,inf,0.0,inf,0.0,15.0,inf
|
||||
pipelined multiplier,inf,inf,77.0,70.0,56.0,inf,70.0,inf,inf
|
||||
pipelined accumulator,inf,inf,inf,inf,27.0,inf,inf,inf,inf
|
||||
pipelined max finder,inf,0.0,24.0,0.0,24.0,24.0,24.0,24.0,24.0
|
||||
pipelined fir,inf,inf,inf,inf,inf,inf,inf,inf,inf
|
||||
polynomial 1,61.0,61.0,61.0,61.0,61.0,61.0,61.0,61.0,61.0
|
||||
polynomial 2,49.0,49.0,0.0,91.0,0.0,91.0,0.0,91.0,49.0
|
||||
polynomial 3,77.0,77.0,77.0,77.0,77.0,77.0,77.0,77.0,77.0
|
||||
polynomial 4,64.0,33.0,96.0,11.0,108.0,108.0,26.0,18.0,33.0
|
||||
polynomial 5,inf,0.0,213.0,59.0,16.0,213.0,16.0,16.0,16.0
|
||||
matrix vector mult,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
relu,8.0,8.0,8.0,8.0,8.0,16.0,8.0,8.0,16.0
|
||||
gradient descent,47.0,47.0,47.0,47.0,47.0,47.0,47.0,47.0,47.0
|
||||
mse loss,inf,216.0,64.0,64.0,216.0,64.0,216.0,64.0,64.0
|
||||
conv2d,inf,0.0,0.0,0.0,inf,0.0,0.0,0.0,0.0
|
||||
compound interest,inf,13060.0,10135.0,10135.0,52950.0,9247.0,inf,10135.0,52950.0
|
||||
ddm,inf,815.0,inf,inf,inf,inf,inf,inf,inf
|
||||
present value,107946.0,107946.0,107946.0,107946.0,107946.0,107946.0,107946.0,107946.0,107946.0
|
||||
currency converter,inf,inf,0.0,0.0,25.0,0.0,inf,inf,inf
|
||||
caesar cipher,6.0,6.0,6.0,6.0,6.0,6.0,6.0,6.0,6.0
|
||||
modular add cipher,6.0,6.0,6.0,6.0,6.0,6.0,6.0,6.0,6.0
|
||||
feistel cipher,inf,inf,inf,inf,inf,inf,inf,inf,inf
|
||||
free fall distance,6.0,6.0,64.0,6.0,6.0,64.0,67.0,64.0,6.0
|
||||
kinetic energy,70.0,70.0,54.0,54.0,54.0,54.0,54.0,54.0,54.0
|
||||
potential energy,6.0,6.0,84.0,0.0,6.0,6.0,6.0,6.0,6.0
|
||||
wavelength,81.0,81.0,81.0,81.0,81.0,81.0,81.0,81.0,81.0
|
||||
carbon footprint,174.0,121.0,110.0,92.0,121.0,121.0,110.0,110.0,110.0
|
||||
heat index,16.0,16.0,201.0,16.0,195.0,16.0,124.0,201.0,201.0
|
||||
air quality index,inf,inf,128.0,104.0,inf,104.0,116.0,128.0,128.0
|
||||
solar radiation average,inf,inf,44.0,44.0,44.0,44.0,inf,44.0,inf
|
||||
|
BIN
figures/overall_pass_at_k.png
Normal file
BIN
figures/overall_pass_at_k.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
131
functional_correctness.py
Normal file
131
functional_correctness.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
# File paths
|
||||
SOLUTIONS_FILE = "solutions.json"
|
||||
PROBLEMS_FILE = "problems.json"
|
||||
TEMP_VERILOG_FILE = "temp.v"
|
||||
TEMP_TESTBENCH_FILE = "testbench.v"
|
||||
TCL_SCRIPT_FILE = "run_testbench.tcl"
|
||||
|
||||
def write_tcl():
|
||||
# Generate the TCL script for Vivado
|
||||
tcl_commands = f"""
|
||||
create_project temp_project ./temp_project -force -part xc7z020clg400-1
|
||||
set_property source_mgmt_mode All [current_project]
|
||||
add_files {TEMP_VERILOG_FILE}
|
||||
add_files -fileset sim_1 {TEMP_TESTBENCH_FILE}
|
||||
set_property top {top_module} [get_filesets sim_1]
|
||||
launch_simulation -simset sim_1 -mode behavioral
|
||||
run 3000ns
|
||||
close_sim
|
||||
exit
|
||||
"""
|
||||
# Write the Tcl script
|
||||
with open(TCL_SCRIPT_FILE, "w", encoding="utf-8") as file:
|
||||
file.write(tcl_commands)
|
||||
|
||||
# Function to extract the top module name from the testbench
|
||||
def extract_top_module_name(testbench_file):
|
||||
with open(testbench_file, 'r', encoding="utf-8") as file:
|
||||
for line in file:
|
||||
match = re.search(r'\s*module\s+(\w+)\s*;', line)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
return match.group(1) # Extract module name
|
||||
return None # Return None if no module found
|
||||
|
||||
|
||||
|
||||
def run_functional_correctness():
|
||||
# Load JSON files
|
||||
with open(SOLUTIONS_FILE, "r", encoding="utf-8") as file:
|
||||
solutions_data = json.load(file)
|
||||
|
||||
with open(PROBLEMS_FILE, "r", encoding="utf-8") as file:
|
||||
problems_data = json.load(file)
|
||||
|
||||
# Map module names to their testbenches
|
||||
module_testbenches = {}
|
||||
for category, problems in problems_data.items():
|
||||
for problem in problems:
|
||||
module_name = problem.get("module")
|
||||
testbench_code = problem.get("Testbench")
|
||||
if module_name and testbench_code:
|
||||
module_testbenches[module_name] = testbench_code
|
||||
|
||||
# print(module_testbenches.keys())
|
||||
|
||||
|
||||
|
||||
# Get Vivado path from environment variable
|
||||
vivado_path = os.environ.get("vivado")
|
||||
if not vivado_path:
|
||||
raise EnvironmentError("Vivado environment variable not set.")
|
||||
vivado_path = os.path.join(vivado_path, "vivado.bat")
|
||||
|
||||
# Iterate over solutions and test them
|
||||
for model, categories in solutions_data.items():
|
||||
for category, modules in categories.items():
|
||||
for module_entry in modules:
|
||||
module_name = module_entry["module"]
|
||||
# print(module_name)
|
||||
# print(module_name in module_testbenches.keys())
|
||||
|
||||
if module_name not in module_testbenches:
|
||||
print(f"Skipping {module_name}: No testbench found.")
|
||||
continue
|
||||
|
||||
|
||||
testbench_code = module_testbenches[module_name]
|
||||
solutions = module_entry["solutions"]
|
||||
|
||||
# Iterate over all solutions
|
||||
for solution_entry in solutions:
|
||||
verilog_code = solution_entry["solution"]
|
||||
|
||||
# Write the Verilog design to a file
|
||||
with open(TEMP_VERILOG_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(verilog_code)
|
||||
|
||||
# Write the testbench to a file
|
||||
with open(TEMP_TESTBENCH_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(testbench_code)
|
||||
|
||||
# Extract the top module name
|
||||
top_module = extract_top_module_name(TEMP_TESTBENCH_FILE)
|
||||
if not top_module:
|
||||
print(f"Error: Could not extract top module from {module_name}. Skipping...")
|
||||
solution_entry["pass"] = "Error: Could not extract top module."
|
||||
continue
|
||||
|
||||
print(f"Testing module: {module_name} (Top Module: {top_module})")
|
||||
|
||||
write_tcl()
|
||||
|
||||
# Run Vivado in batch mode
|
||||
print(f"Running Vivado simulation for {module_name}...")
|
||||
process = subprocess.run([vivado_path, "-mode", "batch", "-source", TCL_SCRIPT_FILE], capture_output=True, text=True)
|
||||
|
||||
# Capture output logs
|
||||
output_log = process.stdout + "\n" + process.stderr
|
||||
print(output_log)
|
||||
test_passed = "All tests passed" in output_log
|
||||
|
||||
# Determine pass/fail status
|
||||
if test_passed:
|
||||
solution_entry["pass"] = "true"
|
||||
else:
|
||||
# Extract relevant error messages
|
||||
error_lines = "\n".join(line for line in output_log.split("\n") if "error" or "fail" in line.lower())
|
||||
solution_entry["pass"] = error_lines if error_lines else "Test failed somehow"
|
||||
|
||||
print(f"Test result for {module_name}: {'PASS' if test_passed else 'FAIL'}")
|
||||
|
||||
# Save results after testing each module
|
||||
with open(SOLUTIONS_FILE, "w", encoding="utf-8") as file:
|
||||
json.dump(solutions_data, file, indent=4)
|
||||
|
||||
print("All tests completed.")
|
||||
111
generate_solutions.py
Normal file
111
generate_solutions.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from openai import OpenAI
|
||||
|
||||
def load_prompt_data(filepath: str) -> dict:
|
||||
"""
|
||||
Loads the prompt data from JSON.
|
||||
"""
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def load_solutions(filepath: str) -> dict:
|
||||
"""
|
||||
Loads the existing solutions JSON, or returns a default if file not found.
|
||||
"""
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_solutions(filepath: str, solutions: dict):
|
||||
"""
|
||||
Saves the solutions dictionary to the solutions.json file (pretty-printed).
|
||||
"""
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(solutions, f, indent=4)
|
||||
|
||||
def call_LLMs(client, model: str, problem: str, module_header: str) -> str:
|
||||
"""
|
||||
Calls the OpenAI chat completion endpoint with the given prompt.
|
||||
"""
|
||||
prompt = f"""
|
||||
Here we assume the SystemVerilog is not supported, so don't use the SystemVerilog syntax, such as break statement.
|
||||
Please write a Verilog module that solves the following problem efficiently, using the exact module header below:
|
||||
|
||||
Problem:
|
||||
{problem}
|
||||
|
||||
Module header (must not be changed):
|
||||
{module_header}
|
||||
|
||||
Remember to return only the JSON format:
|
||||
{{
|
||||
"solution": "<verilog code>"
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful Verilog coding assistant. Please return a JSON object with a key 'solution' containing the Verilog code."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
model=model,
|
||||
max_tokens=3000,
|
||||
temperature=1.5,
|
||||
top_p=0.75,
|
||||
)
|
||||
response_content = response.choices[0].message.content.strip()
|
||||
return response_content
|
||||
except Exception as e:
|
||||
print("Error:", str(e))
|
||||
return json.dumps({"solution": f"Error: {str(e)}"})
|
||||
|
||||
def generate_solutions(api_key: str, model_name: str, k: int, prompt_json_file: str = "problems.json", solutions_json_file: str = "solutions.json"):
|
||||
"""
|
||||
Generates Verilog solutions for problems using an LLM.
|
||||
"""
|
||||
# Initialize OpenAI client
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
# Load the problem data
|
||||
prompt_data = load_prompt_data(prompt_json_file)
|
||||
|
||||
# Load or initialize solutions data
|
||||
solutions_data = load_solutions(solutions_json_file)
|
||||
|
||||
if model_name not in solutions_data:
|
||||
solutions_data[model_name] = {}
|
||||
|
||||
for _ in range(k):
|
||||
for category, problems in prompt_data.items():
|
||||
if category not in solutions_data[model_name]:
|
||||
solutions_data[model_name][category] = []
|
||||
|
||||
for item in problems:
|
||||
problem_statement = item.get("Problem", "")
|
||||
module_header = item.get("Module header", "")
|
||||
module_name = item.get("module")
|
||||
|
||||
response_json_str = call_LLMs(client, model_name, problem_statement, module_header)
|
||||
response_json_str = response_json_str.strip('`').replace('json', '').replace('```', '')
|
||||
|
||||
try:
|
||||
response_json = json.loads(response_json_str)
|
||||
verilog_code = response_json.get("solution", "")
|
||||
except json.JSONDecodeError:
|
||||
print(response_json_str)
|
||||
verilog_code = "Error: Invalid JSON response"
|
||||
|
||||
print(f"Processing module: {module_name}")
|
||||
category_list = solutions_data[model_name][category]
|
||||
module_entry = next((entry for entry in category_list if entry.get("module") == module_name), None)
|
||||
|
||||
if module_entry is None:
|
||||
module_entry = {"module": module_name, "solutions": []}
|
||||
category_list.append(module_entry)
|
||||
|
||||
module_entry["solutions"].append({"solution": verilog_code, "pass": ""})
|
||||
save_solutions(solutions_json_file, solutions_data)
|
||||
362
problems.json
Normal file
362
problems.json
Normal file
File diff suppressed because one or more lines are too long
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
matplotlib==3.10.0
|
||||
openai==1.63.2
|
||||
pandas==2.2.3
|
||||
seaborn==0.13.2
|
||||
194
resource_usage.py
Normal file
194
resource_usage.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
|
||||
def extract_module_name(verilog_code):
|
||||
"""
|
||||
Extract the module name from the Verilog code.
|
||||
Assumes the module declaration is of the form:
|
||||
module <module_name> (
|
||||
Returns the module name as a string, or None if not found.
|
||||
"""
|
||||
match = re.search(r'\bmodule\s+(\w+)', verilog_code)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
def parse_optimized(lines):
|
||||
"""
|
||||
Extract resource usage numbers from the main (optimized) report sections.
|
||||
Returns a dictionary with keys: LUT, FF, DSP, BRAM, IO.
|
||||
"""
|
||||
optimized = {"LUT": None, "FF": None, "DSP": None, "BRAM": None, "IO": None}
|
||||
for line in lines:
|
||||
m = re.search(r'\|\s*Slice LUTs\*?\s*\|\s*(\d+)', line)
|
||||
if m:
|
||||
optimized["LUT"] = int(m.group(1))
|
||||
m = re.search(r'\|\s*Slice Registers\s*\|\s*(\d+)', line)
|
||||
if m:
|
||||
optimized["FF"] = int(m.group(1))
|
||||
m = re.search(r'\|\s*DSPs\s*\|\s*(\d+)', line)
|
||||
if m:
|
||||
optimized["DSP"] = int(m.group(1))
|
||||
m = re.search(r'\|\s*Block RAM Tile\s*\|\s*(\d+)', line)
|
||||
if m:
|
||||
optimized["BRAM"] = int(m.group(1))
|
||||
m = re.search(r'\|\s*Bonded IOB\s*\|\s*(\d+)', line)
|
||||
if m:
|
||||
optimized["IO"] = int(m.group(1))
|
||||
return optimized
|
||||
|
||||
def extract_primitives_section(lines):
|
||||
"""
|
||||
Extracts all lines between the "7. Primitives" header and the "8. Black Boxes" header.
|
||||
"""
|
||||
start_marker = "7. Primitives"
|
||||
end_marker = "8. Black Boxes"
|
||||
start_idx = None
|
||||
end_idx = None
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
if start_idx is None and start_marker in line and (idx + 1 < len(lines) and "------" in lines[idx + 1]):
|
||||
start_idx = idx
|
||||
elif start_idx is not None and end_marker in line and (idx + 1 < len(lines) and "------" in lines[idx + 1]):
|
||||
end_idx = idx
|
||||
break
|
||||
|
||||
if start_idx is None or end_idx is None:
|
||||
return []
|
||||
return lines[start_idx:end_idx]
|
||||
|
||||
def parse_primitives_section(lines):
|
||||
"""
|
||||
Parses the primitives section lines to accumulate resource usage.
|
||||
Returns a dictionary with keys: LUT, FF, DSP, BRAM, IO.
|
||||
In this example:
|
||||
- For LUT: sums up any primitive whose name starts with "LUT" (e.g., LUT2, LUT3, ...)
|
||||
- For IO: sums the usage of IBUF and OBUF.
|
||||
"""
|
||||
resources = {"LUT": 0, "FF": 0, "DSP": 0, "BRAM": 0, "IO": 0}
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if not stripped_line.startswith("|"):
|
||||
continue
|
||||
parts = stripped_line.split("|")
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
ref_name = parts[1].strip()
|
||||
used_str = parts[2].strip()
|
||||
try:
|
||||
used = int(used_str)
|
||||
except ValueError:
|
||||
continue
|
||||
if ref_name.startswith("LUT"):
|
||||
resources["LUT"] += used
|
||||
if ref_name in ("IBUF", "OBUF"):
|
||||
resources["IO"] += used
|
||||
# (Add additional processing for FF, DSP, BRAM if necessary.)
|
||||
return resources
|
||||
|
||||
def run_synthesis(solution_code):
|
||||
"""
|
||||
Writes the given Verilog solution to a temporary file,
|
||||
creates a Tcl script for Vivado to run synthesis and generate a utilization report,
|
||||
runs Vivado in batch mode, and parses the resource usage report.
|
||||
Returns a dictionary with keys "optimized" and "primitives" containing resource usage.
|
||||
"""
|
||||
# Write the Verilog code to a temporary file.
|
||||
verilog_file = "temp.v"
|
||||
with open(verilog_file, "w") as f:
|
||||
f.write(solution_code)
|
||||
|
||||
# Extract the module name from the solution code.
|
||||
top_module = extract_module_name(solution_code)
|
||||
print(top_module)
|
||||
if top_module is None:
|
||||
print("Could not extract module name; using 'temp_top' as a default.")
|
||||
top_module = "temp_top"
|
||||
|
||||
vivado_project = "temp_project"
|
||||
tcl_script = "synthesis_script.tcl"
|
||||
|
||||
# Get the Vivado installation path from the environment variable.
|
||||
vivado_path_env = os.environ.get("vivado")
|
||||
if vivado_path_env is None:
|
||||
print("Error: 'vivado' environment variable is not set.")
|
||||
return None
|
||||
vivado_path = os.path.join(vivado_path_env, "vivado.bat")
|
||||
|
||||
# Create the Vivado Tcl script.
|
||||
tcl_commands = f"""
|
||||
create_project {vivado_project} -force -part xc7z020clg400-1
|
||||
add_files {verilog_file}
|
||||
set_property top {top_module} [current_fileset]
|
||||
|
||||
# Run synthesis only (no simulation)
|
||||
synth_design -top {top_module}
|
||||
|
||||
# Generate resource utilization report
|
||||
report_utilization -file resource_usage.rpt
|
||||
|
||||
quit
|
||||
"""
|
||||
with open(tcl_script, "w") as file:
|
||||
file.write(tcl_commands)
|
||||
|
||||
# Run Vivado in batch mode using the generated Tcl script.
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[vivado_path, "-mode", "batch", "-source", tcl_script],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("Synthesis failed:", e)
|
||||
return None
|
||||
print(result.stdout)
|
||||
# Check for the success message in the output.
|
||||
if "Finished Writing Synthesis Report" in result.stdout:
|
||||
# Read the resource utilization report.
|
||||
with open("resource_usage.rpt", "r") as f:
|
||||
report_lines = f.readlines()
|
||||
optimized_resources = parse_optimized(report_lines)
|
||||
primitives_section = extract_primitives_section(report_lines)
|
||||
primitives_resources = (parse_primitives_section(primitives_section)
|
||||
if primitives_section else {})
|
||||
return {"optimized": optimized_resources, "primitives": primitives_resources}
|
||||
else:
|
||||
print("Synthesis did not complete successfully.")
|
||||
return None
|
||||
|
||||
def run_resource_usage():
|
||||
# Load the original JSON.
|
||||
input_json_file = "solutions.json" # Update this file name if needed.
|
||||
with open(input_json_file, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Traverse all top-level keys (e.g., "4o") and all subcategories.
|
||||
for top_key, top_value in data.items():
|
||||
# print(top_value.keys())
|
||||
# exit()
|
||||
# top_value should be a dict with categories (e.g., "Combinational Logic", "Finite State Machines", etc.)
|
||||
for category, module_list in top_value.items():
|
||||
# if category == "Combinational Logic":
|
||||
# continue
|
||||
for module in module_list:
|
||||
for sol in module["solutions"]:
|
||||
if sol.get("pass", "").strip().lower() == "true":
|
||||
solution_code = sol["solution"]
|
||||
print(f"Running synthesis for module '{module['module']}' in category '{category}'")
|
||||
resource_usage = run_synthesis(solution_code)
|
||||
if resource_usage:
|
||||
sol["resource usage"] = resource_usage
|
||||
else:
|
||||
sol["resource usage"] = {"optimized": {}, "primitives": {}}
|
||||
else:
|
||||
sol["resource usage"] = {"optimized": {}, "primitives": {}}
|
||||
|
||||
# Write the updated JSON (with resource usage added) to a new file.
|
||||
output_json_file = "solutions.json"
|
||||
with open(output_json_file, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
print(f"Updated JSON written to {output_json_file}")
|
||||
|
||||
|
||||
42
setup.py
Normal file
42
setup.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import argparse
|
||||
import subprocess
|
||||
from generate_solutions import generate_solutions
|
||||
from functional_correctness import run_functional_correctness, run_resource_usage
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Command-line interface for Verilog solution generation and evaluation.")
|
||||
|
||||
parser.add_argument("-generate_solutions", nargs=3, metavar=("MODEL_NAME", "K", "API_KEY"), help="Generate Verilog solutions using the specified model, number of iterations, and API key.")
|
||||
parser.add_argument("-functional_correctness", action="store_true", help="Run functional correctness evaluation.")
|
||||
parser.add_argument("-resource_usage", action="store_true", help="Run resource usage evaluation.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.generate_solutions:
|
||||
model_name, k, api_key = args.generate_solutions
|
||||
generate_solutions(api_key, model_name, int(k))
|
||||
|
||||
if args.functional_correctness:
|
||||
run_functional_correctness()
|
||||
subprocess.run(["python", "./evaluate/count_pass.py"])
|
||||
subprocess.run(["python", "./evaluate/plot_pass.py"])
|
||||
|
||||
if args.resource_usage:
|
||||
run_resource_usage()
|
||||
subprocess.run(["python", "./evaluate/count_resource.py"])
|
||||
else:
|
||||
if args.functional_correctness:
|
||||
run_functional_correctness()
|
||||
subprocess.run(["python", "./evaluate/count_pass.py"])
|
||||
subprocess.run(["python", "./evaluate/plot_pass.py"])
|
||||
|
||||
if args.resource_usage:
|
||||
run_resource_usage()
|
||||
subprocess.run(["python", "./evaluate/count_resource.py"])
|
||||
|
||||
if args.resource_usage:
|
||||
run_resource_usage()
|
||||
subprocess.run(["python", "./evaluate/count_resource.py"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
60
solutions/sample.json
Normal file
60
solutions/sample.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"gpt-3.5-turbo": {
|
||||
"Combinational Logic": [
|
||||
{
|
||||
"module": "parity_8bit",
|
||||
"solutions": [
|
||||
{
|
||||
"solution": "module parity_8bit (input [7:0] in, output out); assign out = in[0] ^ in[1] ^ in[2] ^ in[3] ^ in[4] ^ in[5] ^ in[6] ^ in[7]; endmodule",
|
||||
"pass": "true",
|
||||
"resource usage": {
|
||||
"optimized": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
},
|
||||
"primitives": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"solution": "module parity_8bit (input [7:0] in, output out); reg parity; integer i; always @(*) begin parity = 1'b0; for(i=0; i<8; i=i+1) begin if(in[i] == 1'b1) parity = ~parity; end end assign out = parity; endmodule",
|
||||
"pass": "true",
|
||||
"resource usage": {
|
||||
"optimized": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
},
|
||||
"primitives": {
|
||||
"LUT": 2,
|
||||
"FF": 0,
|
||||
"DSP": 0,
|
||||
"BRAM": 0,
|
||||
"IO": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Finite State Machines": [
|
||||
{
|
||||
"module": "fsm_3state",
|
||||
"solutions": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"gpt-4o":{
|
||||
|
||||
}
|
||||
}
|
||||
117752
solutions/solutions.json
Normal file
117752
solutions/solutions.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user