↓ Ir para o conteúdo principal
  1. Posts/

Passthrough de APU AMD para um LXC do Proxmox para IA local

Autor
Gustavo Novello
Apaixonado por virtualização, Linux e tecnologias de código aberto.
Tabela de conteúdos

Rodar Large Language Models (LLMs) locais em CPU é lento. Se o seu servidor de homelab tem uma APU AMD Ryzen, você pode usar essa GPU integrada para acelerar a inferência.

Rodar tudo dentro de um container LXC leve do Proxmox, em vez de uma VM completa, economiza recursos. No entanto, as permissões de containers sem privilégios e as peculiaridades dos drivers da AMD costumam quebrar a configuração.

Meu hardware:

  • CPU: AMD Ryzen 5 4600G com Radeon Graphics (1 Socket)
  • Memória: 128GB
  • SO: Proxmox Virtual Environment 9.1.6

Isto é para testes, aprendizado e self-hosting de modelos leves em hardware de homelab, e não para montar uma estação de trabalho de IA gigante.

Veja como repassar a sua APU AMD a um container LXC, instalar o Docker e implantar uma stack de IA com aceleração por hardware usando Ollama e Open WebUI.


Fase 1: Preparar o host Proxmox
#

Todos os comandos desta fase devem ser executados no shell do host Proxmox (root@pve1).

1. Instalar o firmware da AMD (o repositório non-free)
#

O Proxmox (baseado em Debian) não inclui por padrão o firmware gráfico da AMD necessário. Precisamos habilitar os repositórios de firmware non-free para instalar o pacote.

nano /etc/apt/sources.list

Garanta que as linhas do seu repositório principal do Debian incluam non-free e non-free-firmware. Deve ficar mais ou menos assim:

deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware

(Nota: se você está em uma versão mais antiga do Proxmox, substitua trixie por bookworm).

Atualize os pacotes e instale o firmware:

apt update
apt install -y firmware-amd-graphics

2. Configurar o GRUB para IOMMU
#

Primeiro, habilite o IOMMU para que o host possa acessar diretamente o hardware da GPU.

nano /etc/default/grub

Modifique a linha GRUB_CMDLINE_LINUX_DEFAULT para ficar exatamente assim (remova qualquer flag video=efifb:off ou nomodeset, se existir):

GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt"

Atualize o GRUB para aplicar:

update-grub

3. Desativar o “sequestro” do VFIO
#

Se você já configurou passthrough de VM antes, pode ter colocado os drivers da GPU na blacklist. Precisamos que o host os use para o LXC.

nano /etc/modprobe.d/vfio.conf

Comente (adicione # no início) todas as linhas, para que fique assim:

# options vfio-pci ids=1002:1636...
# softdep amdgpu pre: vfio-pci
# softdep snd_hda_intel pre: vfio-pci

4. Forçar o carregamento do módulo amdgpu
#

Garanta que o host carregue os drivers da AMD no boot.

nano /etc/modules

Adicione estas linhas no final:

vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
amdgpu

5. Verificar blacklists ocultas da AMD
#

Antes de reiniciar, garanta que o Proxmox não esteja bloqueando ativamente o driver. Execute a seguinte verificação:

grep -ir "amdgpu" /etc/modprobe.d/

Se você vir um arquivo como blacklist-amdgpu.conf, será preciso renomeá-lo, ou o driver nunca vai carregar, independentemente da sua configuração em /etc/modules:

mv /etc/modprobe.d/blacklist-amdgpu.conf /etc/modprobe.d/blacklist-amdgpu.conf.bak

6. Aplicar as alterações e reiniciar
#

Regenere as imagens de boot e reinicie o seu servidor Proxmox.

update-initramfs -u -k all
reboot

7. Verificar se os drivers carregaram
#

Quando o host voltar, verifique os arquivos de dispositivo da GPU:

ls -l /dev/dri

Você deve ver card0 e renderD128.


Fase 2: Corrigir as permissões do container (a correção com Udev)
#

Por padrão, containers LXC sem privilégios mapeiam o usuário root para um usuário “nobody” de número alto no host. Se você repassar a GPU agora, o container terá o acesso negado. Corrigimos isso com uma regra Udev no host.

Execute isto no host Proxmox:

# Create the rule to make the GPU globally readable/writable
echo 'KERNEL=="renderD128", MODE="0666"' > /etc/udev/rules.d/99-render.rules
echo 'KERNEL=="card0", MODE="0666"' >> /etc/udev/rules.d/99-render.rules

# Apply the rule immediately
udevadm control --reload-rules && udevadm trigger

Verifique se as permissões mudaram (procure por crw-rw-rw-):

ls -l /dev/dri/renderD128

Fase 3: Criar e configurar o container LXC
#

1. Provisionar o container
#

Crie um novo container LXC sem privilégios usando a CLI do Proxmox. Para a stack de IA, o Ubuntu 24.04 é o template recomendado. Primeiro, atualize o cache de templates e baixe a imagem:

pveam update
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst

Crie o container (usamos o ID 100 como exemplo), atribuindo 4 núcleos de CPU, 4GB de RAM e 30GB de armazenamento:

pct create 100 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
  --unprivileged 1 \
  --hostname local-ai \
  --cores 6 \
  --memory 49152 \
  --swap 0 \
  --rootfs local-lvm:30 \
  --net0 name=eth0,bridge=vmbr0,ip=10.1.99.16/16,gw=10.1.0.1,tag=99

Não inicie o container ainda.

2. Obter os IDs de grupo do host (opcional)
#

Como a regra Udev da Fase 2 torna a GPU gravável por qualquer usuário (0666), alinhar os IDs de grupo não é estritamente necessário para o container iniciar. Mesmo assim, mantê-los alinhados continua sendo uma boa prática estrutural. Encontre os IDs dos grupos render e video no seu host:

getent group render | cut -d: -f3  # Example: 104
getent group video | cut -d: -f3   # Example: 44

3. Habilitar o nesting do LXC
#

Na interface web do Proxmox, selecione o seu container -> Options -> Features. Marque a caixa Nesting. Isso é obrigatório para rodar Docker dentro do LXC.

4. Editar a configuração do LXC
#

Encontre o ID do seu container (por exemplo, 100). Edite o arquivo de configuração dele no host:

nano /etc/pve/lxc/100.conf

Acrescente as linhas a seguir para repassar o hardware (atualize os GIDs se os seus forem diferentes de 104 e 44):

# GPU Passthrough
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file

Inicie (ou reinicie) o seu container LXC.


Fase 4: Implantando o Ollama e o Open WebUI com Docker Compose
#

Rodar containers avulsos exige vários comandos e deixa a rede frágil. Em vez disso, definimos a stack de IA com o Docker Compose.

Entre no console do seu container LXC (pct enter 100 ou SSH).

1. Instalar o Docker
#

Primeiro, pegue o script oficial para instalar o Docker (que inclui o plugin do Compose) no seu container Ubuntu novo:

apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh

2. Definir a configuração da stack
#

Crie um novo diretório para os serviços de IA e escreva o arquivo docker-compose.yml:

mkdir -p ~/ai-stack && cd ~/ai-stack
nano docker-compose.yml

Cole o YAML a seguir no arquivo:

services:
  # -------------------------------------
  # 1. Reverse Proxy (Traefik)
  # -------------------------------------
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.insecure=true" # Enables the Traefik dashboard on port 8080 (optional)
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false" # Security: deny mapping unless explicitly labeled
      # Define both HTTP (80) and HTTPS (443) entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      # Automatically redirect all HTTP traffic to HTTPS (websecure)
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
    ports:
      - "80:80"       # Standard HTTP entrypoint
      - "443:443"     # Secure HTTPS entrypoint
      - "8080:8080"   # Traefik Dashboard
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro" # Secure read-only access to Docker events
    networks:
      - proxy-net
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  # -------------------------------------
  # 2. LLM Backend (Ollama)
  # -------------------------------------
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    user: "root"        # Force hardware permissions
    privileged: true    # Bypass Docker AppArmor blocking IO controls to /dev/dri
    networks:
      - ai-net
    devices:
      - /dev/dri:/dev/dri
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - OLLAMA_HOST=0.0.0.0
      - OLLAMA_VULKAN=1
    healthcheck:
      test: ["CMD", "ollama", "list"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    deploy:
      resources:
        limits:
          memory: 48G
    logging:
      driver: "json-file"
      options:
        max-size: "20m"
        max-file: "3"

  # -------------------------------------
  # 3. Frontend UI (Open-WebUI)
  # -------------------------------------
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: unless-stopped
    networks:
      - ai-net     # Internal network to reach Ollama
      - proxy-net  # Edge network facing Traefik
    volumes:
      - open_webui_data:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      ollama:
        condition: service_healthy # Will not try to start before Ollama's API responds
    # Principle of Least Privilege
    cap_drop:
      - ALL
    deploy:
      resources:
        limits:
          memory: 2G
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    labels:
      - "traefik.enable=true"
      # Tell Traefik exactly which network to use to reach this container!
      - "traefik.docker.network=ai-stack_proxy-net"

      # Catch-all rule: Allows ANY IP or domain to reach Open-WebUI
      - "traefik.http.routers.openwebui.rule=PathPrefix(`/`)"

      # Route traffic through HTTPS and generate a self-signed certificate
      - "traefik.http.routers.openwebui.entrypoints=websecure"
      - "traefik.http.routers.openwebui.tls=true"

      - "traefik.http.services.openwebui.loadbalancer.server.port=8080"

      # Traefik Security Middleware
      - "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
      - "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
      - "traefik.http.middlewares.secure-headers.headers.forceSTSHeader=true"
      - "traefik.http.routers.openwebui.middlewares=secure-headers"

# -------------------------------------
# Networks & Volumes
# -------------------------------------
networks:
  ai-net:
    driver: bridge     # Secure internal layer
  proxy-net:
    driver: bridge     # External ingress layer

volumes:
  ollama_data:
  open_webui_data:

Por que essa arquitetura importa
#

Rodar comandos docker run puros funciona para uma demo rápida, mas não se sustenta em um servidor de homelab permanente. Veja por que estruturamos o docker-compose.yml desta forma:

  1. API do LLM isolada (air-gapped): O Ollama está vinculado exclusivamente à bridge interna ai-net. Não há nenhuma porta exposta no host. A única forma de falar com o modelo é pelo container do Open WebUI, por meio da rede isolada.
  2. Ingress dinâmico com Traefik: Em vez de vincular portas do host de forma estática, o Traefik age como um reverse proxy inteligente. Ele roteia o tráfego de entrada nas portas 80/443 diretamente para o Open WebUI usando labels do Docker, força o redirecionamento automático para HTTPS e injeta cabeçalhos de segurança rígidos (browserXssFilter, contentTypeNosniff).
  3. A armadilha do “Memory Backoff”: Como a APU compartilha a RAM do sistema, o driver Vulkan impõe um limite estrito às alocações únicas de memória. Se você tenta carregar um modelo grande demais, a GPU falha na alocação inicial e “recua” (backs off), jogando essas camadas para a CPU. Se você vê 0% de uso da GPU durante a inferência, é por isso. Ao aumentar o limite de memória do LXC e do Ollama (no nosso caso, para 48GB), damos a folga necessária para o buffer do Vulkan.
  4. A tag latest vs rocm: Muitos usuários de AMD puxam por precaução a tag de imagem rocm. Não faça isso com APUs. Em uma APU Ryzen, a abstração do ROCm muitas vezes falha ao inicializar a GPU integrada e recua silenciosamente para a execução na CPU. Para a aceleração Vulkan em APU, a imagem latest simplesmente funciona.
  5. Princípio do menor privilégio: Removemos todas as capabilities de root do Linux desnecessárias do container do Open WebUI (cap_drop: ALL). Se surgir uma vulnerabilidade no frontend, o raio de impacto fica contido.
  6. Resolvendo o crash do erro 500: Normalmente, o Open WebUI inicia mais rápido do que o Ollama consegue inicializar os drivers da GPU AMD. Quando a interface tenta conectar e falha, ela gera um crash permanente 500: Internal Error. Corrigimos isso construindo um health check nativo com CMD ollama list que sonda a API. O Traefik e o WebUI esperam estruturalmente que o Ollama fique healthy antes de tentar a conexão.

3. Implantar a stack
#

Suba o ambiente inteiro em segundo plano:

docker compose up -d

4. Verificar a aceleração por hardware
#

Verifique se o Ollama detecta a placa gráfica da AMD antes de baixar qualquer modelo de vários gigabytes.

docker logs ollama | grep "AMD"

Saída esperada: ggml_vulkan: Found AMD Radeon Graphics


Bônus: como monitorar o uso da GPU (radeontop)
#

Se você executa inferências pela CLI, pode monitorar as métricas da APU em tempo real com o radeontop, uma alternativa da AMD ao nvidia-smi.

Como você configurou o container para acessar /dev/dri, dá para executá-lo diretamente de dentro do ambiente LXC.

Primeiro, instale o utilitário:

apt install -y radeontop

Em seguida, basta executá-lo para ver a atividade de renderização, as alocações de VRAM e as frequências de clock dos shaders:

radeontop

Quando você gera uma resposta no Open WebUI, a utilização do Graphics pipe sobe brevemente a 100% enquanto a GPU processa os tokens.

radeontop interface showing AMD APU activity

O resultado final
#

Quando estiver rodando, você tem um ambiente de IA local e privado. A aceleração por hardware torna rápidas tarefas como resumir artigos inteiros.

Acessando o Open WebUI
#

Você pode acessar a interface web navegando até https://10.1.99.16 (ou o IP que você atribuiu ao seu container LXC) no navegador. Como o Traefik cuida do roteamento, ele responde automaticamente na porta 443 com um certificado autoassinado.

Aqui está o Open WebUI resumindo um link usando a APU:

Open WebUI summarizing a web page