Apelo técnico do desenvolvedor do Anukari à Apple
(anukari.com)- O Anukari é um sintetizador físico 3D em tempo real, então precisa calcular grandes modelos de massa-mola na GPU, mas no macOS com Apple silicon fica difícil cumprir os requisitos de latência de áudio se o clock da GPU não sobe o suficiente
- A estrutura em que a DAW chama o plugin a cada bloco do buffer de áudio, combinada com as heurísticas de gerenciamento de energia do macOS, pode fazer a GPU parecer ociosa entre os blocos e permanecer em um estado de baixo desempenho
- No profiler Metal do Xcode Instruments, quando o Performance State é definido como Maximum tudo funciona normalmente, e em Minimum piora bastante, confirmando que o principal gargalo é a velocidade de clock da GPU
- Hoje, o projeto usa um contorno de “waste makes haste”, elevando artificialmente a carga da GPU com um pequeno spin kernel, mas em alguns hardwares Apple Pro/Max o problema ainda permanece
- O desenvolvedor pede à equipe do Apple Metal uma extensão do Audio Workgroup para GPU, uma opção de sensibilidade a tempo real no MTLCommandQueue, ou orientação sobre uma solução já existente, e afirma que no Windows o mesmo spin loop não é necessário
O problema de desempenho de GPU no macOS enfrentado pelo Anukari
- O Anukari 3D Physics Synthesizer simula em tempo real um grande modelo físico de massa-mola para gerar áudio
- Para suportar uma quantidade relevante de objetos físicos, a GPU é necessária, e o código de física está mais próximo de ser limitado por ALU do que por memória
- O estado mutável da simulação é armazenado na threadgroup memory da GPU
- Uma estrutura parecida com um cache L1 alocado manualmente, portanto muito rápida
- O uso típico é como plugin AU ou VST3 rodando dentro de uma DAW como Pro Tools ou Ableton
- A DAW chama o Anukari a cada bloco do buffer de áudio
- Em cada bloco, o Anukari executa o kernel de simulação física na GPU, espera o resultado e então retorna
- Os blocos do buffer de áudio conseguem absorver a latência de agendamento do kernel da GPU distribuindo-a por várias amostras, mas o tempo de execução do próprio kernel continua sendo determinante
O conflito entre gerenciamento de energia do macOS e áudio em tempo real
- O Apple silicon pode reduzir a frequência de clock do chip para economizar energia, e o macOS mantém clocks baixos quando entende que a demanda de processamento é pequena
- A forma como o Anukari roda dentro da DAW não combina bem com a maneira como o macOS avalia a demanda da GPU
- Como a GPU fica ociosa entre os blocos do buffer de áudio, a carga média pode parecer, por exemplo, algo em torno de 60%
- A heurística real do macOS não é conhecida, mas o autor supõe algo parecido com load average
- Essa carga pode não ultrapassar o limiar necessário para aumentar o clock da GPU
- O Anukari precisa de baixa latência para cumprir restrições de tempo real e, para isso, precisa de clock alto de GPU
- Não se sabe o quão baixo o clock da GPU da Apple pode cair, mas ele pode cair o suficiente para tornar o Anukari inutilizável
O problema de clock verificado com o Metal profiler
- Com o Metal profiler do Apple Instruments incluído no Xcode, foi confirmado que o Anukari é ALU-bound
- O Metal profiler permite escolher o “Performance State” do Metal durante o profiling
- Essa configuração não pode ser ajustada fora do profiler
- No estado máximo de desempenho, o Anukari funciona perfeitamente
- No estado mínimo de desempenho, o funcionamento piora muito
- A diferença entre os dois estados mostra que a velocidade de clock da GPU é o ponto central do problema de desempenho do Anukari
O contorno “waste makes haste” e seus limites
- Como o macOS não eleva o clock da GPU no momento necessário, o Anukari usa um contorno separado
- Ele executa um segundo trabalho de GPU em paralelo com o trabalho de cálculo de áudio para criar alta carga média e induzir o macOS a aumentar o clock
- Esse trabalho é ajustado para usar o mínimo possível de recursos da GPU, mas ainda assim ativar a heurística de clock
- Na prática, é um spin loop para “aquecer” a GPU
- Essa estratégia é chamada de “waste makes haste” e foi documentada em detalhe em um devlog relacionado
- No MacBook M1 do desenvolvedor, esse método resolveu completamente o problema e o Anukari roda de forma estável
- Mas, após o lançamento do Anukari Beta, alguns usuários de macOS relataram problemas
- Em especial, parece haver muitos problemas de desempenho em hardwares Apple Pro ou Max
- O texto levanta como hipóteses a possibilidade de clock independente por chiplet de GPU e a chance de que a carga do spin seja conservadora demais em GPUs mais potentes
Caminhos de solução pedidos à Apple
- Partindo da premissa de que os engenheiros da Apple saberiam melhor, o autor sugere algumas soluções possíveis
- Solução 1: estender o conceito de Audio Workgroup ao processamento de GPU
- O processamento de áudio no macOS acontece em uma thread ou grupo de threads chamado Audio Workgroup
- O sistema operacional entende que essas threads têm restrições de tempo real e lhes dá prioridade
- A MTLCommandQueue gerenciada por uma thread do Audio Workgroup poderia ser tratada como processamento em tempo real, com ajuste correspondente do clock da GPU
- Solução 2: fornecer na API Metal uma opção na MTLCommandQueue para indicar sensibilidade a tempo real
- O clock do chiplet de GPU que processa essa queue poderia então ser ajustado de acordo
- Solução 3: se já existir uma forma de obter esse comportamento, bastaria a Apple informar
- No topo do texto, o autor acrescenta que a Apple entrou em contato e que o assunto é tratado em outro texto
Game Mode e comparação com o Windows
- O Game Mode da Apple parece semelhante ao que o Anukari precisa, mas é difícil de aplicar
- O Game Mode opera no nível de processo
- O Anukari normalmente é usado como plugin dentro de outro processo, e esse processo não oferece suporte ao Game Mode
- O Anukari não consegue controlar isso diretamente
- O Game Mode também exige fullscreen, e o Anukari normalmente não está em fullscreen
- No Windows, esse problema não acontece
- Não se sabe se isso ocorre porque o Windows dá mais controle de estado de desempenho ao usuário ou porque os drivers da NVIDIA são menos cautelosos com consumo de energia
- No Windows, o spin loop não é necessário
- O texto compara que PCs com Windows e GPUs fracas conseguem rodar o Anukari bem, enquanto um Mac M4 Max caro pode apresentar stutter
Por que pipelining não serve
- A ideia de fazer pipeline do código de GPU para saturar a GPU funciona bem para tarefas orientadas a throughput, mas o Anukari é uma carga sensível à latência
- Se vários kernels de simulação física forem agendados antecipadamente, a CPU pode preparar o próximo bloco enquanto a GPU processa o bloco de áudio atual
- Mas o pipelining aumenta o throughput à custa de maior latência
- Cada execução de kernel do Anukari precisa acessar dados de entrada de áudio em tempo real, como entrada de microfone
- Não dá para usar execução especulativa processando o próximo bloco de áudio antes da hora, porque os dados de entrada necessários ainda não existem
O problema de colocar o spin kernel na mesma MTLCommandQueue
- Se a causa real fosse a execução do spin kernel e do physics kernel em chiplets de GPU diferentes, colocar ambos na mesma MTLCommandQueue poderia parecer a solução
- Na prática, esse método foi tentado, mas não funcionou
- O motivo é que o Anukari é uma carga sensível à latência
- O spin kernel às vezes roda um pouco mais do que deveria
- Esse tempo invade a janela de execução do physics kernel
- Também foi testado usar um spin kernel pequeno e unified memory volátil para que a CPU escrevesse uma flag de “exit kernel early”
- Mesmo com esses mecanismos, ainda há casos em que o spin kernel invade o tempo do physics kernel
Por que GPU kernel hedging é difícil
- Também foi considerada uma abordagem parecida com request hedging em sistemas distribuídos: executar várias cópias do kernel físico e usar o resultado da que terminar primeiro
- Essa abordagem reduziria latência de cauda e variância de latência, além de criar carga na GPU para levar o sistema operacional a elevar o estado de desempenho
- Mas, no Anukari, isso traz vários problemas
- Se um physics kernel demorar mais do que um período de bloco de áudio, aquele fluxo de kernel fica atrasado
- Um fluxo atrasado precisaria se recuperar em blocos futuros, exigindo um fast-forward copiando o estado interno de outro fluxo
- Copiar o estado interno é caro
- O maior estado interno é o buffer de áudio usado para delay lines
- Ele armazena 1 segundo de histórico de áudio para cada microfone
- Seu tamanho é
48,000 samples * 50 mics * 2 channels * 16 voices * 4 bytes, ou 307MB - Em sample rates maiores, isso aumenta ainda mais
- Para fazer isso de forma eficiente, seria preciso rastrear com precisão as regiões dirty de cada fluxo de kernel com hedging e copiar só essas partes
- Mas o layout da memória dos buffers foi otimizado para a carga de leitura do physics kernel
- Mesmo copiando o mínimo possível, ainda seria necessário copiar regiões espalhadas pelo buffer inteiro, o que seria lento
- Mudanças feitas pelo usuário no modelo também precisariam ser propagadas para todos os kernels com hedging
- O physics kernel tem footprint de GPU muito maior que o spin kernel de “waste makes haste”
- O hedging geraria ainda mais carga desnecessária na GPU e poderia reduzir o número de instâncias do Anukari executáveis em paralelo
- Os próprios hedge kernels também poderiam competir entre si e deixar todos mais lentos
Otimizações já feitas e por que a GPU é necessária
- A simulação do Anukari é ALU-bound, então não há muito espaço para otimizações comuns como melhorar padrões de acesso à memória
- Para aumentar o desempenho, é preciso otimizar o throughput aritmético
- Onde possível, são usadas operações FP16 para saturar melhor as ALUs da Apple
- Micro-benchmarks são usados para ajustar a ordem das instruções
- Todo o estado físico é mantido na memória L1
- A ordem dos loads é reorganizada para vectorization
- Também se aproveita o fato de que as threads de um SIMD-group da Apple normalmente compartilham o mesmo instruction pointer
- Objetos físicos diferentes divergem bastante nos caminhos de branch
- Simular dois tipos de objeto dentro do mesmo SIMD-group fica mais lento por causa de instruction masking
- Para evitar isso, o layout de memória dos objetos físicos é otimizado dinamicamente para reduzir o número de tipos de objeto executados dentro de um SIMD-group
- Essa otimização é explicada em detalhe em the new warp alignment optimizer
- Ainda existe alguma margem para otimização aritmética adicional, mas o autor acredita que isso renderia apenas ganhos de um dígito em pontos percentuais
- Em máquinas potentes, o Anukari consegue simular de 768 a 1024 objetos físicos
- Cada objeto pode ser conectado arbitrariamente a outros objetos
- Os objetos normalmente fazem integração implícita de Euler na taxa de amostragem de áudio de 48.000 samples por segundo
- Cada objeto tem de 3 a 10 parâmetros de comportamento
- Alguns comportamentos incluem operações caras como rotação vetorial,
exp()elog() - Para polyphony, a simulação física inteira roda em até 16 cópias paralelas
- Isso não foi viável na CPU; são necessárias as muitas ALUs da GPU, o controle do layout do cache L1 e estruturas de concorrência como
threadgroup_barrier - O Anukari não pode existir sem processamento em GPU
Por que a GPU Audio API não é a solução
- O CEO da GPU Audio, Alexander Talashov, vinha dizendo que o problema poderia ser resolvido se o Anukari usasse a GPU Audio API
- O desenvolvedor avalia a GPU Audio como um bom produto, apresentando-a como uma forma de tornar a GPU acessível para DSP
- Mas conclui que a GPU Audio não é útil para o Anukari
- Diferentemente de uma aplicação DSP tradicional, o Anukari se parece mais com um integrador de equações diferenciais numéricas
- Há alguma parte de DSP, mas a maior parte do cálculo é integração euleriana
- DSPs como compressão de microfone no mundo físico são processados inline dentro do cálculo físico na GPU
- O Anukari programa a GPU diretamente em um nível baixo do Metal
- O que ele precisa é que a Apple eleve de forma confiável a velocidade de clock da GPU
1 comentários
Opiniões no Hacker News
Talvez alguns de vocês tenham visto o Anukari no meu post Show HN: https://news.ycombinator.com/item?id=43873074
Naquela thread, surgiu a questão do desempenho no macOS. O Anukari roda bem na maior parte do Apple silicon, incluindo o M1 básico, e todos os meus testes também foram feitos no M1 básico, com resultados excelentes. O hardware é realmente impressionante
Mas, para fazê-lo funcionar, precisei implementar uma gambiarra estranha que faz o macOS aumentar a velocidade de clock da GPU para que o processamento de áudio fique rápido o bastante. As heurísticas normais que o macOS usa para decidir o estado de desempenho da GPU não entendem a carga de trabalho peculiar do Anukari
Então acabei escrevendo um resumo detalhado demais de toda a situação e queria pedir ajuda para conseguir contato com a pessoa certa na Apple, provavelmente alguém responsável pela API Metal. Ajudem, por favor :)
Você chamou de “um texto muito longo e muito técnico”, mas, ao ler até o fim, não achei longo demais; foi muito claro e bem escrito, além de informativo. Muito bem escrito
Nunca tive um Mac e meu PC também é antigo, sem uma GPU decente, então é improvável que eu consiga testar o Anukari agora, mas ele parece realmente incrível, o que é uma pena. Espero que isso seja resolvido logo
Fico curioso se você já tentou este entitlement: https://developer.apple.com/documentation/bundleresources/en...
Fico me perguntando se
com.apple.developer.sustained-executiontambém funciona no sentido opostoTexto interessante, e o problema também é interessante. Acho que o motivo de a ideia de executar tarefas na mesma fila falhar talvez seja, no fim das contas, o mesmo do problema original. Por causa da velocidade de clock variável, o agendamento preciso se torna impossível, e, dependendo de como o sistema operacional definiu o clock da GPU, o momento em que o spin para se desalinha do momento ideal, gerando aliasing
Nesse caso, talvez a tarefa de spin não seja complexa o suficiente para fazer a GPU subir para o clock máximo. Se ela estivesse realmente rodando no desempenho máximo, deveria ser possível acertar de forma estável o momento de encerrar o spin sem adicionar um PLL em software. Não vi uma explicação detalhada de como o spin foi implementado, mas um loop de spin mais fiel, que force de maneira contínua mais partes da GPU, provavelmente seria mais eficaz para manter o clock no desempenho máximo
Perdi o Show HN, mas, assim que vi, pensei que isso combinaria bem com paisagens sonoras ASMR criativas e áudio multidimensional imersivo. Seria ótimo se você, ou algum usuário, criasse uma demo. Parabéns pelo projeto, e espero que consiga ajuda com a questão envolvendo a Apple
Gostei do texto, e a explicação foi clara e fácil de entender. Com certeza já passei, em outros contextos, por problemas iguais ao que foi descrito
Pessoal, funcionou. Tive uma conversa muito produtiva com exatamente a pessoa certa da equipe do Metal! Obrigado por ajudarem a chamar a atenção da Apple. Eu não esperava nem de longe tanto apoio
https://anukari.com/blog/devlog/productive-conversation-appl...
É bom que agora exista uma solução de contorno, mas o fato de nem sequer ser possível compartilhar qual é essa solução, ironicamente, demonstra exatamente a última frase sobre a forma de comunicação da Apple em https://news.ycombinator.com/item?id=43904921
É algo do tipo: “configure este valor assim e depois mude para aquilo que funciona. Não está documentado, mas agora você sabe”
Ao implementar a solução de contorno, seria bom colocá-la em uma função com um nome bem explícito, para que outras pessoas que enfrentem restrições parecidas de GPU sensível a latência consigam encontrar ao menos uma pista do feitiço mágico por meio de desassemblagem
O HN mais uma vez cumpriu seu propósito original: atravessar as barreiras burocráticas diante do suporte ao cliente de grandes empresas
Parabéns pelo projeto e boa sorte
Trabalhei em duas empresas conhecidas que tinham apps muito famosos na Apple App Store
A equipe da Apple com quem conversávamos não tinha nenhum interesse nos nossos problemas; em vez disso, frequentemente nos convidava ao escritório para discutir os recursos mais recentes que seriam anunciados na WWDC, praticamente nos obrigando a dar suporte a eles. Esse foi o começo e o fim da nossa relação com eles. Para descobrir por que um software bugado da Apple não funcionava, éramos obrigados a abrir um ticket de suporte técnico
O pessoal de relações com desenvolvedores da Apple não é sério
Ainda bem que, como o autor do post original mostra acima, a minha experiência não é uma regra geral. Mas, há cerca de 10 anos, quando eu trabalhava em uma empresa com um app bastante conhecido, uma atualização destruiu completamente o desempenho do app
Exatamente na mesma época, uma concorrente lançou um app sem problemas de desempenho. Descobrimos que o desenvolvedor daquele app concorrente era alguém que havia saído da Apple pouco antes e deixado uma armadilha não documentada no driver de vídeo da Apple, o que quebrou o nosso app. Só conseguimos corrigir o app depois de desassemblar o binário do concorrente e encontrar a mudança não documentada. O desenvolvedor ainda mandou um e-mail zombando do nosso CEO. Que mundo maravilhoso
O profiler do Metal tem um recurso muito útil que permite escolher o estado de desempenho do Metal enquanto perfila uma aplicação. Fora do profiler, isso não pode ser configurado
Isso sugere que deve haver uma API privada. Talvez fosse mais fácil partir para engenharia reversa? Claro, a menos que isso acabe exigindo privilégios especiais que não possam ser contornados sem desativar o SIP
Isso necessariamente precisa ter uma API privada. O texto também diz isto:
“O profiler do Metal tem um recurso muito útil que permite escolher o ‘Performance State’ do Metal enquanto perfila uma aplicação. Fora do profiler, isso não pode ser configurado”
Se não fosse uma API privada, como o profiler do Metal conseguiria fazer isso? Será que não daria para observar o profiler com alguma ferramenta de depuração e descobrir o que acontece internamente?
O problema ao abrir essa API é que muitos desenvolvedores vão deixar o estado de desempenho máximo sempre forçado. Não sei se há realmente uma boa forma de impedir isso ao oferecer a API
Em dispositivos a bateria, já existem infinitas formas de um único app desperdiçar energia. No fim, o modelo já depende de confiar que os desenvolvedores, intencionalmente ou por engano, não rodem tarefas intensivas em energia sem necessidade. Ter mais uma API que pode desperdiçar energia se usada de forma inadequada não muda muito
O texto também trata do Modo de Jogo, um recurso otimizado para esse tipo de caso nos sistemas operacionais mais recentes da Apple. Quando o Modo de Jogo é ativado, aparece uma notificação, e a maioria dos aplicativos não vai querer isso. Até agora, não vi casos de abuso disso
Os desenvolvedores ainda não estão abusando de workgroups de áudio em todos os pools de threads para obter escalonamento em P-cores e alta prioridade. Nesse caso, isso sugere que, quando um workgroup de áudio emite comandos para a GPU, poderia haver algum tipo de timeout para o downclock da GPU com base no último momento em que o workgroup enviou dados
Áudio em GPU hoje é um nicho bem específico, mas a empresa mencionada no texto publicou recentemente um SDK, então pode se popularizar mais. Ainda assim, não me convence muito. Processar na GPU praticamente significa que você não se importa com latência, então acho que bastaria aumentar o tamanho do buffer de entrada/saída
Mesmo que a API seja abusada, provavelmente seria mais eficiente do que rodar trabalho ocupado falso para tentar fazer a mesma coisa. Os apps já conseguem fazer isso sem uma API, ou sem as permissões que uma API poderia exigir
E quanto a uma permissão concedida manualmente? Mesmo que fique escondida em algum lugar, é bem provável que seja necessária para apps muito de nicho
E, no nível do sistema operacional, Zoom, Teams e navegadores web podem ficar negados por padrão :)
A melhor forma de fazer isso:
Passar pelos vídeos da WWDC e encontrar o engenheiro que pareça entender melhor o problema que você está enfrentando agora
Se for
Michael Thomson, enviar um e-mail diretamente no formatomthomson@apple.compthomsonComo observação, seria bom se o Anukari lançasse um pacote de sons do Mick Gordon e dividisse a receita com ele. Ele está criando coisas realmente insanas, e a demo é incrível. Quando se tem uma ferramenta tão poderosa, colaborar com artistas é bom para os negócios e para o mundo. Se você gosta do Mick Gordon — eu gosto
Eu não preciso nem um pouco deste app, mas ele é muito legal. Apps assim trazem a diversão de volta à computação. Não que hoje não haja diversão nenhuma, mas isso me lembra uma época mais antiga em que circulavam programas mais gráficos e experimentais, até a demoscene
Não deixem passar o link https://x.com/Mick_Gordon/status/1918146487948919222 no penúltimo parágrafo. É uma demo feita por Mick Gordon, e @anukarimusic respondeu assim:
“kkkk, é o segundo dia após o lançamento, e você já destruiu completamente todas as demos que eu fiz usando isso todos os dias por 2 anos”
Atualizar 1024 objetos a 48 kHz parece possível na CPU, dependendo de como o código for escrito. Não são 48 milhões de atualizações por segundo? Parece adequado para paralelizar alguns loops entre os núcleos com OpenMP
Para polifonia, o Anukari roda até 16 cópias do modelo físico inteiro. Ou seja,
16 * 1024 * 48K. Preciso atualizar o post do blogComo o usuário pode conectar objetos entre si de forma arbitrária, cada objeto precisa ler e processar conexões com N outras entidades
Para usar a CPU inteira, é preciso sincronizar entre os núcleos a cada etapa da física, e isso é lento
A quantidade de processamento por objeto é considerável. Há muitas funções transcendentais, e aproximações são possíveis, mas também há muitos recursos. Todos os parâmetros podem ser modulados, precisa ser seguro contra NaN, e há muitas outras coisas a considerar
Os usuários querem executar várias instâncias do Anukari em paralelo para várias faixas, efeitos etc.
Vendo de outra forma, dá
4 GHz / (16 voice * 1024 obj * 4 connections * 48,000 sample) = 1.3 cycles per thingA GPU processa essa carga de trabalho num piscar de olhos. É uma arquitetura perfeitamente adequada. Ela consegue processar todos os
16 voice * 1024 objde forma totalmente paralela, a sincronização em cada etapa é simples, e o usuário pode gerenciar o cache L1Se a conta estiver certa, dá 83 ciclos de clock para calcular uma amostra. Com 16 núcleos, em teoria seriam 1333 ciclos, o que não é tanto assim — ainda mais considerando que não dá para usar a CPU quase sempre a 100%