1 pontos por GN⁺ 2025-05-18 | 1 comentários | Compartilhar no WhatsApp
  • O KVSplit tem como objetivo executar contextos mais longos e modelos mais pesados dentro do mesmo orçamento de memória no Apple Silicon, aplicando precisões de quantização diferentes para keys e values no KV cache de attention de LLMs
  • O principal resultado é a configuração K8V4: com 8K tokens, ela reduz o uso de 176,00 MB em FP16 para 71,50 MB, aumenta a velocidade de processamento de tokens de 54.360 tokens/s para 57.438 tokens/s e apresenta variação de perplexity de +0,86%
  • Com base no resultado de que keys são mais sensíveis à quantização do que values, o projeto resume que K4V8, usando o mesmo número total de bits, tem degradação de qualidade cerca de 7 vezes maior que K8V4
  • Os recursos oferecidos incluem aplicação de patch no llama.cpp, build com suporte a Metal, benchmarks de memória, velocidade e perplexity, salvamento de resultados em CSV/JSON, ferramentas de visualização e captura de redução de memória baseada no Activity Monitor
  • A configuração recomendada é K8V4, para equilibrar qualidade e economia de memória; se for necessária a máxima economia de memória, há a opção K4V4, que reduz 72% com perda de qualidade de cerca de 6%

Problema que o KVSplit tenta resolver

  • KVSplit é um projeto para reduzir a memória do KV cache durante inferência de LLMs em Macs com Apple Silicon
  • Ele aplica precisões de quantização diferentes para key e value no KV cache do mecanismo de attention
  • Os objetivos são:
    • Reduzir o uso de memória em até 72%
    • Executar contextos 2 a 3 vezes mais longos dentro do mesmo orçamento de memória
    • Manter ou melhorar a velocidade de inferência em comparação com FP16
    • Oferecer suporte a Metal ajustado para Apple Silicon

Principais resultados de benchmark

  • Os resultados por configuração com 8K tokens são os seguintes:
    • FP16: 176,00 MB, 54.360 tokens/s
    • K8V8: 93,50 MB, 51.503 tokens/s, perplexity +0,03%
    • K8V4: 71,50 MB, 57.438 tokens/s, perplexity +0,86%
    • K4V8: 71,50 MB, 58.690 tokens/s, perplexity +6,06%
    • K4V4: 49,50 MB, 55.193 tokens/s, perplexity +6,15%
  • Na tabela de economia de memória, K8V4 é apresentado com 59% de redução em 8K tokens, e K4V4 com 72% de redução
  • Na tabela de desempenho, K8V4 mostra ganho de velocidade de +5,7% em relação a FP16; K4V8, +8,0%; e K4V4, +1,5%
  • K8V8 reduz a memória em relação a FP16, mas a velocidade cai -5,3%

Uso de memória conforme o comprimento da sequência

  • Quanto maior o comprimento do contexto, maior o efeito de redução de memória do KV cache
  • Com 8192 tokens, o uso de memória é:
    • FP16: 176,00 MB
    • K8V8: 93,50 MB
    • K8V4: 71,50 MB
    • K4V8: 71,50 MB
    • K4V4: 49,50 MB
  • Mesmo com 4096 tokens, contra 88,00 MB em FP16, K8V4/K4V8 usam 35,75 MB e K4V4 usa 24,75 MB
  • Com 128 tokens, os valores apresentados são FP16 com 5,50 MB, K8V4/K4V8 com 2,23 MB e K4V4 com 1,55 MB

Assimetria entre key e value

  • A memória do KV cache é dominada pelo armazenamento do vetor key e do vetor value de cada token
  • A observação central do projeto é que key é muito mais sensível à quantização do que value
  • K8V4 usa key de 8 bits e value de 4 bits para oferecer o seguinte ponto de equilíbrio:
    • Degradação de perplexity de 0,86% em relação a FP16
    • Economia de memória de 59%
    • Velocidade de inferência mais alta que FP16
  • K4V8 usa o mesmo número total de bits que K8V4, mas é descrito como tendo degradação de qualidade cerca de 7 vezes maior que K8V4
  • O projeto explica que essa assimetria permite executar contextos mais longos e modelos maiores em consumer hardware

Instalação e forma de integração

  • A instalação é feita clonando o repositório e executando scripts/install_kvsplit.sh
git clone https://github.com/dipampaul17/KVSplit.git
cd kvsplit

chmod +x scripts/install_kvsplit.sh
./scripts/install_kvsplit.sh
  • O script de instalação permite escolher o método de configuração do ambiente Python:
    • Virtual Environment: cria um ambiente Python independente dentro da pasta do projeto
    • System Python: usa a instalação Python existente
    • Skip Python Setup: o usuário gerencia diretamente o ambiente Python
  • Também é possível escolher o modo de integração com llama.cpp
    • Modo padrão: clona o llama.cpp e aplica o patch de KV split
    • Modo Git submodule: adiciona o llama.cpp como submodule para desenvolvedores ou usuários avançados
  • O processo de instalação inclui configuração do llama.cpp com suporte a Metal para Apple Silicon, ativação de differentiated KV cache quantization, download opcional de modelo de teste e configuração das ferramentas de visualização

Exemplos de uso e opções de CLI

  • Uma comparação rápida pode ser executada com um modelo GGUF do próprio usuário
python scripts/quick_compare.py --model models/your-model.gguf
  • Os alvos de comparação são FP16, K8V8, K8V4, K4V8 e K4V4, mostrando métricas de memória, velocidade e qualidade em conjunto
  • O exemplo de execução do README usa --flash-attn e opções de quantização KV junto com llama-cli
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
  -t 8 --flash-attn --kvq 8
  • O exemplo de K4V8 especifica separadamente os bits de key e value
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
  -t 8 --flash-attn --kvq-key 4 --kvq-val 8
  • O exemplo de contexto de 32K informa que FP16 exige cerca de 1,4 GB, enquanto K8V4 exige cerca de 400 MB
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf \
  -c 32768 -n 4096 -t 8 --flash-attn --kvq 8 \
  -f your-long-document.txt
  • As principais flags de CLI são:
    • -t 8: número de threads; 8 é recomendado na maioria dos chips Apple Silicon
    • --flash-attn: ativa attention otimizada, recomendado no Apple Silicon
    • --kvq N: define os bits de key e value
    • --kvq-key N: define apenas os bits de key
    • --kvq-val N: define apenas os bits de value
    • -c N: tamanho do contexto
    • -n N: número de tokens a gerar
    • -f FILE: arquivo de entrada
    • -m MODEL: caminho do arquivo de modelo .gguf

Benchmarks e ferramentas de visualização

  • O benchmark completo é executado com scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py --config K8V4 --seq-len 4096
  • As visualizações são geradas com scripts/visualize_results.py
python scripts/visualize_results.py
  • O benchmark mede os seguintes itens:
    • Memory Usage: memória de VRAM e do KV cache
    • Performance: tokens/s por comprimento de sequência
    • Quality: perplexity usando llama-perplexity
    • Scaling: variação de memória e desempenho conforme o comprimento da sequência
  • Os resultados são salvos em formato CSV/JSON e geram automaticamente estatísticas resumidas e gráficos de visualização
  • capture_memory.sh é uma ferramenta para capturar a economia de memória no Activity Monitor

Otimização para Apple Silicon e limitações

  • KVSplit é otimizado para o Metal framework da Apple
  • Ele enfatiza eficiência de memória em dispositivos com restrições de memória, como a série M do Apple Silicon
  • O README informa que a economia real de memória pode diferir um pouco do cálculo teórico por causa do alinhamento de página de 256B do llama.cpp
  • Os chips compatíveis incluem M1, M2, M3 e M4

Configuração recomendada e roadmap

  • A configuração recomendada é K8V4:
    • key de 8 bits, value de 4 bits
    • 59% de economia de memória
    • 0,86% de perda de qualidade
    • +5,7% de velocidade de inferência em relação a FP16
  • A máxima economia de memória é com K4V4:
    • key de 4 bits e value de 4 bits
    • 72% de economia de memória
    • Cerca de 6% de perda de qualidade
    • Apresentada como adequada para aplicações menos sensíveis
  • Para contextos muito longos, K8V4 ou K4V4 são recomendados; quanto maior o comprimento do contexto, mais a economia de memória se acumula
  • Os planos futuros são:
    • Adaptive Precision baseada na importância dos tokens
    • Layer-Specific Quantization, usando precisões diferentes por camada
    • Otimizações por modelo para Mistral, Phi-3 etc.
    • Demo web
    • Suporte a iOS e iPadOS
  • A licença é MIT, e contribuições podem ser feitas por issue ou pull request

1 comentários

 
GN⁺ 2025-05-18
Opiniões no Hacker News
  • Interessante. Fico curioso se há alguma intuição por trás de por que esses resultados aparecem. Também queria saber se isso foi descoberto por essa intuição ou por experimentos aleatórios.
    Na etapa "apply patch" do script de instalação, parece que ainda há um placeholder. Em vez de aplicar um patch depois de git clone, acho que seria mais amigável ao usuário fazer um fork do llama.cpp e incluí-lo como submódulo Git.
    Além disso, como cada pessoa tem uma configuração local de Python diferente, seria bom permitir separar as partes relacionadas ao llama.cpp das partes relacionadas ao Python, em vez de fixar uma dependência do Python do Homebrew.

    • Boa pergunta sobre a intuição. A diferença vem do papel central que cada componente desempenha na atenção.
      As chaves determinam a quais tokens prestar atenção e criam o padrão real de atenção por meio de cálculos de similaridade. Os valores apenas armazenam as informações que serão transmitidas depois que a atenção é decidida.
      Se você quantizar os vetores de chave de forma agressiva demais, distorce o cálculo de similaridade de todas as interações entre tokens. Um pequeno erro nas chaves pode fazer a atenção ir para um token completamente errado.
      Os valores são muito mais tolerantes. O erro de quantização dos vetores de valor só afeta o conteúdo informacional daquele token individual depois que o padrão de atenção já foi definido.
      É parecido com a diferença entre o sistema de catálogo de uma biblioteca e os livros em si. Se o número do catálogo (chave) estiver corrompido, você vai parar em uma estante completamente errada; mas, se algumas palavras do livro (valor) ficarem borradas, você ainda está lendo o livro certo e só encontra um pouco de ruído ocasional.
      Matematicamente, as chaves entram no cálculo do softmax, e pequenos erros são amplificados exponencialmente durante a normalização. Os valores passam apenas por uma média ponderada linear, então os erros tendem a se cancelar.
      Inicialmente encontrei essa assimetria em artigos como "More for Keys, Less for Values" e "KV-AdaQuant", e queria quantificar exatamente qual era o impacto na inferência em Apple Silicon. Foi impressionante ver que, com a mesma memória, a diferença de qualidade entre K8V4 e K4V8 era de 7 vezes.
      Obrigado também pelo feedback sobre a instalação; vou corrigir o placeholder e tornar as dependências de Python mais flexíveis.
    • Na verdade, o patch não é aplicado ao llama.cpp. Isso porque o parsing de argumentos foi movido para arg.cpp há 8 meses.
      Mas isso não importa, porque as opções para configurar a quantização de K e V já foram adicionadas ao llama.cpp em 2023.
      Não entendo por que esse patch existe. Não vejo motivo, exceto fazer uma configuração que já existe parecer nova ao transformá-la em outros argumentos de linha de comando.
      Recomendo fortemente que ninguém execute o arquivo install.sh desse novo repositório. Especialmente quando ele é desnecessário para algo tão simples quanto aplicar um único arquivo de patch.
  • Isso é diferente de usar --cache-type-k e --cache-type-v?

    • Não. Parece uma tentativa gerada por LLM para ganhar estrelas no GitHub.
      Listei algumas das outras coisas estranhas do repositório em outro comentário.
    • Imagino que seja um pouco diferente. MLX/MPS não tem suporte nativo a 4 bits e, se me lembro bem, talvez nem a 8 bits. No lançamento inicial, também não havia suporte a bf16.
      Então acho que, com o método antigo type_k/v, o mínimo possível em GPUs Apple seria 16 bits f16/bf16. Mas não sou especialista nos internos do llama.cpp, então posso estar errado.
  • Fico curioso se esse patch também poderia ser feito no MLX. Como estou vendo velocidades melhores no MLX, se isso fosse combinado com essa abordagem, usuários de Mac poderiam ter conversas longas em uma velocidade utilizável.

    • Provavelmente seria possível, mas estou mexendo nas entranhas do MLX agora e, embora seja um framework bem projetado, percebi que ele está bem menos maduro no sentido de você poder simplesmente pegar um código de exemplo em que alguém já tenha benchmarkado a "melhor forma".
      Pessoalmente, o que mais me empolga — por incrível que pareça — são os bindings para Haskell. Há alguns dias alguém observou que a avaliação preguiçosa do Haskell combina bastante bem com esse paradigma, e que a abordagem quase puramente funcional para grafos de compilação também ajuda. Fazer aprendizado de máquina em Haskell parece divertido.
  • Fico curioso se a quantização KV diferencial (por exemplo, K8V4) pode ser aplicada a modelos já convertidos para o formato .gguf. Ou seria necessário reconstruir o modelo com suporte especial?
    Se for compatível com qualquer arquivo .gguf, também queria saber se há restrições quanto ao tipo de modelo (Mistral, Phi-3 etc.) ou às configurações do tokenizer.

    • Sim. Um dos principais pontos fortes do KVSplit é que ele pode usar modelos .gguf existentes como estão, sem reconstrução nem conversão especial. A quantização acontece no cache KV em tempo de execução, não durante o carregamento ou a conversão do modelo.
      Isso é possível porque o cache KV é criado durante a inferência conforme os tokens são processados, e é completamente separado dos pesos do modelo. As flags --kvq-key e --kvq-val apenas informam ao llama.cpp como armazenar esses tensores intermediários na memória.
      Testei com sucesso em variantes de Llama-3, Mistral, Phi-2/Phi-3, TinyLlama e Qwen.
      A única limitação é que é necessário usar o backend Metal do llama.cpp e, atualmente, a implementação de Flash Attention do llama.cpp contorna os formatos personalizados de cache KV, então é preciso desativar o Flash Attention com -fa 0. A técnica em si deve funcionar com qualquer arquitetura Transformer que use o mecanismo padrão de atenção.
  • Tive tempo para ler o código. Se entendi corretamente este PR, o patch é desnecessário, porque esse recurso já existia no llama.cpp desde 2023: https://github.com/ggml-org/llama.cpp/pull/4312
    Em vez de oferecer um fork do llama.cpp com as mudanças aplicadas em commits, o repositório faz você executar um script install.sh. Esse script faz checkout do branch master do llama.cpp sem fixar uma revisão e depois aplica um patch curto. Só isso já é um sinal de alerta de que há algo estranho
    O repositório tem 4 arquivos de patch diferentes, e há ainda mais uma versão de patch embutida como Heredoc dentro do script de instalação. O script também contém duas versões do código para clonar o repositório e tentar aplicar o patch
    O install.sh sobrescreve um arquivo de patch com outro na linha cp patch/split_kv_quant.diff patch/fixed_kv_patch.diff. Assim, o fixed_kv_patch.diff que foi commitado no repositório é sobrescrito antes de ser aplicado
    Pelo que vejo, parece que a intenção original era usar este patch: https://github.com/dipampaul17/KVSplit/blob/main/patch/split... (correção: pelos comentários no fim, na verdade parece ser este: https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... )
    A única coisa que esse patch adiciona é um argumento --kvq, que supostamente configura a quantização de K e V ao mesmo tempo, mas logo acima já existem argumentos nativos para configurar a quantização de K e de V separadamente. Será que, enquanto ficava movendo esses patches de um lado para outro, o autor realmente não percebeu que a funcionalidade já existia?
    Recomendo fortemente não executar scripts shell de repositórios novos como esse. Ainda mais quando o script é tão complicado assim
    O post no HN recebeu mais de 200 upvotes e o repositório no GitHub já passa de 200 estrelas, continuando a crescer, mas o conteúdo parece enganoso. O comentário nesta thread que apontou os problemas e acabou recebendo um monte de flags estava, na verdade, correto. Também é preocupante que o autor continue respondendo nesta thread, mas evite a pergunta sobre a funcionalidade já existir
    Correção: li o script shell errado. Na verdade, parece que ele aplica este patch: https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... Depois de aplicar o patch, estranhamente ele sobrescreve fixed_kv_patch.diff com split_kv_quant.diff, mas depois disso não faz mais nada. Não sei se isso é resultado de vibe coding ou apenas uma edição de código descuidada, mas quero repetir que você não deve executar scripts shell assim de repositórios desconhecidos
    Correção 2: está ainda mais confuso. O script install.sh referencia a URL antiga do repositório llama.cpp (https://github.com/ggerganov/llama.cpp), que mudou há algum tempo e agora redireciona. Os patches tentam modificar o parsing de argumentos em common.cpp, mas esse código foi movido para arg.cpp há 8 meses (https://github.com/ggml-org/llama.cpp/commit/bfe76d4a17228bf...). Então esse script de instalação e o repositório se baseiam em código de por volta de 2024 e usam uma opção adicionada ao llama.cpp por volta de 2023. O que diabos está acontecendo?

    • Exato. Talvez eu estivesse deixando passar algo e o autor pudesse esclarecer aqui, então preferi não mencionar o restante das partes suspeitas
      Há muitos sinais de alerta. Na melhor das hipóteses, parece alguém tentando inflar o perfil no GitHub com código gerado por LLM. Basta olhar a atividade do perfil em 12 de maio
    • Finalmente apareceu algo que faz sentido. Só o fato de este projeto funcionar aplicando patches, em vez de fazer um fork do projeto original e commitar as mudanças, já é motivo suficiente para preocupação
      Mas toda a atividade do autor do post original no GitHub é suspeita. Em 12 de maio, ele enviou PRs de salada de LLM para vários projetos populares, e só o do JAX foi recusado. Mesmo assim, com isso ele conseguiu fixar projetos populares no perfil como se fosse colaborador
      É difícil expressar em palavras o quanto isso é repulsivo. Qualquer pessoa trabalhando na área de IA está sendo cúmplice da poluição da informação, e as consequências ainda nem conseguimos prever. A internet morta e o dilúvio de salada de IA são só o começo
  • Em Apple Silicon com 64 GB ou 128 GB, isso é significativamente mais rápido ou melhor do que em 36 GB ou 48 GB?
    Tenho lido que contextos grandes e modelos grandes são dolorosamente lentos mesmo no Apple Silicon mais rápido e maior que o dinheiro pode comprar.
    Por isso, fico curioso se isso permite aproveitar melhor a memória maior, ou se, na prática, modelos relativamente pequenos ainda são a resposta no Apple Silicon.

    • A economia de memória do KVSplit aumenta proporcionalmente ao tamanho do contexto, então Macs com muita RAM, como 64 GB/128 GB, obtêm um ganho maior em termos absolutos. Um Mac Studio com 128 GB poderia potencialmente lidar até com janelas de contexto de centenas de milhares de tokens.
      Porém, o KVSplit não muda fundamentalmente a velocidade de computação; ele muda apenas a eficiência de memória. Nos benchmarks, o K8V4 aumentou a vazão em 14,5%, mas isso veio da melhora na localidade de memória, não de uma redução na quantidade de computação.
      O principal motivo pelo qual modelos grandes são “dolorosamente lentos” no Apple Silicon não é a restrição de memória, e sim o limite de desempenho computacional. Um modelo de 70B parâmetros vai rodar com uma velocidade de geração de tokens parecida, independentemente da RAM disponível ou da otimização do cache KV.
      O KVSplit permite usar melhor a memória disponível. Ele é especialmente valioso quando o gargalo é o tamanho do contexto, e não o tamanho do modelo.
      Para uso prático no Apple Silicon, o ponto ideal ainda é usar modelos menores (7B~13B) com janelas de contexto estendidas. Assim dá para processar muito mais texto mantendo uma velocidade de geração razoável.
      Se seu fluxo de trabalho exige ao mesmo tempo contextos gigantes e modelos grandes, ainda é melhor considerar GPUs de nível servidor, mas o KVSplit empurra um pouco mais longe o que é possível no hardware da Apple.
  • É um trabalho excelente e parece muito interessante, mas eu precisaria de uma explicação um pouco mais em alto nível para entender.
    Por exemplo, isso permite rodar um modelo com janela de contexto de 2048 tokens em uma janela de 4~6K? Ou permite rodar um modelo de 128K, como o gemma3, em uma janela de contexto de 256K ou mais?
    Qual é o caso de uso ideal para modelos locais?

    • A configuração K8V4 economiza 59% de memória, então, na prática, permite rodar um contexto 2,4 vezes maior no mesmo hardware. Um modelo com contexto de 2048 tokens poderia processar cerca de 5000 tokens, e um modelo com contexto de 8K poderia chegar a cerca de 19,5K.
      Na prática, isso significa conseguir processar um livro inteiro de uma vez no MacBook, analisar uma base de código grande sem dividir arquivos, ou manter um histórico longo de conversa em aplicações de chat.
      A economia de memória é linear em relação ao tamanho do contexto. Quanto maior a janela de contexto, maior a memória economizada em termos absolutos. No meu MacBook M4, com contexto de 8K, o cache KV caiu de 176 MB para 72 MB. Em um contexto de 128K, a mesma proporção de economia libera memória na casa dos gigabytes.
      Essa otimização é mais valiosa quando você bate no limite da janela de contexto, e não no limite de parâmetros do modelo. Se você recebe erro de falta de memória por causa de entradas longas, e não por causa dos pesos de um modelo grande, o KVSplit resolve diretamente esse gargalo.
    • Ele reduz o uso de memória de um modelo específico. Como usar essa folga fica a critério do usuário.
      Aumentar a janela de contexto depois do treinamento não é simples, então, se você não souber exatamente o que está fazendo, é melhor procurar um modelo treinado com uma janela de contexto maior.
      Modelos locais servem para várias coisas, como trabalho offline, privacidade/segurança etc. Mas a maioria acaba sendo usada para experimentar ajustando modelos.
  • Algo estranho está acontecendo, então é melhor não instalar isso nem executar aquele script.
    Já sinalizei o post.

  • Excelente ideia e tentativa. Isso também se aplica a GPU? E parece que também seria compatível com outras técnicas de quantização; nesse caso, provavelmente cada uma precisaria de um patch separado?

    • Sim. Há uma boa chance de essa abordagem também ser possível em GPUs NVIDIA/AMD. O princípio básico de que as chaves precisam de maior precisão do que os valores é independente do hardware.
      O backend CUDA do llama.cpp já oferece suporte à configuração separada de tipos de cache com as flags --cache-type-k e --cache-type-v. Este patch específico foca em uma otimização voltada ao Metal, mas a técnica central é transferível.
      Também há compatibilidade com outros métodos de quantização. Esta otimização de cache KV é complementar à quantização dos pesos do modelo (Q4_K_M, GPTQ, AWQ etc.). É possível usar precisão assimétrica no cache KV junto com qualquer formato de pesos do modelo.
      A quantização do cache KV acontece em tempo de execução durante o processamento dos tokens e é separada dos pesos do modelo, portanto não entra em conflito com a forma como o próprio modelo foi quantizado. Ela atua em partes diferentes do pipeline de inferência.
      O que exigiria trabalho adicional é a integração com engines de inferência especializadas que têm processamento customizado de cache KV, como vLLM ou TensorRT-LLM. Cada uma precisaria implementar separadamente a precisão KV assimétrica.
      Em GPUs, o ganho mais imediato provavelmente viria de integrar essa percepção diretamente às implementações de FlashAttention. Em hardware CUDA, a redução na largura de banda de memória pode resultar em um aumento maior de velocidade.
  • Uma perplexidade +0,86% em tamanhos pequenos de contexto não é algo relativamente grande? Como fica em tamanhos de contexto mais realistas, como 64~128K?

    • O ponto principal parece ser reduzir o uso de memória. Isso permite rodar contextos mais longos que antes eram impossíveis dentro da mesma memória limitada.
      Ou então dá para usar a memória livre para outras coisas, como uma IDE.