Quanta VRAM é que eu preciso? A pergunta que me fazem sempre, e a conta que se deve fazer…

Olá a todos!

Prefácio: este texto nasce de uma pergunta que me fazem em quase todas as conversas sobre infraestrutura on-premise, normalmente numa chamada teams e normalmente antes de o projeto estar fechado. “Um 70B cabe numa placa de 24 GB?” A resposta honesta é que a pergunta está mal formulada, e este post é a minha tentativa de a reformular de uma vez por todas, com contas em vez de opiniões. No fim há uma calculadora que fizeram para isto, mas o objectivo é que consigam fazer a conta de cabeça mesmo sem ela.

Há um padrão que se repete com uma regularidade quase cómica. Alguém decide montar IA dentro de casa, vai ao Hugging Face, vê que o ficheiro do modelo que quer diz 16 GB, olha para a caixa da placa gráfica que diz 24 GB, faz a subtracção mental, sorri, e encomenda. Depois instala, corre, e a coisa até arranca. E um mês mais tarde, quando três pessoas começam a usar aquilo ao mesmo tempo com documentos grandes colados no contexto, o servidor entra em colapso, a geração passa a três tokens por segundo, e alguém me liga a perguntar se o modelo “está mal instalado”.

Não está mal instalado. Está mal dimensionado, que é uma doença diferente e bastante mais cara.

O erro está em comparar duas grandezas que parecem comparáveis e não são: o tamanho do ficheiro do modelo e o número impresso na caixa da placa. Nenhum dos dois é o que vocês pensam que é, e entre um e outro há pelo menos três compartimentos de memória que ninguém contou.>

Imagem: © Paramount Pictures / Star Trek - The Next Generation
Imagem e personagens: © Paramount Pictures / Star Trek – The Next Generation

O modelo mental que resolve isto

Antes das contas, o esqueleto. Uso este há uns anos e nunca me deixou ficar mal:

capacidade utilizável = VRAM anunciada x 0,90
total necessário      = pesos do modelo + KV cache + workspace e folga

E a regra de ouro: compara-se o total necessário com a capacidade utilizável, nunca com o número da caixa.
Aquele 0,90 não é uma constante da física. É um ponto de partida conservador que uso para planear antes de ter hardware para medir. Numa máquina Linux headless, dedicada, sem ambiente gráfico e sem mais nada a competir pela placa, é capaz de conseguirem 0,94 ou 0,95. Numa estação Windows onde a mesma placa também está a desenhar dois monitores 4K, pode não passar dos 0,85. O valor certo é o que a vossa máquina vos disser depois de estar a correr. O 0,90 serve para fazer a encomenda.

Compartimento um: o piso exacto dos pesos

Comecemos pela parte que é aritmética pura e que, por isso, é a única em que não há discussão possível.
Um peso em quatro bits ocupa meio byte. Multiplica-se o número de parâmetros por 0,5 e está feito. Para os tamanhos que aparecem em noventa por cento das conversas:

Modelo Piso em 4 bits
7B 3,26 GiB
13B 6,05 GiB
32B 14,90 GiB
70B 32,60 GiB

Reparem numa coisa antes de irem mais longe, porque é uma armadilha que apanha muito boa gente. Um 7B em quatro bits são 3 500 000 000 bytes. Isso são 3,5 GB se contarmos em unidades decimais, que é o que o Hugging Face mostra ao lado do ficheiro, e são 3,26 GiB se contarmos em unidades binárias, que é o que o nvidia-smi vos devolve em MiB. É o mesmo ficheiro. É a mesma memória. São dois números diferentes no ecrã, com uma diferença de sete por cento entre eles, e vi mais do que uma pessoa perder uma tarde a tentar perceber para onde é que os megabytes tinham fugido.
Escolham uma unidade e fiquem com ela até ao fim da conta. Eu conto tudo em GiB, porque é a unidade em que a placa reporta, e é a placa que decide se aquilo cabe ou não.

Porque é que o ficheiro real é sempre maior do que o piso

Aquela tabela é um piso teórico, e faço questão de lhe chamar piso e não estimativa. Nenhum ficheiro que vocês descarreguem vai ter exactamente aquele tamanho, e vai ser sempre para cima.
O motivo é que “quatro bits” nunca é quatro bits em todo o lado. Um GGUF Q4_K_M não guarda os pesos todos com a mesma precisão: guarda blocos de pesos em quatro bits com escalas e offsets associados, mantém algumas camadas em precisão superior porque quantizá-las destrói a qualidade, e ainda leva metadados, o tokenizador e a tabela de embeddings pelo caminho. Na prática, um Q4_K_M anda entre os 4,5 e os 5 bits por peso, o que quer dizer que um 7B que devia pesar 3,26 GiB aterra à volta dos 3,8 GiB. Quinze a vinte por cento acima do piso.
Nos formatos GPTQ e AWQ a mecânica é diferente mas a conclusão é a mesma: há sempre escalas, há sempre zero-points, há sempre alguma coisa que não é peso e que ocupa lugar. Não é desperdício, é o preço de a quantização não destruir o modelo.

Daqui sai a primeira regra prática, e é a mais importante deste post inteiro:

Comecem pelo ficheiro concreto que vão correr, não pela contagem de parâmetros. Vão ao repositório, vejam o tamanho real daquele quant específico, e usem esse número. A tabela de cima serve para saber se vale a pena continuar a conversa; não serve para encomendar hardware.

E já agora, uma nota sobre modelos MoE, porque estão em todo o lado e confundem toda a gente. Num modelo de arquitectura mista com trinta e cinco mil milhões de parâmetros totais e três mil milhões activos por token, a memória que ele ocupa é a dos parâmetros totais. Os trinta e cinco. A velocidade é que se aproxima da dos três. É uma arquitectura excelente e é exactamente por isso que a uso no meu router, mas quem lê “3B activos” e encomenda uma placa a pensar num modelo de 3B vai ter uma surpresa desagradável no dia da instalação.

Compartimento dois: a capacidade que a placa realmente vos dá

Aplicando os noventa por cento aos rótulos que se compram em Portugal hoje:

Rótulo da placa Planeamento realista
8 GB 7,2 GiB
12 GB 10,8 GiB
16 GB 14,4 GiB
24 GB 21,6 GiB
32 GB 28,8 GiB

Aqueles dez por cento que desaparecem não são uma margem de segurança inventada por precaução. São coisas concretas, e vale a pena saber quais são, porque algumas conseguem-se recuperar e outras não.
O contexto de CUDA, que se cria quando o primeiro processo toca na placa, come algumas centenas de megabytes e come-os por processo. Se estão a correr o Ollama e um serviço de embeddings separado na mesma placa, pagam duas vezes. A fragmentação do alocador de memória rouba mais um bocado, e piora à medida que o processo vai vivendo: uma placa que aceita um pedido às nove da manhã pode recusar o mesmo pedido às seis da tarde sem que nada tenha mudado no pedido. O ambiente gráfico, se existir, leva entre meio giga e um giga e meio, e num servidor a sério isto resolve-se não instalando ambiente gráfico nenhum.

E depois há a parte de que quase ninguém se lembra e que num sistema on-premise a sério é decisiva: o modelo de linguagem raramente está sozinho na placa. Quem monta RAG tem um modelo de embeddings residente, que ocupa um a dois gigas. Quem quer resultados decentes tem também um reranker. Quem faz transcrição tem um Whisper. Quem tem um classificador à entrada, como eu tenho, tem mais um modelo pequeno permanentemente carregado. Somem isso tudo antes de decidir quanto sobra para o modelo principal, porque somar depois é como descobrir que a mobília não cabe depois de assinar a escritura.

Compartimento três: o KV cache, que é onde a conta rebenta

Esta é a parte que separa quem já pôs isto em produção de quem só correu um modelo no portátil.

Os pesos são estáticos. Carregam-se uma vez, ocupam sempre o mesmo, e é por isso que são fáceis de planear. O KV cache é dinâmico: cresce com o comprimento das conversas e multiplica-se pelo número de pessoas a falar ao mesmo tempo. É a única coisa na placa que responde ao sucesso do projecto crescendo, o que faz dele o candidato número um a fazer explodir um sistema que funcionava perfeitamente na demonstração.

A equação de partida é esta:

2 x camadas x cabeças KV x dimensão da cabeça x tokens em cache x bytes por elemento x sequências concorrentes

O 2 é porque se guardam duas coisas por cada posição, as chaves e os valores. O resto lê-se na configuração do modelo, no config.json ou nos metadados do GGUF. Não é preciso adivinhar nada.

Vamos a um caso concreto, porque em abstracto isto não convence ninguém. Suponham um modelo com 64 camadas, 8 cabeças KV, dimensão de cabeça 128, com o cache em 16 bits:

  • 8 000 tokens de contexto, uma pessoa a falar: 2 GiB
  • 32 000 tokens, uma pessoa: 8 GiB
  • 32 000 tokens, quatro pessoas em simultâneo: 32 GiB

Leiam a última linha outra vez. Trinta e dois gigabytes só de cache, sem um único peso de modelo lá dentro. Numa placa de 24 GB, o cache sozinho já não cabia.
Existem truques para fazer offload para a memoria RAM normal do computador de alguns dos conteúdos, mas com obvia penalização na velocidade.
É este o número que ninguém põe na folha de cálculo, e é este o número que decide se o vosso sistema aguenta a equipa toda ou só a demonstração ao chefe. Duplicar o contexto duplica o cache. Duplicar os pedidos concorrentes em contexto cheio duplica outra vez. É crescimento multiplicativo em cima de uma quantidade fixa de memória, e o resultado é que a coisa passa de folgada a impossível muito mais depressa do que a intuição sugere.

Há três formas de atacar isto, e todas têm custo.

A primeira é escolher modelos com GQA agressivo. Aquele exemplo tinha 8 cabeças KV; um modelo com 64 cabeças KV e tudo o resto igual precisaria de oito vezes mais cache. Esta característica não aparece em nenhuma tabela comparativa de benchmarks e é, para quem corre com concorrência, mais determinante do que metade das métricas que aparecem.

A segunda é quantizar o próprio cache. Passar de 16 para 8 bits corta a memória do cache ao meio e é suportado pelos runtimes mais comuns. Custa alguma qualidade, sobretudo em contextos muito longos e em tarefas de recuperação de detalhe. Meçam antes de assumir que é grátis, e meçam com o vosso trabalho real e não com perguntas de brincadeira.

A terceira, e é a que recomendo primeiro, é ser honesto sobre o contexto de que precisam. Muita gente configura 128k porque estava lá disponível, e depois nunca ultrapassa os 6k em utilização real. Estão a pagar memória por um contexto imaginário. Vejam os logs, vejam a distribuição real dos comprimentos das vossas conversas, e configurem para o percentil 95 do que acontece de facto, não para o máximo do que é possível.

Ah, e confirmem qual é o valor por omissão do vosso runtime. É quase de certeza mais baixo do que aquilo que vão usar em produção, o que significa que o benchmark simpático que fizeram na semana passada foi feito com um cache minúsculo e não vos diz nada sobre o dia em que alguém colar um contrato de quarenta páginas no chat.

Compartimento quatro: workspace e folga

Falta o resto. Buffers de activação durante a inferência, o grafo de computação que o runtime constrói, os tensores temporários do processamento em lote do prompt, e a folga que impede que uma variação normal de utilização vos atire uma excepção de memória à cara.

Este compartimento é o mais difícil de calcular antecipadamente com precisão, porque depende do runtime, da versão do runtime, e do tamanho do lote. Por isso não o calculo: reservo. Um a dois gigabytes em placas pequenas, dois a três em placas grandes ou com concorrência, e ajusta-se depois de medir.

Quem achar que é margem a mais, faça as contas ao custo de um processo de inferência morrer a meio da tarde num sistema que trinta pessoas estão a usar, e compare com o custo de dois gigabytes de VRAM.

A minha ordem de operações

Junto tudo isto numa sequência que sigo sempre pela mesma ordem, e a ordem importa:

  1. Partir do checkpoint exacto, não da contagem de parâmetros. O ficheiro concreto, daquele quant, daquele repositório, com o tamanho que lá está escrito.
  2. Somar o KV cache para o contexto e a concorrência que vão mesmo ter, não para os que soam bem numa apresentação.
  3. Somar workspace e folga.
  4. Comparar o total com a capacidade utilizável, e não com o rótulo da caixa.
  5. Medir, assim que houver máquina: pico de memória, tempo até ao primeiro token, e tokens por segundo.

Os quatro primeiros passos fazem-se numa folha de cálculo antes de gastar um euro. O quinto é o único que vos diz a verdade, e é por isso que os outros quatro existem: para chegarem ao quinto com uma máquina que tem hipótese de passar.

Caber não é correr

Chegámos ao ponto que mais custa a aceitar, e que é a razão pela qual eu insisto tanto no passo 5.

Um modelo que cabe em memória não é um modelo que serve. São coisas independentes.

Os runtimes modernos são simpáticos de mais nesta parte. Quando o modelo não cabe todo na placa, não recusam: partem-no, deixam algumas camadas na VRAM e empurram as outras para a RAM do sistema, e anunciam que carregou. Tecnicamente carregou. Só que a partir daí, cada token gerado obriga a fazer viajar dados pelo PCIe, e o PCIe é uma ordem de grandeza mais lento do que a memória da placa. O que era uma resposta em dois segundos passa a ser uma resposta em quarenta, e a diferença entre um sistema que as pessoas adoptam e um sistema que as pessoas contornam está algures nesse intervalo.

O offload para CPU não é um erro do software. É uma decisão de engenharia razoável, tomada por omissão, que serve muito bem para experimentar e serve muito mal para produção. Quem monta on-premise deve saber quando ela está a acontecer, e a única forma de saber é olhar para quantas camadas foram efectivamente para a placa e comparar com o total.

Por isso as três métricas do passo 5, e por isso três e não uma:

O pico de memória diz-vos se cabe, medido durante um pedido em contexto cheio e não com o modelo parado a seguir ao arranque.

O tempo até ao primeiro token é a métrica da experiência de utilização. É o silêncio entre carregar em enter e ver alguma coisa acontecer, e é o número que decide se as pessoas voltam à ferramenta amanhã.

Os tokens por segundo dizem-vos se a resposta chega em tempo útil, e devem ser medidos com a concorrência real. Um sistema que faz 40 tokens por segundo para um utilizador e 6 para cinco utilizadores em simultâneo tem um problema que a medição individual nunca revelaria.

Uma nota sobre duas placas, que é o caminho por onde muita gente tenta fugir a estas contas. Duas placas de 24 GB não são uma placa de 48 GB. São 48 GB com uma ligação lá pelo meio, e o proveito que tiram disso depende do runtime, do modelo, e de as camadas dividirem bem. Às vezes resulta muito bem. Às vezes ficam com o dobro do consumo eléctrico e um sistema mais lento do que tinham antes. Se a alternativa for uma placa única com a memória necessária, e se o orçamento a permitir, a placa única é quase sempre a decisão certa.

O que isto muda na hora de comprar

A conclusão prática de tudo isto não é um número. É uma mudança de sequência.

A forma habitual de decidir é escolher a placa e depois ver que modelos lá cabem. A forma que funciona é a inversa: decidir que trabalho é que o sistema tem de fazer, que modelos servem esse trabalho, com que contexto e com quantas pessoas em simultâneo, somar os quatro compartimentos, e só então ir ver que placa é que tem essa memória.

E há uma decisão de gestão escondida aqui que vale mais do que qualquer optimização técnica. Se as contas derem que o modelo grande não cabe com a concorrência que precisam, a resposta correcta muitas vezes não é comprar uma placa maior. É correr três modelos pequenos especializados na placa que já têm, cada um bom no seu domínio, todos residentes ao mesmo tempo, sem trocas de modelo e sem esperas. Já escrevi aqui sobre isso e continuo a achar que é o caminho que a maior parte das organizações devia seguir antes de assinar uma factura de cinco dígitos.

A calculadora

Como faço estas contas com demasiada frequência, e como escrever a mesma folha de cálculo pela décima vez é a definição de trabalho que devia estar automatizado, procurei e achei isto numa página no ResearchAudio.
É gratuita, corre inteiramente no browser, e não envia os vossos dados para lado nenhum. Isso importa mais do que parece: quem está a dimensionar infraestrutura está a escrever números que descrevem a arquitectura interna da organização, e essa informação não tem que passear por servidores de terceiros só para fazer uma multiplicação.

https://tools.researchaudio.io/llm-gpu-memory-calculator/

Usem-na como aquilo que ela é: uma folha de planeamento que vos dá um número defensável para levar a uma reunião de orçamento, e não um substituto de medir o sistema depois de o ter montado.

E agora um pedido

Esta é a parte em quem faz essa calculadora precisa de vocês.

A folha está calibrada com as minhas medições e com as de alguns clientes, o que é uma amostra pequena e enviesada para o tipo de hardware e de trabalho que eu vejo. Se já mediram pico de memória real para uma combinação concreta de modelo, quantização, contexto e placa, mandem-me. Interessa-lhes o quarteto todo, e interessa-lhes sobretudo onde é que a previsão falhou: as combinações em que a folha diz que cabe e não cabe, ou em que diz que não cabe e afinal sobrava espaço. Os casos em que acertamos não ensinam nada a ninguém. Interessam onde houve um desvio do padrão.

Se aparecerem dados suficientes, eles publicarão a comparação entre o previsto e o medido, com os desvios à vista e sem os esconder. Uma folha de cálculo que ninguém confrontou com a realidade é uma opinião com formatação bonita.

Meçam a vossa. Depois digam-me se bate certo.

Até à próxima. Nuno

PS: as contas deste post foram todas verificadas à mão antes de publicadas, e o texto foi escrito com o auxílio de modelos LLM privados 🙂
As imagens presentes neste artigo são propriedade dos respetivos detentores de direitos e são aqui reproduzidas em baixa resolução, ao abrigo do direito de citação para fins de crítica, comentário e informação, nos termos do artigo 75.º, n.º 2, do Código do Direito de Autor e dos Direitos Conexos. Não se pretende com esta utilização causar qualquer prejuízo à exploração normal da obra. Caso seja detentor dos direitos e pretenda a remoção ou a correção da atribuição, contacte-nos através de [email protected] e o pedido será atendido assim que possível.