Breadcrumb

  1. Red Hat Interactive Learning Portal
  2. OpenShift AI learning
  3. Get started with vLLM
  4. Measure what matters: benchmarking and evaluation

Get started with vLLM

Learn how to compress, serve, and benchmark LLMs with vLLM.

This lesson focuses on advanced methods for performance optimization, such as benchmarking and evaluation workflows to handle production-grade scenarios, using two open source tools: GuideLLM and lm_eval. You’ll analyze advanced metrics such as time-to-first-token (TTFT) and inter-token latencies across custom load tests. Plus, you will see how to isolate user experience and hardware bottlenecks, validate model accuracy, and make data-driven decisions for LLM deployments.

Prerequisites:

In this lesson, you will:

  • Run a real benchmark against your vLLM server and interpret the results using GuideLLM.
  • Evaluate model quality with lm_eval on a standardized task.
  • Read a published model card data to understand quantization.
  • Reason about whether a quantization tradeoff is worth deploying.

How do we evaluate LLMs in production?

Benchmarks and evaluations (evals) are structural processes designed to measure how an LLM performs across the critical tradeoffs of speed, cost, and accuracy.

This relationship creates a “trilemma” among cost, speed, and accuracy. In practice, attempting to maximize any two of these pillars inherently compromises the third. For example, building a system that is both incredibly fast and highly accurate will inevitably demand expensive, high-end compute, while optimizing for low cost and high speed will force a drop in accuracy (Figure 1).

Slide showing the relationship between cost, speed, and accuracy, in which optimizing any two of them hurts the third.
Figure 1: Optimizing any two of these three key metrics (cost, speed, and accuracy) hurts the third.

So where does your deployment need to land on this triangle? Without clear measurements, you can’t always answer that question. That’s what this lesson will answer in two distinct ways:

  • Evaluations (Evals): Typically focus on validation and accuracy. For example, they utilize specialized datasets or tests to check a model’s capabilities in a variety of domains, from knowledge to reasoning to computer use (for agentic tasks). You can also use evals to ensure stability after a model undergoes optimization techniques like quantization, ensuring that it still outputs high-quality, accurate answers for its targeted tasks.
  • Benchmarks: Focus on operational performance and efficiency under load. Tools like GuideLLM are used to simulate realistic real-world traffic patterns (using various concurrency (10 users at the same task) or Poisson (random load) distributions) to stress-test the model's serving engine. This measures fine-grained performance metrics such as latency, throughput (requests per second), and memory/hardware utilization.

These practices are essential for meeting Service Level Objectives (SLOs) and other production metrics because they provide pre-deployment capacity planning, hardware evaluation, and regression testing. 
Testing how the system scales when concurrent requests flood the infrastructure allows developers to pinpoint exactly where performance degrades. Ultimately, benchmarks and evals provide the data necessary to guarantee that the deployed model can handle maximum user loads and run efficiently within strict latency and cost constraints without sacrificing the accuracy required by the business.

Complete the setup

  1. In our Jupyter Hub environment, navigate to the benchmarks-evals folder and open the guidellm-lm-eval.ipynb notebook

    We’ll be using the vLLM API key to connect to a running model, which will be used for both the benchmark and accuracy evaluations.

    Paste your API key to connect to the Sandbox (Figure 2).

    Input:

    # Paste your token here (get it with: oc whoami -t)
    TOKEN = "YOUR_TOKEN_HERE"
    The module for this lesson, which is found in the left-hand side menu.
    Figure 2: The necessary folder and notebook can be found in the left-hand side menu.
  2. Send a quick test request to confirm that everything is working.

    Input:

    !pip install -q -r requirements.txt
    
    import warnings
    warnings.filterwarnings("ignore")
    
    import time, requests, json, os, glob
    from openai import OpenAI
    
    VLLM_URL = "https://isvc-qwen3-8b-fp8-predictor.sandbox-shared-models.svc.cluster.local:8443"
    
    client = OpenAI(base_url=f"{VLLM_URL}/v1", api_key=TOKEN)
    os.makedirs("outputs", exist_ok=True)
    
    r = requests.get(f"{VLLM_URL}/v1/models",
                    headers={"Authorization": f"Bearer {TOKEN}"}, timeout=10)
    if r.status_code == 401:
       raise RuntimeError("Token expired — run `oc whoami -t` and paste a fresh one above.")
    r.raise_for_status()
    MODEL = r.json()["data"][0]["id"]
    print(f"Connected — model: {MODEL}")

    Output:

    Connected — model: isvc-qwen3-8b-fp8

 

Benchmark with GuideLLM

First we'll create the directory outputs where we'll save the benchmark results. Here is the command we can use to run guidellm. We can run it from the terminal, but here we're running from this code cell using the subprocess module.

Let’s break down the flags we’re passing:

  • Target VLLM_URL: Points GuideLLM at our local vLLM server. It hits the OpenAI-compatible endpoints.
  • profile synchronous: Sends requests one at a time, waiting for each to finish before sending the next. This gives us a clean baseline of single-request latency, with no batching or queueing in the picture. Other profiles (concurrent, throughput, sweep) ramp up their load to stress-test how the server holds up under traffic.
  • max-requests 10: Caps the run at 10 requests. This is deliberately tiny so that the cell finishes quickly in this environment; a real benchmark would use a few hundred to a few thousand requests, or switch to a time-bounded run with --max-seconds.
  • data prompt_tokens=32,output_tokens=16,samples=32: synthetic workload spec: Each request is ~32 prompt tokens in, ~16 output tokens out, drawn from a pool of 32 pre-generated prompts. Keeping samples >= max-requests means that no prompt repeats, so we don't accidentally inflate prefix-cache hits.
  • output-dir ./outputs: Where GuideLLM writes the JSON report.

We’ll now run the cell. It will take a moment to finish. When it's done, we’ll have a structured benchmark file with per-request distributions, ready to interpret.

GuideLLM saves these results in two formats: JSON and CSV with pre-computed statistics for means, percentiles, min., and max., so that we don't have to calculate them ourselves.

import subprocess
cmd = [
  "guidellm", "benchmark",
  "--target", VLLM_URL,
  "--model", MODEL,
  "--backend-kwargs", json.dumps({"api_key": TOKEN}),
  "--profile", "synchronous",
  "--max-requests", "10",
  "--data", "prompt_tokens=32,output_tokens=16,samples=32",
  "--output-dir", "./outputs",
]
env = os.environ.copy()
env["OPENAI_API_KEY"] = TOKEN
print(f"Running: {' '.join(cmd)}\n")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env)
if result.returncode == 0:
  print("Benchmark complete!")
  tail = result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout
  print(tail)
else:
  print(f"GuideLLM exited with code {result.returncode}")
  print(f"STDOUT:\n{result.stdout[-1000:]}")
  print(f"STDERR:\n{result.stderr[-1000:]}")

Output:

Running: guidellm benchmark --target http://localhost:8000 --model Qwen/Qwen3-0.6B --profile synchronous --max-requests 10 --data prompt_tokens=32,output_tokens=16,samples=32 --output-dir ./outputs
Benchmark complete!
| synchronous | 1.7     | 2.0    | 122.2 | 292.5 | 101.7 | 123.2 | 105.6 | 125.5 |
|=============|=========|========|=======|=======|=======|=======|=======|=======|
ℹ Server Throughput Statistics (All Requests)
|=============|=======|======|=========|==============|===============|==============|
| Benchmark   | Requests             ||| Input Tokens | Output Tokens | Total Tokens |
| Strategy    | Concurrency || Per Sec | Per Sec      | Per Sec       | Per Sec      |
|             | Mdn   | Mean | Mean                                               ||||
|-------------|-------|------|---------|--------------|---------------|--------------|
| synchronous | 1.0   | 1.0  | 0.6     | 26.9         | 9.8           | 34.4         |
|=============|=======|======|=========|==============|===============|==============|
✔ Benchmarking complete, generated 1 benchmark(s)
…   json    : outputs/benchmarks.json
…   csv     : outputs/benchmarks.csv

Interpret benchmark results

Let's now extract from the json files the key numbers:

  • The time to first token (TTFT): Time to first token measures how quickly the model starts generating a response to minimize perceived waiting time.
  • The inter-token latency: Inter-token latency tracks the generation speed of subsequent words to ensure a smooth reading pace.
  • The end-to-end latency: End-to-end latency captures the total time required to complete the request to ensure the entire interaction meets strict operational deadlines.

These metrics matter because they directly quantify user experience and system responsiveness for an LLM application (Figure 3). 

A visual timeline showcasing how TTFT (prefill delay), ITL (generation pacing), and total End-to-End Latency interact during a real-time streaming response.
Figure 3: A visual timeline showcasing how TTFT (prefill delay), ITL (generation pacing), and total End-to-End Latency interact during a real-time streaming response.

When we run the cell, the output presents the mean, the 50th, 95th, and 99th percentiles of each metric. This is what we'd put in a report to our team to use or evaluate whether an LLM deployment meets our service-level objectives (SLOs).

Since averages hide outliers, we might notice the p95 (meaning 95% of requests are faster than that value) is worse. We see the same with the p99. This is important because 5 in 100 users could be waiting 20 times longer than usual.

When evaluating a deployment, we always look at the p95 and p99, because if there's a big gap between the mean and the p95, we have tail latency issues, and users will be sure to feel those issues.

with open("outputs/benchmarks.json") as f:
   report = json.load(f)
bench = report["benchmarks"][0]
metrics = bench["metrics"]
n_requests = metrics["request_totals"]["successful"]
print(f"Profile: {bench['type_']}  |  Requests: {n_requests}\n")
display_metrics = {
   "TTFT (ms)":       "time_to_first_token_ms",
   "ITL (ms)":        "inter_token_latency_ms",
   "E2E Latency (s)": "request_latency",
   "Output tokens":   "output_token_count",
}
print(f"{'Metric':<20} {'Mean':>8} {'p50':>8} {'p95':>8} {'p99':>8}")
print("-" * 55)
for label, key in display_metrics.items():
   dist = metrics[key]["successful"]
   p = dist["percentiles"]
   print(f"{label:<20} {dist['mean']:>8.2f} {p['p50']:>8.2f} "
         f"{p['p95']:>8.2f} {p['p99']:>8.2f}")
throughput = metrics["output_tokens_per_second"]["successful"]
req_rate = metrics["requests_per_second"]["successful"]
print(f"\nThroughput: {req_rate['mean']:.2f} req/s  |  "
     f"{throughput['mean']:.1f} output tokens/s")

Output:

Profile: generative_benchmark  |  Requests: 10
Metric                   Mean      p50      p95      p99
-------------------------------------------------------
TTFT (ms)              141.10   122.24   292.45   292.45
ITL (ms)                99.98   101.74   123.18   123.18
E2E Latency (s)          1.64     1.69     2.01     2.01
Output tokens           16.00    16.00    16.00    16.00
Throughput: 0.61 req/s  |  9.8 output tokens/s


Performance benchmarks tell us how fast a deployment is, but not whether the model gives good answers. That's a totally different type of test. We could have the fastest inference server in the world, but if the model's accuracy drops 15% from quantization, then it's not deployable. That's where lm_eval comes in.

Evaluate model quality with lm_eval

lm_eval is a standardized evaluation harness that measures task performance, or how well a model answers on knowledge, reasoning, and coding benchmarks. While GuideLLM asks "how well does this deployment perform?", lm_eval will help us determine "how well this model answers".

We'll point lm_eval at the same running server using the OpenAI completions endpoint, just like we did earlier, and run Hellaswag, a common sense reasoning benchmark. It's the same evaluation that appears on most model cards.

We're using lm_eval’s simple evaluate function on 20 examples to keep it quick, but in production, we would run the full test set once or multiple times.

Run the cell. It'll take a moment to finish, then we’ll review the results.

import lm_eval
os.environ.setdefault("OPENAI_API_KEY", "unused")
TASK = "hellaswag"
print(f"Running lm_eval on {MODEL} via vLLM server ({TASK}, 20 examples)...\n")
results = lm_eval.simple_evaluate(
   model="local-completions",
   model_args=(
       f"model={MODEL},"
       f"base_url={VLLM_URL}/v1/completions,"
       "tokenized_requests=False,"
       "num_concurrent=1"
   ),
   tasks=[TASK],
   limit=20,
)

Output:

Running lm_eval on Qwen/Qwen3-0.6B via vLLM server (hellaswag, 20 examples)...
100%|██████████| 20/20 [00:00<00:00, 2215.22it/s]
Requesting API: 100%|██████████| 80/80 [00:12<00:00,  6.61it/s]

Print out the accuracy metrics. We see a result of 30%, with a standard deviation of around 10%, which isn't the best, but remember that this is a model that could probably fit on a smart fridge.

task_results = results["results"][TASK]
print(f"Model: {MODEL}")
print(f"Task: {TASK}  |  Examples: 20\n")
for metric, value in task_results.items():
   if isinstance(value, (int, float)):
       print(f"  {metric}: {value:.4f}")

Output:

Model: Qwen/Qwen3-0.6B
Task: hellaswag  |  Examples: 20
 acc,none: 0.3000
 acc_stderr,none: 0.1051
 acc_norm,none: 0.3000
 acc_norm_stderr,none: 0.1051

These evaluations give us numerical evidence to make educated model deployment decisions, from deployment performance using GuideLLM, to model accuracy using lm_eval.

We can also use published model card data that's been evaluated by the publisher, before we even get started.

How to read published benchmark data

In a previous lesson, we learned how to quantize a model with llm-compressor. In practice, quantized model publishers include accuracy tables on their model cards so that users can evaluate the tradeoff without running every benchmark themselves.

Here's the accuracy table from the RedHatAI/Qwen3-0.6B-quantized.w4a16 model card—the W4A16 variant of the model we've been working with. The Recovery column shows how much of the base model's accuracy the quantized version retains. Most benchmarks with meaningful base scores show 97–100% recovery, a strong result for a major model size reduction.

Also note that the accuracy of the model using Hellaswag is 43%, which is higher than the 30% we obtained. That's because we only used 20 samples and a single shot.

Category

Benchmark

Qwen3-0.6B

W4A16 (this model)

Recovery

OpenLLM v1

MMLU (5-shot)

42.82

39.80

93.0%

 

ARC Challenge (25-shot)

32.85

30.72

93.5%

 

GSM-8K (5-shot)

1.82

2.20

 

Hellaswag (10-shot)

43.04

41.02

95.3%

 

Winogrande (5-shot)

54.54

54.62

100.1%

 

TruthfulQA (0-shot)

51.61

48.77

94.5%

 

Average

37.78

36.19

95.8%

OpenLLM v2

MMLU-Pro (5-shot)

17.25

14.27

 

IFEval (0-shot)

62.83

55.81

88.8%

 

BBH (3-shot)

4.23

1.63

 

Math-lvl-5 (4-shot)

18.26

10.26

 

GPQA (0-shot)

0.00

0.00

 

MuSR (0-shot)

0.00

0.00

 

Average

17.10

13.66

Multilingual

MGSM (0-shot)

19.70

19.90

Reasoning

AIME 2024

9.69

3.44

 

AIME 2025

13.13

6.98

 

GPQA diamond

29.29

27.78

94.8%

 

Math-lvl-5

71.60

70.60

98.6%

 

LiveCodeBench

12.83

8.35

We now have three sources of evidence:

Source

What it tells you

GuideLLM

How the deployment performs (latency, throughput, consistency)

lm_eval

How the model answers (accuracy on a task you ran yourself)

Model card

How the model performs across many benchmarks, evaluated by the publisher

When deciding whether to deploy a quantized model, we need both dimensions. An optimization that doubles throughput but drops accuracy by 15% may not be worth it. An accurate model that can't meet latency SLOs isn't deployable either.

Lesson summary

In this lesson, we explored advanced benchmarking and evaluation techniques using GuideLLM and lm_eval to isolate performance bottlenecks and measure model accuracy under production-grade conditions. We analyzed metrics like TTFT variances and standardized evaluation performance to make precise, data-driven decisions for our vLLM deployments.


Learning path summary

In this learning path, we navigated the entire lifecycle of enterprise-ready large language model serving, moving from a base Hugging Face model to a highly optimized production-ready architecture. 

By leveraging the llm-compressor library and advanced algorithms (ex., GPTQ), we successfully reduced memory overhead into an efficient format (W4A16) without sacrificing model accuracy. 

From there, we got hands-on with vLLM, deploying our models with modern inference optimizations, including PagedAttention, continuous batching, and prefix caching. This allowed us to eliminate memory fragmentation and maximize hardware utilization across both CPU and multi-GPU environments.

We concluded by evaluating model performance and accuracy, stress-testing our deployments under real-world requests using GuideLLM and lm_eval. By analyzing performance metrics, such as TTFT variances and inter-token latencies, we successfully identified potential bottlenecks and verified the precision tradeoffs of our quantized models. 

With these configuration strategies and benchmarking insights, we completed the journey, ready to deploy open-source models that are not only highly accurate, but also scalable, cost-effective, and ready for production-grade workloads.

Ready to learn more?

Now that you’ve completed this introduction to vLLM, try these resources:

Previous resource
Serve LLMs efficiently with vLLM