<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Thoughts and Learnings of AI Eng]]></title><description><![CDATA[Thoughts and Learnings of AI Eng]]></description><link>https://akshaysuryawanshi.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 12:02:07 GMT</lastBuildDate><atom:link href="https://akshaysuryawanshi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[From Replicas to FP8: My First vLLM Performance Investigation]]></title><description><![CDATA[Testing vLLM replicas, NVIDIA MPS, and FP8 KV cache and attention on one laptop GPU, measuring throughput, latency, goodput, and answer quality.]]></description><link>https://akshaysuryawanshi.hashnode.dev/from-replicas-to-fp8-my-first-vllm-performance-investigation</link><guid isPermaLink="true">https://akshaysuryawanshi.hashnode.dev/from-replicas-to-fp8-my-first-vllm-performance-investigation</guid><category><![CDATA[vLLM]]></category><category><![CDATA[llm inference]]></category><category><![CDATA[GPU optimization]]></category><category><![CDATA[NVIDIA MPS]]></category><category><![CDATA[FP8]]></category><dc:creator><![CDATA[Akshay Suryawanshi]]></dc:creator><pubDate>Fri, 16 Oct 2026 23:06:53 GMT</pubDate><content:encoded><![CDATA[<p>In my previous post, I tested running an inference engine called vLLM with the Qwen3-0.6B model. I chose this model because it fits well enough to try out a few things with it. This post is about optimizing GPU utilization and not a model quality exploration.</p>
<h2>Background</h2>
<p>As a test, I ran vLLM with the following configuration,</p>
<pre><code class="language-shell">vllm serve Qwen/Qwen3-0.6B \
  --host 127.0.0.1 --port 8000 \
  --dtype bfloat16 --tensor-parallel-size 1 \
  --max-model-len 2048 \
  --max-num-seqs 8 \
  --max-num-batched-tokens 2048 \
  --gpu-memory-utilization 0.94 \
  --served-model-name Qwen/Qwen3-0.6B
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/64a85c64-6c2b-409e-8502-a629f859a47d.png" alt="" style="display:block;margin:0 auto" />

<p>vLLM processed more output tokens per second as I increased concurrency on the same GPU.</p>
<h2>Problem</h2>
<p>A question I have been trying to answer: what if we wanted to run more than one model? To be fair, I don't have any workflows that require me to do so, but the question I am trying to answer is: can we run more than one instance, let's say, for different use cases? An example that comes to mind: companies are looking into fine-tuning specialized models on top of small general-purpose models. Now, running one model might not be sufficient since there could be other specialized models.</p>
<p>The solution to this problem is simple: let's run two vLLM instances, each pretending to service a specific workload by serving a specialized model. I don't have a fine-tuned specialized model, but we are pretending here.</p>
<h3>Experiment 1</h3>
<p>So let's run two vLLM instances, same model, same config, except half the memory available. So <code>--gpu-memory-utilization</code> is set to 0.47 for both instances. Both the single- and dual-engine runs used max-num-seqs 128, with prefix caching disabled.</p>
<p>To test, we run the same concurrency sweep benchmark.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/a07a4df9-5eed-4b6b-92fe-cff77f2a1835.png" alt="" style="display:block;margin:0 auto" />

<p>Interesting result. The throughput didn't scale as expected. Instead, it became quite worse. I checked with Astra why that could be. No definitive answer as to why. So let's try to find where the slowness could be coming from. We have the following candidates,</p>
<ul>
<li><p>GPU contention, two inference engines with active workloads maybe contending with each other. Which is fair, but that's a huge efficiency drop.</p>
</li>
<li><p>CPU contention, maybe workload scheduling by vLLM itself is contending for CPU cycles.</p>
</li>
<li><p>Memory bandwidth: my laptop GPU is just not cut for this type of workload. Which is not so great when thinking about local LLM applications in the future.</p>
</li>
<li><p>Something else,</p>
</li>
</ul>
<h3>Experiment 2</h3>
<p>Astra mentioned NVIDIA MPS (Multi-Process Service), which lets CUDA kernels from separate processes run concurrently on the GPU. That can improve utilization, although the two vLLM engines still schedule their own batches.</p>
<p>So we enabled MPS and ran our concurrency sweep benchmark test again,</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/09f90527-5a66-4f0e-8c01-3c1b32ca3227.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/436bb9e5-0ca4-4cb3-b218-3c4b3b7fd962.png" alt="" style="display:block;margin:0 auto" />

<p>Definitely better results, but the gap between single-instance and multi-instance still exists.</p>
<h3>Experiment 3</h3>
<p>Ok so I wanted to understand where this throughput drop is coming from. I asked Astra to run the experiment again and capture some more metrics,</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/aceb40fc-9851-4aac-a031-fb4b1758d4ac.png" alt="" style="display:block;margin:0 auto" />

<p>Important discovery: Larger decode batches moved fewer DRAM bytes per token in our isolated probes. That supports batching efficiency as one contributor to the single-engine advantage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/746bca09-e6f7-42fd-83ae-3ae83caab6de.png" alt="" style="display:block;margin:0 auto" />

<p>Now I am wondering if these regressions show up in production environments of inference providers. I can't know that because I don't work at such companies yet, and I don't have a datacenter-grade GPU to test this on. Anyway, on to our laptop GPU.</p>
<h3>Experiment 4</h3>
<p>Alright, I want to try to improve utilization beyond what is achievable here. I am not thinking about model quality because my workload is mostly benchmark traffic. And keeping a real production fleet in mind, with recent datacenter-grade hardware, I do not want to be able to serve only one model, especially when specialized models can be great for automation. Also, what about running multimodal inference? These days, a single general-purpose model can do multimodal inference, but I want the ability to run different types of models.</p>
<p>I think there is enough cause here that we can further our experiments. I decided to test a different multi-instance setup.</p>
<ul>
<li><p>One instance runs Qwen3-0.6B with BF16 weights and KV cache, reserving 1.5 GiB for KV cache.</p>
</li>
<li><p>Another runs Whisper Small with FP16 weights and KV cache, reserving 1 GiB for KV cache.</p>
</li>
</ul>
<p>We started by capturing single-engine throughput and latencies of both models. And then captured them running as a dual-engine multimodal setup. The goal is to understand if the regression surfaces in this setup also.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/624425c7-d814-4f36-89d2-3aab60f67b0e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/8b1fc44c-3d58-426a-8d06-97b01639e506.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/e549410a-d6fa-4735-9eca-407de9b8f327.png" alt="" style="display:block;margin:0 auto" />

<p>Each service lost some of its solo speed. However, their combined normalized progress exceeded a time-split reference based on the measured solo rates, so co-location still delivered useful aggregate work.</p>
<h3>Experiment 5</h3>
<p>Going back to finding where the slowness came from. I instructed Astra to profile GPU utilization; it used tools like Nsight Systems and Nsight Compute, and CPU scheduling delay using the bcc tool, <code>runqlat</code> and capturing other important metrics,</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/eb227a0b-9e15-4238-9f61-8f11c488de3f.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/3ed91fbb-4ea7-4f1c-a7b0-0894f4d75bcc.png" alt="" style="display:block;margin:0 auto" />

<p>CPU run-queue waits were small in the sampled Qwen threads. GPU graph spans increased while sharing, and isolated larger-batch probes used fewer DRAM bytes per token. Memory traffic is a credible contributor, but these measurements do not isolate it as the sole bottleneck.</p>
<p>Astra traces noted an important finding. Our isolated decode probes showed that larger batches and lower precision could reduce DRAM bytes read per decoded token,</p>
<ul>
<li><p><strong>Batch 16 versus batch 32:</strong> approximately <strong>116.41 → 89.00 MB/token</strong>.</p>
</li>
<li><p><strong>BF16 versus FP8 KV/attention:</strong> approximately <strong>116.38 → 85.81 MB/token</strong>.</p>
</li>
</ul>
<p>That provides two insights: First, a dual-instance setup with the same model duplicates model weights, which could have been utilized better in single-instance setups. Second, our 256GB/s memory bandwidth could be insufficient here for the amount of memory moving per token at any time.</p>
<p>The second insight is a good learning point; This made me appreciate why memory bandwidth matters so much for these workloads.</p>
<h3>Experiment 6</h3>
<p>Memory traffic looks like a strong lead, but we haven’t isolated the whole cause. And vLLM's efficiency is really good at this point for what I am testing. Is there anything we can do to split the resource usage in exact half so that there is no contention? So we tried a few things,</p>
<ol>
<li><p>Does this problem surface with k8s-based deployments? I had little reason to suspect that it wouldn't. It surfaced in the same way. We tried the official NVIDIA controls for the same.</p>
</li>
<li><p>Can we do CPU-like pinning with GPUs? We tried nvtaskset, a research tool that uses undocumented NVIDIA controls to restrict GPU execution resources. Astra fixed a CUDA 13 build incompatibility, and the tool worked, but the tested partitions didn’t improve Qwen throughput.</p>
</li>
</ol>
<p>The only remaining exploration related to resource management was the use of MIG (Multi-Instance GPU). As per the documentation, it partitions not only GPU compute, but also its L2 caches and VRAM, to the best extent possible. The support spreadsheet says that B200 can be partitioned into 7 partitions. Unfortunately, MIG is not supported on my GPU, so I can't do much about it.</p>
<h3>Experiment 7</h3>
<p>What if we brought in an admission controller to manage incoming requests across two engine instances using shared request limits and queues? Astra came up with a bunch of options; we tried a RayServe-based simple controller. Our particular admission policy didn’t close the performance gap, so I stopped the experiment.</p>
<h3>Experiment 8</h3>
<p>Going back to understanding the contention more. I asked Astra to review the resource usage metrics and traces we had captured and do another pass at inferring what the cause of the regression we could be seeing was, apart from the batching-efficiency evidence we had already collected. We came back to the contention point very likely being the amount of memory transferred per token. Duplicated weights, yes, they are a factor, but we have plenty of memory. The earlier controlled tests had no KV-cache preemptions. The final sweep did: dual FP16 at total concurrency 64 recorded 160 preemptions across two repeats. So now we have one strong lead, and it could possibly be solved by just faster hardware, like HBM.</p>
<p>To test the memory-traffic idea, we compared BF16 with FP8 KV cache and attention operands, for both single and dual engines, at total concurrency 32.</p>
<ul>
<li><p>One engine, BF16 weights, <strong>BF16 KV and attention operands</strong>.</p>
</li>
<li><p>One engine, BF16 weights, <strong>FP8 KV and attention operands</strong>.</p>
</li>
<li><p>Two engines with MPS, BF16 weights, <strong>BF16 KV and attention operands</strong>.</p>
</li>
<li><p>Two engines with MPS, BF16 weights, <strong>FP8 KV and attention operands</strong>.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/770247bc-c5c6-46d7-ae42-d24fb9bd5448.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/6b14b2aa-d954-4816-aa38-869b9320dc5d.png" alt="" style="display:block;margin:0 auto" />

<p>Voila! Both topologies improved with FP8. Dual FP8 still trailed single BF16 in this first test. Isolated decode-graph reads dropped to about 86 MB/token.</p>
<p>Let's test with a multi-modal setup (MPS ON, concurrency 8 per model, 16 total).</p>
<ul>
<li><p>Baseline: Qwen with <strong>BF16 weights and KV</strong>; Whisper with <strong>FP16 weights and KV</strong>.</p>
</li>
<li><p>Candidate: Qwen unchanged; Whisper with <strong>FP16 weights and FP8 KV</strong>.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/7fc6a47a-bffa-4be7-929a-443b8762cb0b.png" alt="" style="display:block;margin:0 auto" />

<p>Hmmm. Slightly better but still an improvement.</p>
<p>I think this is a good path forward. The thing with precision tuning is that it can cause a decrease in the quality of answers. And that's totally understandable.</p>
<h3>Experiment 9</h3>
<p>Let’s measure how answer quality changes when we reduce the precision of KV cache and attention operands. I am new to all of this, so I asked Astra to find the most common benchmarks and run them against a setup of FP16 and FP8 KV cache precisions. So, benchmarking quality with the following config,</p>
<ol>
<li><p>Single-instance vLLM, BF16 weights, FP16 KV Cache and Attention operands, Qwen3-0.6B.</p>
</li>
<li><p>Single-instance vLLM, BF16 weights, FP8 KV Cache and Attention operands, Qwen3-0.6B.</p>
</li>
</ol>
<p>The first Qwen FP8 pilot used uncalibrated scales. The later quality tests and final sweep used calibrated FP8. Keeping BF16 model weights while using literal FP16 attention required an experiment-local adapter.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/7877c66c-2720-4429-a67e-3a49c266f7ea.png" alt="" style="display:block;margin:0 auto" />

<p>There is some degradation and some improvements. Strong note though, this is not quality equivalence in the same sense, like its a small test. Some observations from the quality tests,</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/5b9dd995-ef29-4525-8a19-e5182caf02d4.png" alt="" style="display:block;margin:0 auto" />

<p>Notably, Instruction-following scores fell on our tested subset, while math and science scores rose. Useful information.</p>
<h3>Experiment 10</h3>
<p>Time to close the loop. Let's see the final result with the following configurations,</p>
<ul>
<li><p><strong>One engine:</strong> BF16 weights, FP16 KV and attention, MPS OFF</p>
</li>
<li><p><strong>Two engines:</strong> BF16 weights, FP8 KV/attention engine, MPS ON</p>
</li>
<li><p><strong>Two engines:</strong> BF16 weights, FP16 KV and attention, MPS ON</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/66150b05-524e-4613-9cec-e3db34adf2d2.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6acd98407217b0a258ce14a6/3881ad3b-5191-4a5e-baa9-7d0a794679c9.png" alt="" style="display:block;margin:0 auto" />

<p>So there it is: Dual FP8 achieved the highest throughput at the largest tested concurrency, but single FP16 retained the best latency-SLO goodput. The useful choice depends on the workload’s latency and quality requirements.</p>
<h2>Conclusion</h2>
<p>The quality loss could be acceptable depending on the use case. The tradeoff here is better utilization of expensive hardware, especially when planning to run multiple small specialist models. A well-balanced topology here could be one engine running at FP16 for KV cache and attention, and another at reduced precision. A smart router in front of them could send simple conversational traffic to the reduced-precision engine, whereas complicated requests could be routed to the higher-precision engine. We haven’t tested whether simple conversations actually tolerate the lower precision better.</p>
]]></content:encoded></item><item><title><![CDATA[A Database Engineer’s First Look at LLM Inference.]]></title><description><![CDATA[A database engineer's first LLM inference experiment: running Qwen3-0.6B with vLLM on an 8GB laptop GPU and making sense of latency and throughput.]]></description><link>https://akshaysuryawanshi.hashnode.dev/first-look-at-llm-inference</link><guid isPermaLink="true">https://akshaysuryawanshi.hashnode.dev/first-look-at-llm-inference</guid><category><![CDATA[inference engine]]></category><category><![CDATA[inference]]></category><category><![CDATA[Inference Labs]]></category><category><![CDATA[ai inference]]></category><dc:creator><![CDATA[Akshay Suryawanshi]]></dc:creator><pubDate>Wed, 14 Oct 2026 14:44:03 GMT</pubDate><content:encoded><![CDATA[<p>I’ve spent much of my career working with database infrastructure, especially MySQL. I’m now exploring LLM inference to find out which of those engineering instincts transfer, and I’m starting by running a model on my laptop.</p>
<p>Database systems are Retrieval engines, whereas Gen AI systems are Generative engines. Databases store and retrieve stateful data from persistent storage, with tunable access paths. On the other hand, Gen AI systems generate a response to a request based on probabilistic training of the model. So one retrieves the data it stores, and the other, quite magically, generates data.</p>
<p>Database systems serve a strict SQL dialect; Gen AI systems work with natural language, with a very loose structure. I would consider this a superpower of these systems. Responses are similarly natural language text, code, or something else; the important part is that it's all generated. Fascinating technology.</p>
<p>Models themselves are basically a set of files on your disk after you have downloaded the models. I use HuggingFace for that. The software that serves requests using the model is called an Inference Engine.</p>
<p>From an infrastructure engineer's perspective, I think it is fair to think of Inference Engine deployment as somewhat equivalent to Database Server deployments. One difference is the hardware. GPUs are commonly used for inference at scale; other accelerators, such as TPUs, also exist. We'll explore the hardware some other time, but the gist is that LLMs utilize parallel processing capability provided by GPUs to provide a reliable experience.</p>
<p>Another interesting aspect to understand is that the LLMs, when serving, are frozen. There are no updates to the model weights during inference; that's a large difference from our database analogy. Compared with the database workloads I'm used to, GPU-hosted inference puts much more emphasis on GPU compute and memory capacity and bandwidth.</p>
<h2>The hardware</h2>
<p>I think we have a rough mental model now on how to think about Inference engines. So getting more hands-on by actually running a model would be interesting. I have a decent laptop, Lenovo ThinkPad P1, 16-core CPU, 64GB RAM, NVIDIA RTX 3000 Ada Generation Laptop GPU, 2TB NVMe disk. Of these resources, the most important is the GPU: 4608 CUDA cores, 8GB VRAM, 256 GB/s bandwidth, ~20 TFLOPS, and 319 TOPS sparse FP8.</p>
<p>Some explanation of these numbers:</p>
<ul>
<li><p>8GB VRAM stores the model weights and some buffers. For comparison, the NVIDIA B300 has 288GB, 36x more. Can run bigger models; we'll see soon why.</p>
</li>
<li><p>256 GB/s bandwidth; for now, understand that higher memory bandwidth helps when memory transfers are the bottleneck. Blackwell B300 has upto 8TB/s, ~31x more.</p>
</li>
</ul>
<p>It is not fair that I compare a laptop-class GPU with a datacenter-grade accelerator, but it's important to understand the constraints before we start and what to expect in real Production Environments.</p>
<p>I'll start with the simplest approach; we'll use vLLM to run a small model like Qwen3-0.6B. That model name is a mouthful, so here's how I understand it,</p>
<ul>
<li><p>Qwen - It's the model family name, like GPT models or the Claude model family. For now, it's just a name. 3 is the major version. I still haven't checked what changes cause a major version. Will report that later.</p>
</li>
<li><p>0.6B - This model has 600 million learned parameters. Parameter count matters for deployment because it affects weight storage and computational requirements. It alone does not indicate the model's answer quality.</p>
</li>
</ul>
<p>The parameter count is especially interesting in deployments because it helps determine how much GPU memory the weights need. The KV cache and other runtime allocations need additional space.</p>
<table>
<thead>
<tr>
<th>Model Size</th>
<th>Raw 4-bits</th>
<th>Raw 16-bits</th>
</tr>
</thead>
<tbody><tr>
<td>4B</td>
<td>2GB</td>
<td>8GB</td>
</tr>
<tr>
<td>8B</td>
<td>4GB</td>
<td>16GB</td>
</tr>
<tr>
<td>14B</td>
<td>7GB</td>
<td>28GB</td>
</tr>
<tr>
<td>32B</td>
<td>16GB</td>
<td>64GB</td>
</tr>
</tbody></table>
<p>A 14B model at four bits looks close to my GPU’s capacity on paper. Whether it actually fits requires checking the complete runtime memory footprint.</p>
<h2>Running the Model</h2>
<p>Below is the command I run to start a vLLM engine to serve the said model,</p>
<pre><code class="language-shell">VLLM_USE_FLASHINFER_SAMPLER=0 vllm serve Qwen/Qwen3-0.6B \
  --served-model-name Qwen/Qwen3-0.6B \
  --host 127.0.0.1 \
  --port 8000 \
  --dtype bfloat16 \
  --tensor-parallel-size 1 \
  --max-model-len 2048 \
  --max-num-seqs 8 \
  --max-num-batched-tokens 2048 \
  --gpu-memory-utilization 0.65 &amp;

VLLM_PID=$!
</code></pre>
<p>I'll try to explain the flags we used,</p>
<p>VLLM_USE_FLASHINFER_SAMPLER=0 causes vLLM to not use the FlashInfer top-k/top-p sampler on CUDA. We did not have it available.</p>
<ul>
<li><p>--served-model-name - The name of the model exposed through the API.</p>
</li>
<li><p>--host --port - The address where the API is hosted.</p>
</li>
<li><p>--dtype bfloat16 - We run with BF16 precision, a 16-bit floating-point format. Given this is a small model, the memory requirement of ~1.2 GB should fit comfortably in our VRAM, before the KV cache and other runtime allocations.</p>
</li>
<li><p>--tensor-parallel-size 1 - Let's skip this one for now.</p>
</li>
<li><p>--max-model-len 2048 - This is the context size of a request to the model plus the output length. The same one we see in ChatGPT and other apps.</p>
</li>
<li><p>--max-num-seqs 8 - This is an upper limit on sequences processed in an iteration. The token budget also affects scheduling.</p>
</li>
<li><p>--max-num-batched-tokens - This is an upper limit on how many tokens would be processed in an iteration.</p>
</li>
<li><p>--gpu-memory-utilization 0.65 - This gives vLLM a budget of 65% of GPU memory for its model executor, including weights, runtime allocations, and the KV cache.</p>
</li>
</ul>
<p>So that runs the model.</p>
<h3>First Request</h3>
<pre><code class="language-shell">$ curl -sS http://127.0.0.1:8000/v1/chat/completions   -H "Content-Type: application/json"   -d '{
    "model": "Qwen/Qwen3-0.6B",
    "messages": [
      {
        "role": "user",
        "content": "What is the most famous greeting in programming?"
      }
    ],
    "max_tokens": 128,
    "temperature": 0.7,
    "chat_template_kwargs": {
      "enable_thinking": false
    }
  }' | python3 -m json.tool
</code></pre>
<p>Response excerpt</p>
<pre><code class="language-plaintext">"The most famous greeting in programming is often **\"Hello, world!\"**. It is a simple and universally recognized program that is used in various programming environments, from simple text editors to more complex applications. This greeting is often celebrated in programming communities and is a classic example of a programming language."
</code></pre>
<p>It answered the question, but then wandered into an incorrect claim about programming languages. The engine works; whether the answers are good is a separate question.</p>
<h3>Benchmarking</h3>
<p>To get an idea of what kind of performance to expect, I ran a benchmark. This benchmark tool is provided by vLLM; from a database analogy perspective, it is similar to sysbench. This is one synthetic serving benchmark using random 512-token inputs and fixed 128-token outputs, with a maximum concurrency of 8. It measures serving performance and not answer quality.</p>
<ul>
<li><p>vLLM version - vllm-0.30.0-nohash</p>
</li>
<li><p>OS - Ubuntu</p>
</li>
<li><p>Kernel - 6.8.0-146-generic</p>
</li>
<li><p>NVIDIA driver - 580.178.04</p>
</li>
</ul>
<pre><code class="language-shell">vllm bench serve \
  --backend openai \
  --base-url http://127.0.0.1:8000 \
  --endpoint /v1/completions \
  --model Qwen/Qwen3-0.6B \
  --dataset-name random \
  --random-input-len 512 \
  --random-output-len 128 \
  --random-range-ratio 0 \
  --random-prefix-len 0 \
  --num-prompts 128 \
  --num-warmups 8 \
  --max-concurrency 8 \
  --request-rate inf \
  --ignore-eos \
  --temperature 0 \
  --seed 42 \
  --percentile-metrics ttft,tpot,itl,e2el \
  --metric-percentiles 50,95,99 \
  --disable-tqdm \
  --save-result \
  --result-filename qwen3-06b-benchmark.json
</code></pre>
<p>Some important flags,</p>
<ul>
<li><p>--random-input-len 512 - That's the prompt size in tokens that the benchmark will fire at the engine.</p>
</li>
<li><p>--random-output-len 128 - That's the output response size in tokens. I use tokens as a unit here because that's what LLMs work with. Token vocabulary is provided by the model.</p>
</li>
<li><p>--max-concurrency 8 - The benchmark keeps at most 8 requests in flight. The engine can process up to 8 sequences per iteration, depending on its token budget.</p>
</li>
<li><p>--percentile-metrics ttft, tpot, itl, e2el</p>
<ul>
<li><p>TTFT (Time To First Token) measures the elapsed time from when the benchmark client submits a request until its first streamed output arrives. Prefill contributes to this, but TTFT does not isolate prefill time. We measure this specifically to understand prefill phase performance. Prefill is the phase where the request prompt is processed.</p>
</li>
<li><p>TPOT (Time Per Output Token) tells us the average time taken per output token after the first. For reference, the calculation is (end-to-end latency - TTFT) / (output tokens - 1).</p>
</li>
<li><p>ITL (Inter-Token Latency) measures the gap between successive streamed outputs received by the benchmark client.</p>
</li>
<li><p>E2EL (End-to-End Latency) is the complete request-response latency. It helps us capture phases such as tokenization, prefill, decoding, and output generation.</p>
</li>
</ul>
</li>
</ul>
<h3>Understanding the Results</h3>
<p>Now let's review the results of this benchmark run,</p>
<pre><code class="language-shell">Model:                              Qwen/Qwen3-0.6B
Weight dtype:                       bfloat16
GPU:                                RTX 3000 Ada Laptop, 8 GB
Measured requests:                  128
Successful / failed:                128 / 0
Input / output tokens per request:  512 / 128
Configured concurrency:             8

Benchmark duration:                 18.20 s
Request throughput:                 7.03 requests/s
Output token throughput:            900.35 tokens/s
Total input + output throughput:    4,501.74 tokens/s

Mean time to first token:           84.35 ms
P95 time to first token:            121.85 ms
Mean time per output token*:        8.28 ms
Mean request latency:              1,135.82 ms
P95 request latency:               1,146.26 ms

* Excludes the first token.
</code></pre>
<p>I have formatted the results for display. Let's go through the important ones.</p>
<ul>
<li><p>We did a total of 128 requests, shown by Measured requests. It took 18.20s to do so.</p>
</li>
<li><p>Across eight concurrent requests, the server produced approximately 900 output tokens per second in aggregate.</p>
</li>
<li><p>Mean time to first token is ~85 ms with a p95 of about 122 ms. Prefill contributes to this, but this run doesn't tell me how much time was spent processing the prompt versus waiting or handling the request. Does batching hurt TTFT here? That needs another experiment.</p>
</li>
<li><p>The mean time per output token is 8.28 ms. This is the decode phase where the model is iterating through all its layers to generate one token, excluding the first one. This model has 28 transformer layers. Likewise, TPOT is the average time per subsequent output token.</p>
</li>
<li><p>Mean request latency and p95 for the same indicate the overall end-to-end latency of the request. With roughly eight requests in flight, about 1.14 seconds per request across 128 requests gives us ~18 seconds. So the math checks out.</p>
</li>
</ul>
<p>Some thoughts about the important phases of inferencing.</p>
<h3>Tokenization</h3>
<ul>
<li><p>Each model ships with a token vocabulary.</p>
</li>
<li><p>There is a tokenizer that converts your prompts into tokens that the model understands.</p>
</li>
</ul>
<h3>Prefill</h3>
<ul>
<li><p>This is the phase where the tokenized input prompt is processed by the model.</p>
</li>
<li><p>Since all the input tokens are known beforehand, the model can process many prompt positions in parallel within each layer. During prefill, the model also builds the prompt's KV cache, which will be reused during decoding.</p>
</li>
<li><p>Prefill is therefore a major contributor to TTFT, although measured TTFT also includes time spent queueing, handling the request, and returning the first output to the client.</p>
</li>
<li><p>Batching at this phase can improve GPU utilization, but it can also increase individual request latency; I need to measure the trade-off rather than assume it.</p>
</li>
</ul>
<h3>Decode</h3>
<ul>
<li><p>This is the phase after the first output token has been selected.</p>
</li>
<li><p>The engine feeds the newest token through the model to predict the next token, then repeats the process.</p>
</li>
<li><p>For this Qwen3-0.6B model, each forward pass traverses all 28 transformer layers. Without caching, the model would need to recompute earlier tokens' representations repeatedly. The KV cache avoids this by retaining the key and value vectors computed for previously processed tokens in each layer.</p>
</li>
<li><p>Multiple requests can have their decode steps batched together while keeping their attention state logically separate. They share the same model weights, so batching helps spread the cost of reading those weights across more tokens.</p>
</li>
</ul>
<h3>Detokenization</h3>
<ul>
<li>We still have tokens from the decode phase. The tokenizer converts the generated token IDs back into text, which is either streamed or returned to the client, based on how outputs are set to be received.</li>
</ul>
<p>So that's basically it about Inference Engines that I have learned so far. It is an interesting technology with many solutions differing in their implementation. For example, the KV Cache we talked about is prone to memory fragmentation, leading to waste. vLLM implemented PagedAttention, which borrows paging ideas from the operating system's virtual memory to GPU memory, mapping logical blocks to potentially noncontiguous physical blocks.</p>
<p>We also ran a small and simpler model to demonstrate how inference works; an interesting challenge would be trying to fit a larger model in our system. More on that later.</p>
]]></content:encoded></item></channel></rss>