Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

475 lines
16 KiB
Python

"""
MMMU evaluation for VLMs using the run_eval simple-evals interface.
"""
from __future__ import annotations
import base64
import io
import re
from typing import List, Optional, Tuple
from datasets import concatenate_datasets, load_dataset
from PIL import Image
from sglang.test import simple_eval_common as common
from sglang.test.simple_eval_common import (
HTML_JINJA,
Eval,
EvalResult,
SamplerBase,
SingleEvalResult,
map_with_progress,
)
class MMMUVLMEval(Eval):
DOMAIN_CAT2SUB_CAT = {
"Art and Design": ["Art", "Art_Theory", "Design", "Music"],
"Business": ["Accounting", "Economics", "Finance", "Manage", "Marketing"],
"Science": ["Biology", "Chemistry", "Geography", "Math", "Physics"],
"Health and Medicine": [
"Basic_Medical_Science",
"Clinical_Medicine",
"Diagnostics_and_Laboratory_Medicine",
"Pharmacy",
"Public_Health",
],
"Humanities and Social Science": [
"History",
"Literature",
"Sociology",
"Psychology",
],
"Tech and Engineering": [
"Agriculture",
"Architecture_and_Engineering",
"Computer_Science",
"Electronics",
"Energy_and_Power",
"Materials",
"Mechanical_Engineering",
],
}
def __init__(
self,
num_examples: Optional[int] = 100,
num_threads: int = 32,
seed: int = 42,
response_answer_regex: str = None,
):
"""Create MMMU VLM eval (Math subset, 100 fixed samples by default)."""
self.num_examples = num_examples
self.num_threads = num_threads
self.seed = seed
# Prepare samples deterministically across all MMMU subjects (validation split)
self.samples = self._prepare_mmmu_samples(self.num_examples)
# For example, "<\|begin_of_box\|>foo<\|end_of_box\|>" could be used to extract "foo" as the answer from the response text
self.response_answer_regex = response_answer_regex
@staticmethod
def _to_data_uri(image: Image.Image) -> str:
if image.mode == "RGBA":
image = image.convert("RGB")
buf = io.BytesIO()
image.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
return f"data:image/png;base64,{b64}"
@staticmethod
def _build_mc_mapping(options: List[str]) -> Tuple[dict, List[str]]:
index2ans = {}
all_choices = []
ch = ord("A")
for opt in options:
letter = chr(ch)
index2ans[letter] = opt
all_choices.append(letter)
ch += 1
return index2ans, all_choices
def _prepare_mmmu_samples(self, k: int) -> List[dict]:
# Subjects and domains copied from MMMU data_utils to categorize results
subjects: List[str] = []
for subs in self.DOMAIN_CAT2SUB_CAT.values():
subjects.extend(subs)
# Load validation split of each subject
datasets = []
for subj in subjects:
try:
d = load_dataset("MMMU/MMMU", subj, split="validation")
# attach subject info via transform
d = d.add_column("__subject__", [subj] * len(d))
datasets.append(d)
except Exception:
continue
if not datasets:
raise RuntimeError("Failed to load MMMU datasets")
merged = concatenate_datasets(datasets)
# Deterministic selection: sort by id (fallback to subject+index)
def _key(idx):
ex = merged[idx]
return str(ex.get("id", f"{ex['__subject__']}:{idx}"))
order = sorted(range(len(merged)), key=_key)
picked_indices = order[:k]
samples: List[dict] = []
for idx in picked_indices:
ex = merged[idx]
subject = ex["__subject__"]
image = ex.get("image_1")
if image is None or not hasattr(image, "convert"):
continue
data_uri = self._to_data_uri(image)
question = ex.get("question", "")
answer = ex.get("answer")
raw_options = ex.get("options")
question_type = "open"
index2ans = None
all_choices = None
options = None
if raw_options:
try:
options = (
raw_options
if isinstance(raw_options, list)
else list(eval(raw_options))
)
if isinstance(options, list) and len(options) > 0:
index2ans, all_choices = self._build_mc_mapping(options)
question_type = "multiple-choice"
except Exception:
options = None
# Build final textual prompt; include choices if MC
prompt_text = f"{question}\n"
if options:
letters = [chr(ord("A") + i) for i in range(len(options))]
for letter, opt in zip(letters, options):
prompt_text += f"{letter}. {opt}\n"
prompt_text += (
"\nAnswer the following multiple-choice question. "
"The last line of your response should be of the "
"following format: 'Answer: $LETTER' (without quotes) "
"where LETTER is one of the options. "
"Think step by step before answering."
)
else:
prompt_text += "\nAnswer: "
samples.append(
{
"id": ex.get("id", f"{subject}:{idx}"),
"final_input_prompt": prompt_text,
"image_data": data_uri,
"answer": answer,
"question_type": question_type,
"index2ans": index2ans,
"all_choices": all_choices,
"category": subject,
}
)
return samples
@staticmethod
def _split_prompt_for_image(prompt: str) -> tuple[str, str]:
"""Split a prompt containing an inline image tag into prefix and suffix.
If no tag is present, treat the whole prompt as prefix and empty suffix.
"""
if "<" in prompt and ">" in prompt:
prefix = prompt.split("<")[0]
suffix = prompt.split(">", 1)[1]
return prefix, suffix
return prompt, ""
@staticmethod
def build_chat_messages_from_prompt(prompt: str, image_data) -> List:
"""Split a prompt containing an inline image tag into prefix and suffix.
If no tag is present, treat the whole prompt as prefix and empty suffix.
"""
# Build a vision+text message for OpenAI-compatible API
prefix, suffix = MMMUVLMEval._split_prompt_for_image(prompt)
content: List[dict] = []
if prefix:
content.append({"type": "text", "text": prefix})
content.append({"type": "image_url", "image_url": {"url": image_data}})
if suffix:
content.append({"type": "text", "text": suffix})
prompt_messages = [{"role": "user", "content": content}]
return prompt_messages
def __call__(self, sampler: SamplerBase) -> EvalResult:
def fn(sample: dict):
prompt = sample["final_input_prompt"]
image_data = sample["image_data"]
prompt_messages = MMMUVLMEval.build_chat_messages_from_prompt(
prompt, image_data
)
# Sample
response_text = sampler(prompt_messages)
response_text = response_text or ""
if self.response_answer_regex:
match = (
re.search(self.response_answer_regex, response_text)
if response_text is not None
else None
)
response_text = (
match.group(1).strip() if match is not None else response_text
)
# Parse and score
gold = sample["answer"]
if (
sample["question_type"] == "multiple-choice"
and sample["all_choices"]
and sample["index2ans"]
):
pred = _parse_multi_choice_response(
response_text, sample["all_choices"], sample["index2ans"]
)
score = 1.0 if (gold is not None and pred == gold) else 0.0
extracted_answer = pred
else:
parsed_list = _parse_open_response(response_text)
score = (
1.0 if (gold is not None and _eval_open(gold, parsed_list)) else 0.0
)
extracted_answer = ", ".join(map(str, parsed_list))
html_rendered = common.jinja_env.from_string(HTML_JINJA).render(
prompt_messages=prompt_messages,
next_message=dict(content=response_text, role="assistant"),
score=score,
correct_answer=gold,
extracted_answer=extracted_answer,
)
convo = prompt_messages + [dict(content=response_text, role="assistant")]
return SingleEvalResult(
html=html_rendered,
score=score,
metrics={"__category__": sample["category"]},
convo=convo,
)
results = map_with_progress(fn, self.samples, self.num_threads)
# Build category table and overall accuracy
# Gather per-sample correctness and category
per_cat_total: dict[str, int] = {}
per_cat_correct: dict[str, int] = {}
htmls = []
convos = []
scores: List[float] = []
for r in results:
# __category__ stored under metrics
cat = r.metrics.get("__category__") if r.metrics else None
if cat is None:
cat = "Unknown"
per_cat_total[cat] = per_cat_total.get(cat, 0) + 1
if r.score:
per_cat_correct[cat] = per_cat_correct.get(cat, 0) + 1
htmls.append(r.html)
convos.append(r.convo)
if r.score is not None:
scores.append(r.score)
evaluation_result = {}
for cat, tot in per_cat_total.items():
corr = per_cat_correct.get(cat, 0)
acc = (corr / tot) if tot > 0 else 0.0
evaluation_result[cat] = {"acc": round(acc, 3), "num_example": tot}
printable_results = {}
# Domains first
for domain, cats in self.DOMAIN_CAT2SUB_CAT.items():
acc_sum = 0.0
num_sum = 0
for cat in cats:
if cat in evaluation_result:
acc_sum += (
evaluation_result[cat]["acc"]
* evaluation_result[cat]["num_example"]
)
num_sum += evaluation_result[cat]["num_example"]
if num_sum > 0:
printable_results[f"Overall-{domain}"] = {
"num": num_sum,
"acc": round(acc_sum / num_sum, 3),
}
# add each sub-category row if present
for cat in cats:
if cat in evaluation_result:
printable_results[cat] = {
"num": evaluation_result[cat]["num_example"],
"acc": evaluation_result[cat]["acc"],
}
# Overall
total_num = sum(v["num_example"] for v in evaluation_result.values())
overall_acc = (
sum(v["acc"] * v["num_example"] for v in evaluation_result.values())
/ total_num
if total_num > 0
else 0.0
)
printable_results["Overall"] = {"num": total_num, "acc": round(overall_acc, 3)}
# Build EvalResult
return EvalResult(
score=overall_acc, metrics=printable_results, htmls=htmls, convos=convos
)
def _parse_multi_choice_response(
response: str, all_choices: List[str], index2ans: dict
) -> str:
# loosely adapted from benchmark mmmu eval
# First, look for explicit "Answer: X" pattern (last occurrence)
answer_matches = re.findall(r"[Aa]nswer\s*:\s*\*?\*?\s*\(?([A-Z])\)?", response)
if answer_matches:
candidate = answer_matches[-1]
if candidate in all_choices:
return candidate
for char in [",", ".", "!", "?", ";", ":", "'"]:
response = response.strip(char)
response = " " + response + " "
# Prefer explicit letter with bracket e.g. (A)
candidates: List[str] = []
for choice in all_choices:
if f"({choice})" in response:
candidates.append(choice)
if not candidates:
for choice in all_choices:
if f" {choice} " in response:
candidates.append(choice)
if not candidates and len(response.split()) > 5:
# try match by option text
for idx, ans in index2ans.items():
if ans and ans.lower() in response.lower():
candidates.append(idx)
if not candidates:
# fallback to first choice
return all_choices[0]
if len(candidates) == 1:
return candidates[0]
# choose the last occurrence
starts = []
for can in candidates:
pos = response.rfind(f"({can})")
if pos == -1:
pos = response.rfind(f" {can} ")
if pos == -1 and index2ans.get(can):
pos = response.lower().rfind(index2ans[can].lower())
starts.append(pos)
return candidates[int(max(range(len(starts)), key=lambda i: starts[i]))]
def _check_is_number(s: str) -> bool:
try:
float(s.replace(",", ""))
return True
except Exception:
return False
def _normalize_str(s: str):
s = s.strip()
if _check_is_number(s):
s = s.replace(",", "")
try:
v = round(float(s), 2)
return [v]
except Exception:
return [s.lower()]
return [s.lower()] if len(s) > 1 else [" " + s, s + " "]
def _extract_numbers(s: str) -> List[str]:
import re as _re
pattern_commas = r"-?\b\d{1,3}(?:,\d{3})+\b"
pattern_scientific = r"-?\d+(?:\.\d+)?[eE][+-]?\d+"
pattern_simple = r"-?(?:\d+\.\d+|\.\d+|\d+\b)(?![eE][+-]?\d+)(?![,\d])"
return (
_re.findall(pattern_commas, s)
+ _re.findall(pattern_scientific, s)
+ _re.findall(pattern_simple, s)
)
def _parse_open_response(response: str) -> List[str]:
import re as _re
def get_key_subresponses(resp: str) -> List[str]:
resp = resp.strip().strip(".").lower()
subs = _re.split(r"\.\s(?=[A-Z])|\n", resp)
indicators = [
"could be ",
"so ",
"is ",
"thus ",
"therefore ",
"final ",
"answer ",
"result ",
]
keys = []
for i, s in enumerate(subs):
cands = [*indicators]
if i == len(subs) - 1:
cands.append("=")
shortest = None
for ind in cands:
if ind in s:
part = s.split(ind)[-1].strip()
if not shortest or len(part) < len(shortest):
shortest = part
if shortest and shortest not in [":", ",", ".", "!", "?", ";", ":", "'"]:
keys.append(shortest)
return keys or [resp]
key_resps = get_key_subresponses(response)
pred_list = key_resps.copy()
for r in key_resps:
pred_list.extend(_extract_numbers(r))
out = []
for x in pred_list:
out.extend(_normalize_str(x))
# dedup
return list(dict.fromkeys(out))
def _eval_open(gold, preds: List[str]) -> bool:
if isinstance(gold, list):
norm_answers = []
for ans in gold:
norm_answers.extend(_normalize_str(ans))
else:
norm_answers = _normalize_str(gold)
for p in preds:
if isinstance(p, str):
for na in norm_answers:
if isinstance(na, str) and na in p:
return True
else:
if p in norm_answers:
return True
return False