InferenceX is SemiAnalysis's public dashboard of NVIDIA-vs-AMD LLM inference benchmarks (throughput, accuracy evals, GPU reliability). The front end is a JavaScript single-page app, so it looks like you'd need a headless browser — but the whole dashboard is backed by three plain, unauthenticated JSON endpoints. No API key, no browser, no scraping of rendered HTML. Just HTTP GET.
This gist documents those endpoints and ships a dependency-free Python client
(scrape_inferencex.py, stdlib only). Part 2 below covers a companion
source — Artificial Analysis — for the
capability / use-case dimension InferenceX doesn't measure
(scrape_artificial_analysis.py).
The InferenceX site is model-first: you pick a model, then see GPUs. A very
common customer question is the reverse — "Given the hardware I already own
(B200 / GB200 / GB300), which models run efficiently, and for which kind of
workload?" The API makes that pivot trivial: pull each model's rows, filter by
hardware, and group by workload shape.
Workload shape ≈ use case. InferenceX measures three input/output-length
scenarios (isl = input tokens, osl = output tokens). They're a good proxy
for the shape of a use case:
| ISL / OSL | Shape | Typical use cases |
|---|---|---|
| 1024 / 1024 | balanced | Chat, assistants |
| 8192 / 1024 | long in, short out | RAG, summarization, document Q&A |
| 1024 / 8192 | short in, long out | Reasoning, agents, long-form / code generation |
Coverage today (2026-06-30): B200 has 9 models; GB200 / GB300 / B300 have 7 each — across the whole InferenceX model set (DeepSeek-R1 / V4, GLM-5, Kimi-K2.5, Llama-3.3-70B, MiniMax-M2.5 / M3, Qwen-3.5, gpt-oss-120b).
# Hardware-first pivot: for one GPU, rank models by throughput within each scenario.
import scrape_inferencex as ix
from collections import defaultdict
GPU = "b200" # b200 | b300 | gb200 | gb300 | h100 | h200
SCENARIOS = {(1024,1024):"chat", (8192,1024):"RAG/summarize", (1024,8192):"reason/codegen"}
best = defaultdict(dict) # scenario -> {model: peak tok/s/gpu}
for db, display in ix.discover_display_names().items():
for r in ix.get_benchmarks(display):
if r["hardware"] != GPU:
continue
scen = SCENARIOS.get((r.get("isl"), r.get("osl")))
t = (r.get("metrics") or {}).get("tput_per_gpu")
if scen and t:
best[scen][display] = max(best[scen].get(display, 0), t)
for scen, models in best.items():
print(f"\n{GPU} — {scen}:")
for m, t in sorted(models.items(), key=lambda kv: -kv[1]):
print(f" {m:24s} {t:8.0f} tok/s/gpu")Two honest boundaries to keep in mind:
- InferenceX is text LLM serving only — no image/video/audio generation. The "use cases" it speaks to are text workload shapes, not modalities.
- It measures performance, not capability. The only quality eval is
gsm8k(math). So InferenceX answers "which models run efficiently on my hardware," not "which model is best at coding/agents." For the capability/use-case-fit half of the question, join against a model-quality source such as Artificial Analysis (per-model coding / math / agentic indices). The peak-throughput numbers above also ignore the latency/interactivity trade-off — for a fair "efficient" ranking, filter to a target per-user speed first (e.g.metrics.mean_intvty >= 10tok/s/user).
Base URL: https://inferencex.semianalysis.com
| Endpoint | Params | Returns |
|---|---|---|
GET /api/v1/benchmarks |
?model=<DISPLAY_NAME> (required) |
Speed/throughput rows for one model, across every GPU + framework + ISL/OSL/concurrency config |
GET /api/v1/evaluations |
none | Every accuracy-eval row (model × task × GPU) |
GET /api/v1/reliability |
none | Per-day GPU reliability rows (n_success / total) |
All three return a JSON array of row objects.
# Accuracy evals — no params, returns everything (~1800 rows)
curl -s --compressed https://inferencex.semianalysis.com/api/v1/evaluations | jq length
# Throughput for one model (note: --compressed is required, see gotchas)
curl -s --compressed 'https://inferencex.semianalysis.com/api/v1/benchmarks?model=DeepSeek-R1-0528' | jq '.[0]'/benchmarks (speed) — one row per measured config:
{
"hardware": "b300", "framework": "trtllm", "model": "dsr1",
"precision": "fp4", "disagg": true, "is_multinode": false,
"prefill_tp": 4, "decode_tp": 4, "num_prefill_gpu": 4, "num_decode_gpu": 4,
"isl": 1024, "osl": 1024, "conc": 4,
"metrics": { "tput_per_gpu": 14773.4, "mean_ttft": 0.17, "mean_tpot": 0.0096,
"p99_ttft": 0.82, "p90_e2el": 9.84, "...": "..." }
}The headline metric is metrics.tput_per_gpu (output tok/s per GPU). Latency
percentiles (*_ttft, *_tpot, *_itl, *_e2el) are also in metrics.
/evaluations (accuracy):
{
"hardware": "mi355x", "framework": "vllm", "model": "minimaxm3",
"precision": "fp4", "task": "gsm8k", "conc": 512, "date": "2026-06-30",
"metrics": { "em_strict": 0.953, "em_flexible": 0.953, "n_eff": 1319 },
"run_url": "https://github.com/SemiAnalysisAI/InferenceX/actions/runs/..."
}/reliability:
{ "hardware": "gb200", "date": "2026-06-30", "n_success": 0, "total": 0 }-
Responses are gzip-encoded. Send
Accept-Encoding: gzipand decompress, or use a client that auto-decompresses.requestsdoes this for you; rawurllibdoes not — you'll get aJSONDecodeErroron binary bytes. Withcurlyou must pass--compressed. -
/benchmarkswants the human DISPLAY name, not the internal db name.?model=DeepSeek-R1-0528works;?model=dsr1returns{"error":"Unknown model"}. Meanwhile/evaluationsand/reliabilityreport the internal db name in theirmodelfield (dsr1,glm5, …). So you need a db→display map to go between them. -
There is no
modelsendpoint. Discover the db names from/evaluations(themodelfield). The db→display map lives only in the site's JS bundle.scrape_inferencex.pyships a current snapshot (DISPLAY_NAMES) and adiscover_display_names()helper that re-extracts it from the bundle so you're not stuck hand-maintaining it. -
Several db names can share one display name (e.g.
glm5andglm5.1both →GLM-5; thekimik2.*family →Kimi-K2.5). One/benchmarkscall for that display name returns rows for all of them — dedupe your model loop by display name or you'll fetch the same payload twice. -
GPU vendor isn't a field — infer it from
hardware: NVIDIA ={b200, b300, gb200, gb300, h100, h200}, AMD ={mi300x, mi325x, mi355x}.
python3 scrape_inferencex.py # runs a demo: discovers models, counts rows,
# prints best tok/s/gpu per GPU for one configAs a library:
import scrape_inferencex as ix
ix.discover_models() # ['dsr1', 'dsv4', 'glm5', ...] (db names)
ix.get_benchmarks("DeepSeek-R1-0528") # list of speed rows
ix.get_evaluations() # all accuracy rows
ix.get_reliability() # all reliability rows
ix.discover_display_names() # live db->display map from the JS bundleTo pull everything to disk:
import json, scrape_inferencex as ix
names = ix.discover_display_names()
allbench = {}
for db, display in names.items():
allbench.setdefault(display, ix.get_benchmarks(display))
json.dump(
{"benchmarks": allbench,
"evaluations": ix.get_evaluations(),
"reliability": ix.get_reliability()},
open("inferencex_dump.json", "w"))The JSON above is the data behind every chart, so prefer it. But if you need
pixel-perfect screenshots of the Pareto curves (e.g. for a report), drive the
SPA with Playwright: load https://inferencex.semianalysis.com/inference, wait
for networkidle + svg.main-svg / .js-plotly-plot to render, click the
Accuracy Evals / GPU Reliability tabs, scroll to trigger lazy loads, and
screenshot each figure / top-level svg element. That's strictly a fallback —
the API is faster, exact, and doesn't break when the layout changes.
InferenceX answers "which models run efficiently on my hardware." It does
not tell you which model is actually good at coding, math, or agentic work
(its only quality eval is gsm8k). Artificial Analysis
fills that gap: it benchmarks model capability across many tasks, which is
the natural "use case per model" signal. Pull both and you can answer the full
customer question — "Given my B200s, for code generation, which models are both
fast (InferenceX) and capable (AA)?"
One JSON call returns all models (~540), so you fetch once and cache.
GET https://artificialanalysis.ai/api/v2/data/llms/models
header: x-api-key: <YOUR_KEY>
- Key: free, from your account at https://artificialanalysis.ai/ (API
section). Set it as
AA_API_KEY. The free tier is rate-limited (a modest number of requests/day) — but this endpoint returns everything in one shot, so one cached call covers you. - Attribution is REQUIRED by AA's terms: credit "Artificial Analysis" and link https://artificialanalysis.ai/ anywhere you display the data.
- Response shape:
{"data": [ {model}, ... ]}.
Each model carries an evaluations object. The individual benchmarks group into
use-case buckets (this is the mapping scrape_artificial_analysis.py uses):
| Use case | AA benchmarks |
|---|---|
| Overall | artificial_analysis_intelligence_index |
| Coding | artificial_analysis_coding_index, livecodebench, scicode, terminalbench_hard, terminalbench_v2_1 |
| Math | artificial_analysis_math_index, aime, aime_25, math_500 |
| Reasoning / knowledge | gpqa, mmlu_pro, hle |
| Agentic / tool-use | tau2, tau_banking, terminalbench_hard |
| Instruction-following | ifbench |
| Long-context reasoning | lcr |
A model row also includes pricing (price_1m_input_tokens,
price_1m_output_tokens, price_1m_blended_3_to_1),
median_output_tokens_per_second, median_time_to_first_token_seconds,
model_creator, slug, and release_date.
Scale note: the
*_indexfields are 0–100; the raw benchmarks (gpqa,livecodebench,tau2, …) are 0–1. The helper scales raw benchmarks ×100 so a bucket that mixes both stays on one scale.
export AA_API_KEY=... # free key from artificialanalysis.ai
python3 scrape_artificial_analysis.py # top models per use case + sample profiles
python3 scrape_artificial_analysis.py --json > aa_models.json # full dumpAs a library:
import scrape_artificial_analysis as aa
models = aa.fetch_models() # ~540 model dicts (cache this)
aa.rank_for_use_case(models, "coding", top=10) # [(name, score, model), ...]
aa.use_case_profile(models[0]) # {'coding': 52.9, 'math': ..., ...}- One model → many AA rows. AA lists reasoning vs non-reasoning variants
and effort tiers separately (e.g.
gpt-oss-120b (low)/(high),Qwen3.5 397B A17B (Reasoning)/(Non-reasoning)). When joining to a single InferenceX model you must pick the variant that matches how it was served (InferenceX rows carryspec_method, precision, disagg — use those as hints), or compare against the variant the customer would actually run. - Name matching to InferenceX is fuzzy. AA uses spaced display names
(
DeepSeek R1 0528,Kimi K2.5) vs InferenceX's (DeepSeek-R1-0528,Kimi-K2.5). Normalize (lowercase, strip spaces/dashes) before matching, and expect to hand-map a few. (Internally we reuse the matcher inllm-tracker/enrichment/aa_matcher.py.) - Open-vs-proprietary isn't a field. AA returns everything (Claude, GPT,
Gemini included). If you only want open-weights models for an on-prem GPU
story, filter by creator/name —
scrape_artificial_analysis.pyhas a_looks_open()heuristic you'll want to tune. - Speed/price are AA's hosted measurements, not yours — use AA for capability, and InferenceX (or your own runs) for performance on the target GPU. Don't cross the streams.
InferenceX ──▶ given GPU (B200) + workload shape ──▶ models ranked by tok/s/gpu (efficiency)
Artificial ──▶ per-model coding/math/agentic index ──▶ models ranked by capability (use-case fit)
Analysis
└──────────────── match on normalized model name ────────────────┘
= "fast AND capable on my hardware, for this use case"
InferenceX endpoints and the AA API verified live 2026-06-30. Both are third-party services that can change or gate access at any time; InferenceX's endpoints are undocumented, and AA's API requires a key + attribution. Be polite — cache results, don't hammer.