- Posted on
- • Artificial Intelligence
Artificial Intelligence Hardware Case Studies
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Hardware Case Studies for Linux Users: What Works, Why, and How To Reproduce It
If your model feels “slow,” odds are your hardware is mismatched to the job. The right Linux-friendly hardware choice can cut latency, cost, and power draw by double-digit percentages—without rewriting your whole stack. Below are practical, reproducible case studies you can run from a Bash shell to see how different choices (CPU-only, single GPU, and portable C++ stacks) perform for real workloads.
This article explains why the question matters, then walks through 3 hands-on, Linux-native case studies with commands you can paste into a terminal. Each includes actionable steps and the “why” behind them.
Why hardware case studies are worth your time
Performance isn’t just about FLOPs. Latency, memory bandwidth, PCIe topology, and kernel/driver maturity decide whether your silicon actually gets used.
Cost efficiency depends on the whole system. CPUs with vector extensions (AVX2/AVX-512) can beat aging GPUs for small-batch inference; a single prosumer GPU can beat a fleet of CPUs for fine-tuning; and portable C/C++ inference stacks make edge devices viable.
Linux is where AI runs. Package managers, shells, and drivers are the control plane. Knowing how to install, test, and measure makes you faster and safer.
Prerequisites: tools you’ll use repeatedly
Install common build, monitoring, and developer tools. Pick the command block for your distro.
Debian/Ubuntu (apt):
sudo apt update sudo apt install -y python3 python3-venv python3-pip git cmake build-essential \ numactl lm-sensors nvtop pciutils hwloc wget curl sudo sensors-detect --autoFedora/RHEL (dnf):
sudo dnf install -y python3 python3-pip git cmake gcc gcc-c++ make \ numactl lm_sensors nvtop pciutils hwloc wget curl sudo sensors-detect --autoopenSUSE (zypper):
sudo zypper refresh sudo zypper install -y python3 python3-pip git cmake gcc gcc-c++ make \ numactl lm_sensors nvtop pciutils hwloc wget curl sudo sensors-detect --auto
Notes:
If python3 -m venv is missing on your distro, also install python3-venv (apt) or python3-virtualenv (dnf/zypper).
nvtop shows GPU utilization in a top-like UI (works with NVIDIA; AMD/Intel support varies by driver).
Case Study 1: CPU-first inference with ONNX Runtime on x86_64
Scenario: An API endpoint does small-batch (1–4) image classification. Replacing a low-end GPU with a modern CPU delivers lower tail latency and simpler ops.
What you’ll learn:
Use ONNX Runtime with optimized CPU kernels (oneDNN/OpenMP).
Pin threads and the process to a NUMA node to stabilize latency.
Measure power/thermals with lm-sensors.
Step 1 — Create a clean Python environment:
python3 -m venv ~/venvs/onnxrt && source ~/venvs/onnxrt/bin/activate
pip install --upgrade pip
pip install onnxruntime onnx numpy pillow requests
Step 2 — Download a small model (ResNet50) and a sample image:
wget -O resnet50-v1-12.onnx https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-12.onnx
wget -O cat.jpg https://raw.githubusercontent.com/pytorch/hub/master/images/dog.jpg
Step 3 — Benchmark script (save as cpu_infer.py):
import onnxruntime as ort
import numpy as np
from PIL import Image
import time, os
os.environ.setdefault("OMP_NUM_THREADS", "8") # tune for your CPU
os.environ.setdefault("MKL_NUM_THREADS", "8") # if MKL present
os.environ.setdefault("OMP_WAIT_POLICY", "ACTIVE")
sess = ort.InferenceSession("resnet50-v1-12.onnx", providers=["CPUExecutionProvider"])
inp_name = sess.get_inputs()[0].name
img = Image.open("cat.jpg").resize((224,224)).convert("RGB")
x = np.asarray(img).astype("float32") / 255.0
x = np.transpose(x, (2,0,1))[None, ...] # NCHW
mean = np.array([0.485, 0.456, 0.406])[None, :, None, None]
std = np.array([0.229, 0.224, 0.225])[None, :, None, None]
x = (x - mean) / std
# warmup
for _ in range(10):
sess.run(None, {inp_name: x})
# timed runs
N = 100
t0 = time.time()
for _ in range(N):
sess.run(None, {inp_name: x})
t1 = time.time()
print(f"Avg latency: {(t1 - t0)/N*1000:.2f} ms")
Step 4 — Pin to a NUMA node and run:
numactl --cpunodebind=0 --membind=0 python cpu_infer.py
Step 5 — Watch thermals/power:
watch -n1 sensors
Actionable findings:
Threading matters: OMP_NUM_THREADS close to physical core count (not hyper-threads) often gives best P50–P99 latency.
NUMA pinning avoids cross-socket memory hops.
For even more speed, convert the model to int8 (post-training quantization) using onnxruntime’s quantization toolkit, then rerun the same script. CPU int8 often halves latency at tiny accuracy cost for many vision tasks.
Case Study 2: Prosumer NVIDIA GPU fine-tuning with PyTorch + LoRA
Scenario: You have a single 16–24 GB NVIDIA GPU (e.g., 3060/4060Ti/4070/3090/4090) and want to fine-tune a chat model for your domain. Using 4-bit quantization and LoRA fits real models on modest VRAM.
What you’ll learn:
Install GPU-enabled PyTorch wheels (no CUDA toolkit needed; only the driver).
Use bitsandbytes for 4-bit quantization to reduce VRAM.
Monitor GPU utilization with nvtop.
Step 1 — Create an environment and install packages:
python3 -m venv ~/venvs/lora && source ~/venvs/lora/bin/activate
pip install --upgrade pip
# Install PyTorch with prebuilt CUDA wheels (requires recent NVIDIA driver)
pip install --index-url https://download.pytorch.org/whl/cu121 torch torchvision torchaudio
pip install transformers datasets accelerate bitsandbytes peft sentencepiece
Step 2 — Start nvtop in another terminal to watch utilization:
nvtop
Step 3 — Minimal LoRA fine-tune (save as lora_ft.py):
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from datasets import load_dataset
from peft import LoraConfig, get_peft_model
import torch, os
os.environ["CUDA_VISIBLE_DEVICES"] = os.getenv("CUDA_VISIBLE_DEVICES", "0")
model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" # pick a small model first
tok = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto",
load_in_8bit=False
)
# 4-bit quantization alternative:
# import bitsandbytes as bnb
# model = AutoModelForCausalLM.from_pretrained(model_name, load_in_4bit=True, device_map="auto")
lora_cfg = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM")
model = get_peft_model(model, lora_cfg)
ds = load_dataset("yhavinga/ultrachat_200k", split="train[:0.1%]") # tiny slice for demo
def format_example(ex):
prompt = ex.get("prompt", "Hello")
resp = ex.get("response", "Hi")
text = f"<s>[INST] {prompt} [/INST] {resp}</s>"
ids = tok(text, truncation=True, max_length=512)
ids["labels"] = ids["input_ids"].copy()
return ids
ds = ds.map(format_example, remove_columns=ds.column_names)
args = TrainingArguments(
output_dir="./out",
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4,
num_train_epochs=1,
fp16=True,
logging_steps=5,
save_steps=50,
)
trainer = Trainer(model=model, args=args, train_dataset=ds)
trainer.train()
print("Done. LoRA adapters saved in ./out")
Step 4 — Run it:
python lora_ft.py
Actionable findings:
4-bit (bitsandbytes) or 8-bit loading can cut VRAM by 2–4x at modest quality cost—often the difference between OOM and done.
Batch size × sequence length dictates memory. Gradient accumulation helps when VRAM is tight.
nvtop should show high GPU utilization during training. If not, your bottleneck is likely dataloading or CPU.
Package manager recap (already installed above):
nvtop (apt/dnf/zypper) for monitoring.
Python packages via pip. You only need a recent NVIDIA driver; the PyTorch wheels bundle CUDA runtime.
Case Study 3: Portable C++ inference with llama.cpp (CPU-first, optional GPU later)
Scenario: You want a single, portable binary that runs LLM inference on almost anything (server CPUs, laptops, edge boxes). llama.cpp makes this practical and fast with quantized GGUF models.
What you’ll learn:
Build a high-performance C++ inference binary.
Run quantized models on CPU to compare latency vs. GPU rigs.
Add GPU acceleration later if you have CUDA/ROCm installed.
Step 1 — Clone and build:
git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
make -j$(nproc) # CPU build
# Optional: CUDA build if you have toolkit installed
# make LLAMA_CUBLAS=1 -j$(nproc)
Step 2 — Obtain a GGUF model
Convert or download a small open model in GGUF format (e.g., TinyLlama, Phi-2 converted).
Place it as ./models/model.gguf. Example placeholder:
mkdir -p models
# Put your GGUF file here:
# cp /path/to/your/model.gguf models/model.gguf
Step 3 — Run a prompt and measure tokens/sec:
./main -m models/model.gguf -n 128 -p "You are a helpful assistant. Explain Linux process scheduling in simple terms."
Step 4 — Compare threading strategies:
# Try 1 thread per physical core; then 2x for SMT
./main -m models/model.gguf -n 128 -t $(nproc --ignore=1)
./main -m models/model.gguf -n 128 -t $(nproc)
# Pin to a NUMA node for consistency
numactl --cpunodebind=0 --membind=0 ./main -m models/model.gguf -n 128 -t 8
Actionable findings:
Quantization (Q4_K_M, Q5_K_M, etc.) trades a little quality for big speedups and memory savings—ideal for CPUs.
The best -t (threads) value is often “physical cores,” not hyper-threads. Use lscpu to inspect your topology:
lscpu | egrep 'Model name|CPU\(s\)|Thread|Core|Socket'
Cross-cutting lessons you can apply today
Measure, don’t guess:
- Latency: use time, perf (if available), and logging in your code.
- Thermals/power: sensors and watch.
- GPU: nvtop and nvidia-smi if NVIDIA drivers are present.
NUMA and PCIe topology matter:
- See your layout:
lspci -tv numactl -H- Keep processes and memory close to the device that does the work.
Prefer portable, repeatable environments:
- Python venvs for PyTorch/ONNX Runtime.
- Self-contained C++ builds (llama.cpp) when deploying to diverse machines.
Optimize the right lever for your workload:
- Small-batch, low-latency APIs: CPU with int8 often wins.
- Training/fine-tuning: a single solid GPU with mixed precision and LoRA.
- Edge: quantized C++ runtimes to avoid heavy dependencies.
Conclusion and next steps (CTA)
Pick one case that matches your reality and run the exact commands:
If you serve small requests with tight SLAs, start with Case Study 1 and pin threads/NUMA.
If you customize LLMs on a single GPU, try Case Study 2 and watch nvtop while you iterate.
If you need portable inference, build llama.cpp (Case Study 3) and compare tokens/sec on your hardware.
From there:
Automate your measurements with simple Bash scripts.
Record latency, throughput, and power at each iteration.
Use those numbers to justify your next hardware purchase—or to prove you don’t need one yet.
If you want a follow-up, ask for a deep dive on:
Quantizing your own models (int8/4-bit) for CPU or GPU.
Containerizing inference (Docker/Podman) with GPU passthrough.
ROCm vs. CUDA tips on Fedora/openSUSE/Ubuntu.
Linux gives you the tools. These case studies show you how to use them, today.