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:
- engine local minimalista e muito rápida
- benchmark e tuning
- API compatível
- registry/download
- multi-request throughput
- 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
tokioaxumhyperbindgen
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.cppem commit fixo - não depender de versão flutuante
- build reproduzível
Critério de pronto
- projeto compila vazio
- submodule do
llama.cppfixado - 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
InferenceEngineLoadedModelGenerationSessionSamplerConfigRuntimeProfile
Regras
- 1 modelo carregado
- 1 caminho de execução
- sem hot-swap automático ainda
- sem múltiplos engines
Perfis que eu criaria
turbo_4bbalanced_9bcpu_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_layersn_ctxn_batchubatchn_threadsflash 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
- acertar quantização do modelo
- acertar
n_gpu_layers - acertar
n_ctx - ativar
flash attention - testar KV cache mais leve
- testar batch e ubatch
- 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/completionsGET /v1/modelsGET /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/searchPOST /registry/downloadGET /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_layersn_ctxn_batchubatch- 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.