I have been reading plenty about inference systems: KV cache, PagedAttention, prefill, decode, batching, and disaggregated serving. But reading about the machinery is different from running it and watching the machine do work.
This post is the first in the series that closes that gap. The goal was intentionally small: run one open-source model on a GPU, record what environment I actually got, generate 100 tokens, and capture enough numbers to make the next experiment less hand-wavy.
This is not a benchmark. It is a first measured smoke test.
The Setup
I used Google Colab as the GPU machine. Google Colab is the easiest way to get hands-on access to a T4 GPU for free.
The model was:
TinyLlama/TinyLlama-1.1B-Chat-v1.0
I started with TinyLlama for a practical reason: it is ungated on Hugging Face and small enough to fit on the GPU Colab usually gives me. I originally tried a Llama 3.2 model, but that repository requires Hugging Face access approval and authentication. That is not the problem I wanted to solve in this first run. TinyLlama is also Llama-shaped, which keeps the architecture question relevant.
The serving engine was vLLM. I used vLLM here because I want the learning path to move toward real inference infrastructure, one layer down from a high-level text generation API.
Why an Inference Engine?
The simplest way to expose a model is to load it with PyTorch or Transformers and put a FastAPI endpoint in front of it. That is useful for proving that a model can produce text, but it does not answer the systems questions that show up when many requests share the same GPU.
An inference engine is the runtime layer that manages that pressure. It is responsible for things like batching requests, scheduling prefill and decode work, managing KV-cache memory, choosing attention kernels, streaming tokens, and keeping GPU utilization high without blowing up latency.
That is why this run starts with vLLM instead of a hand-rolled API wrapper. vLLM, SGLang, TensorRT-LLM, and llama.cpp are examples of inference runtimes with different design points. They are not just text generation APIs. They are where serving behavior lives: memory allocation, queueing, token scheduling, kernel choices, and tradeoffs between throughput and latency.
For this first post, I only used vLLM’s offline LLM.generate API. That is still the shallow end of the pool, but it puts the experiment on the right runtime layer. The next step is to run vLLM as an OpenAI-compatible server and measure streaming behavior directly.
Step 1: Check the GPU
Before installing anything, I checked what Colab assigned:
$ nvidia-smi
NVIDIA-SMI 580.82.07
Driver Version: 580.82.07
GPU: Tesla T4
Memory: 0 MiB / 15360 MiB
This matters because the output of a model run is not portable without the hardware context. A 100-token generation on a T4, L4, A100, or H100 means very different things.
At this point I only captured the hardware and Python version. I left PyTorch and vLLM version checks until after the install/restart, so the model process starts from a clean runtime.
import sys
print(f"Python: {sys.version.split()[0]}")
The important habit is not the exact code. The habit is to record the runtime before believing the result.
For this first run, nvidia-smi was enough for GPU and VRAM measurements. If this becomes a repeatable Python measurement harness later, I will use nvidia-ml-py for NVIDIA GPU telemetry and psutil for CPU/process telemetry. I am not installing either yet because the first version does not need them.
Step 2: Install vLLM
The install command was:
pip install -q -U uv
uv pip install --system vllm --torch-backend=auto
pip uninstall -y -q torchaudio || true
The --torch-backend=auto flag is not decoration. vLLM and PyTorch wheels are tied to CUDA backends. In Colab, the assigned runtime can change, and hard-coding a backend can install a wheel that imports cleanly until it looks for the wrong CUDA runtime library.
I hit that once already: vLLM tried to load libcudart.so.13, but the Colab runtime did not provide it. Letting uv choose the torch backend avoids turning the first inference experiment into a CUDA wheel debugging session.
I removed torchaudio after installing vLLM because this text-generation run does not use audio, and a mismatched audio wheel can break imports.
One Colab-specific wrinkle: I ran the vLLM code as a normal Python process:
python phase0_vllm_run.py
vLLM 0.28 initializes CUDA and distributed runtime machinery even for a single-GPU run. In that path it can call sys.stdout.fileno(). Colab’s captured notebook stream does not always provide a real file descriptor, which can fail with UnsupportedOperation: fileno. Launching a small script gives vLLM the process boundary it expects.
Step 3: Write the Runner
The runner keeps model loading, generation, timing, and result capture together:
import subprocess
import time
from vllm import LLM, SamplingParams
model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
load_start = time.time()
llm = LLM(
model=model_name,
dtype="half",
max_model_len=1024,
gpu_memory_utilization=0.8,
enforce_eager=True,
disable_log_stats=True,
)
load_seconds = time.time() - load_start
prompt = "KV caching helps inference servers because"
sampling = SamplingParams(max_tokens=100, temperature=0.7)
generate_start = time.time()
outputs = llm.generate([prompt], sampling)
generate_seconds = time.time() - generate_start
There are a few choices packed into those lines.
dtype="half" tells vLLM to use FP16 weights where possible. That keeps memory lower than FP32 and is normal for this kind of small GPU smoke test.
max_model_len=1024 caps the context length. I did not need a long-context run yet, and keeping the maximum sequence length small reduces memory pressure.
gpu_memory_utilization=0.8 tells vLLM how much of the GPU memory it is allowed to plan around. This matters because vLLM pre-allocates and manages memory for serving, including KV-cache blocks.
enforce_eager=True disables CUDA graph capture and torch compilation. That is not the fastest serving configuration, but it keeps this first Colab run simpler and avoids spending the first experiment debugging compile and graph-capture behavior.
Does the Model Need to Be Written for vLLM?
This was my first architecture question after the run. vLLM printed:
Resolved architecture: LlamaForCausalLM
That line matters. vLLM does not need every individual checkpoint to be written specifically for it. It needs support for the model architecture.
TinyLlama works because its Hugging Face config maps to LlamaForCausalLM, and vLLM already has an execution path for that architecture. The model repository provides weights, config, tokenizer files, and generation settings. vLLM provides the serving runtime: model execution, batching, attention kernels, and KV-cache management.
PagedAttention is part of the serving engine. The checkpoint does not say “use PagedAttention.” vLLM decides how to store and schedule KV-cache blocks at runtime.
The practical rule is:
supported architecture + compatible weights/tokenizer = vLLM can usually run it
unsupported architecture or custom layers = vLLM needs model support
That distinction helped me separate the model artifact from the serving engine. TinyLlama was not built for this notebook. It worked because it is Llama-shaped, and vLLM knows how to serve that shape.
Step 4: Generate 100 Tokens
The generation prompt was deliberately boring:
prompt = "KV caching helps inference servers because"
sampling = SamplingParams(max_tokens=100, temperature=0.7)
outputs = llm.generate([prompt], sampling)
The prompt was chosen because it points at the system concept I am trying to understand. The model’s answer was not the important result. The important result was that a serving engine loaded weights, accepted a prompt, generated tokens, and produced measurable runtime behavior.
What I Measured
| Metric | Value |
|---|---|
| Date | 2026-08-30 |
| Environment | Google Colab |
| GPU | Tesla T4 |
| Python | 3.13.15 |
| PyTorch | 2.13.0+cu132 |
| PyTorch CUDA | 13.2 |
| vLLM | 0.28.0 |
| Model | TinyLlama/TinyLlama-1.1B-Chat-v1.0 |
| Model load time | 102.7s |
| Generation time | 3.97s |
| Generated tokens | 100 |
| Throughput | 25.2 tok/s |
| VRAM before load | 0 MiB |
| VRAM after load | 11,753 MiB |
| VRAM after generation | 11,753 MiB |
| Approx model/runtime VRAM | 11,753 MiB |
The saved result is intentionally small:
Model: TinyLlama/TinyLlama-1.1B-Chat-v1.0
Load time: 102.7s
Generation time: 3.97s
Generated tokens: 100
Throughput: 25.2 tok/s
I am treating this as a successful smoke test, not a benchmark. The load path includes model download/cache behavior and vLLM initialization. The generation number is a single offline request, not a serving latency distribution.
What I Had to Fix
Four things broke before the first clean run.
First, I installed a vLLM/PyTorch combination that expected a CUDA runtime library that Colab did not have. That is why I switched to:
uv pip install --system vllm --torch-backend=auto
Second, I tried to load a gated Llama model from Hugging Face without authenticating. The serving stack was working, but the model download failed with a 401. That is why the first run now uses TinyLlama.
Third, torchaudio was compiled for a different CUDA version than the PyTorch wheel selected for vLLM. This run does not use audio, so the simplest fix was to remove torchaudio instead of trying to align an unused package.
Fourth, running vLLM directly inside Colab’s notebook process hit UnsupportedOperation: fileno. That was not a model problem or a CUDA problem. It was a process/runtime boundary problem: vLLM expected normal stdout behavior while the notebook kernel was intercepting output. Running the same code through python phase0_vllm_run.py is the cleaner Colab path.
None of these failures were deep. But they are exactly the sort of setup details that disappear from polished inference papers and then eat the first few hours of hands-on work.
What This Run Does Not Measure Yet
This run used offline LLM.generate from a Python process, not the OpenAI-compatible vLLM server with streaming responses.
That means I measured:
total generation elapsed time
generated token count
aggregate throughput
basic VRAM movement
I did not yet measure:
TTFT: time to first token
TPOT: time per output token after the first token
p95/p99 latency
goodput under an SLO
KV-cache hit rate
memory fragmentation
That distinction matters. A first model run is useful, but it is not a serving benchmark. The next step is to run vLLM as a server, send a streaming request, and record TTFT and TPOT directly.
The Systems Lesson
The model output made a common mistake: it described KV cache like a distributed key-value store of input/output pairs. That is not what KV cache means in Transformer inference.
In this context, KV cache is cached attention state. During prefill, the model processes the prompt and stores key/value tensors for prior tokens. During decode, each new token can attend over those cached tensors instead of recomputing the whole prefix.
That is why the systems questions become concrete:
Where is the KV cache stored?
How much memory does it consume?
When does it grow?
Can multiple requests share it?
What happens when it does not fit?
Can prefill and decode run on different machines without too much transfer cost?
This is the bridge from “I ran a model” to inference infrastructure.
Next
The next run should use the vLLM OpenAI-compatible server and a streaming client. That will turn this smoke test into a real latency measurement:
TTFT: request start -> first streamed token
TPOT: average gap between streamed output tokens
After that, the next useful experiment is PagedAttention as a memory allocator: compare naive contiguous KV-cache reservation against fixed-size paged blocks under variable prompt and output lengths.