Stable Diffusion XL 1.0 rodando com 298MB de RAM
(github.com/vitoplantamura)- OnnxStream é uma biblioteca de inferência ultracompacta criada para rodar Stable Diffusion 1.5 e SDXL 1.0 Base em um Raspberry Pi Zero 2 com 512MB de RAM, sem adicionar swap nem gravar resultados intermediários em disco
- Ela separa o motor de inferência de
WeightsProvider, oferecendo suporte aDiskNoCache,DiskPrefetche carregamento, cache e prefetch personalizados; também pode buscar pesos diretamente via HTTP e processá-los sem gravar em disco - O SDXL 1.0 Base usa arquivos ONNX exportados da implementação do Hugging Face Diffusers 0.19.3, e leva cerca de 11 horas para gerar uma imagem de 10 steps no Raspberry Pi Zero 2
- O decodificador VAE do SDXL consome 4,4GB de RAM em FP32 e não é fácil resolver isso só com FP16 ou UINT8, então o uso de memória caiu para 298MB com decodificação em blocos sobrepostos e blending
- Em CPU, pode usar até 55 vezes menos memória que o OnnxRuntime, em troca de ser entre 0,5 e 2 vezes mais lento, com limitações como batch size 1 e foco em alguns operadores ONNX
Objetivos e design do OnnxStream
- OnnxStream começou com o objetivo de rodar Stable Diffusion 1.5 em um Raspberry Pi Zero 2
- O Raspberry Pi Zero 2 é um microcomputador com 512MB de RAM
- O mínimo recomendado de RAM/VRAM para Stable Diffusion normalmente é 8GB
- As condições do objetivo eram: sem espaço de swap adicional e sem descarregar resultados intermediários em disco
- Frameworks comuns de machine learning tendem a priorizar latência de inferência ou throughput, o que facilita aumento no uso de RAM
- O OnnxStream é uma biblioteca de inferência pequena e fácil de modificar, com foco em minimizar o uso de memória
- A estrutura central separa o motor de inferência do componente de fornecimento de pesos do modelo, o
WeightsProvider- Classes derivadas podem implementar formas de carregar, armazenar em cache e fazer prefetch dos parâmetros do modelo
- Um
WeightsProvidercustomizado pode baixar dados diretamente de um servidor HTTP e processá-los sem leitura ou gravação em disco - As implementações padrão são
DiskNoCacheeDiskPrefetch
- Em CPU, usa até 55 vezes menos memória que o OnnxRuntime, embora possa ser de 0,5 a 2 vezes mais lento
Rodando Stable Diffusion 1.5
- O exemplo de Stable Diffusion 1.5 gera imagens com o OnnxStream usando diferentes precisões do decodificador VAE
- O único componente que não cabia na RAM do Raspberry Pi Zero 2 era o decodificador VAE
- A causa eram conexões residuais, tensores grandes e convoluções grandes dentro do modelo
- Nem precisão simples nem meia precisão cabiam na RAM do Raspberry Pi Zero 2
- A solução foi quantização estática de 8 bits
- A imagem com decodificador VAE em precisão W8A8 foi gerada no Raspberry Pi Zero 2 e levou cerca de 1,5 hora com a opção
MAX_SPEED - A imagem W16A16 para comparação foi gerada em um PC usando o mesmo latent
Suporte a Stable Diffusion XL 1.0 Base
- O exemplo de Stable Diffusion do OnnxStream oferece suporte a SDXL 1.0 Base, sem incluir o Refiner
- Os arquivos ONNX foram exportados da implementação SDXL 1.0 da biblioteca Diffusers do Hugging Face, na versão 0.19.3
- O SDXL 1.0 tem custo computacional muito maior que o SD 1.5
- A maior diferença é que ele pode gerar imagens de 1024x1024 em vez de 512x512
- Em um PC de 12 núcleos com 32GB de RAM, gerar uma imagem de 10 steps com Hugging Face Diffusers leva 26 minutos
- O mínimo recomendado de VRAM para SDXL normalmente é 12GB
- O OnnxStream consegue rodar o SDXL 1.0 com menos de 300MB de RAM, então também funciona no Raspberry Pi Zero 2
- Sem swap adicional
- Sem escrita em disco durante a inferência
- A geração de uma imagem de 10 steps no Raspberry Pi Zero 2 leva cerca de 11 horas
Otimizações de memória específicas para SDXL
- O SDXL 1.0 usa o mesmo conjunto de otimizações do SD 1.5, com algumas diferenças
- O modelo UNET usa quantização dinâmica UINT8 para rodar com menos de 300MB de RAM no Raspberry Pi Zero 2
- A quantização é limitada a um subconjunto específico de grandes tensores intermediários
- O decodificador VAE do SDXL 1.0 é mais difícil de tratar que o do SD 1.5
- O decodificador VAE do SDXL 1.0 é 4 vezes maior que o do SD 1.5
- Em FP32 no OnnxStream, ele usa 4,4GB de RAM
- No SD 1.5, era possível reduzir o uso de RAM para 260MB com quantização estática UINT8 do decodificador VAE
- O decodificador VAE do SDXL 1.0 sofre overflow em aritmética FP16, e a faixa numérica das ativações é grande demais para obter imagens de boa qualidade com quantização UINT8
- Existem soluções FP16 como sdxl-vae-fp16-fix, mas mesmo reduzindo a memória pela metade ainda fica em 2,2GB, o que continua grande demais para o Raspberry Pi Zero 2
- A solução final foi a decodificação em blocos inspirada na implementação do decodificador VAE do Diffusers
- O tensor resultante da diffusion tem shape
(1,4,128,128) - Ele é dividido em 5x5 blocos sobrepostos, 25 no total, cada um com shape
(1,4,32,32) - Cada bloco se sobrepõe 25% ao bloco da esquerda e ao bloco de cima
- O resultado da decodificação de cada bloco é um tensor
(1,3,256,256), depois mesclado na imagem final - Com o blending desativado, as bordas dos blocos ficam visíveis; com ele ativado, elas desaparecem no resultado final
- O tensor resultante da diffusion tem shape
- Com essa abordagem, o uso de RAM do decodificador VAE do SDXL cai de 4,4GB para 298MB
Recursos suportados e dependências
- O OnnxStream reúne em um pacote pequeno os recursos necessários para inferência com economia de memória
- Separação entre motor de inferência e
WeightsProvider DiskNoCache,DiskPrefetcheWeightsProvidercustomizado- attention slicing
- quantização dinâmica assimétrica unsigned de 8 bits por percentil
- quantização estática W8A8 assimétrica unsigned por percentil
- calibração de modelo quantizado
- suporte a FP16
- implementação de 25 operadores ONNX comuns
- Separação entre motor de inferência e
- As operações são executadas em sequência, mas cada operador funciona com multithreading
- A implementação segue a estrutura de um único arquivo de implementação e um arquivo de cabeçalho, e a classe
XnnPackencapsula as chamadas ao XNNPACK - Alguns primitives de aceleração dependem de XNNPACK
- MatMul
- Convolution
- element-wise Add/Sub/Mul/Div
- Sigmoid
- Softmax
Comparação de desempenho e limitações
- O Stable Diffusion é composto por três modelos
- text encoder: 672 operations, 123 milhões de parameters
- UNET: 2050 operations, 854 milhões de parameters
- VAE decoder: 276 operations, 49 milhões de parameters
- Para gerar uma imagem de 10 steps com batch size 1, são necessárias as seguintes execuções
- text encoder 2 vezes
- UNET 20 vezes
- VAE decoder 1 vez
- Com base no UNET FP16, a diferença de memória e tempo entre OnnxStream e OnnxRuntime é grande
- OnnxStream: cerca de 0,133GB, 18,2 a 19,8 segundos
- OnnxRuntime: 5,085 a 7,353GB, 7,28 a 12,8 segundos
- O OnnxStream usa até 55 vezes menos memória, mas é de 0,5 a 2 vezes mais lento
- No text encoder FP32, o OnnxStream usa 0,147GB e o OnnxRuntime usa 0,641GB
- No VAE decoder FP32, o OnnxStream usa 1,004GB e o OnnxRuntime usa 1,330 a 2,026GB
- Os resultados da comparação vêm com algumas condições
- A primeira execução do OnnxRuntime foi uma inferência de warm-up antes da reutilização do
InferenceSession - O OnnxStream tem design eager e não possui conceito de warm-up, mas execuções posteriores podem se beneficiar do cache de arquivos de pesos do sistema operacional
- Atualmente, o OnnxStream não oferece suporte a entradas com batch size diferente de 1
- O OnnxRuntime pode usar batch size 2 na execução do UNET e acelerar bastante a diffusion como um todo
- Alterar
EnableCpuMemArenaeExecutionModenasSessionOptionsdo OnnxRuntime não trouxe diferença significativa nos testes - O NCNN foi muito parecido com o OnnxRuntime em uso de memória e tempo de inferência
- O ambiente de teste foi Windows Server 2019, 16GB de RAM, CPU 8750H com AVX2, SSD 970 EVO Plus e 8 núcleos virtuais no VMWare
- A primeira execução do OnnxRuntime foi uma inferência de warm-up antes da reutilização do
Attention slicing e quantização
- Durante a execução do UNET, o attention slicing e a quantização W8A8 do decodificador VAE foram cruciais para reduzir a memória a um nível viável no Raspberry Pi Zero 2
- O attention slicing evita materializar a matriz completa
Q @ K^Tao calcular scaled dot-product attention em multi-head attention - Quando o modelo UNET tem 8 attention heads, os shapes dos tensores são os seguintes
Q:(8,4096,40)K^T:(8,40,4096)- Resultado do primeiro MatMul:
(8,4096,4096) - Em precisão FP32, isso vira um tensor de 512MB
- A solução é dividir
Qverticalmente e executar a atenção para cada chunk- O shape de
Q_slicedé(1,x,40) xé o valor de 4096 dividido poronnxstream::Model::m_attention_fused_ops_parts- O valor padrão é 2, mas pode ser customizado
- O shape de
- Com isso, o uso total de memória do modelo UNET FP32 cai de 1,1GB para 300MB
- O FlashAttention pode ser uma alternativa mais eficiente, mas exigiria kernels customizados para cada arquitetura suportada, como AVX e NEON, além de contornar o XNNPACK
Conversão de modelos e modo de execução
- O OnnxStream executa modelos definidos em
path_to_model_folder/model.txt- Todas as operações do modelo ficam em
model.txtem formato ASCII - Os arquivos de pesos devem existir como uma série de arquivos
.binna mesma pasta
- Todas as operações do modelo ficam em
- O objeto
Modelpermite configurar vários parâmetros opcionais- Especificar outro weights provider
- Ler e gravar arquivos de faixa de clipping de ativações do modelo quantizado
- Modo de calibração do modelo
- Uso de aritmética FP16
- Uso de aritmética UINT8
- Uso de quantização dinâmica UINT8
- Ativação de attention slicing
- Definição da quantidade de divisões da attention
- Para usar arquivos ONNX no OnnxStream, o notebook
onnx2txt.ipynbexportamodel.txte os arquivos de pesos.bin - Ao exportar um
nn.Moduledo PyTorch para ONNX, há algumas condições- Ao chamar
torch.onnx.export,dynamic_axesdeve ficar vazio - O OnnxStream não oferece suporte a entradas com shape dinâmico
- É fortemente recomendado executar o ONNX Simplifier antes da conversão
- Ao chamar
Preparação de build e execução
- É possível compilar o exemplo de Stable Diffusion em Linux, Mac, Windows e Termux
- No Windows, usa-se o
x64 Native Tools Command PromptdoVisual Studio Tools - No Mac, é preciso instalar o cmake com
brew install cmake
- No Windows, usa-se o
- Primeiro é necessário compilar o XNNPACK
- Como os prototypes de função do XNNPACK podem mudar a qualquer momento, o procedimento inclui fazer checkout de um commit compatível
- O ponto de referência é um commit da branch master anterior a
2023-06-27 00:00
- Depois disso, clone o repositório do OnnxStream e faça o build em
src/buildcom cmakeMAX_SPEED=ON- Definindo
XNNPACK_DIRpara o caminho do clone do XNNPACK
- A opção
MAX_SPEEDpode melhorar o desempenho, mas usa mais memória durante o build- No Windows, traz cerca de 10% de ganho de desempenho
- No Raspberry Pi, traz mais de 50% de ganho de desempenho
- O executável gerado pode não funcionar, e houve problemas nos testes com Termux
- Se houver problema, a recomendação é primeiro mudar para
MAX_SPEED=OFF
- Os weights do exemplo Stable Diffusion 1.5 podem ser baixados na área de Releases do repositório e têm cerca de 2GB
- Os weights do Stable Diffusion XL 1.0 Base podem ser baixados no Hugging Face e têm cerca de 8GB
Opções do exemplo de Stable Diffusion
- O executável de exemplo controla seleção de modelo, entrada e saída, prompt e método de decodificação por opções de linha de comando
--xl: roda Stable Diffusion XL 1.0 em vez de Stable Diffusion 1.5--models-path: define a pasta do modelo Stable Diffusion--ops-printf: imprime no stdout a operação atual durante a inferência--output: define o arquivo PNG de saída--decode-latents: pula a diffusion e decodifica o arquivo de latents especificado--prompt: define o prompt positivo--neg-prompt: define o prompt negativo--steps: define o número de diffusion steps--save-latents: salva os latents no arquivo especificado após a diffusion
- Também há opções específicas para Raspberry Pi e para o decodificador
--decoder-calibrate: calibra o VAE decoder quantizado apenas no SD 1.5--decoder-fp16: usa VAE decoder FP16 apenas no SD 1.5--not-tiled: desativa o tiled VAE decoder apenas no SDXL 1.0--rpi: configura o modelo para rodar no Raspberry Pi--rpi-lowmem: aplica a configuração de baixa memória para Raspberry Pi Zero 2 apenas no SDXL 1.0
1 comentários
Opiniões no Hacker News
Interessante. A frase central é esta: “O OnnxStream pode ser apenas 0,5 a 2 vezes mais lento que o OnnxRuntime, usando até 55 vezes menos memória”
A troca entre memória de vídeo/uso de memória e tempo de inferência parece vantajosa não só em casos com memória limitada, como no Raspberry Pi, mas também em outras situações
Fico curioso se, na prática, caso esse método de descarregar pesos permita processar batches maiores dentro da mesma memória, não seria possível aumentar bastante o throughput mesmo com maior latência
Provavelmente queriam escrever “1,5 a 2 vezes mais lento”
Em inferência, depois que você passa da questão de “este modelo cabe neste sistema?”, normalmente o gargalo é a largura de banda de memória, então não acho que essa técnica ajude muito a aumentar o throughput com batches maiores. É bem provável que uma única instância já esteja saturando o controlador de memória
Mas talvez ajude no lado de treinamento
11 horas me faz lembrar da época em que eu fazia ray tracing em um Amiga 500. Um render “final” era definitivamente algo para deixar rodando durante a noite
Mas, embora as cáusticas pareçam brilhantes, estatisticamente são um fenômeno bem raro; para obtê-las direito, é preciso remover os limites do motor de renderização e simplesmente deixar rodando a noite toda
O resultado é uma imagem de uma cena comum feita por um artista pouco habilidoso, mas com cáusticas bonitas. Melhor eu continuar no meu emprego principal
Foi mais ou menos quando achei que precisava de outro hobby. Logo antes disso, alguém muito talentoso lançou uma ferramenta que renderizava a cena primeiro em OpenGL para visualização. Não teria funcionado no Amiga, mas na minha máquina rodava por pouco
Estou usando Stable Diffusion com invoke.ai em um MBP; alguém tem recomendações para ajustar melhor os parâmetros do SD? Mesmo usando o mesmo prompt e configurações aparentemente iguais, por exemplo o mesmo modelo com Euler A, não consigo de jeito nenhum chegar à qualidade das imagens que vejo na internet
Para SD 1.5 básico uso o Volta por ser rápido: https://github.com/VoltaML/voltaML-fast-stable-diffusion/com...
Uma qualidade de imagem realmente boa no SD 1.5 exige usar sem economizar modelos com fine-tuning, LoRA, ControlNet e outros recursos de reforço. Por exemplo, fazer a imagem seguir uma imagem-base como estrutura, ou definir prompts por áreas específicas da imagem. O InvokeAI também tem muitos recursos de fato, e muitos desses reforços ficam escondidos na UI de nós, mas outras UIs, como o Volta, os expõem de forma mais direta
Em geral, modelos com fine-tuning impressionantes são bem menos versáteis que os pesos básicos, mas no uso real isso não costuma ser um grande problema e pode melhorar bastante os resultados
Ele exige mais tempo e memória do que o Invoke ou uma placa de vídeo Nvidia, mas não é tão ruim. Uma imagem padrão de qualidade em 512x768px fica em torno de 1 a 2 s/it, e uma imagem de alta qualidade em 1024x1536px usando Hires Fix fica por volta de 14 a 20 s/it
Acho que seria muito legal embutir isso em um porta-retratos digital ou quadro de parede
https://github.com/rvdveen/epaper-slow-generative-art/
É uma conquista impressionante, mas é claro que leva um tempo enorme para gerar uma imagem. No README diz 11 horas
Por exemplo, comparar o tempo, o custo, todo o hardware necessário e a energia para gerar 100 imagens com 100 Pi Zero 2 — nem precisaria ser o W — versus algo como um PC intermediário médio
Provavelmente o PC ainda venceria
O Zero 2 parece ter sido usado mais como desafio do que por praticidade, então o Pi 4 ou 5 talvez fossem pontos de referência melhores
Impressionante
De fato, aproxima-se a era em que até lâmpadas e torradeiras abrigarão uma inteligência excepcional
Acompanho esta área há muitos anos, mas a última década foi surpreendente
Digo “foi” porque a aceleração dos últimos 6 a 18 meses está em outro patamar completamente diferente
Não me preocupa o que poderemos fazer daqui a 2 anos, mas sim o quanto a velocidade de avanço vai aumentar ainda mais. E então vai acelerar de novo, e de novo
Vamos criar uma startup
A esta altura, as tentativas de regular tecnologias do tipo Stable Diffusion não já acabaram? Se conseguirem reduzir o modelo e a infraestrutura de inferência a ponto de rodar até em um PS2, parece impossível impedir essa tecnologia sem um Estado de vigilância totalitário — e talvez nem assim
O mesmo vale para “pirataria” de mídia ou ransomware
Estados regulam há muito tempo coisas que não conseguem impor de forma puramente técnica
Sei que é uma hipérbole divertida, mas para rodar em um PS2 teria que reduzir muito mais do que isso
Mal posso esperar pelo dia em que sair um Stable Diffusion para Windows 3.1
Insano. Levando 11 horas ou não, eu jamais teria imaginado que SD pudesse rodar em um hardware como o Pi Zero