Skip to main content

h2-amd

An AMD GPU host — discrete card, integrated GPU, or APU. Usually this is the laptop you are already working on. llama.cpp with the Vulkan backend serves the models; LiteLLM fronts them, exactly as on h1-nvidia.

:4000 LiteLLM <- the only externally reachable port
|
compose network (http://llama-<slug>:8080)
|
127.0.0.1:8001 llama-qwen2.5-coder-7b
|
postgres (no published ports)

Container and volume names are identical to h1-nvidia's. That is deliberate: the same restart task, the same verify playbook, and the same debugging commands work on both.

Running it​

make deps
make bootstrap HOST=h2-amd
make up HOST=h2-amd
make verify HOST=h2-amd

On a local connection sudo usually needs a password, and make cannot supply one. Run bootstrap directly:

ansible-playbook -i inventory/hosts.ini hosts/h2-amd/ansible/00-bootstrap.yml -K

Why Vulkan and not ROCm​

ROCm supports a narrow list of discrete AMD cards. The Vulkan backend works on iGPUs, APUs, and consumer dGPUs alike. For a repo whose promise is "works on the AMD machine you already have", portability beats peak throughput.

Unified memory: read GTT, not VRAM​

An APU reports a token VRAM carve-out — often 512 MiB or 2 GiB — and maps the rest of the budget through GTT. Reading the VRAM figure alone declares most AMD laptops unable to run a 7B model, which is wrong by an order of magnitude.

cat /sys/class/drm/card*/device/mem_info_vram_total # the carve-out
cat /sys/class/drm/card*/device/mem_info_gtt_total # the real budget

classify.py takes the larger of the two and says which it used. Set vram_budget_gib in ansible/vars.yml from its usable_gib output, not from the BIOS figure.

The render node​

Containers reach the GPU through a DRM render node, set as render_node in ansible/vars.yml:

render_node: /dev/dri/renderD128

renderD128 is the first GPU. A machine with both an iGPU and a discrete card also has renderD129, and which is which is not guaranteed across boots on some kernels. Resolve it before assuming:

ls -l /dev/dri/by-path

Access also needs the container to be in the host's render group. Docker sets container groups itself rather than inheriting them, so the compose file passes the numeric GID — the playbook reads it from the host with getent rather than hardcoding a number that differs between distributions.

Quantization and tool calling interact​

The deployment this was ported from found Q4_K_M improvising tool-call wrappers — <xml> tags, ```json fences — that llama.cpp's chat parser does not recognise, dumping the call into message.content instead of tool_calls. Q6_K restored adherence.

So if T3.4 fails, the diagnosis order is:

  1. Is --jinja in the model's extra_args? Without it llama.cpp does not use the GGUF's embedded chat template, which is what carries the ChatML <tools>/<tool_call> plumbing.
  2. Raise the quantization: Q4_K_M → Q6_K.
  3. Only then look at the template.

Things that will bite you​

Silent CPU fallback. If Vulkan cannot see the device, llama.cpp runs on the CPU and answers correctly, just very slowly. Nothing errors. Check:

docker logs llama-qwen2.5-coder-7b 2>&1 | grep -i vulkan
# want: ggml_vulkan: Found 1 Vulkan devices

server-vulkan is a moving tag. Pin it once the host works.

Health is /health, not /v1/models. llama-server answers /v1/models while the model is still loading, so gating on it races the weights. The playbook waits on /health.

LiteLLM caches its routing table at startup. Same as on h1-nvidia: the playbook restarts it when the rendered config changes.