Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

1293 lines
45 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Benchmark execution logic for oMLX admin panel.
Provides single-request and continuous-batching benchmarks with
real-time progress reporting via SSE events.
"""
import asyncio
import json
import logging
import re
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
from pydantic import BaseModel, field_validator
from .external_api import ExternalAPIClient, ExternalEndpointConfig
try:
import mlx.core as mx
HAS_MLX = True
except ImportError:
HAS_MLX = False
logger = logging.getLogger(__name__)
# Module-level storage for active benchmark runs
_benchmark_runs: dict[str, "BenchmarkRun"] = {}
# Valid prompt lengths for single request tests
VALID_PROMPT_LENGTHS = [1024, 4096, 8192, 16384, 32768, 65536, 131072, 200000]
# Valid batch sizes for continuous batching tests
VALID_BATCH_SIZES = [2, 4, 8]
class BenchmarkRequest(BaseModel):
"""Request model for starting a benchmark."""
model_id: str
prompt_lengths: list[int]
generation_length: int = 128
batch_sizes: list[int] = []
force_lm_engine: bool = False
# When set, the benchmark runs against a remote OpenAI-compatible
# endpoint instead of a local engine and model_id is the remote
# model name (not validated against the local catalog).
external: Optional[ExternalEndpointConfig] = None
@field_validator("prompt_lengths")
@classmethod
def validate_prompt_lengths(cls, v: list[int]) -> list[int]:
if not v:
raise ValueError("At least one prompt length is required")
for pl in v:
if pl not in VALID_PROMPT_LENGTHS:
raise ValueError(
f"Invalid prompt length {pl}. Must be one of {VALID_PROMPT_LENGTHS}"
)
return sorted(v)
@field_validator("batch_sizes")
@classmethod
def validate_batch_sizes(cls, v: list[int]) -> list[int]:
for bs in v:
if bs not in VALID_BATCH_SIZES:
raise ValueError(
f"Invalid batch size {bs}. Must be one of {VALID_BATCH_SIZES}"
)
return sorted(v)
@dataclass
class BenchmarkRun:
"""Tracks the state of a running benchmark.
SSE delivery model: events are appended to `events` (append-only
log) under `cond`. Subscribers replay `events` from offset 0 then
wait on `cond` for new entries. `terminal` is set once the final
event (`upload_done` / `error`) has been published so subscribers
know to close their stream rather than wait for a follow-up.
"""
bench_id: str
request: BenchmarkRequest
status: str = "running" # running, completed, cancelled, error
events: list[dict] = field(default_factory=list)
cond: asyncio.Condition = field(default_factory=asyncio.Condition)
terminal: bool = False
task: Optional[asyncio.Task] = None
results: list[dict] = field(default_factory=list)
error_message: str = ""
# Experimental flags active when the benchmark started. When non-empty
# the run's results are not uploaded to omlx.ai community benchmarks
# because experimental features skew the numbers.
experimental_features: list[str] = field(default_factory=list)
# Mirror of the upload SSE events so REST consumers (e.g. native Swift
# app polling /results) can render leaderboard status without opening
# the stream. Phases: "idle" → "uploading" → "done" | "skipped". The
# browser HTML still consumes the SSE stream directly; this is purely
# additive state that lives alongside it.
upload_state: dict = field(default_factory=lambda: {
"phase": "idle",
"results": [], # per-context-length: {context_length, id?, url?, duplicate?, error?}
"total": 0,
"success_count": 0,
"failed_count": 0,
"owner_hash": None, # display hash, populated on upload_done
"skipped_reason": None, # e.g. "experimental_features"
"skipped_features": [],
})
# Event types that close the SSE stream for a bench run. `done` is NOT
# terminal — it marks "tests finished, upload starting"; the real end of
# stream is `upload_done` (or `error`).
_BENCH_TERMINAL_TYPES = frozenset({"upload_done", "error"})
_EXPERIMENTAL_FEATURE_FLAGS = (
("dflash_enabled", "dflash"),
("specprefill_enabled", "specprefill"),
("turboquant_kv_enabled", "turboquant"),
("mtp_enabled", "mtp"),
("vlm_mtp_enabled", "vlm_mtp"),
)
def _detect_experimental_features(model_settings: Any) -> list[str]:
"""Return benchmark-skewing model features enabled in settings."""
return [
feature
for attr, feature in _EXPERIMENTAL_FEATURE_FLAGS
if getattr(model_settings, attr, False)
]
def get_run(bench_id: str) -> Optional[BenchmarkRun]:
"""Get a benchmark run by ID."""
return _benchmark_runs.get(bench_id)
def get_active_run() -> Optional[BenchmarkRun]:
"""Return the currently-running throughput benchmark, if any.
Discovery surface for clients that need to attach to an in-progress
run without knowing the bench_id upfront (page refresh, second tab).
Returns the first run with status == "running"; throughput benches
are 1-at-a-time so there's never more than one.
"""
for run in _benchmark_runs.values():
if run.status == "running":
return run
return None
def create_run(request: BenchmarkRequest) -> BenchmarkRun:
"""Create and register a new benchmark run."""
bench_id = f"bench-{uuid.uuid4().hex[:12]}"
run = BenchmarkRun(bench_id=bench_id, request=request)
_benchmark_runs[bench_id] = run
return run
def cleanup_old_runs(max_runs: int = 10) -> None:
"""Remove old completed runs to prevent memory leaks."""
completed = [
(bid, r)
for bid, r in _benchmark_runs.items()
if r.status in ("completed", "cancelled", "error")
]
if len(completed) > max_runs:
for bid, _ in completed[:-max_runs]:
del _benchmark_runs[bid]
def _generate_prompt(tokenizer: Any, target_tokens: int) -> str:
"""Generate a prompt with exactly target_tokens tokens.
Uses a unique UUID prefix to prevent SSD cache hits from previous sessions.
"""
unique_prefix = f"BENCH-{uuid.uuid4().hex} "
filler = (
"The quick brown fox jumps over the lazy dog. "
"In the realm of artificial intelligence, large language models "
"have demonstrated remarkable capabilities across diverse tasks. "
)
# Build a large enough text
text = unique_prefix + filler * (target_tokens // 10 + 1)
tokens = tokenizer.encode(text)
if len(tokens) < target_tokens:
# Need more tokens, repeat more
text = unique_prefix + filler * (target_tokens // 5 + 1)
tokens = tokenizer.encode(text)
# Truncate to exact target length
tokens = tokens[:target_tokens]
return tokenizer.decode(tokens)
# The external path has no tokenizer, so prompt lengths are approximated by
# repeating this filler (~30 tokens per repetition in common BPE vocabs).
# Results report the endpoint's actual usage.prompt_tokens.
_EXTERNAL_FILLER = (
"The quick brown fox jumps over the lazy dog. "
"In the realm of artificial intelligence, large language models "
"have demonstrated remarkable capabilities across diverse tasks. "
)
_EXTERNAL_FILLER_TOKENS = 30
def _generate_external_prompt(target_tokens: int) -> str:
"""Generate an approximately target_tokens-long prompt without a tokenizer.
Uses a unique UUID prefix so remote prefix caches cannot skew results.
"""
unique_prefix = f"BENCH-{uuid.uuid4().hex} "
repeats = max(1, target_tokens // _EXTERNAL_FILLER_TOKENS)
return unique_prefix + _EXTERNAL_FILLER * repeats
def _compute_single_metrics(
prompt_tokens: int,
completion_tokens: int,
start_time: float,
first_token_time: float,
end_time: float,
peak_memory: int,
cached_tokens: int,
prefill_duration_s: float | None = None,
generation_duration_s: float | None = None,
generation_measured: bool = True,
) -> dict:
"""Compute all metrics for a single request benchmark."""
ttft_s = first_token_time - start_time
prefill_duration = (
prefill_duration_s if prefill_duration_s is not None else ttft_s
)
gen_duration = (
generation_duration_s
if generation_duration_s is not None
else end_time - first_token_time
)
e2e_duration = end_time - start_time
ttft_ms = ttft_s * 1000
if generation_measured and completion_tokens > 1 and gen_duration > 0:
tpot_ms = (gen_duration / (completion_tokens - 1)) * 1000
gen_tps = completion_tokens / gen_duration
else:
tpot_ms = 0.0
gen_tps = 0.0
processing_tps = prompt_tokens / max(prefill_duration, 1e-9)
total_throughput = (prompt_tokens + completion_tokens) / max(e2e_duration, 1e-9)
return {
"ttft_ms": round(ttft_ms, 1),
"tpot_ms": round(tpot_ms, 2),
"gen_tps": round(gen_tps, 1),
"processing_tps": round(processing_tps, 1),
"e2e_latency_s": round(e2e_duration, 3),
"total_throughput": round(total_throughput, 1),
"peak_memory_bytes": peak_memory,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"cached_tokens": cached_tokens,
}
def _get_batch_benchmark_core(engine: Any) -> Any | None:
"""Return the scheduler core when this engine supports batch benchmarks."""
engine_core = getattr(engine, "_engine", None)
if engine_core is None:
return None
if not callable(getattr(engine_core, "add_request", None)):
return None
if not callable(getattr(engine_core, "stream_outputs", None)):
return None
return engine_core
async def _send_event(run: BenchmarkRun, event: dict) -> None:
"""Append an event to the run's log and wake any subscribers.
Sets `run.terminal` when the event ends the stream so subscribers
can return rather than wait for an event that will never come.
"""
async with run.cond:
run.events.append(event)
if event.get("type") in _BENCH_TERMINAL_TYPES:
run.terminal = True
run.cond.notify_all()
async def _run_single_test(
engine: Any,
prompt: str,
max_tokens: int,
pp_len: int,
) -> dict:
"""Run a single request benchmark test and return metrics."""
# Reset peak memory tracking
try:
mx.reset_peak_memory()
except Exception:
pass
start_time = time.perf_counter()
first_token_time = None
last_generated_token_time = None
last_output = None
prev_completion_tokens = 0
async for output in engine.stream_generate(
prompt=prompt,
max_tokens=max_tokens,
temperature=0.0,
top_p=1.0,
):
# Detect first generated token via completion_tokens count,
# not new_text. Some models (e.g. Harmony/gpt-oss) produce
# protocol tokens that don't yield visible new_text.
completion_delta = output.completion_tokens - prev_completion_tokens
if completion_delta > 0:
generated_at = getattr(output, "generated_at", None)
generated_until = getattr(output, "generated_until", None)
output_first_token_time = (
float(generated_at) if generated_at is not None else time.perf_counter()
)
if first_token_time is None:
first_token_time = output_first_token_time
if generated_until is not None:
last_generated_token_time = float(generated_until)
elif completion_delta == 1:
last_generated_token_time = output_first_token_time
prev_completion_tokens = output.completion_tokens
last_output = output
end_time = time.perf_counter()
if first_token_time is None:
first_token_time = end_time
# Get peak memory
try:
peak_memory = mx.get_peak_memory()
except Exception:
peak_memory = 0
prompt_tokens = last_output.prompt_tokens if last_output else 0
completion_tokens = last_output.completion_tokens if last_output else 0
cached_tokens = last_output.cached_tokens if last_output else 0
if cached_tokens > 0:
logger.warning(
f"Benchmark test pp{pp_len} had {cached_tokens} cached tokens "
f"(expected 0). Results may not reflect true prefill performance."
)
prefill_duration_s = None
generation_duration_s = None
producer_generation_duration_s = None
metric_completion_tokens = completion_tokens
if first_token_time is not None and last_generated_token_time is not None:
measured_duration = last_generated_token_time - first_token_time
if measured_duration > 0:
producer_generation_duration_s = measured_duration
if last_output is not None:
prompt_tps = float(getattr(last_output, "prompt_tps", 0.0) or 0.0)
if prompt_tps > 0 and prompt_tokens > 0:
prefill_duration_s = prompt_tokens / prompt_tps
canvas_tps = float(getattr(last_output, "diffusion_canvas_tps", 0.0) or 0.0)
canvas_tokens = int(getattr(last_output, "diffusion_canvas_tokens", 0) or 0)
if canvas_tps > 0 and canvas_tokens > 0:
metric_completion_tokens = canvas_tokens
generation_duration_s = canvas_tokens / canvas_tps
else:
generation_tps = float(getattr(last_output, "generation_tps", 0.0) or 0.0)
if generation_tps > 0 and completion_tokens > 0:
generation_duration_s = completion_tokens / generation_tps
if generation_duration_s is None:
generation_duration_s = producer_generation_duration_s
generation_measured = generation_duration_s is not None
return _compute_single_metrics(
prompt_tokens=prompt_tokens,
completion_tokens=metric_completion_tokens,
start_time=start_time,
first_token_time=first_token_time,
end_time=end_time,
peak_memory=peak_memory,
cached_tokens=cached_tokens,
prefill_duration_s=prefill_duration_s,
generation_duration_s=generation_duration_s,
generation_measured=generation_measured,
)
async def _run_batch_test(
engine: Any,
prompts: list[str],
prompt_tokens: int,
max_tokens: int,
batch_size: int,
) -> dict:
"""Run a continuous batching benchmark test.
Submits batch_size concurrent requests via the engine core and measures
aggregate throughput including pp TPS and tg TPS.
Args:
prompts: List of prompts (one per request). For same-prompt tests,
all entries are identical. For different-prompt tests, each
has a unique UUID prefix.
prompt_tokens: Number of prompt tokens per request (for pp TPS calc).
"""
from ..request import SamplingParams
engine_core = _get_batch_benchmark_core(engine)
if engine_core is None:
raise ValueError("Engine does not support batch benchmarks")
sampling_params = SamplingParams(
max_tokens=max_tokens,
temperature=0.0,
top_p=1.0,
)
async def _single_request(prompt: str) -> dict:
"""Run a single request within the batch."""
start = time.perf_counter()
first_token = None
tokens = 0
prev_tokens = 0
request_id = await engine_core.add_request(
prompt=prompt,
sampling_params=sampling_params,
)
async for output in engine_core.stream_outputs(request_id):
if first_token is None and output.completion_tokens > prev_tokens:
first_token = time.perf_counter()
prev_tokens = output.completion_tokens
if output.finished:
tokens = output.completion_tokens
end = time.perf_counter()
if first_token is None:
first_token = end
return {
"ttft_s": first_token - start,
"first_token_abs": first_token,
"end_abs": end,
"completion_tokens": tokens,
}
# Submit all requests concurrently
wall_start = time.perf_counter()
results = await asyncio.gather(
*[_single_request(prompts[i]) for i in range(batch_size)]
)
wall_end = time.perf_counter()
# Aggregate metrics
total_gen_tokens = sum(r["completion_tokens"] for r in results)
total_prompt_tokens = prompt_tokens * batch_size
wall_time = wall_end - wall_start
avg_ttft_ms = (sum(r["ttft_s"] for r in results) / batch_size) * 1000
# pp TPS: total prompt tokens / time until ALL requests finish prefill
max_first_token = max(r["first_token_abs"] for r in results)
prefill_wall_time = max_first_token - wall_start
pp_tps = total_prompt_tokens / max(prefill_wall_time, 1e-9)
# tg TPS: total generated tokens / generation wall time
# Generation starts when the last request finishes prefill
gen_wall_time = wall_end - max_first_token
tg_tps = total_gen_tokens / max(gen_wall_time, 1e-9)
return {
"pp_tps": round(pp_tps, 1),
"tg_tps": round(tg_tps, 1),
"avg_ttft_ms": round(avg_ttft_ms, 1),
"e2e_latency_s": round(wall_time, 3),
"total_gen_tokens": total_gen_tokens,
"batch_size": batch_size,
}
async def _run_external_single_test(
client: ExternalAPIClient,
prompt: str,
max_tokens: int,
) -> dict:
"""Run a single-request benchmark against an external endpoint.
Token counts come from the endpoint's streamed usage payload, never
from counting SSE chunks (providers batch multiple tokens per chunk).
Prefill duration is not observable remotely, so pp TPS falls back to
prompt_tokens / TTFT (network latency included). Peak memory is not
measurable for a remote host.
"""
stats = await client.stream_chat_completion(
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.0,
)
gen_duration = stats.last_content_time - stats.first_content_time
metrics = _compute_single_metrics(
prompt_tokens=stats.prompt_tokens,
completion_tokens=stats.completion_tokens,
start_time=stats.start_time,
first_token_time=stats.first_content_time,
end_time=stats.end_time,
peak_memory=0,
cached_tokens=stats.cached_tokens,
prefill_duration_s=None,
generation_duration_s=gen_duration if gen_duration > 0 else None,
generation_measured=gen_duration > 0,
)
metrics["peak_memory_bytes"] = None
return metrics
async def _run_external_batch_test(
client: ExternalAPIClient,
prompts: list[str],
max_tokens: int,
batch_size: int,
) -> dict:
"""Run a concurrent-requests benchmark against an external endpoint.
Mirrors _run_batch_test aggregation, with actual per-request token
counts taken from each stream's usage payload.
"""
wall_start = time.perf_counter()
stats_list = await asyncio.gather(*[
client.stream_chat_completion(
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.0,
)
for prompt in prompts
])
wall_end = time.perf_counter()
total_gen_tokens = sum(s.completion_tokens for s in stats_list)
total_prompt_tokens = sum(s.prompt_tokens for s in stats_list)
wall_time = wall_end - wall_start
avg_ttft_ms = (
sum(s.first_content_time - s.start_time for s in stats_list) / batch_size
) * 1000
# pp TPS: total prompt tokens / time until ALL requests emit content
max_first_token = max(s.first_content_time for s in stats_list)
pp_tps = total_prompt_tokens / max(max_first_token - wall_start, 1e-9)
# tg TPS: total generated tokens / generation wall time
tg_tps = total_gen_tokens / max(wall_end - max_first_token, 1e-9)
return {
"pp_tps": round(pp_tps, 1),
"tg_tps": round(tg_tps, 1),
"avg_ttft_ms": round(avg_ttft_ms, 1),
"e2e_latency_s": round(wall_time, 3),
"total_gen_tokens": total_gen_tokens,
"batch_size": batch_size,
}
OMLX_AI_API_URL = "https://omlx.ai/api/benchmarks"
# Quantization patterns to strip from model directory names
_QUANT_SUFFIXES = re.compile(
r"[-_](2bit|3bit|4bit|6bit|8bit|fp16|bf16|fp32|MXFP4|NVFP4)$", re.IGNORECASE
)
_MLX_SUFFIXES = re.compile(r"[-_]?MLX[-_]?", re.IGNORECASE)
def _detect_quantization(model_path: str) -> str:
"""Detect model quantization from config.json or directory name.
Fallback chain: config.json → directory name → "unknown"
"""
config_path = Path(model_path) / "config.json"
if config_path.exists():
try:
with open(config_path) as f:
config = json.load(f)
qconfig = config.get("quantization_config", {})
bits = qconfig.get("bits")
if bits is not None:
return f"{bits}bit"
except Exception:
pass
# Fallback: extract from directory name
dirname = Path(model_path).name
match = re.search(
r"(2bit|3bit|4bit|6bit|8bit|fp16|bf16|MXFP4|NVFP4)", dirname, re.IGNORECASE
)
if match:
return match.group(1).lower()
return "unknown"
def _clean_model_name(model_id: str, quantization: str) -> str:
"""Clean model directory name for display as model_name.
Strips quantization suffixes and MLX markers.
e.g. "Qwen3-30B-A3B-4bit" → "Qwen3-30B-A3B"
"""
name = model_id
name = _QUANT_SUFFIXES.sub("", name)
name = _MLX_SUFFIXES.sub("", name)
return name.strip("-_ ")
def _sanitize_upload_error(resp: Any) -> str:
"""Extract a user-presentable error string from a failed upload response.
Avoids dumping raw HTML bodies (e.g. Cloudflare's "Just a moment..."
challenge interstitial) into the dashboard's red-x error column.
Detects CF mitigation specifically so users get actionable context
instead of a 5KB markup blob.
Resolution order:
1. Cloudflare challenge — header ``cf-mitigated: challenge`` is
authoritative; a body sniff for "just a moment" / "cf-chl" covers
edge transports that strip the header.
2. JSON envelope — the omlx.ai API's normal error shape; extract
``error`` / ``detail`` / ``message`` if present, truncated.
3. Plain-text body — short responses only; HTML-looking bodies are
collapsed to a one-line "non-JSON response (N bytes)" hint.
4. Fallback to the bare HTTP status code.
"""
headers = getattr(resp, "headers", {}) or {}
cf_mitigated = str(headers.get("cf-mitigated", "")).lower()
body = getattr(resp, "text", "") or ""
status = getattr(resp, "status_code", "?")
body_head = body[:512].lower()
if cf_mitigated == "challenge" or "just a moment" in body_head or "cf-chl" in body_head:
return (
f"Upload blocked by Cloudflare (HTTP {status}). "
f"This is a server-side issue with omlx.ai — retry later or "
f"report it to the maintainer."
)
try:
data = resp.json()
msg = data.get("error") or data.get("detail") or data.get("message")
if msg:
return str(msg)[:300]
except Exception:
pass
text = body.strip()
if "<" in text and ">" in text:
return f"HTTP {status} — unexpected non-JSON response ({len(body)} bytes)"
return text[:300] or f"HTTP {status}"
async def _upload_to_omlx_ai(run: BenchmarkRun, engine_pool: Any) -> None:
"""Upload benchmark results to omlx.ai community benchmarks.
Sends each single-request result as a separate submission,
grouped by submission_group. Upload failures don't affect
the benchmark run status.
"""
import requests
from .._version import __version__
from ..utils.hardware import (
compute_owner_hash,
get_chip_name,
get_gpu_core_count,
get_io_platform_uuid,
get_os_version,
get_total_memory_gb,
parse_chip_info,
)
# Skip upload when experimental features were active during the run.
# These features skew throughput and would pollute the community
# leaderboard if mixed in unmarked.
if run.experimental_features:
run.upload_state["phase"] = "skipped"
run.upload_state["skipped_reason"] = "experimental_features"
run.upload_state["skipped_features"] = list(run.experimental_features)
await _send_event(run, {
"type": "upload_skipped",
"reason": "experimental_features",
"features": list(run.experimental_features),
})
logger.info(
f"Benchmark upload skipped: experimental features active: "
f"{run.experimental_features}"
)
return
run.upload_state["phase"] = "uploading"
await _send_event(run, {
"type": "progress",
"phase": "upload",
"message": "Uploading to community benchmarks...",
"current": 0,
"total": 0,
})
# Collect hardware info
chip_string = get_chip_name()
chip_name, chip_variant = parse_chip_info(chip_string)
memory_gb = round(get_total_memory_gb())
gpu_cores = get_gpu_core_count()
os_version = get_os_version()
omlx_version = __version__
# Compute owner_hash
owner_hash_full = None
owner_hash_display = None
io_uuid = get_io_platform_uuid()
if io_uuid:
owner_hash_full = compute_owner_hash(io_uuid, chip_name, gpu_cores, memory_gb)
# Display hash is without the verify character
owner_hash_display = owner_hash_full[:-1]
# Get model info
entry = engine_pool.get_entry(run.request.model_id)
model_path = entry.model_path if entry else ""
quantization = _detect_quantization(model_path)
model_name = _clean_model_name(run.request.model_id, quantization)
# Generate submission group
submission_group = str(uuid.uuid4())
# Collect single results and batch results
single_results = [r for r in run.results if r.get("test_type") == "single"]
uploadable_single_results = [
r for r in single_results if float(r.get("gen_tps", 0.0) or 0.0) > 0.0
]
skipped_count = len(single_results) - len(uploadable_single_results)
batch_results = [r for r in run.results if r.get("test_type") == "batch"]
# Build batching_results from batch data
batching_results = []
pp1024_single = next(
(r for r in single_results if r.get("pp") == 1024), None
)
if (
pp1024_single
and float(pp1024_single.get("gen_tps", 0.0) or 0.0) > 0.0
and batch_results
):
baseline_tps = pp1024_single["gen_tps"]
batching_results.append({
"batch_size": 1,
"tg_tps": baseline_tps,
"speedup": 1.0,
})
for br in batch_results:
speedup = round(br["tg_tps"] / baseline_tps, 2) if baseline_tps > 0 else 1.0
batching_results.append({
"batch_size": br["batch_size"],
"tg_tps": br["tg_tps"],
"speedup": speedup,
})
success_count = 0
failed_count = 0
if skipped_count:
logger.info(
f"Benchmark upload skipped {skipped_count} result(s) without "
f"measurable generation throughput"
)
for result in uploadable_single_results:
context_length = result["pp"]
peak_mem_gb = None
if result.get("peak_memory_bytes") and result["peak_memory_bytes"] > 0:
peak_mem_gb = round(result["peak_memory_bytes"] / (1024**3), 2)
payload = {
"chip_name": chip_name,
"chip_variant": chip_variant,
"memory_gb": memory_gb,
"gpu_cores": gpu_cores,
"omlx_version": omlx_version,
"os_version": os_version,
"model_name": model_name,
"quantization": quantization,
"context_length": context_length,
"pp_tps": result["processing_tps"],
"tg_tps": result["gen_tps"],
"ttft_ms": result.get("ttft_ms"),
"peak_memory_gb": peak_mem_gb,
"submission_group": submission_group,
}
if owner_hash_full:
payload["owner_hash"] = owner_hash_full
# Attach batching_results only to the first submission (lowest context_length)
if (
context_length == uploadable_single_results[0]["pp"]
and batching_results
):
payload["batching_results"] = batching_results
try:
resp = await asyncio.to_thread(
requests.post,
OMLX_AI_API_URL,
json=payload,
timeout=15,
)
if resp.status_code == 201:
data = resp.json()
success_count += 1
result_dict = {
"context_length": context_length,
"id": data.get("id"),
"url": data.get("url"),
}
run.upload_state["results"].append(result_dict)
await _send_event(run, {
"type": "upload",
"data": result_dict,
})
elif resp.status_code == 409:
data = resp.json()
success_count += 1 # Duplicate is still ok
result_dict = {
"context_length": context_length,
"id": data.get("existing_id"),
"url": data.get("existing_url"),
"duplicate": True,
}
run.upload_state["results"].append(result_dict)
await _send_event(run, {
"type": "upload",
"data": result_dict,
})
else:
failed_count += 1
error_msg = _sanitize_upload_error(resp)
result_dict = {
"context_length": context_length,
"error": error_msg,
}
run.upload_state["results"].append(result_dict)
await _send_event(run, {
"type": "upload",
"data": result_dict,
})
# Surface the sanitized message to ops; the full body
# (truncated) goes to debug so it can still be retrieved
# from the log file if needed.
logger.warning(
f"Benchmark upload failed for pp{context_length}: "
f"{resp.status_code} {error_msg}"
)
if (resp.text or "")[:1] not in ("{", "["):
logger.debug(
"Benchmark upload non-JSON body (truncated): %r",
(resp.text or "")[:500],
)
except Exception as e:
failed_count += 1
result_dict = {
"context_length": context_length,
"error": str(e),
}
run.upload_state["results"].append(result_dict)
await _send_event(run, {
"type": "upload",
"data": result_dict,
})
logger.warning(f"Benchmark upload error for pp{context_length}: {e}")
run.upload_state["phase"] = "done"
run.upload_state["total"] = len(uploadable_single_results)
run.upload_state["success_count"] = success_count
run.upload_state["failed_count"] = failed_count
run.upload_state["skipped_count"] = skipped_count
run.upload_state["owner_hash"] = owner_hash_display
await _send_event(run, {
"type": "upload_done",
"data": {
"owner_hash": owner_hash_display,
"total": len(uploadable_single_results),
"success": success_count,
"failed": failed_count,
"skipped": skipped_count,
},
})
logger.info(
f"Benchmark upload complete: {success_count}/"
f"{len(uploadable_single_results)} succeeded, skipped={skipped_count}"
)
async def run_benchmark(run: BenchmarkRun, engine_pool: Any) -> None:
"""Execute a complete benchmark run.
Phases:
1. Unload all loaded models
2. Load the target model
3. Run single request tests
4. Run batch tests
5. Unload the benchmark model
"""
request = run.request
if request.external is not None:
await _run_external_benchmark(run)
return
total_tests = len(request.prompt_lengths) + len(request.batch_sizes)
current_test = 0
overall_start = time.perf_counter()
try:
# Snapshot experimental flags at run start. Settings can change mid-run,
# and the produced numbers are tied to whatever was active when
# generation actually ran.
model_settings = None
sm = getattr(engine_pool, "_settings_manager", None)
if sm is not None:
try:
model_settings = sm.get_settings(request.model_id)
run.experimental_features.extend(
_detect_experimental_features(model_settings)
)
except Exception as e:
logger.warning(
f"Benchmark: failed to read experimental flags for "
f"{request.model_id}: {e}"
)
# Phase 1: Unload all loaded models
loaded_ids = engine_pool.get_loaded_model_ids()
if loaded_ids:
await _send_event(run, {
"type": "progress",
"phase": "unload",
"message": f"Unloading {len(loaded_ids)} model(s)...",
"current": 0,
"total": total_tests,
})
for model_id in loaded_ids:
try:
await engine_pool._unload_engine(model_id)
logger.info(f"Benchmark: unloaded {model_id}")
except Exception as e:
logger.warning(f"Benchmark: failed to unload {model_id}: {e}")
# Phase 2: Load the target model
await _send_event(run, {
"type": "progress",
"phase": "load",
"message": f"Loading {request.model_id}...",
"current": 0,
"total": total_tests,
})
# VLM MTP requires VLMBatchedEngine (which has set_vlm_mtp_drafter),
# so don't force LM-only loading when VLM MTP is enabled.
vlm_mtp_active = (
model_settings is not None
and getattr(model_settings, "vlm_mtp_enabled", False)
and getattr(model_settings, "vlm_mtp_draft_model", None)
)
force_lm = True if request.force_lm_engine else not vlm_mtp_active
engine = await engine_pool.get_engine(
request.model_id,
force_lm=force_lm,
)
logger.info(f"Benchmark: loaded {request.model_id}")
# Generate prompts for all needed lengths
tokenizer = engine.tokenizer
prompts: dict[int, str] = {}
for pp_len in request.prompt_lengths:
prompts[pp_len] = _generate_prompt(tokenizer, pp_len)
# Ensure pp1024 prompt exists for batch tests
if request.batch_sizes and 1024 not in prompts:
prompts[1024] = _generate_prompt(tokenizer, 1024)
# Warmup: run a short request to trigger JIT compilation,
# Metal shader compilation, and KV cache initialization.
# Without this, the first real benchmark test absorbs all
# one-time overhead and shows artificially low pp TPS.
await _send_event(run, {
"type": "progress",
"phase": "warmup",
"message": "Warming up (JIT compile)...",
"current": 0,
"total": total_tests,
})
warmup_prompt = _generate_prompt(tokenizer, 32)
warmup_max_tokens = (
request.generation_length
if getattr(engine, "is_diffusion_model", False)
else 8
)
async for _ in engine.stream_generate(
prompt=warmup_prompt, max_tokens=warmup_max_tokens, temperature=0.0
):
pass
logger.info("Benchmark: warmup complete")
# Phase 3: Single request tests
single_pp1024_gen_tps = None
for pp_len in request.prompt_lengths:
current_test += 1
await _send_event(run, {
"type": "progress",
"phase": "single",
"message": f"Single: pp{pp_len}/tg{request.generation_length}",
"current": current_test,
"total": total_tests,
})
metrics = await _run_single_test(
engine=engine,
prompt=prompts[pp_len],
max_tokens=request.generation_length,
pp_len=pp_len,
)
result = {
"test_type": "single",
"pp": pp_len,
"tg": request.generation_length,
**metrics,
}
run.results.append(result)
await _send_event(run, {"type": "result", "data": result})
# Store pp1024 gen_tps for speedup calculation
if pp_len == 1024:
single_pp1024_gen_tps = metrics["gen_tps"]
# Phase 4: Batch tests
# Each request has a unique UUID prefix (no cache hits)
max_batch = max(request.batch_sizes) if request.batch_sizes else 0
batch_prompts = [_generate_prompt(tokenizer, 1024) for _ in range(max_batch)]
# Skip batch tests for engines without scheduler core (e.g. VLM/Diffusion)
batch_core = _get_batch_benchmark_core(engine)
if request.batch_sizes and batch_core is None:
logger.info(
"Batch test skipped: engine does not support concurrent batching"
)
current_test += len(request.batch_sizes)
for batch_size in request.batch_sizes if batch_core is not None else []:
current_test += 1
await _send_event(run, {
"type": "progress",
"phase": "batch",
"message": f"Batch {batch_size}x: pp1024/tg{request.generation_length}",
"current": current_test,
"total": total_tests,
})
batch_metrics = await _run_batch_test(
engine=engine,
prompts=batch_prompts[:batch_size],
prompt_tokens=1024,
max_tokens=request.generation_length,
batch_size=batch_size,
)
result = {
"test_type": "batch",
"pp": 1024,
"tg": request.generation_length,
**batch_metrics,
}
run.results.append(result)
await _send_event(run, {"type": "result", "data": result})
# Phase 5: Unload benchmark model
await _send_event(run, {
"type": "progress",
"phase": "cleanup",
"message": f"Unloading {request.model_id}...",
"current": total_tests,
"total": total_tests,
})
try:
await engine_pool._unload_engine(request.model_id)
logger.info(f"Benchmark: unloaded {request.model_id} after benchmark")
except Exception as e:
logger.warning(f"Benchmark: failed to unload {request.model_id}: {e}")
# Done
overall_duration = time.perf_counter() - overall_start
run.status = "completed"
await _send_event(run, {
"type": "done",
"summary": {
"model_id": request.model_id,
"total_time": round(overall_duration, 1),
"total_tests": total_tests,
},
})
# Upload results to omlx.ai (failures don't affect benchmark status)
try:
await _upload_to_omlx_ai(run, engine_pool)
except Exception as e:
logger.warning(f"Benchmark upload to omlx.ai failed: {e}")
await _send_event(run, {
"type": "upload_done",
"data": {
"owner_hash": None,
"total": 0,
"success": 0,
"failed": 0,
"error": str(e),
},
})
except asyncio.CancelledError:
run.status = "cancelled"
await _send_event(run, {
"type": "error",
"message": "Benchmark cancelled by user",
})
# Try to unload the model on cancellation
try:
await engine_pool._unload_engine(request.model_id)
except Exception:
pass
except Exception as e:
logger.error(f"Benchmark error: {e}", exc_info=True)
run.status = "error"
run.error_message = str(e)
await _send_event(run, {
"type": "error",
"message": str(e),
})
# Try to unload the model on error
try:
await engine_pool._unload_engine(request.model_id)
except Exception:
pass
async def _run_external_benchmark(run: BenchmarkRun) -> None:
"""Execute a benchmark run against an external OpenAI-compatible endpoint.
No local model phases (unload/load/JIT warmup) and no community
upload — external numbers measure someone else's hardware.
"""
request = run.request
total_tests = len(request.prompt_lengths) + len(request.batch_sizes)
current_test = 0
overall_start = time.perf_counter()
client = ExternalAPIClient(request.external)
try:
# Warmup doubles as preflight: fail fast on bad URL/key and on
# endpoints that do not return streamed usage (hard requirement
# for accurate token counts) before any long test runs.
await _send_event(run, {
"type": "progress",
"phase": "warmup",
"message": "Warming up external endpoint...",
"current": 0,
"total": total_tests,
})
await client.stream_chat_completion(
messages=[{"role": "user", "content": _generate_external_prompt(32)}],
max_tokens=8,
temperature=0.0,
)
logger.info("Benchmark: external endpoint warmup complete")
# Single request tests
for pp_len in request.prompt_lengths:
current_test += 1
await _send_event(run, {
"type": "progress",
"phase": "single",
"message": f"Single: pp{pp_len}/tg{request.generation_length}",
"current": current_test,
"total": total_tests,
})
metrics = await _run_external_single_test(
client=client,
prompt=_generate_external_prompt(pp_len),
max_tokens=request.generation_length,
)
result = {
"test_type": "single",
"pp": pp_len,
"tg": request.generation_length,
**metrics,
}
run.results.append(result)
await _send_event(run, {"type": "result", "data": result})
# Batch tests: concurrent requests with unique pp1024 prompts
for batch_size in request.batch_sizes:
current_test += 1
await _send_event(run, {
"type": "progress",
"phase": "batch",
"message": f"Batch {batch_size}x: pp1024/tg{request.generation_length}",
"current": current_test,
"total": total_tests,
})
batch_metrics = await _run_external_batch_test(
client=client,
prompts=[_generate_external_prompt(1024) for _ in range(batch_size)],
max_tokens=request.generation_length,
batch_size=batch_size,
)
result = {
"test_type": "batch",
"pp": 1024,
"tg": request.generation_length,
**batch_metrics,
}
run.results.append(result)
await _send_event(run, {"type": "result", "data": result})
# Done
overall_duration = time.perf_counter() - overall_start
run.status = "completed"
await _send_event(run, {
"type": "done",
"summary": {
"model_id": request.model_id,
"total_time": round(overall_duration, 1),
"total_tests": total_tests,
},
})
# External results measure remote hardware — never upload them to
# the omlx.ai community leaderboard. Mirrors the experimental-
# features skip so REST pollers see the same upload_state shape.
run.upload_state["phase"] = "skipped"
run.upload_state["skipped_reason"] = "external_endpoint"
await _send_event(run, {
"type": "upload_skipped",
"reason": "external_endpoint",
"features": [],
})
except asyncio.CancelledError:
run.status = "cancelled"
await _send_event(run, {
"type": "error",
"message": "Benchmark cancelled by user",
})
except Exception as e:
logger.error(f"External benchmark error: {e}", exc_info=True)
run.status = "error"
run.error_message = str(e)
await _send_event(run, {
"type": "error",
"message": str(e),
})
finally:
await client.aclose()