A tese desta página: um modelo é um arquivo de tensores mais um grafo de operações. Não há mais nada lá dentro nem banco de dados, nem regras escritas, nem cópia dos textos de treino. Todo número que você vai ver sobre um modelo (7B, 40 GB, "não cabe na 4090") é consequência aritmética dessas duas coisas, e esta página calcula todos eles na sua frente.
O que um modelo NÃO é
não é um banco de dados: não há linha para consultar nem chave para buscar
não é um programa: o arquivo não contém código, só números
não é uma cópia do treino: 15 trilhões de tokens não cabem em 16 GB
não tem tamanho fixo: o mesmo modelo ocupa 4×, 2× ou 0,5× conforme a precisão
Por que isso importa na prática
dimensionar a máquina antes de alugar a GPU por hora
entender por que dobrar o contexto dobra a memória e não o modelo
ler uma model card e saber o que cada número implica
escolher entre denso e MoE sabendo o que cada um cobra
Explore cada etapa →
A matemática, num só lugar
As 4 contas que sustentam tudo. Clique em qualquer uma para pular direto para a etapa que a explica.
Honestidade · a conta conferida
fórmula contra número publicado
A calculadora desta página não é ilustrativa: ela roda a fórmula de verdade. Para você não ter que acreditar, aqui está ela aplicada a cinco modelos abertos, comparada com a contagem que cada fabricante publica.
A fórmula acerta os modelos abertos porque a arquitetura deles é pública. Ela erra de propósito em detalhes que quase não pesam: viés das camadas, tensores de RoPE, e os buffers que cada implementação inventa.
Baixar um modelo é baixar um .safetensors. O formato é deliberadamente burro: um cabeçalho JSON dizendo o nome, o tipo e o shape de cada tensor, e logo depois os bytes crus, um tensor atrás do outro. É isso. Nenhuma instrução, nenhum texto, nenhuma lógica a lógica mora no código de inferência, que é outro arquivo e costuma ter algumas centenas de linhas.
O cabeçalho, recortado
data_offsets é o que torna o formato rápido: o carregador sabe exatamente em que byte cada matriz começa e termina, então dá para mapear o arquivo em memória e deixar o sistema operacional paginar. É também o que torna o formato seguro diferente de um .bin de pickle, aqui não existe caminho para executar código ao carregar.
abrir o arquivo sem carregar o modelo
from safetensors import safe_open
# framework="pt" só define o tipo devolvido; nada é carregado ainda.# O arquivo é mapeado em memória: ler o cabeçalho de um modelo de# 140 GB custa milissegundos e zero byte de RAM.withsafe_open("model-00001-of-00004.safetensors", framework="pt") as f:
total = 0for name in f.keys():
shape = f.get_slice(name).get_shape()
n = 1for dim in shape:
n *= dim
total += n
print(f"{name:<52} {str(shape):<18} {n:>12,}")
print(f"\ntotal: {total:,} parâmetros")
# model.embed_tokens.weight [128256, 4096] 525.336.576# model.layers.0.self_attn.q_proj [4096, 4096] 16.777.216# model.layers.0.self_attn.k_proj [1024, 4096] 4.194.304 ← GQA# ...
# o cabeçalho é JSON puro: os 8 primeiros bytes dão o tamanho dele,# e o resto é legível sem nenhuma biblioteca de machine learning
head -c 8 model.safetensors | xxd -p # tamanho, little-endian u64# despeja o cabeçalho inteiro e conta os tensores
python3 -c '
import json, struct, sys
with open("model.safetensors", "rb") as f:
n = struct.unpack("<Q", f.read(8))[0]
h = json.loads(f.read(n))
print(len(h) - 1, "tensores")
print(h["__metadata__"])
'# o config.json ao lado é onde moram os números que você arrasta aqui
jq '{hidden_size, num_hidden_layers, num_attention_heads,
num_key_value_heads, vocab_size, intermediate_size}' config.json
Armadilha de produção: o modelo quase nunca vem num arquivo só vem em shards (model-00001-of-00004.safetensors) com um index.json mapeando cada tensor ao seu shard. Baixar 3 de 4 shards dá um diretório que parece completo e falha no meio do carregamento, depois de você já ter pago o boot da GPU. Confira o índice, não a pasta.
O número que dá nome ao modelo 7B, 70B é a soma de todos os pesos de todas as matrizes. Não é uma medida de inteligência nem de qualidade: é o tamanho do arquivo em unidades de "número". E ele sai de uma conta curta, que você pode conferir arrastando os controles abaixo.
Comece de um modelo real
A configuração
O total
Fórmula · contagem de parâmetros
P = V·d + L · ( 2d² + 2·d·d_kv + m·d·d_ff + 2d ) + d + V·d
Cada símbolo: V é o tamanho do vocabulário; d é o d_model, a largura do vetor que atravessa o modelo; L é o número de blocos; d_kv é n_kv_heads × d_head; d_ff é a largura interna do MLP; m é 2 ou 3 conforme o MLP tenha duas matrizes (GELU) ou três (SwiGLU). O resultado costuma cair entre 10⁸ (modelos de brinquedo) e 10¹² (fronteira). O último termo é a cabeça de saída, e some quando ela é amarrada ao embedding.
Onde o peso realmente está
Em qualquer modelo moderno de porte, o MLP domina: costuma ficar entre 60% e 70% dos parâmetros, contra 20% a 30% da atenção. A intuição popular é o contrário atenção é a parte famosa , mas a atenção é barata em pesos e cara em tempo. Arraste d_ff e veja a barra se mover.
O vocabulário pesa mais do que parece
Num modelo pequeno, a tabela de embedding pode ser a maior matriz do arquivo. No GPT-2 small, embedding + posição são ~32% do total. É por isso que modelos pequenos amarram a cabeça de saída ao embedding: economiza V×d de uma vez. Escolha o preset GPT-2 e olhe a barra.
a mesma conta que roda nesta página
defcount_params(cfg: dict) -> int:
d = cfg["hidden_size"]
layers = cfg["num_hidden_layers"]
heads = cfg["num_attention_heads"]
kv = cfg.get("num_key_value_heads", heads) # sem GQA, kv == heads
vocab = cfg["vocab_size"]
ff = cfg["intermediate_size"]
d_head = d // heads
d_kv = d_head * kv
# Q e O são sempre d×d. K e V encolhem com GQA: é aí que a# economia aparece, e ela vale muito mais no KV-cache que aqui.
attn = 2 * d * d + 2 * d * d_kv
# SwiGLU tem gate, up e down. GELU clássico tem só up e down.
mats = 3if cfg["hidden_act"].startswith("silu") else2
mlp = mats * d * ff
# RMSNorm é um vetor por norma, não uma matriz: desprezível,# mas incluído porque a soma tem que fechar com o arquivo.
norms = 2 * d
head = 0if cfg.get("tie_word_embeddings") else vocab * d
return vocab * d + layers * (attn + mlp + norms) + d + head
# confira contra o arquivo, não contra a model card:# sum(p.numel() for p in model.parameters())
Um parâmetro é um número real, e número real não existe no computador existe uma aproximação com um orçamento de bits. Quantos bits você dá a cada peso é uma decisão sua, tomada depois do treino, e ela multiplica ou divide o tamanho do modelo sem tocar em nenhum valor aprendido.
Escolha a precisão dos pesos
Por que bf16, e não fp16
Os dois ocupam 2 bytes, mas gastam de forma diferente: o fp16 dá 10 bits à mantissa e 5 ao expoente; o bf16 dá 7 à mantissa e 8 ao expoente o mesmo alcance do fp32. Em treino isso decide o jogo: gradientes muito pequenos viram zero em fp16 e sobrevivem em bf16. Você troca casas decimais, que a rede não usa, por alcance, que ela usa.
Quantizar não é arredondar
int8 e int4 não guardam o peso: guardam um inteiro mais uma escala por grupo de pesos (tipicamente 32, 64 ou 128 deles). É por isso que "int4" na prática ocupa ~4,5 bits por peso, não 4 a escala e o zero-point de cada grupo também ocupam lugar. E é por isso que o tamanho do grupo aparece no nome dos arquivos: Q4_K_M, gptq-4bit-128g.
Armadilha de produção: a perda de quantização não aparece no chat aparece nas tarefas longas. Um modelo em int4 continua fluente, educado e convincente, e começa a errar em contas de várias etapas, em código que precisa fechar sintaxe e em respostas que dependem de um detalhe específico do contexto. Se você vai quantizar, meça no seu conjunto de tarefas antes: a diferença que importa não é a perplexidade, é a taxa de acerto no que você faz.
Carregar os pesos é só a primeira parcela da conta. Enquanto o modelo responde, ele guarda para cada token já lido um par de vetores K e V por camada, para não ter que recalcular tudo a cada token novo. Esse é o KV-cache, e ele cresce linearmente com o contexto e com o lote não com o tamanho do modelo.
O regime de execução
A fatura
Fórmula · KV-cache
KV = 2 · L · (h_kv · d_head) · n · b · bytes
O 2 são as duas matrizes, K e V. L é o número de camadas: cada uma guarda o seu próprio cache. h_kv · d_head é a largura do cache por token e é exatamente aqui que a GQA age: reduzir n_kv_heads de 32 para 8 corta o cache por 4 sem tocar na qualidade de forma perceptível. n é o contexto e b o lote. Repare no que não está na fórmula: d_ff, vocabulário, número de cabeças de consulta. O cache não sabe o tamanho do modelo.
Cabe nesta GPU?
Honestidade · esta conta é uma ordem de grandeza
A VRAM real depende da implementação, e a diferença não é pequena. PagedAttention (vLLM) aloca o cache em blocos e corta o desperdício de fragmentação, que numa implementação ingênua passa de 30%. Flash-attention derruba as ativações que estimamos aqui. Cache quantizado em int8 corta o KV pela metade. E o overhead fixo de contexto CUDA varia por driver. Use esta conta para dimensionar, não para fechar contrato: ela dá a ordem de grandeza, não a fatura.
estimar antes de alugar a GPU
defvram_estimate(cfg, params, *, ctx, batch, w_bits=16, kv_bits=16):
"""Devolve bytes. Ordem de grandeza, não fatura."""
d_head = cfg["hidden_size"] // cfg["num_attention_heads"]
d_kv = d_head * cfg.get("num_key_value_heads", cfg["num_attention_heads"])
weights = params * (w_bits / 8)
# 2 = K e V. Cresce com ctx e batch, NÃO com o tamanho do modelo.
kv = 2 * cfg["num_hidden_layers"] * d_kv * ctx * batch * (kv_bits / 8)
# pico é o prefill: a maior matriz intermediária viva de uma vez
act = batch * ctx * cfg["intermediate_size"] * 2# contexto CUDA, kernels, fragmentação do alocador
overhead = 1.0e9 + (weights + kv + act) * 0.05return weights + kv + act + overhead
# A conta que importa na hora de escolher a máquina:# quantos tokens de contexto sobram DEPOIS dos pesos?defmax_context(cfg, params, gpu_bytes, *, batch=1, w_bits=16):
d_head = cfg["hidden_size"] // cfg["num_attention_heads"]
d_kv = d_head * cfg.get("num_key_value_heads", cfg["num_attention_heads"])
free = gpu_bytes * 0.90 - params * (w_bits / 8) - 1.0e9
per_token = 2 * cfg["num_hidden_layers"] * d_kv * 2 * batch
returnmax(0, int(free / per_token))
# o que a placa está realmente fazendo, a cada segundo
nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu \
--format=csv -l 1# vLLM: deixe o servidor medir por você em vez de chutar.# --gpu-memory-utilization reserva a fração da placa para pesos+cache;# o log de boot imprime quantos blocos de KV couberam.
vllm serve meta-llama/Meta-Llama-3-8B-Instruct \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--kv-cache-dtype fp8 # corta o KV pela metade# "# GPU blocks: 14563" no log × block_size (16) = tokens de cache# disponíveis no total, somando TODAS as requisições simultâneas.
Armadilha de produção: o KV-cache é por requisição simultânea, não por usuário. Um serviço que funciona perfeitamente em teste com um cliente pode morrer com dez, sem que nada no modelo tenha mudado o que mudou foi o lote. Arraste o controle de lote acima e veja: com contexto longo, a memória sai do controle bem antes do que a intuição sugere. Dimensione pelo pico de concorrência, e prefira um servidor que enfileira a um que estoura.
A arquitetura é curta de descrever, e é essa a parte surpreendente: entra um embedding, passam N blocos idênticos, sai uma projeção para o vocabulário. Os blocos têm exatamente a mesma forma o que muda de um para o outro são só os valores dos pesos, aprendidos no treino.
O grafo inteiro
Por que blocos idênticos
Porque é a única forma de escalar sem reprojetar nada. Dobrar o modelo é mudar dois números no config.json: hidden_size e num_hidden_layers. Nenhuma linha de código muda, nenhum kernel novo é escrito, e a mesma implementação de inferência serve de 100M a 500B. Essa uniformidade é o que tornou a lei de escala operacionalmente possível não só teoricamente.
A esteira residual
Repare que o shape não muda ao atravessar um bloco: entra [lote, contexto, d] e sai [lote, contexto, d]. Cada bloco não substitui o vetor, ele soma a sua contribuição a uma esteira que atravessa o modelo inteiro. É o que permite empilhar 80 camadas sem a rede desandar e é o assunto da próxima página da série.
Num modelo denso, todo parâmetro participa de todo token. Num MoE (mistura de especialistas), o MLP de cada bloco é replicado em E cópias e um roteador acende só k delas por token. O resultado é um modelo que tem os parâmetros de um gigante e o custo de conta de um médio pagando a memória do gigante mesmo assim.
O roteador
Parâmetros totais
Ativos por token
O que o MoE compra
Capacidade por FLOP. Cada especialista pode se especializar num regime diferente da distribuição sintaxe de uma língua, código, matemática e o roteador escolhe. Você ganha o conhecimento de um modelo grande pagando a latência de um pequeno. O Mixtral 8×7B é o exemplo canônico: 46,7B de parâmetros totais, 12,9B ativos por token.
O que o MoE cobra
Memória cheia e roteamento instável. Todos os especialistas precisam estar carregados: a VRAM é a do modelo total, não a do ativo. Some a isso o desbalanceamento se o roteador prefere alguns especialistas, os outros treinam mal e a capacidade que você pagou fica ociosa. Em serving distribuído, ainda há o custo de rede quando o especialista escolhido está em outra GPU.
Armadilha de produção: ao dimensionar um MoE, o número que decide a GPU é o total, e o que decide a latência é o ativo. Trocar os dois é o erro clássico: alguém lê "12,9B ativos" no anúncio, provisiona uma placa de 24 GB e descobre no boot que precisa de 94 GB para os pesos. E o inverso também acontece: provisionar CPU/latência como se fosse um 47B denso e desperdiçar metade da máquina.
O caminho inteiro
No fim: um modelo é um arquivo de tensores mais um grafo de operações, e todo número que se diz sobre ele é aritmética em cima desses dois fatos. Isso é uma boa notícia operacional: dá para prever o custo antes de gastar, dimensionar a máquina antes de alugar, e ler uma model card sabendo o que cada linha implica. O que não se prevê a partir daqui é a qualidade nenhuma dessas contas diz se o modelo responde bem. Para isso continua valendo o único método que funciona: medir no seu conjunto de tarefas.