A GPU que se recusava a trocar de marcha

Fazendo bisect de uma regressão de DPM no kernel em hardware de 12 anos, com o mantenedor upstream acompanhando de perto

Publicado em 24 de setembro de 2026

Esta história começa com as mesmas GPUs do guia de passthrough: um par de AMD FirePro D500 (Tahiti, GCN 1.0, de 2013) dentro de um Mac Pro 6,1 que hoje serve como servidor doméstico rodando Proxmox. Com o passthrough funcionando, a próxima pergunta óbvia era se uma dessas GPUs aguentaria rodar um LLM local — llama.cpp sobre Vulkan, via o driver open-source RADV.

Aguentava, até um prompt longo derrubar a VM inteira com um vk::DeviceLostError. Reportar essa falha upstream abriu uma conversa de cinco dias com um mantenedor do RADV que acabou se desdobrando em três bugs distintos, custou sete travamentos inexplicados do servidor, e terminou com meu nome citado numa mensagem de commit do kernel ao lado das palavras “FirePro D500”.

Esta é a história do bug do meio: uma GPU que não saía da primeira marcha.


Números em resumo

21,7 → 6,1

tok/s perdidos com a GPU presa no menor estado de energia

1

commit exato isolado, entre 5 candidatos, em 8 reboots

7

travamentos completos do host pelo caminho, sem rastro nenhum nos logs

2

patches enviados para a lista amd-gfx, aguardando merge


Como começou: uma falha, e uma descoberta colateral

O bug original era fácil de descrever: mandar ao llama.cpp um prompt de mais de 600–800 tokens na D500 via RADV, e o contexto de compute morria com vk::DeviceLostError. Registrei o problema para o mantenedor do RADV, Timur Kristóf, no GitLab do Mesa. O diagnóstico dele: Mesa desatualizado, mais um patch de soft-reset do GFX6 — de autoria dele mesmo — que tinha acabado de entrar no Linux 7.3 e deveria resolver.

E resolveu — um prompt de 1161 tokens que antes travava o host inteiro rodou limpo, com uma GPU e com duas. Só que validar uma correção num kernel release-candidate significa bootar nele, e foi exatamente aí que o dano colateral começou. Passar a GPU por passthrough para uma VM de desktop sob o 7.3-rc3 passou a disparar um erro AER no barramento PCIe durante o reset da GPU — AER é sigla de Advanced Error Reporting, o mecanismo próprio do barramento PCIe pra sinalizar tráfego corrompido ou perdido no link — e o driver do guest desligava o dispositivo no meio da sessão, reproduzido 4 vezes em 4 no kernel RC, 0 em 1 na produção. Esse relato ainda está guardado na gaveta (veja a seção final), porque a descoberta seguinte fez ele parecer pequeno em comparação.


O verdadeiro culpado: preso em P0

Antes de fechar a issue original, o Timur pediu uma comparação maçã-com-maçã: mesmo comando, mesmo prompt, mesma GPU, só o kernel mudando. Os números não bateram nada com minha suposição.

Uma nota rápida sobre a notação: P0, P3 e por aí vai são os estados de DPM (gerenciamento dinâmico de energia) da GPU — P0 é ocioso/menor clock, e o número sobe conforme a placa acelera sob carga, até o maior estado suportado aqui (P3). Uma placa saudável fica ociosa em P0 e sobe de estado em poucos segundos assim que trabalho de verdade chega nela.

Kernel de produção 7.3-rc3
DPM sob carga P0 → P3 em ~2s (sclk 30 → 72,5, mclk 15 → 127) preso em P0 o tempo todo
Velocidade de geração 21,7 tok/s 6,1 tok/s

A queda de throughput que eu vinha atribuindo a um prompt mais longo não tinha nada a ver com o tamanho do contexto. O gerenciamento dinâmico de energia da placa (DPM) simplesmente nunca saía do menor clock sob o kernel novo — confirmado fazendo polling em amdgpu_pm_info a cada segundo enquanto a carga rodava.


O bisect: de “em algum lugar entre dois releases” a um commit só

O Timur pediu três coisas: forçar o maior estado de energia manualmente, fazer bisect entre o kernel 6.19 e o 7.3, e escrever passos exatos de reprodução. Forçar o estado acabou sendo o sinal mais limpo de todos — na produção, echo high > power_dpm_force_performance_level é aceito; no 7.3-rc3, retorna EINVAL na hora (o código de erro genérico do Linux pra “argumento inválido” — o kernel recusando o pedido de cara, não um valor formatado errado). Nenhuma medição de throughput necessária, só um código de erro.

# kernel de producao: aceito
echo high > /sys/class/drm/card0/device/power_dpm_force_performance_level

# 7.3-rc3: Invalid argument
echo high > /sys/class/drm/card0/device/power_dpm_force_performance_level
-bash: echo: write error: Invalid argument

Um bisect por release estreitou o intervalo entre o 7.1 (comporta-se como produção) e o 7.2 (já quebrado). O Timur então observou que só um punhado de commits tinha tocado si_dpm.c nesse intervalo e pediu para eu reverter cada um e achar o culpado. Encontrei cinco candidatos, não quatro — e em vez de discutir a diferença, simplesmente testei os cinco.

Isso significou compilar um kernel customizado localmente pela primeira vez neste projeto (num contêiner LXC de sobra com 24 núcleos — o próprio Xeon do Mac Pro, ironicamente, compilando o kernel que mais tarde corrigiria a própria GPU dele). Oito reboots e uma busca binária depois, um commit se destacou como necessário e suficiente sozinho:

e6c5d36756e7 drm/amd/pm/si: Fix updating clock limits from power states

Um patch que, pela própria mensagem de commit, tinha sido feito para corrigir notebooks. Quebrou um desktop em vez disso.


Uma resposta certa pelo motivo errado

O Timur pediu duas capturas de dmesg com debug do driver ligado, uma de cada lado do commit, para ver a diferença real nas tabelas de DPM enviadas ao controlador embutido da placa (a SMC). Lendo si_dpm.c com esses logs em mãos, a cadeia ficou visível: a VBIOS da D500 traz um estado de energia battery com os clocks travados no mínimo — inofensivo sozinho, já que nada num desktop deveria jamais selecioná-lo. O commit novo passou a usar esse estado para popular a noção que o driver tem de “clock máximo permitido em energia DC (bateria)”, que antes caía de volta nos limites generosos de AC. Com limites de DC reais e minúsculos agora em vigor, o driver marcava o estado de alta performance como não compatível com DC — incondicionalmente, sem considerar se a máquina estava de fato na bateria.

Minha primeira tentativa de correção, sugerida pelo Timur como teste de isolamento, foi forçar essa flag de compatibilidade para true. Funcionou completamente: 21,3 tok/s, DPM subindo a P3 exatamente como na produção. Postei como resultado positivo. O Timur discordou mesmo assim: essa flag, segundo ele, “não deveria importar a menos que a SMC ache que está na bateria” — e um Mac Pro nunca está na bateria. Meu hardcode estava mascarando uma flag completamente diferente, que nenhum de nós dois vinha olhando.


Duas flags com nomes quase idênticos

O driver trata AC/DC em dois lugares sem relação entre si, e os nomes são parecidos o suficiente para confundir de propósito:

Flag Escopo O que ela realmente faz
dc_compatible / PPSMC_SWSTATE_FLAG_DC Por estado de energia A que todo mundo (eu incluído) presumiu que importava. Acabou sendo irrelevante neste hardware.
PPSMC_SYSTEMFLAG_GPIO_DC De sistema, setada uma vez no boot Diz à SMC “confie num pino GPIO físico pra saber se está na bateria”. O verdadeiro portão.

A flag de sistema é setada sempre que a VBIOS declara a capability de plataforma HARDWAREDC — o que a VBIOS da D500 faz, apesar da placa viver num desktop sem bateria e sem nenhum GPIO desse tipo ligado a nada. Com essa flag setada, a SMC confia num pino que lê permanentemente “não é AC”, e ignora tudo o mais: a flag por-estado, e qualquer notificação de software.

Dois builds de isolamento fecharam a questão. Os dois desativavam PPSMC_SYSTEMFLAG_GPIO_DC e deixavam dc_compatible no valor normal (quebrado); um deles também mandava uma mensagem explícita de “rodando em AC” para a SMC, o outro não mandava nada. Os dois corrigiram a regressão por completo. Nem a flag por-estado nem a notificação de software importavam no mínimo — só a flag de sistema do GPIO.


A correção, e o caminho até o upstream

Antes de postar esse achado, havia uma objeção óbvia a antecipar: desativar PPSMC_SYSTEMFLAG_GPIO_DC incondicionalmente sempre que HARDWAREDC estivesse setado também quebraria notebooks de verdade com um GPIO genuinamente cabeado. Então o relato incluiu um escopo proposto — condicionar a correção ao subsystem ID PCI desta placa, o 106b:0126 da Apple.

O Timur concordou com a causa raiz ponto a ponto e propôs um escopo mais limpo, dele: condicionar à flag de dispositivo AMD_IS_MOBILITY, que o driver já usa para distinguir SKUs de notebook das de desktop — cobrindo qualquer placa desktop GCN1 com uma VBIOS igualmente confusa, não só este Mac Pro.

-	if (adev->pm.dpm.platform_caps & ATOM_PP_PLATFORM_CAP_HARDWAREDC)
+	if ((adev->flags & AMD_IS_MOBILITY) &&
+	    (adev->pm.dpm.platform_caps & ATOM_PP_PLATFORM_CAP_HARDWAREDC))
 		table->systemFlags |= PPSMC_SYSTEMFLAG_GPIO_DC;

Duas linhas. Testado numa árvore release-candidate limpa, sem nenhum outro patch aplicado: auto agora sobe a P3 a 21,32 tok/s, igualando a produção exatamente; high forçado é aceito, sem mais EINVAL. Paridade completa restaurada.

O Timur formalizou a correção num commit propriamente dito, citando a FirePro D500 nominalmente na mensagem, e enviou uma série de dois patches para a lista amd-gfx — o caminho real de review para o kernel mainline. O segundo patch é um endurecimento complementar do mesmo código, que eu não testei pessoalmente; os dois estão aguardando review no momento.


O que ainda ficou em aberto

Duas linhas de investigação da mesma semana nunca foram resolvidas. O bug de AER no PCIe da primeira seção — um reset de GPU em passthrough que se torna fatal sob o kernel RC — tem um relato completo com dmesg do host e do guest lado a lado, mas ainda não decidi se ele pertence ao bugzilla.kernel.org ou à lista linux-pci, então ainda não foi postado.

E pelo caminho, o servidor doméstico travou por completo sete vezes sem absolutamente nada nos logs — sem panic, sem OOM, sem AER, pstore vazio todas as vezes. A maioria dessas quedas se correlacionava com uma sequência específica na minha própria automação (alternar a GPU entre o driver do kernel e o vfio-pci em sucessão rápida em torno de um reboot), e remover essa transição perigosa da automação corrigiu o problema estruturalmente. A causa raiz, no nível do kernel, de por que essa sequência trava o host, porém, nunca foi encontrada — ela só foi evitada.


O que ficou comigo

  • Um resultado pode estar certo e ainda assim estar errado. O hardcode de dc_compatible corrigiu o sintoma por completo. Foi preciso um mantenedor que entendia o hardware melhor do que os resultados dos testes para dizer “esse não deveria ser o motivo” e mandar todo mundo olhar uma camada mais fundo.
  • Um código de erro vale mais que um benchmark. Assim que power_dpm_force_performance_level high passou a dar um EINVAL categórico em vez de um número de throughput, cada teste seguinte levou segundos em vez de uma rodada de inferência inteira.
  • Compilar o próprio kernel é seguro barato. Cinco commits candidatos, testados diretamente em vez de discutidos, fecharam uma divergência com o mantenedor numa única volta em vez de várias.
  • Hardware de doze anos ainda encontra bugs de verdade. Uma GPU de workstation de 2013, ainda rodando uma conversa de GitLab de 2026, acabou moldando dois patches a caminho do kernel Linux mainline.

Links


Uma nota sobre como isto foi escrito: a investigação, as decisões e os testes descritos acima são meus. O texto em si foi redigido e editado com o auxílio de IA generativa (Claude), a partir das minhas anotações e do histórico real do projeto.