Skip to content

Commit 1d2d7fd

Browse files
committed
chore: add ZenDNN blog post
Signed-off-by: Kevin Carter <kevin.carter@rackspace.com>
1 parent 5d17b47 commit 1d2d7fd

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
---
2+
date: 2025-12-17
3+
title: Running AI Inference on AMD EPYC Without a GPU in Sight
4+
authors:
5+
- cloudnull
6+
description: >
7+
Running AI Inference on AMD EPYC Without a GPU in Sight
8+
categories:
9+
- OpenStack
10+
- Zen
11+
- AMD
12+
- ZenDNN
13+
- ZenTorch
14+
- Virtualization
15+
16+
---
17+
18+
# Running AI Inference on AMD EPYC Without a GPU in Sight
19+
20+
**Spoiler: You don't need a $40,000 GPU to run LLM inference. Sometimes 24 CPU cores and the right software stack will do just fine.**
21+
22+
The AI infrastructure conversation has become almost synonymous with GPU procurement battles, NVIDIA allocation queues, and eye-watering hardware costs. But here's a reality that doesn't get enough attention: for many inference workloads, especially during development, testing, and moderate-scale production, modern CPUs with optimized software can deliver surprisingly capable performance at a fraction of the cost.
23+
24+
<!-- more -->
25+
26+
I recently spent some time exploring AMD's ZenDNN optimization library paired with vLLM on Rackspace OpenStack Flex, and the results challenge the assumption that CPU inference is merely a curiosity. Let me walk through what I found.
27+
28+
## The Setup: AMD EPYC 9454 on OpenStack Flex
29+
30+
For this testing, I spun up a general-purpose VM in Rackspace OpenStack Flex's DFW3 environment using the `gp.5.24.96` flavor.
31+
32+
| Resource | Specification |
33+
|----------|---------------|
34+
| vCPUs | 24 |
35+
| RAM | 96 GB |
36+
| Root Disk | 240 GB |
37+
| Ephemeral | 128 GB |
38+
| Processor | AMD EPYC 9454 (Genoa) |
39+
| Hourly Cost | $0.79 |
40+
41+
The AMD EPYC 9454 is a 4th-generation Zen 4 processor with AVX-512 support, including the BF16 and VNNI extensions that matter for inference workloads. These aren't just marketing checkboxes; they translate directly into optimized matrix operations that LLMs depend on.
42+
43+
!!! note "Containerization with Docker"
44+
45+
This post isn't going into how to install [Docker](https://docs.docker.com/engine/install), but before getting started, it should be installed.
46+
47+
## Getting vLLM
48+
49+
vLLM is an open-source library designed for efficient large language model inference. It supports CPU and GPU backends and features a pluggable architecture that allows integration with optimization libraries like ZenDNN. To get started, clone the vLLM repository.
50+
51+
```bash
52+
git clone https://github.com/vllm-project/vllm
53+
```
54+
55+
## Building vLLM with ZenTorch
56+
57+
AMD's ZenDNN library provides optimized deep learning primitives specifically tuned for Zen architecture processors. The ZenTorch plugin integrates these optimizations into PyTorch, and by extension, into vLLM's inference pipeline.
58+
59+
Build the initial Docker Image for vLLM with CPU optimizations enabled and the AVX-512 extensions activated.
60+
61+
```shell
62+
docker build -f docker/Dockerfile.cpu \
63+
--build-arg VLLM_CPU_AVX512BF16=1 \
64+
--build-arg VLLM_CPU_AVX512VNNI=1 \
65+
--build-arg VLLM_CPU_DISABLE_AVX512=0 \
66+
--tag vllm-cpu \
67+
--target vllm-openai \
68+
.
69+
```
70+
71+
With the base container built, we now add the layers to make sure we can leverage ZenDNN optimizations. The build process involves creating a custom Docker image that layers ZenDNN-pytorch-plugin on top of vLLM's CPU-optimized base image.
72+
73+
!!! example "Dockerfile for vLLM with ZenTorch at `docker/Dockerfile.cpu-amd`"
74+
75+
```dockerfile
76+
FROM vllm-cpu:latest
77+
RUN apt-get update -y \
78+
&& apt-get install -y --no-install-recommends make cmake ccache git curl wget ca-certificates \
79+
gcc-12 g++-12 libtcmalloc-minimal4 libnuma-dev ffmpeg \
80+
libsm6 libxext6 libgl1 jq lsof libjemalloc2 gfortran \
81+
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 10 --slave /usr/bin/g++ g++ /usr/bin/g++-12
82+
83+
RUN git clone https://github.com/amd/ZenDNN-pytorch-plugin.git && \
84+
cd ZenDNN-pytorch-plugin && \
85+
uv pip install -r requirements.txt && \
86+
CC=gcc CXX=g++ python3 setup.py bdist_wheel && \
87+
uv pip install dist/*.whl
88+
89+
ENTRYPOINT ["vllm", "serve"]
90+
```
91+
92+
Now build the final Docker image with ZenTorch enabled.
93+
94+
```bash
95+
docker build -f docker/Dockerfile.cpu-amd \
96+
--build-arg VLLM_CPU_AVX512BF16=1 \
97+
--build-arg VLLM_CPU_AVX512VNNI=1 \
98+
--build-arg VLLM_CPU_DISABLE_AVX512=0 \
99+
--tag vllm-cpu-zentorch \
100+
.
101+
```
102+
103+
Runtime configuration binds vLLM to available CPU cores and allocates substantial memory for the KV cache to maximize throughput. If you plan to use smaller instances, adjust these values accordingly.
104+
105+
For the test environment I set the shared memory size to 95G to accommodate larger models.
106+
107+
??? example "computing SHM_SIZE"
108+
109+
```bash
110+
export SHM_SIZE="$(($(free -m | awk '/Mem/ {print $2}') - 1024))"
111+
```
112+
113+
For the test environment I set the CPU core binding to use all but one core for vLLM processing.
114+
115+
??? example "computing CORES"
116+
117+
```bash
118+
export CORES="$(($(nproc) - 1))"
119+
```
120+
121+
Now run the vLLM container with ZenTorch enabled.
122+
123+
!!! note "The HF_TOKEN variable should be set to a valid HuggingFace token with model access."
124+
125+
If you intend to use a model with access restrictions, ensure your HuggingFace token is set in the `HF_TOKEN` environment variable. Models like LLama 3.2 require an acceptance to their terms as well as authentication using a read-only token.
126+
127+
```bash
128+
docker run --net=host \
129+
--ipc=host \
130+
--shm-size=${SHM_SIZE}m \
131+
--privileged=true \
132+
--detach \
133+
--volume /var/lib/huggingface:/root/.cache/huggingface \
134+
--env HUGGING_FACE_HUB_TOKEN="${HF_TOKEN}" \
135+
--env VLLM_PLUGINS="zentorch" \
136+
--env VLLM_CPU_KVCACHE_SPACE=50 \
137+
--env VLLM_CPU_OMP_THREADS_BIND=${CORES} \
138+
--env VLLM_CPU_NUM_OF_RESERVED_CPU=1 \
139+
--name vllm-server \
140+
--rm \
141+
vllm-cpu-zentorch:latest --dtype=bfloat16 \
142+
--max-num-seqs=5 \
143+
--model=${MODEL}
144+
```
145+
146+
## Benchmark Results: What Can CPU Inference Actually Do?
147+
148+
I ran vLLM's built-in benchmark suite across several model families with 128-token input/output sequences and 4 concurrent requests. Here's what the numbers look like.
149+
150+
!!! example "Benchmark setup and command"
151+
152+
```bash
153+
# Install
154+
apt install python3.12-venv
155+
python3 -m venv ~/.venvs/vllm
156+
~/.venvs/vllm/bin/pip install vllm ijson
157+
158+
# Run benchmark
159+
HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-"None"} ~/.venvs/vllm/bin/python3 \
160+
-m vllm.entrypoints.cli.main bench serve --backend vllm \
161+
--base-url http://localhost:8000 \
162+
--model ${MODEL} \
163+
--tokenizer ${MODEL} \
164+
--random-input-len 128 \
165+
--random-output-len 128 \
166+
--num-prompts 20 \
167+
--max-concurrency 4 \
168+
--temperature 0.7
169+
```
170+
171+
### Qwen3 Family
172+
173+
| Model | Parameters | Output Tokens/sec | TTFT (median) | Tokens per Output (median) |
174+
|-------|------------|-------------------|---------------|---------------------------|
175+
| Qwen3-0.6B | 0.6B | 121.17 | 247ms | 29.74ms |
176+
| Qwen3-1.7B | 1.7B | 69.00 | 542ms | 52.55ms |
177+
| Qwen3-4B | 4B | 35.77 | 1,366ms | 99.59ms |
178+
| Qwen3-8B | 8B | 20.65 | 2,156ms | 176.40ms |
179+
180+
### Llama 3.2 Family
181+
182+
| Model | Parameters | Output Tokens/sec | TTFT (median) | Tokens per Output (median) |
183+
|-------|------------|-------------------|---------------|---------------------------|
184+
| Llama-3.2-1B | 1B | 93.89 | 385ms | 38.46ms |
185+
| Llama-3.2-3B | 3B | 43.61 | 934ms | 83.52ms |
186+
187+
### Gemma 3 Family
188+
189+
| Model | Parameters | Output Tokens/sec | TTFT (median) | Tokens per Output (median) |
190+
|-------|------------|-------------------|---------------|---------------------------|
191+
| Gemma-3-1b-it | 1B | 83.81 | 337ms | 43.66ms |
192+
| Gemma-3-4b-it | 4B | 36.38 | 1,050ms | 102.40ms |
193+
| Gemma-3-12b-it | 12B | 13.93 | 3,873ms | 260.42ms |
194+
195+
## Resource Utilization: What the System Actually Does
196+
197+
Beyond throughput numbers, understanding resource consumption patterns matters for capacity planning. Here's what the system looked like under load during these benchmarks.
198+
199+
!!! info "Dashboard: System metrics showing CPU, memory, network, and load patterns during vLLM inference testing"
200+
201+
![NewRelic Dashboard](assets/images/2025-12-17/dashboard.png){ align=left : style="max-width:512px;width:75%;" }
202+
203+
* CPU load patterns (1-minute load spiking to 5-6 during inference)
204+
* Memory utilization bands (50-70% during active runs)
205+
* Network traffic spikes during HuggingFace model downloads (16 MB/s peak)
206+
* Process table data showing VLLM::EngineCore threads (50-2000% CPU, 106-151 threads)
207+
208+
### CPU Behavior
209+
210+
The load average tells the real story. During active inference, the 1-minute load spiked to 5-6 on this 24-vCPU system, significant but not saturated. The CPU usage percentage chart shows bursty patterns: idle between requests, then concentrated utilization during token generation.
211+
212+
The process table captures vLLM's multi-threaded architecture in action. Multiple `VLLM::EngineCore` processes consumed 50-2000% CPU (remember, 100% = one core, so 2000% means 20 cores active). Thread counts ranged from 106 to 151 per engine process, reflecting the parallelized inference pipeline.
213+
214+
### Memory Patterns
215+
216+
Memory utilization climbed to 50-70% during model loading and sustained inference, consuming roughly 48-67GB of the 96GB available. This tracks with model size plus KV cache allocation (configured at 50GB via `VLLM_CPU_KVCACHE_SPACE`).
217+
218+
Container-level metrics show memory consumption scaling with model complexity.
219+
220+
| Model Size Class | Memory Consumption |
221+
|-----------------|-------------------|
222+
| Sub-1B models | ~27-57 GB |
223+
| 3-4B models | ~56-60 GB |
224+
| 8B+ models | ~69-74 GB |
225+
226+
The larger memory footprint relative to model parameter count reflects vLLM's continuous batching and KV cache management overhead, memory traded for throughput optimization.
227+
228+
### Network and Storage I/O
229+
230+
Network traffic spiked dramatically during model downloads from HuggingFace Hub, reaching 16 MB/s receive rates. Once models cached locally in `/var/lib/huggingface`, subsequent runs showed minimal network activity.
231+
232+
Disk I/O patterns were write-heavy during model caching (21GB+ written across test runs) with modest read activity. The root disk sat at 17% utilization, model weights and container layers fit comfortably within the 240GB allocation.
233+
234+
### Container Resource Summary
235+
236+
Across all benchmark runs, the vLLM containers exhibited these aggregate characteristics.
237+
238+
| Metric | Range | Notes |
239+
|--------|-------|-------|
240+
| CPU % | 44-873% | Multi-core utilization during inference |
241+
| Memory | 682MB - 74GB | Scales with model size |
242+
| Thread Count | 73-253 | Parallel inference workers |
243+
| Network Rx | 46-97 GB | Model downloads from HuggingFace |
244+
245+
The key insight: CPU inference is memory-bandwidth bound more than compute-bound. The EPYC 9454's 12-channel DDR5 memory architecture matters as much as its core count for this workload class.
246+
247+
## Reading the Results
248+
249+
Let's be direct about what these numbers mean for practical use cases.
250+
251+
**Sub-2B models are genuinely usable.** The Qwen3-0.6B and 1.7B models deliver 69-121 tokens per second with sub-second time-to-first-token. That's responsive enough for interactive applications, chatbots, code completion, document summarization. You're not waiting around.
252+
253+
**4B models hit a sweet spot for quality vs. speed.** At 35-43 tokens per second, models like Qwen3-4B and Llama-3.2-3B provide meaningfully better outputs than their smaller siblings while remaining practical for batch processing and near-real-time applications. A 1.3-second TTFT is noticeable but not painful.
254+
255+
**8B+ models work but require patience.** The Qwen3-8B at ~21 tokens/sec and Gemma-3-12b at ~14 tokens/sec are slower but absolutely functional for use cases where quality trumps latency, document analysis, async processing, development and testing workflows.
256+
257+
## The Economics: GPU-Free Doesn't Mean Value-Free
258+
259+
Here's where this gets interesting from an infrastructure planning perspective.
260+
261+
That `gp.5.24.96` flavor runs at $0.79/hour, roughly $575/month for continuous operation. Compare that to GPU instance pricing where you're looking at $2-4/hour for entry-level accelerator access, assuming availability.
262+
263+
For development teams iterating on prompts, testing model behavior, or running moderate inference loads, CPU-based instances provide a dramatically lower barrier to entry. You can spin up the infrastructure in minutes without joining a GPU allocation queue.
264+
265+
This isn't about replacing GPU infrastructure for training or high-throughput production inference. It's about recognizing that not every AI workload requires the same hardware profile, and that forcing GPU dependency on all AI workloads is both expensive and often unnecessary.
266+
267+
## Practical Applications
268+
269+
Where does CPU inference with ZenDNN actually make sense?
270+
271+
**Development and testing environments.** Every AI application needs a place to iterate that doesn't burn through GPU budget. CPU inference lets teams test model behavior, refine prompts, and validate integrations without competing for accelerator resources.
272+
273+
**Batch processing at moderate scale.** Processing thousands of documents overnight? Analyzing logs for anomalies? Generating embeddings for search indexing? These workloads often care more about cost-per-token than tokens-per-second.
274+
275+
**Edge and hybrid deployments.** Not every deployment location has GPU infrastructure. Branch offices, on-premise installations, and resource-constrained environments can still run inference workloads.
276+
277+
**Burst capacity.** When your GPU fleet is fully loaded, CPU instances can absorb overflow traffic rather than dropping requests or queuing indefinitely.
278+
279+
## Running This Yourself
280+
281+
The complete setup on Rackspace OpenStack Flex involves.
282+
283+
1. Launch an AMD EPYC instance (gp.5 flavor family)
284+
2. Install Docker and clone the vLLM repository
285+
3. Build the CPU-optimized image with ZenTorch
286+
4. Configure CPU binding and memory allocation
287+
5. Deploy and test
288+
289+
The vLLM server exposes an OpenAI-compatible API, so existing tooling and integrations work without modification:
290+
291+
```bash
292+
curl http://localhost:8000/v1/models | jq
293+
```
294+
295+
From there, your application code doesn't need to know whether inference is happening on a GPU or CPU, the API contract remains identical.
296+
297+
## The Bigger Picture
298+
299+
The AI infrastructure narrative has over-indexed on GPU scarcity and the assumption that meaningful work requires accelerators. That's true for training and high-throughput production inference, but it misses a substantial category of workloads where CPU-based solutions deliver genuine value.
300+
301+
AMD's investment in ZenDNN, combined with vLLM's architecture that supports pluggable backends, creates a practical path for organizations to deploy AI capabilities without GPU dependency. Running this on OpenStack Flex demonstrates that cloud infrastructure doesn't need to be hyperscaler-specific to support modern AI workloads.
302+
303+
The 24-core EPYC VM running inference at 120 tokens per second for a 0.6B model, or 35 tokens per second for a 4B model, isn't a compromise. It's the right tool for a substantial portion of the AI workload landscape.
304+
305+
Sometimes the most expensive hardware isn't the most appropriate hardware. And sometimes, 24 CPU cores are exactly what you need.
128 KB
Loading

0 commit comments

Comments
 (0)