Skip to content

Commit 9bd8a66

Browse files
committed
Adds benchmarking scripts for llm.
1 parent 051e4a6 commit 9bd8a66

13 files changed

Lines changed: 3292 additions & 715 deletions

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,14 @@ am workspace create test
4646

4747
#### Example
4848
An example implementation can be found [here](https://github.com/ppak10/additive-manufacturing-agent)
49+
50+
## Benchmark
51+
To run the benchmark commands, install the optional `benchmark` dependencies:
52+
53+
```bash
54+
uv pip install "additive-manufacturing[benchmark]"
55+
```
56+
57+
```bash
58+
am benchmark <model>
59+
```

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ Issues = "https://github.com/ppak10/additive-manufacturing/issues"
3333
am = "am.cli:app"
3434

3535
[project.optional-dependencies]
36+
benchmark = [
37+
"accelerate>=1.12.0",
38+
"datasets>=4.6.0",
39+
"sentence-transformers>=5.2.3",
40+
"torch>=2.10.0",
41+
"transformers>=5.2.0",
42+
]
3643
cuda12 = [
3744
"jax[cuda12]>=0.6.2",
3845
]

src/am/benchmark/__init__.py

Whitespace-only changes.

src/am/benchmark/cli.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import typer
2+
3+
4+
def register_benchmark(app: typer.Typer):
5+
from pathlib import Path
6+
from rich import print as rprint
7+
8+
from am.benchmark.llm import benchmark_llm, TASKS
9+
from am.cli.options import NumProc
10+
from wa.cli.options import WorkspaceOption
11+
12+
@app.command(name="benchmark", rich_help_panel="Benchmark Commands")
13+
def benchmark(
14+
model: str,
15+
tasks: list[str] = typer.Option(TASKS, help="Tasks to benchmark"),
16+
batch_size: int = typer.Option(
17+
8, help="Number of prompts to process in parallel."
18+
),
19+
url: str = typer.Option(
20+
None, help="URL of a running vLLM server (e.g. http://localhost:8000/v1)."
21+
),
22+
workspace_option: WorkspaceOption = None,
23+
num_proc: NumProc = 1,
24+
) -> None:
25+
"""
26+
Runs additive manufacturing benchmark for large language models.
27+
"""
28+
29+
from wa.cli.utils import get_workspace
30+
31+
workspace = get_workspace(workspace_option)
32+
33+
try:
34+
workspace_folder_path = None
35+
if workspace:
36+
model_name = model.replace("/", "--")
37+
workspace_folder = workspace.create_folder(
38+
name_or_path=Path("benchmarks") / model_name, append_timestamp=True
39+
)
40+
workspace_folder_path = workspace_folder.path
41+
42+
benchmark_llm(
43+
model=model,
44+
tasks=tasks,
45+
batch_size=batch_size,
46+
url=url,
47+
num_proc=num_proc,
48+
out_path=workspace_folder_path,
49+
)
50+
51+
except Exception as e:
52+
import traceback
53+
54+
traceback.print_exc()
55+
rprint(f"⚠️ [yellow]Unable to run benchmark for {model}: {e}[/yellow]")
56+
raise typer.Exit(code=1)
57+
58+
return benchmark

src/am/benchmark/llm/__init__.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import importlib
2+
from pathlib import Path
3+
4+
from .constants import TASKS
5+
from .inference import _run_openai_compatible, _run_transformers
6+
from .general_knowledge_multiple_choice import (
7+
_benchmark_general_knowledge_multiple_choice,
8+
)
9+
from .general_knowledge_short_answer import _benchmark_general_knowledge_short_answer
10+
from .melt_pool_geometry_prediction import _benchmark_melt_pool_geometry_prediction
11+
12+
13+
def benchmark_llm(
14+
model: str,
15+
tasks: list[str] = TASKS,
16+
num_proc: int = 1,
17+
batch_size: int = 8,
18+
url: str | None = None,
19+
out_path: Path | None = None,
20+
) -> dict:
21+
22+
missing = [
23+
pkg
24+
for pkg in ("datasets", "sentence_transformers", "transformers")
25+
if importlib.util.find_spec(pkg) is None
26+
]
27+
if missing:
28+
raise ImportError(
29+
f"Missing benchmark packages: {', '.join(missing)}. "
30+
'Run: uv pip install "additive-manufacturing[benchmark]"'
31+
)
32+
33+
if url is not None:
34+
print(f"Using vLLM server: {url}")
35+
runner = lambda questions, max_tokens: _run_openai_compatible(
36+
url, model, questions, max_tokens
37+
)
38+
else:
39+
from transformers import pipeline
40+
41+
print(f"Loading model: {model}")
42+
llm_pipeline = pipeline(
43+
"text-generation",
44+
model=model,
45+
return_full_text=False,
46+
device_map="auto",
47+
)
48+
runner = lambda questions, max_tokens: _run_transformers(
49+
llm_pipeline, questions, batch_size, max_tokens
50+
)
51+
52+
reports = {}
53+
for task in tasks:
54+
if task == "general_knowledge_multiple_choice":
55+
reports[task] = _benchmark_general_knowledge_multiple_choice(
56+
runner, model, num_proc, out_path
57+
)
58+
elif task == "general_knowledge_short_answer":
59+
reports[task] = _benchmark_general_knowledge_short_answer(
60+
runner, model, num_proc, out_path
61+
)
62+
elif task == "melt_pool_geometry_prediction":
63+
reports[task] = _benchmark_melt_pool_geometry_prediction(
64+
runner, model, num_proc, out_path
65+
)
66+
else:
67+
print(f"Unknown task '{task}', skipping.")
68+
69+
return reports

src/am/benchmark/llm/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
DATASET_NAME = "ppak10/Additive-Manufacturing-Benchmark"
2+
EVALUATOR_MODEL = "all-MiniLM-L6-v2"
3+
RUBRIC_EVALUATOR_MODEL = "cross-encoder/nli-deberta-v3-large"
4+
MAX_NEW_TOKENS_SHORT_ANSWER = 4096
5+
MAX_NEW_TOKENS_MELT_POOL = 1024
6+
MAX_NEW_TOKENS_MULTIPLE_CHOICE = 256
7+
TASKS = [
8+
"general_knowledge_multiple_choice",
9+
"general_knowledge_short_answer",
10+
"melt_pool_geometry_prediction",
11+
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import json
2+
import re
3+
from datetime import datetime, timezone
4+
from pathlib import Path
5+
6+
from datasets import load_dataset
7+
8+
from .constants import DATASET_NAME, MAX_NEW_TOKENS_MULTIPLE_CHOICE
9+
10+
11+
def _build_mc_prompt(row: dict) -> str:
12+
lines = [
13+
"You are an expert in additive manufacturing.",
14+
"Answer the following multiple-choice question by responding with only the letter of the correct answer (A, B, C, or D).",
15+
"",
16+
f"Question: {row['question']}",
17+
"",
18+
"Choices:",
19+
]
20+
for choice in row["choices"]:
21+
lines.append(f" {choice['label']}. {choice['text']}")
22+
lines += [
23+
"",
24+
"Answer (single letter only):",
25+
]
26+
return "\n".join(lines)
27+
28+
29+
def _parse_mc_answer(response: str) -> str | None:
30+
"""Extract the first A/B/C/D letter from a model response."""
31+
match = re.search(r"\b([A-D])\b", response.strip())
32+
return match.group(1) if match else None
33+
34+
35+
def _benchmark_general_knowledge_multiple_choice(
36+
runner,
37+
model: str,
38+
num_proc: int,
39+
out_path: Path | None,
40+
) -> dict:
41+
config = "general_knowledge_multiple_choice"
42+
print(f"\n[{config}] Loading dataset...")
43+
train_data = load_dataset(DATASET_NAME, config, num_proc=num_proc)["train"]
44+
45+
prompts = [_build_mc_prompt(row) for row in train_data]
46+
responses = runner(prompts, MAX_NEW_TOKENS_MULTIPLE_CHOICE)
47+
48+
results = []
49+
correct_count = 0
50+
no_response_count = 0
51+
52+
for i, row in enumerate(train_data):
53+
predicted = _parse_mc_answer(responses[i])
54+
correct = row["correct_answer"]
55+
is_correct = predicted == correct if predicted is not None else False
56+
if predicted is None:
57+
no_response_count += 1
58+
if is_correct:
59+
correct_count += 1
60+
61+
results.append(
62+
{
63+
"source": row["source"],
64+
"process": row["process"],
65+
"question": row["question"],
66+
"choices": row["choices"],
67+
"correct_answer": correct,
68+
"response": responses[i],
69+
"predicted": predicted,
70+
"is_correct": is_correct,
71+
}
72+
)
73+
74+
total = len(results)
75+
answered = total - no_response_count
76+
accuracy = round(correct_count / total, 4) if total else 0.0
77+
78+
report = {
79+
"model": model,
80+
"dataset": DATASET_NAME,
81+
"config": config,
82+
"timestamp": datetime.now(timezone.utc).isoformat(),
83+
"total_questions": total,
84+
"no_response": no_response_count,
85+
"answered": answered,
86+
"correct": correct_count,
87+
"accuracy": accuracy,
88+
"results": results,
89+
}
90+
91+
_print_gkmc_report(report)
92+
93+
if out_path is not None:
94+
report_file = Path(out_path) / f"{config}.json"
95+
with open(report_file, "w") as f:
96+
json.dump(report, f, indent=2)
97+
print(f"Report saved to: {report_file}")
98+
99+
return report
100+
101+
102+
def _print_gkmc_report(report: dict):
103+
sep = "-" * 60
104+
print(sep)
105+
print("General Knowledge Multiple Choice — Report")
106+
print(sep)
107+
print(f"Model: {report['model']}")
108+
print(f"Total questions: {report['total_questions']}")
109+
print(f"No response: {report['no_response']}")
110+
print(f"Answered: {report['answered']}")
111+
print(f"Correct: {report['correct']}")
112+
print(f"Accuracy: {report['accuracy']:.2%}")
113+
print(sep)
114+
for i, r in enumerate(report["results"], 1):
115+
mark = "✓" if r["is_correct"] else "✗"
116+
print(f"[{i:>3}] {mark} {r['source']} ({r['process']})")
117+
print(f" Q: {r['question'][:100]}")
118+
print(f" Predicted: {r['predicted']} Correct: {r['correct_answer']}")
119+
print(sep)
120+
print(
121+
f"Accuracy: {report['accuracy']:.2%} ({report['correct']}/{report['total_questions']})"
122+
)
123+
print(sep)

0 commit comments

Comments
 (0)