Fine-tuning de um modelo open-source pequeno (Gemma 2 2B, Llama 3.2 1B/3B, Phi-3 mini) para um domínio específico (ex.: suporte técnico de um produto) e uma aplicação de chat 100% local escrita em C#.
A ideia tem duas metades, com responsabilidades bem separadas:
| Parte | Linguagem | Onde roda | Papel |
|---|---|---|---|
| Treino / Fine-tuning | Python (QLoRA) | GPU consumer | Adapta o modelo base ao domínio |
| Aplicação de chat | C# / .NET 10 | CPU ou GPU local | Conversa, baixa modelos, inspeciona dados |
A fronteira entre as duas é o arquivo .gguf (modelo quantizado). O Python
treina e exporta; o C# carrega e conversa — sem depender de nenhuma API externa.
App de console (terminal), interativo, com três funções:
.gguf local..jsonl em data/ (contagem + prévia em tabela).Há também uma UI web (Blazor Server) com as mesmas três funções — veja
Interface Web. A análise que motivou essa escolha está
em docs/blazor-analysis.md.
Mini-LLM-Local-C-Sharp/
├── MiniLlmLocal.sln
├── src/
│ ├── MiniLlmLocal.Core/ # Biblioteca: motor de chat, catálogo/download, leitura de dataset
│ │ ├── Chat/ # IChatEngine, LlamaChatEngine (LLamaSharp), opções
│ │ ├── Models/ # ModelInfo, ModelCatalog, ModelDownloader, LocalModelStore
│ │ ├── Data/ # TrainingExample, DatasetReader (JSONL)
│ │ └── AppPaths.cs # Descobre models/ e data/ a partir da raiz do repo
│ ├── MiniLlmLocal.Console/ # App de teste em terminal (Spectre.Console)
│ │ └── Menus/ # ChatMenu, ModelsMenu, DataMenu
│ └── MiniLlmLocal.Web/ # UI web (Blazor Server) — mesmas funções
│ ├── Components/Pages/ # Chat, Models, Data, Home
│ └── Services/ # ChatSessionState (engine por circuito)
├── tests/
│ └── MiniLlmLocal.Tests/ # xUnit (catálogo, dataset, store, progresso)
├── training/ # Python: QLoRA + conversão para GGUF
│ ├── requirements.txt
│ ├── finetune.py
│ └── convert_to_gguf.py
├── data/
│ └── support-dataset.jsonl # Dataset de exemplo (suporte técnico, pt-BR)
├── models/ # (.gguf ficam aqui — ignorado pelo Git)
└── docs/
└── blazor-analysis.md
Pré-requisitos: .NET 10 SDK.
# 1. Compilar e testar
dotnet build
dotnet test
# 2. Rodar o app de teste
dotnet run --project src/MiniLlmLocal.Console
No app:
support-dataset.jsonl para ver os exemplos.Dentro do chat: /reset limpa o histórico, /sair volta ao menu.
O primeiro download e o primeiro carregamento podem levar alguns segundos/minutos. A inferência em CPU funciona, mas é mais lenta — veja GPU abaixo.
Além do console, há uma UI web em Blazor Server (MiniLlmLocal.Web) que
reaproveita 100% do MiniLlmLocal.Core, com páginas de Chat (streaming),
Modelos (download com barra de progresso) e Dados.
dotnet run --project src/MiniLlmLocal.Web
Abra a URL exibida no terminal (ex.: https://localhost:7xxx).
Detalhes de arquitetura (por que Server e não WebAssembly, gestão de sessão do
LLamaContext, etc.) estão em docs/blazor-analysis.md.
Segurança: o app serve por padrão em
localhost. Não exponha à rede sem autenticação — qualquer um usaria sua GPU/CPU e leria seus dados.
Por padrão o MiniLlmLocal.Console referencia o backend de CPU do LLamaSharp
(LLamaSharp.Backend.Cpu), que funciona em qualquer máquina.
Para acelerar com GPU NVIDIA, troque o backend no
src/MiniLlmLocal.Console/MiniLlmLocal.Console.csproj:
<!-- Remova o backend de CPU e adicione o de CUDA 12: -->
<PackageReference Include="LLamaSharp.Backend.Cuda12.Windows" Version="0.27.0" />
E aumente GpuLayerCount (em ChatEngineOptions, padrão 0 = só CPU) para um
valor alto (ex.: 999) para descarregar o modelo inteiro na GPU. Há também
backends para Vulkan/OpenCL no NuGet do LLamaSharp.
O fluxo completo: dataset → QLoRA → merge → GGUF → app C#.
Esta é a confusão mais comum. O fine-tuning não pega o .gguf que você usa no
Chat e "injeta" os dados nele. Ele faz outra coisa:
google/gemma-2-2b-it)
— repositório oficial, não um arquivo .gguf..gguf NOVO e separado (ex.: meu-modelo-suporte-Q4_K_M.gguf).O modelo que você já tinha em models/ continua intacto. Depois do treino
você passa a ter dois arquivos, e escolhe qual carregar no Chat:
models/
├── gemma-2-2b-it-Q4_K_M.gguf ← base "genérico" (baixado pelo app)
└── meu-modelo-suporte-Q4_K_M.gguf ← NOVO: treinado nos seus dados ← selecione este
Por que não dá para "treinar no .gguf carregado"?
.gguf é um formato quantizado, otimizado só para inferência (rodar rápido),
não para treino.float16, que vivem no
repositório do Hugging Face — por isso começamos do modelo base, não do .gguf..gguf novo →
carrega no Chat.Fluxo correto, visualmente:
google/gemma-2-2b-it → QLoRA com seus dados → .gguf NOVO → Chat carrega o novo
(base, no HF/nuvem) (Colab ou GPU local) (em models/) (e responde no seu domínio)
No notebook e nos scripts, a variável/flag
BASE_MODEL/--base-modeldeve apontar para o repositório base do Hugging Face (ex.:google/gemma-2-2b-it), nunca para um arquivo.gguf. É esse base que é treinado.
E os "Dados" do app? A aba 📊 Dados serve só para inspecionar o
.jsonl; ela não treina nada nem alimenta o Chat em tempo de execução. Os dados só "entram" no modelo através do treino descrito aqui.
Sem GPU potente? QLoRA precisa de ~6–8 GB de VRAM. Use o notebook pronto
training/colab_finetune.ipynbno Google Colab (GPU T4 16 GB grátis). As instruções manuais mais abaixo valem para treino em GPU local.
O notebook fica em training/colab_finetune.ipynb. Para executá-lo:
Arquivo → Abrir notebook → aba GitHub, cole
https://github.com/ricardosmdemello/Mini-LLM-Local-C-Sharp e selecione
training/colab_finetune.ipynb; ouArquivo → Fazer upload de notebook e escolha o arquivo
training/colab_finetune.ipynb do seu computador.Ambiente de execução → Alterar o tipo de ambiente de execução → GPU (T4).Shift+Enter em cada uma, ou
Ambiente de execução → Executar tudo). O notebook clona o repo, instala as
dependências, treina, converte para GGUF e oferece o download do .gguf..gguf baixado na pasta models/ do projeto local — ele
aparece automaticamente no Chat (console e web).Dica: edite a variável
BASE_MODELna célula 4 para trocar o modelo (google/gemma-2-2b-it,meta-llama/Llama-3.2-1B-Instruct,microsoft/Phi-3-mini-4k-instruct).
cd training
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Instale o PyTorch com CUDA conforme sua GPU (https://pytorch.org), depois:
pip install -r requirements.txt
Para modelos gated (Gemma, Llama oficiais): huggingface-cli login.
python finetune.py \
--base-model google/gemma-2-2b-it \
--dataset ../data/support-dataset.jsonl \
--output-dir ./output/gemma-2-2b-suporte \
--merge
Isso treina apenas adaptadores LoRA (cabe em ~6–8 GB de VRAM) e, com --merge,
gera o modelo mesclado em output/.../merged.
Requer o llama.cpp compilado (instruções
no topo de convert_to_gguf.py):
export LLAMA_CPP_DIR=/caminho/para/llama.cpp
python convert_to_gguf.py \
--model-dir ./output/gemma-2-2b-suporte/merged \
--outfile ../models/gemma-2-2b-suporte-Q4_K_M.gguf \
--quantize Q4_K_M
O .gguf cai em models/ e aparece automaticamente na lista de modelos do app C#.
JSONL (um objeto JSON por linha), estilo instruction tuning:
{"instruction": "Você é o suporte técnico... Responda à dúvida.", "input": "Minha internet está lenta.", "output": "Vamos diagnosticar: 1. Reinicie..."}
instruction — a tarefa / persona.input — contexto opcional (a mensagem do cliente). Pode ser "".output — a resposta desejada.O mesmo formato é lido pelo DatasetReader (C#, para inspeção) e pelo
finetune.py (Python, para treino).
LlamaChatEngine usa a ChatSession padrão do
LLamaSharp. Para máxima fidelidade por família de modelo, considere aplicar o
chat template oficial. Os AntiPrompts já cobrem os marcadores de fim de
turno de Gemma (<end_of_turn>), Llama 3 (<|eot_id|>) e Phi-3 (<|end|>)..gguf/.safetensors são grandes e
ficam fora do Git (veja .gitignore).transformers, peft, trl, bitsandbytes (QLoRA)Fine-tuning de um modelo open-source pequeno (Gemma 2 2B, Llama 3.2 1B/3B, Phi-3 mini) para um domínio específico (ex.: suporte técnico de um produto) e uma aplicação de chat 100% local escrita em C#.
A ideia tem duas metades, com responsabilidades bem separadas:
| Parte | Linguagem | Onde roda | Papel |
|---|---|---|---|
| Treino / Fine-tuning | Python (QLoRA) | GPU consumer | Adapta o modelo base ao domínio |
| Aplicação de chat | C# / .NET 10 | CPU ou GPU local | Conversa, baixa modelos, inspeciona dados |
A fronteira entre as duas é o arquivo .gguf (modelo quantizado). O Python
treina e exporta; o C# carrega e conversa — sem depender de nenhuma API externa.
App de console (terminal), interativo, com três funções:
.gguf local..jsonl em data/ (contagem + prévia em tabela).Há também uma UI web (Blazor Server) com as mesmas três funções — veja
Interface Web. A análise que motivou essa escolha está
em docs/blazor-analysis.md.
Mini-LLM-Local-C-Sharp/
├── MiniLlmLocal.sln
├── src/
│ ├── MiniLlmLocal.Core/ # Biblioteca: motor de chat, catálogo/download, leitura de dataset
│ │ ├── Chat/ # IChatEngine, LlamaChatEngine (LLamaSharp), opções
│ │ ├── Models/ # ModelInfo, ModelCatalog, ModelDownloader, LocalModelStore
│ │ ├── Data/ # TrainingExample, DatasetReader (JSONL)
│ │ └── AppPaths.cs # Descobre models/ e data/ a partir da raiz do repo
│ ├── MiniLlmLocal.Console/ # App de teste em terminal (Spectre.Console)
│ │ └── Menus/ # ChatMenu, ModelsMenu, DataMenu
│ └── MiniLlmLocal.Web/ # UI web (Blazor Server) — mesmas funções
│ ├── Components/Pages/ # Chat, Models, Data, Home
│ └── Services/ # ChatSessionState (engine por circuito)
├── tests/
│ └── MiniLlmLocal.Tests/ # xUnit (catálogo, dataset, store, progresso)
├── training/ # Python: QLoRA + conversão para GGUF
│ ├── requirements.txt
│ ├── finetune.py
│ └── convert_to_gguf.py
├── data/
│ └── support-dataset.jsonl # Dataset de exemplo (suporte técnico, pt-BR)
├── models/ # (.gguf ficam aqui — ignorado pelo Git)
└── docs/
└── blazor-analysis.md
Pré-requisitos: .NET 10 SDK.
# 1. Compilar e testar
dotnet build
dotnet test
# 2. Rodar o app de teste
dotnet run --project src/MiniLlmLocal.Console
No app:
support-dataset.jsonl para ver os exemplos.Dentro do chat: /reset limpa o histórico, /sair volta ao menu.
O primeiro download e o primeiro carregamento podem levar alguns segundos/minutos. A inferência em CPU funciona, mas é mais lenta — veja GPU abaixo.
Além do console, há uma UI web em Blazor Server (MiniLlmLocal.Web) que
reaproveita 100% do MiniLlmLocal.Core, com páginas de Chat (streaming),
Modelos (download com barra de progresso) e Dados.
dotnet run --project src/MiniLlmLocal.Web
Abra a URL exibida no terminal (ex.: https://localhost:7xxx).
Detalhes de arquitetura (por que Server e não WebAssembly, gestão de sessão do
LLamaContext, etc.) estão em docs/blazor-analysis.md.
Segurança: o app serve por padrão em
localhost. Não exponha à rede sem autenticação — qualquer um usaria sua GPU/CPU e leria seus dados.
Por padrão o MiniLlmLocal.Console referencia o backend de CPU do LLamaSharp
(LLamaSharp.Backend.Cpu), que funciona em qualquer máquina.
Para acelerar com GPU NVIDIA, troque o backend no
src/MiniLlmLocal.Console/MiniLlmLocal.Console.csproj:
<!-- Remova o backend de CPU e adicione o de CUDA 12: -->
<PackageReference Include="LLamaSharp.Backend.Cuda12.Windows" Version="0.27.0" />
E aumente GpuLayerCount (em ChatEngineOptions, padrão 0 = só CPU) para um
valor alto (ex.: 999) para descarregar o modelo inteiro na GPU. Há também
backends para Vulkan/OpenCL no NuGet do LLamaSharp.
O fluxo completo: dataset → QLoRA → merge → GGUF → app C#.
Esta é a confusão mais comum. O fine-tuning não pega o .gguf que você usa no
Chat e "injeta" os dados nele. Ele faz outra coisa:
google/gemma-2-2b-it)
— repositório oficial, não um arquivo .gguf..gguf NOVO e separado (ex.: meu-modelo-suporte-Q4_K_M.gguf).O modelo que você já tinha em models/ continua intacto. Depois do treino
você passa a ter dois arquivos, e escolhe qual carregar no Chat:
models/
├── gemma-2-2b-it-Q4_K_M.gguf ← base "genérico" (baixado pelo app)
└── meu-modelo-suporte-Q4_K_M.gguf ← NOVO: treinado nos seus dados ← selecione este
Por que não dá para "treinar no .gguf carregado"?
.gguf é um formato quantizado, otimizado só para inferência (rodar rápido),
não para treino.float16, que vivem no
repositório do Hugging Face — por isso começamos do modelo base, não do .gguf..gguf novo →
carrega no Chat.Fluxo correto, visualmente:
google/gemma-2-2b-it → QLoRA com seus dados → .gguf NOVO → Chat carrega o novo
(base, no HF/nuvem) (Colab ou GPU local) (em models/) (e responde no seu domínio)
No notebook e nos scripts, a variável/flag
BASE_MODEL/--base-modeldeve apontar para o repositório base do Hugging Face (ex.:google/gemma-2-2b-it), nunca para um arquivo.gguf. É esse base que é treinado.
E os "Dados" do app? A aba 📊 Dados serve só para inspecionar o
.jsonl; ela não treina nada nem alimenta o Chat em tempo de execução. Os dados só "entram" no modelo através do treino descrito aqui.
Sem GPU potente? QLoRA precisa de ~6–8 GB de VRAM. Use o notebook pronto
training/colab_finetune.ipynbno Google Colab (GPU T4 16 GB grátis). As instruções manuais mais abaixo valem para treino em GPU local.
O notebook fica em training/colab_finetune.ipynb. Para executá-lo:
Arquivo → Abrir notebook → aba GitHub, cole
https://github.com/ricardosmdemello/Mini-LLM-Local-C-Sharp e selecione
training/colab_finetune.ipynb; ouArquivo → Fazer upload de notebook e escolha o arquivo
training/colab_finetune.ipynb do seu computador.Ambiente de execução → Alterar o tipo de ambiente de execução → GPU (T4).Shift+Enter em cada uma, ou
Ambiente de execução → Executar tudo). O notebook clona o repo, instala as
dependências, treina, converte para GGUF e oferece o download do .gguf..gguf baixado na pasta models/ do projeto local — ele
aparece automaticamente no Chat (console e web).Dica: edite a variável
BASE_MODELna célula 4 para trocar o modelo (google/gemma-2-2b-it,meta-llama/Llama-3.2-1B-Instruct,microsoft/Phi-3-mini-4k-instruct).
cd training
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Instale o PyTorch com CUDA conforme sua GPU (https://pytorch.org), depois:
pip install -r requirements.txt
Para modelos gated (Gemma, Llama oficiais): huggingface-cli login.
python finetune.py \
--base-model google/gemma-2-2b-it \
--dataset ../data/support-dataset.jsonl \
--output-dir ./output/gemma-2-2b-suporte \
--merge
Isso treina apenas adaptadores LoRA (cabe em ~6–8 GB de VRAM) e, com --merge,
gera o modelo mesclado em output/.../merged.
Requer o llama.cpp compilado (instruções
no topo de convert_to_gguf.py):
export LLAMA_CPP_DIR=/caminho/para/llama.cpp
python convert_to_gguf.py \
--model-dir ./output/gemma-2-2b-suporte/merged \
--outfile ../models/gemma-2-2b-suporte-Q4_K_M.gguf \
--quantize Q4_K_M
O .gguf cai em models/ e aparece automaticamente na lista de modelos do app C#.
JSONL (um objeto JSON por linha), estilo instruction tuning:
{"instruction": "Você é o suporte técnico... Responda à dúvida.", "input": "Minha internet está lenta.", "output": "Vamos diagnosticar: 1. Reinicie..."}
instruction — a tarefa / persona.input — contexto opcional (a mensagem do cliente). Pode ser "".output — a resposta desejada.O mesmo formato é lido pelo DatasetReader (C#, para inspeção) e pelo
finetune.py (Python, para treino).
LlamaChatEngine usa a ChatSession padrão do
LLamaSharp. Para máxima fidelidade por família de modelo, considere aplicar o
chat template oficial. Os AntiPrompts já cobrem os marcadores de fim de
turno de Gemma (<end_of_turn>), Llama 3 (<|eot_id|>) e Phi-3 (<|end|>)..gguf/.safetensors são grandes e
ficam fora do Git (veja .gitignore).transformers, peft, trl, bitsandbytes (QLoRA)