Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ target_link_libraries(benchmarks libgemma hwy hwy_contrib nlohmann_json::nlohman
add_executable(debug_prompt evals/debug_prompt.cc)
target_link_libraries(debug_prompt libgemma hwy hwy_contrib nlohmann_json::nlohmann_json)

add_library(model_comparison evals/model_comparison.cc)
target_include_directories(model_comparison PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

add_executable(model_comparison_test evals/model_comparison_test.cc)
target_link_libraries(model_comparison_test model_comparison)

add_executable(gemma_mmlu evals/run_mmlu.cc)
target_link_libraries(gemma_mmlu libgemma model_comparison hwy hwy_contrib
nlohmann_json::nlohmann_json)

## Tests
set(GEMMA_ENABLE_TESTS OFF CACHE BOOL "Enable Gemma tests")
if (GEMMA_ENABLE_TESTS)
Expand Down Expand Up @@ -445,6 +455,27 @@ endif() # GEMMA_ENABLE_TESTS

## Tools

# Standalone W8A8 vs BF16 MatMul benchmark (no gtest). The _biased variant
# forces the encoding x86 uses, to show what the bias correction costs.
add_executable(bench_matmul_i8 ops/bench_matmul_i8.cc)
target_link_libraries(bench_matmul_i8 libgemma hwy hwy_contrib)

add_executable(bench_matmul_i8_biased ops/bench_matmul_i8.cc)
target_compile_definitions(bench_matmul_i8_biased PRIVATE
GEMMA_MM_I8_FORCE_BIASED_B=1)
target_link_libraries(bench_matmul_i8_biased libgemma hwy hwy_contrib)

# W8A8 correctness, built once per A encoding so that the x86 biased-u8 path
# is covered on non-x86 hosts as well.
add_executable(matmul_i8_test ops/matmul_i8_test.cc)
target_compile_definitions(matmul_i8_test PRIVATE GEMMA_MM_I8_FORCE_BIASED_B=0)
target_link_libraries(matmul_i8_test libgemma hwy hwy_contrib)

add_executable(matmul_i8_biased_test ops/matmul_i8_test.cc)
target_compile_definitions(matmul_i8_biased_test PRIVATE
GEMMA_MM_I8_FORCE_BIASED_B=1)
target_link_libraries(matmul_i8_biased_test libgemma hwy hwy_contrib)

add_executable(migrate_weights io/migrate_weights.cc)
target_link_libraries(migrate_weights libgemma hwy hwy_contrib)

Expand Down
133 changes: 133 additions & 0 deletions evals/compare_mmlu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Compare baseline and compressed gemma_mmlu outputs.

Each input is the stdout captured from gemma_mmlu and may contain unrelated
lines. Only lines beginning with ``MMLU_RESULT `` are parsed.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any


RESULT_PREFIX = "MMLU_RESULT "


def load_results(path: Path) -> dict[int, dict[str, Any]]:
results: dict[int, dict[str, Any]] = {}
with path.open(encoding="utf-8") as source:
for line_number, line in enumerate(source, start=1):
if not line.startswith(RESULT_PREFIX):
continue
try:
result = json.loads(line[len(RESULT_PREFIX) :])
question_id = int(result["id"])
result["correct"] = bool(result["correct"])
result["expected"] = str(result["expected"])
result["predicted"] = str(result["predicted"])
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
raise ValueError(
f"{path}:{line_number}: invalid MMLU_RESULT: {error}"
) from error
if question_id in results:
raise ValueError(f"{path}:{line_number}: duplicate id {question_id}")
results[question_id] = result

if not results:
raise ValueError(f"{path}: no {RESULT_PREFIX.strip()} lines found")
return results


def compare_results(
baseline: dict[int, dict[str, Any]], variant: dict[int, dict[str, Any]]
) -> dict[str, int | float]:
baseline_ids = set(baseline)
variant_ids = set(variant)
if baseline_ids != variant_ids:
missing = sorted(baseline_ids - variant_ids)
extra = sorted(variant_ids - baseline_ids)
raise ValueError(
"result IDs differ: "
f"missing from variant={missing[:10]}, extra in variant={extra[:10]}"
)

correct_to_incorrect = 0
incorrect_to_correct = 0
wrong_to_wrong_changes = 0
answer_changes = 0
baseline_correct = 0
variant_correct = 0

for question_id in sorted(baseline_ids):
base = baseline[question_id]
changed = variant[question_id]
if base["expected"] != changed["expected"]:
raise ValueError(
f"id {question_id}: expected answers differ: "
f"{base['expected']!r} != {changed['expected']!r}"
)

base_correct = base["correct"]
changed_correct = changed["correct"]
baseline_correct += int(base_correct)
variant_correct += int(changed_correct)
answer_changed = base["predicted"] != changed["predicted"]
answer_changes += int(answer_changed)

if base_correct and not changed_correct:
correct_to_incorrect += 1
elif not base_correct and changed_correct:
incorrect_to_correct += 1
elif not base_correct and not changed_correct and answer_changed:
wrong_to_wrong_changes += 1

samples = len(baseline_ids)
flips = correct_to_incorrect + incorrect_to_correct
return {
"samples": samples,
"baseline_correct": baseline_correct,
"variant_correct": variant_correct,
"baseline_accuracy": baseline_correct / samples,
"variant_accuracy": variant_correct / samples,
"accuracy_delta": (variant_correct - baseline_correct) / samples,
"correct_to_incorrect": correct_to_incorrect,
"incorrect_to_correct": incorrect_to_correct,
"flips": flips,
"flips_fraction": flips / samples,
"flips_percent": 100.0 * flips / samples,
"wrong_to_wrong_changes": wrong_to_wrong_changes,
"answer_changes": answer_changes,
"answer_changes_fraction": answer_changes / samples,
"answer_changes_percent": 100.0 * answer_changes / samples,
}


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compare baseline and variant gemma_mmlu output streams."
)
parser.add_argument("baseline", type=Path, help="baseline gemma_mmlu stdout")
parser.add_argument("variant", type=Path, help="variant gemma_mmlu stdout")
return parser.parse_args()


def main() -> int:
args = parse_args()
try:
metrics = compare_results(
load_results(args.baseline), load_results(args.variant)
)
except (OSError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
return 1

print(f"MMLU_FLIPS {json.dumps(metrics, sort_keys=True)}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
77 changes: 77 additions & 0 deletions evals/compare_mmlu_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3

import json
import tempfile
import unittest
from pathlib import Path

from compare_mmlu import compare_results, load_results


def result(question_id: int, expected: str, predicted: str) -> dict[str, object]:
return {
"id": question_id,
"expected": expected,
"predicted": predicted,
"correct": expected == predicted,
}


class CompareMmluTest(unittest.TestCase):
def test_flip_counts(self) -> None:
baseline = {
1: result(1, "A", "A"),
2: result(2, "A", "B"),
3: result(3, "A", "C"),
4: result(4, "D", "D"),
}
variant = {
1: result(1, "A", "B"),
2: result(2, "A", "A"),
3: result(3, "A", "D"),
4: result(4, "D", "D"),
}

metrics = compare_results(baseline, variant)

self.assertEqual(metrics["correct_to_incorrect"], 1)
self.assertEqual(metrics["incorrect_to_correct"], 1)
self.assertEqual(metrics["flips"], 2)
self.assertEqual(metrics["flips_percent"], 50.0)
self.assertEqual(metrics["wrong_to_wrong_changes"], 1)
self.assertEqual(metrics["answer_changes"], 3)
self.assertEqual(metrics["answer_changes_percent"], 75.0)
self.assertEqual(metrics["accuracy_delta"], 0.0)

def test_loads_prefixed_results_and_ignores_other_lines(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "run.log"
rows = [result(7, "B", "B"), result(8, "C", "A")]
path.write_text(
"startup noise\n"
+ "\n".join(f"MMLU_RESULT {json.dumps(row)}" for row in rows)
+ "\nMMLU_SUMMARY {}\n",
encoding="utf-8",
)

loaded = load_results(path)

self.assertEqual(set(loaded), {7, 8})
self.assertTrue(loaded[7]["correct"])
self.assertFalse(loaded[8]["correct"])

def test_requires_matching_question_ids(self) -> None:
with self.assertRaisesRegex(ValueError, "result IDs differ"):
compare_results(
{1: result(1, "A", "A")}, {2: result(2, "A", "A")}
)

def test_requires_matching_expected_answers(self) -> None:
with self.assertRaisesRegex(ValueError, "expected answers differ"):
compare_results(
{1: result(1, "A", "A")}, {1: result(1, "B", "B")}
)


if __name__ == "__main__":
unittest.main()
Loading