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.
tok/s perdidos com a GPU presa no menor estado de energia
commit exato isolado, entre 5 candidatos, em 8 reboots
travamentos completos do host pelo caminho, sem rastro nenhum nos logs
patches enviados para a lista amd-gfx, aguardando merge
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.
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 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.
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.
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.
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.
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.
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.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.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.