Files
ForgeLM/plan.md
T

19 KiB

Abaixo está o plano reestruturado em Markdown, separado por etapas de implementação, e já ajustado para o seu foco real: extrair o máximo de TPS possível com Qwen3.5 4B/9B em uma RTX 3070 8 GB ou hardware mais fraco, sem jogar qualidade fora. O plano-base do arquivo já estava correto ao manter llama.cpp/C++ no core, Rust no hot path e Python apenas no registry/download, com Axum/Tokio + SSE e comunicação por IPC.

Também fiz um ajuste importante de foco: para o seu caso, eu não começaria tentando ser um “LM Studio completo” logo de cara. Eu começaria como um runtime local de altíssimo desempenho para modelos pequenos/médios, primeiro em texto puro, depois abriria multi-model, features extras e UI mais rica. Isso aumenta a chance de você realmente bater TPS alto cedo. O próprio ecossistema atual já ajuda nisso: o llama.cpp hoje já oferece continuous batching, flash attention, prompt caching, tipos de KV cache quantizado e speculative decoding; e os cards da Qwen3.5 já destacam modos de serving com foco em eficiência e modo text-only/language-model-only para liberar memória para KV cache. (GitHub)


Plano de Implementação — LocalLLM Inference Engine

1. Objetivo do projeto

Construir um backend local de inferência para LLMs com foco em:

  • TPS alto
  • latência baixa
  • uso agressivamente otimizado de VRAM/RAM
  • boa qualidade prática em modelos pequenos e médios
  • arquitetura modular
  • compatibilidade com API estilo OpenAI

Meta prática inicial

O alvo não deve ser “suportar tudo”. O alvo inicial deve ser:

  • rodar Qwen3.5-4B como perfil principal de alto desempenho
  • rodar Qwen3.5-9B como perfil balanceado
  • extrair o máximo da RTX 3070 8 GB
  • degradar bem para CPU/hardware mais fraco
  • manter o sistema preparado para expansão futura

A linha Qwen3 densa aberta originalmente trouxe variantes como 4B e 8B; já a linha Qwen3.5 aberta em 2026 inclui, entre outras, 4B e 9B, o que bate com o foco que você quer. (Qwen)


2. Ajustes que eu faria no plano original

2.1. Fazer a V1 em texto puro

Mesmo que alguns modelos Qwen3.5 tenham componentes multimodais, o seu objetivo é TPS alto em inferência textual. Então a primeira versão deve ser:

  • text-only
  • single-model ativo
  • single runtime path
  • sem visão
  • sem embeddings
  • sem tool-calling complexo na engine

Os próprios materiais da Qwen3.5 destacam modo text-only / language-model-only justamente para liberar memória e permitir mais KV cache. (Hugging Face)

2.2. Focar em throughput real, não em generalidade cedo demais

No plano-base, a arquitetura em camadas está boa. O que eu mudaria é a ordem de implementação:

  1. engine local minimalista e muito rápida
  2. benchmark e tuning
  3. API compatível
  4. registry/download
  5. multi-request throughput
  6. recursos avançados

2.3. Adotar llama.cpp como engine principal da V1

Para o seu caso, isso continua sendo a melhor decisão do plano original. O llama.cpp já é otimizado para uma variedade grande de hardware e hoje expõe exatamente os knobs que mais importam para seu objetivo: batching contínuo, flash attention, prompt cache, speculative decoding e tuning de KV cache. (GitHub)

2.4. Manter Rust no hot path

Essa parte do plano original continua correta: Rust + bindgen + Axum/Tokio no hot path, e Python só no caminho frio de registry/Hugging Face.


3. Stack final recomendada

3.1. Core de inferência

  • C/C++
  • llama.cpp compilado como shared library
  • backend CUDA para NVIDIA

3.2. Hot path do servidor

  • Rust
  • tokio
  • axum
  • hyper
  • bindgen

3.3. Registry de modelos

  • Python 3.11+
  • huggingface_hub
  • processo separado
  • IPC local

3.4. UI de teste

  • HTML simples
  • SSE nativo
  • sem framework no começo

Isso está alinhado com o documento-base enviado.


4. Diretriz de performance para a RTX 3070

A RTX 3070 tem 8 GB de VRAM e compute capability 8.6. Para esse projeto, isso significa que a build CUDA do llama.cpp deve ser tratada como prioridade e vale compilar direcionando a arquitetura CUDA da placa. (NVIDIA)

Build recomendada da engine

  • -DGGML_CUDA=ON
  • -DCMAKE_CUDA_ARCHITECTURES=86

Isso ajuda a evitar build genérica demais para a placa. (GitHub)

Consequência prática

Com 8 GB, você não deve desenhar a V1 pensando em:

  • contexto gigantesco por padrão
  • vários modelos carregados ao mesmo tempo
  • multimodal cedo
  • concorrência alta por processo logo no início

Você deve desenhar a V1 pensando em:

  • 1 modelo ativo por vez
  • contexto moderado
  • alto aproveitamento de GPU
  • prompt cache
  • continuous batching
  • KV cache otimizado
  • flash attention

Etapas de implementação

Etapa 0 — Definição do alvo técnico

Objetivo

Definir o recorte exato da V1 para não desperdiçar tempo com features que não aumentam TPS.

Decisões

  • V1 será texto puro

  • V1 terá um modelo carregado por vez

  • V1 vai priorizar:

    • Qwen3.5-4B como modo turbo
    • Qwen3.5-9B como modo balanceado
  • API no estilo OpenAI desde cedo

  • multi-model simultâneo fica para depois

Entregáveis

  • documento de metas
  • perfis de execução
  • critérios de benchmark

Critério de pronto

Você consegue responder:

  • qual modelo será o “modelo de desenvolvimento”
  • qual contexto padrão será usado
  • qual métrica manda no projeto: TPS, latência ou concorrência

Minha recomendação

Use como alvo inicial:

  • Qwen3.5-4B para desenvolvimento e tuning pesado
  • Qwen3.5-9B só depois que a 4B estiver extremamente bem afinada

Etapa 1 — Foundation do workspace

Objetivo

Criar a base do projeto com a estrutura modular correta desde o início.

Estrutura recomendada

localllm/
├── Cargo.toml
├── crates/
│   ├── llama-sys/
│   ├── inference-core/
│   ├── inference-server/
│   ├── api-contracts/
│   └── metrics/
├── registry/
│   ├── service.py
│   └── requirements.txt
├── ui/
│   └── index.html
├── vendor/
│   └── llama.cpp/
└── models/

Essa separação deriva da estrutura do plano-base, com ajuste pequeno de nomes para ficar mais clara para crescimento futuro. O documento-base já sugeria workspace Rust, crate de bindings, pool de inferência, servidor API, registry Python, UI e vendor/llama.cpp fixado como submódulo.

Decisões críticas

  • travar o llama.cpp em commit fixo
  • não depender de versão flutuante
  • build reproduzível

Critério de pronto

  • projeto compila vazio
  • submodule do llama.cpp fixado
  • layout estável

Etapa 2 — Build da engine CUDA

Objetivo

Compilar o llama.cpp corretamente para sua GPU e validar carregamento básico.

O que implementar

  • build CMake do llama.cpp
  • geração da shared library
  • smoke test local
  • script de build dev/prod

Recomendação

Ter dois modos:

  • dev build
  • native optimized build

Critério de pronto

  • shared library gerada
  • modelo GGUF carregando
  • primeiro token gerado localmente

Ajuste importante

Nesta fase, não implemente API ainda. Primeiro prove que o core gera tokens rápido.


Etapa 3 — FFI Rust com bindgen

Objetivo

Criar o binding Rust ↔ llama.cpp sem overhead no runtime.

O plano-base já propõe exatamente isso com build.rs + bindgen, e essa continua sendo a decisão correta.

O que implementar

  • crate llama-sys

  • build.rs

  • wrappers mínimos e seguros

  • tipos Rust para:

    • model
    • context
    • sampler
    • token stream

Regras desta etapa

  • wrapper fino
  • sem abstrações demais
  • sem inventar engine paralela antes da hora

Critério de pronto

  • Rust consegue:

    • abrir modelo
    • tokenizar prompt
    • chamar decode
    • amostrar próximo token
    • encerrar limpo

Etapa 4 — Engine local minimalista, single-model

Objetivo

Ter um runtime local realmente rápido antes de colocar rede na frente.

O que implementar

  • InferenceEngine
  • LoadedModel
  • GenerationSession
  • SamplerConfig
  • RuntimeProfile

Regras

  • 1 modelo carregado
  • 1 caminho de execução
  • sem hot-swap automático ainda
  • sem múltiplos engines

Perfis que eu criaria

  • turbo_4b
  • balanced_9b
  • cpu_fallback

Critério de pronto

  • prompt entra
  • tokens saem
  • TPS é medido
  • memória é medida
  • warmup existe

Otimizações obrigatórias nesta etapa

  • warmup de engine
  • reuse de buffers
  • evitar alocações em loop
  • pré-tokenização de system prompt
  • prompt cache quando aplicável

O llama.cpp já suporta prompt caching e serving server-side com opções voltadas para esse tipo de fluxo. (GitHub)


Etapa 5 — Perf tuning de geração pura

Objetivo

Ajustar o runtime até ele realmente ficar forte na 3070.

O que testar

  • n_gpu_layers
  • n_ctx
  • n_batch
  • ubatch
  • n_threads
  • flash attention
  • tipo do KV cache
  • quantização do modelo

O documento-base já previa expor n_gpu_layers, n_ctx, n_threads e n_batch. Hoje, além disso, o llama.cpp já documenta --flash-attn, --batch-size, --ubatch-size e tipos de KV cache quantizado. (GitHub)

Ordem de tuning recomendada

  1. acertar quantização do modelo
  2. acertar n_gpu_layers
  3. acertar n_ctx
  4. ativar flash attention
  5. testar KV cache mais leve
  6. testar batch e ubatch
  7. só depois testar concorrência

Critério de pronto

Você tem uma tabela interna com:

  • TPS
  • latência até primeiro token
  • VRAM usada
  • RAM usada
  • qualidade percebida

para pelo menos:

  • Qwen3.5-4B
  • Qwen3.5-9B

Minha recomendação prática

Para o seu foco, eu usaria:

  • 4B como perfil padrão para “TPS máximo”
  • 9B como perfil “qualidade máxima viável”

E faria os testes em quantizações médias antes de subir demais. Isso não é regra fixa do modelo; é estratégia de engenharia para sua GPU.


Etapa 6 — API local OpenAI-compatible

Objetivo

Colocar a engine atrás de uma API limpa sem destruir a performance.

O plano-base já propõe Axum + SSE + contratos estilo OpenAI, o que continua certo.

Endpoints iniciais

  • POST /v1/chat/completions
  • GET /v1/models
  • GET /health

O que deixar de fora no início

  • completions legadas se não forem necessárias
  • embeddings
  • files
  • assistants
  • websocket

Por que SSE

O plano-base justifica bem SSE como fluxo unidirecional simples. Isso é compatível com seu foco e mantém o streaming barato.

Critério de pronto

  • request JSON entra
  • stream de tokens sai em SSE
  • formato é previsível
  • métrica de TPS vai na resposta final

Etapa 7 — Scheduler de inferência e concorrência controlada

Objetivo

Sair do single-request puro para throughput real multi-request.

O documento-base já propõe um pool com semáforo e contexts dedicados. Eu manteria a ideia, mas implementaria depois da engine single-model estar muito boa.

O que implementar

  • fila de requests
  • limite de concorrência
  • reutilização de contexts
  • backpressure
  • cancelamento

Regra importante

Não escale concorrência só porque dá. Na 3070, concorrência excessiva pode reduzir TPS total por request, aumentar jitter e piorar latência.

Estratégia correta

Ter perfis:

  • low-latency mode
  • high-throughput mode

Critério de pronto

  • 2 a N requests simultâneos
  • servidor não explode memória
  • total de TPS agregado aumenta ou pelo menos não degrada absurdamente

Observação importante

O llama.cpp server já suporta parallel requests e continuous batching, então vale estudar a arquitetura dele como referência prática antes de reinventar o scheduler inteiro. (GitHub)


Etapa 8 — Continuous batching

Objetivo

Aumentar throughput agregado de verdade.

O que implementar

  • agrupamento de requests compatíveis
  • união de prompts em janela de execução
  • step scheduler
  • contabilidade de tokens por request

Por que isso importa

Para o seu foco, continuous batching provavelmente entrega mais ganho real do que sair adicionando feature de produto.

O llama.cpp já documenta serving com continuous batching. (GitHub)

Critério de pronto

  • requests curtos simultâneos têm ganho agregado
  • scheduler não mata a latência de requests isolados

Etapa 9 — KV cache tuning

Objetivo

Aumentar capacidade útil e throughput sem depender apenas de VRAM bruta.

O que implementar

  • opção de tipo de KV cache
  • presets de contexto
  • política de truncamento inteligente
  • medição de impacto em qualidade

O llama.cpp já suporta tipos de KV cache quantizado para K/V, com defaults em f16 e alternativas mais leves. (GitHub)

Estratégia

No seu caso, isso é especialmente importante porque:

  • a 3070 tem só 8 GB
  • Qwen3.5-9B pode ficar apertado dependendo da quantização e do contexto
  • texto puro com KV cache otimizado pode render muito mais do que tentar “empurrar tudo” na VRAM

Critério de pronto

  • presets claros:

    • curto
    • médio
    • longo
  • comparação objetiva entre qualidade e desempenho


Etapa 10 — Speculative decoding / draft model

Objetivo

Buscar o próximo salto de TPS.

O llama.cpp já documenta suporte a speculative decoding com modelo draft. A Qwen3.5 também já cita exemplos de serving com foco em MTP/eficiência. (GitHub)

Quando fazer

Só depois que:

  • engine base estiver estável
  • batching estiver bom
  • cache estiver afinado

O que implementar

  • modelo principal

  • modelo draft pequeno

  • política de aceitação/rejeição

  • benchmark comparando:

    • TPS
    • latência
    • uso extra de VRAM/RAM

Observação

Essa etapa pode dar ganhos grandes, mas também pode complicar demais cedo. Ela é etapa de turbo, não de fundação.


Etapa 11 — Registry de modelos

Objetivo

Adicionar busca, download e metadata sem poluir o hot path.

O plano-base está correto em jogar isso para um processo Python separado com huggingface_hub, IPC local e atualização de metadata.

O que implementar

  • POST /registry/search
  • POST /registry/download
  • GET /registry/models

Regra de arquitetura

Nada de Python no caminho de geração de tokens.

Critério de pronto

  • busca GGUF
  • download resumível
  • checksum
  • atualização do índice local

Etapa 12 — Observabilidade e benchmark suite

Objetivo

Transformar tuning em processo técnico e não em chute.

O que medir

  • TTFT
  • TPS
  • tokens/s agregados
  • VRAM
  • RAM
  • filas
  • taxa de cancelamento
  • throughput por perfil

O que implementar

  • endpoint de métricas
  • logs estruturados
  • benchmark CLI
  • benchmark replayável

Critério de pronto

Você consegue responder com dados:

  • qual quantização é a melhor na sua máquina
  • qual contexto é o limite bom
  • quando 9B vale a pena sobre 4B
  • quando speculative decoding compensa

Etapa 13 — UI mínima de teste

Objetivo

Ter um frontend simples para validar streaming, sem virar foco do projeto.

O plano-base já propõe uma UI HTML estática simples, servida pelo próprio servidor. Para a V1, isso é o ideal.

O que implementar

  • seletor de perfil
  • chat simples
  • toggle stream
  • exibição de TPS
  • exibição de uso de memória
  • log de benchmark rápido

Critério de pronto

A UI só existe para testar o backend. Nada além disso.


Perfis recomendados para sua V1

Perfil A — Turbo

Modelo

Qwen3.5-4B

Objetivo

Máximo TPS com boa qualidade prática.

Uso ideal

  • chat geral
  • coding leve a moderado
  • agentes rápidos
  • máquinas mais fracas

Estratégia

  • quantização equilibrada
  • contexto moderado
  • text-only
  • flash attention
  • prompt cache
  • batching leve

Os cards atuais da Qwen3.5-4B mostram suporte a serving moderno e reforçam o modo text-only para eficiência de memória. (Hugging Face)


Perfil B — Balanceado

Modelo

Qwen3.5-9B

Objetivo

Subir qualidade sem matar completamente a velocidade.

Uso ideal

  • tarefas mais difíceis
  • respostas mais consistentes
  • coding mais pesado
  • raciocínio melhor que 4B

Estratégia

  • contexto ainda controlado
  • quantização mais conservadora que no 4B
  • tuning fino de n_gpu_layers
  • KV cache otimizado
  • talvez speculative decoding no futuro

Os cards públicos atuais da linha Qwen3.5 incluem 9B e destacam arquitetura voltada a serving eficiente. (Hugging Face)


O que eu removeria da V1

Não colocaria agora

  • multimodal
  • vários modelos carregados ao mesmo tempo
  • websocket
  • embeddings
  • reranking
  • LoRA dinâmica
  • quantização on-the-fly
  • scheduler super complexo antes do benchmark base
  • UI sofisticada

Motivo

Nada disso é o principal motor de TPS para o seu alvo.


Ordem exata que eu seguiria

Fase 1

Build CUDA + llama.cpp + primeiro token

Fase 2

Bindings Rust + runtime local single-model

Fase 3

Benchmark bruto de Qwen3.5-4B

Fase 4

Tuning pesado:

  • n_gpu_layers
  • n_ctx
  • n_batch
  • ubatch
  • flash attention
  • KV cache

Fase 5

API OpenAI-compatible com SSE

Fase 6

Fila, scheduler e concorrência controlada

Fase 7

Continuous batching

Fase 8

Qwen3.5-9B balanceado

Fase 9

Registry Python / HF Hub

Fase 10

Speculative decoding e turbo mode


Conclusão técnica

Seu plano-base está bom na arquitetura macro: Rust no hot path, llama.cpp como core, Python só no registry, SSE/OpenAI-compatible, IPC local e workspace modular.

A principal melhoria para o seu objetivo é esta:

transformar o projeto, na V1, de “infraestrutura genérica de inferência local” para “runtime de altíssimo desempenho para modelos textuais pequenos e médios”.

Isso significa:

  • começar por Qwen3.5-4B

  • tratar Qwen3.5-9B como segundo alvo

  • usar RTX 3070 como orçamento real de VRAM

  • explorar cedo:

    • flash attention
    • continuous batching
    • prompt caching
    • KV cache tuning
    • speculative decoding depois

Essa é a rota com maior chance de entregar o que você quer de verdade: TPS alto sem desandar qualidade. Base original: Ajustes de foco e recursos atuais do ecossistema: (GitHub)

Posso converter isso em seguida para um README.md de projeto, já no formato de especificação técnica pronta para repositório.