ricardosmdemello/Mini-LLM-Local-C-Sharp

C#

0

0 commits

updated Jun 17, 2026

See the code

README

Mini LLM Local (C# + .NET 10)

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:

ParteLinguagemOnde rodaPapel
Treino / Fine-tuningPython (QLoRA)GPU consumerAdapta o modelo base ao domínio
Aplicação de chatC# / .NET 10CPU ou GPU localConversa, 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.


✨ O que o app de teste faz

App de console (terminal), interativo, com três funções:

  • 💬 Conversar com a IA — chat com streaming token a token sobre um modelo .gguf local.
  • 📦 Modelos — lista os modelos baixados e baixa novos do catálogo (Gemma/Llama/Phi) com barra de progresso.
  • 📊 Ver dados de treino — inspeciona os datasets .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.


🧱 Estrutura do repositório

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

🚀 Começando rápido (só o app C#)

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:

  1. Vá em 📦 Modelos → Baixar modelo do catálogo e baixe, por exemplo, o Gemma 2 2B Instruct (~1,7 GB).
  2. Volte e escolha 💬 Conversar com a IA, selecione o modelo e converse.
  3. Em 📊 Ver dados de treino, abra o 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.


🌐 Interface Web (Blazor)

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.

🖥️ CPU vs. GPU (inferência no C#)

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.


🎓 Fine-tuning (Python, GPU)

O fluxo completo: dataset → QLoRA → merge → GGUF → app C#.

⚠️ Entenda antes: o treino não altera o modelo carregado no Chat

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:

  1. Baixa o modelo base original do Hugging Face (ex.: google/gemma-2-2b-it) — repositório oficial, não um arquivo .gguf.
  2. Treina esse base nos seus dados com QLoRA (em Python/GPU).
  3. Gera um arquivo .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"?

  • O .gguf é um formato quantizado, otimizado só para inferência (rodar rápido), não para treino.
  • O treino acontece sobre os pesos originais em float16, que vivem no repositório do Hugging Face — por isso começamos do modelo base, não do .gguf.
  • Resultado: o caminho é sempre base (HF) → treina → exporta .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-model deve 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.

Como executar o treino

Sem GPU potente? QLoRA precisa de ~6–8 GB de VRAM. Use o notebook pronto training/colab_finetune.ipynb no Google Colab (GPU T4 16 GB grátis). As instruções manuais mais abaixo valem para treino em GPU local.

Opção A — Treinar no Google Colab (recomendado, sem GPU local)

O notebook fica em training/colab_finetune.ipynb. Para executá-lo:

  1. Acesse colab.research.google.com.
  2. Abra o notebook de uma destas formas:
    • Direto do GitHub: Arquivo → Abrir notebook → aba GitHub, cole https://github.com/ricardosmdemello/Mini-LLM-Local-C-Sharp e selecione training/colab_finetune.ipynb; ou
    • Upload: Arquivo → Fazer upload de notebook e escolha o arquivo training/colab_finetune.ipynb do seu computador.
  3. Ative a GPU: Ambiente de execução → Alterar o tipo de ambiente de execução → GPU (T4).
  4. Rode as células de cima para baixo (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.
  5. Coloque o .gguf baixado na pasta models/ do projeto local — ele aparece automaticamente no Chat (console e web).

Dica: edite a variável BASE_MODEL na 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).

Opção B — Treinar localmente (requer GPU NVIDIA com ~6–8 GB de VRAM)

1. Ambiente

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.

2. Treinar (QLoRA 4-bit)

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.

3. Converter para GGUF

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

4. Usar no app

O .gguf cai em models/ e aparece automaticamente na lista de modelos do app C#.


📄 Formato do dataset

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).


⚠️ Notas e limitações

  • Template de prompt: o 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|>).
  • Licenças: Gemma e Llama têm termos próprios de uso; Phi-3 é MIT. Revise antes de uso comercial. Os modelos do catálogo apontam para mirrors GGUF públicos no Hugging Face.
  • Modelos não versionados: arquivos .gguf/.safetensors são grandes e ficam fora do Git (veja .gitignore).

🛠️ Stack

  • .NET 10 · C#
  • LLamaSharp 0.27 (binding .NET do llama.cpp)
  • Spectre.Console (UI de terminal)
  • xUnit (testes)
  • Python: transformers, peft, trl, bitsandbytes (QLoRA)

ricardosmdemello/Mini-LLM-Local-C-Sharp

C#

0

0 commits

updated Jun 17, 2026

See the code

README

Mini LLM Local (C# + .NET 10)

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:

ParteLinguagemOnde rodaPapel
Treino / Fine-tuningPython (QLoRA)GPU consumerAdapta o modelo base ao domínio
Aplicação de chatC# / .NET 10CPU ou GPU localConversa, 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.


✨ O que o app de teste faz

App de console (terminal), interativo, com três funções:

  • 💬 Conversar com a IA — chat com streaming token a token sobre um modelo .gguf local.
  • 📦 Modelos — lista os modelos baixados e baixa novos do catálogo (Gemma/Llama/Phi) com barra de progresso.
  • 📊 Ver dados de treino — inspeciona os datasets .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.


🧱 Estrutura do repositório

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

🚀 Começando rápido (só o app C#)

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:

  1. Vá em 📦 Modelos → Baixar modelo do catálogo e baixe, por exemplo, o Gemma 2 2B Instruct (~1,7 GB).
  2. Volte e escolha 💬 Conversar com a IA, selecione o modelo e converse.
  3. Em 📊 Ver dados de treino, abra o 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.


🌐 Interface Web (Blazor)

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.

🖥️ CPU vs. GPU (inferência no C#)

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.


🎓 Fine-tuning (Python, GPU)

O fluxo completo: dataset → QLoRA → merge → GGUF → app C#.

⚠️ Entenda antes: o treino não altera o modelo carregado no Chat

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:

  1. Baixa o modelo base original do Hugging Face (ex.: google/gemma-2-2b-it) — repositório oficial, não um arquivo .gguf.
  2. Treina esse base nos seus dados com QLoRA (em Python/GPU).
  3. Gera um arquivo .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"?

  • O .gguf é um formato quantizado, otimizado só para inferência (rodar rápido), não para treino.
  • O treino acontece sobre os pesos originais em float16, que vivem no repositório do Hugging Face — por isso começamos do modelo base, não do .gguf.
  • Resultado: o caminho é sempre base (HF) → treina → exporta .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-model deve 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.

Como executar o treino

Sem GPU potente? QLoRA precisa de ~6–8 GB de VRAM. Use o notebook pronto training/colab_finetune.ipynb no Google Colab (GPU T4 16 GB grátis). As instruções manuais mais abaixo valem para treino em GPU local.

Opção A — Treinar no Google Colab (recomendado, sem GPU local)

O notebook fica em training/colab_finetune.ipynb. Para executá-lo:

  1. Acesse colab.research.google.com.
  2. Abra o notebook de uma destas formas:
    • Direto do GitHub: Arquivo → Abrir notebook → aba GitHub, cole https://github.com/ricardosmdemello/Mini-LLM-Local-C-Sharp e selecione training/colab_finetune.ipynb; ou
    • Upload: Arquivo → Fazer upload de notebook e escolha o arquivo training/colab_finetune.ipynb do seu computador.
  3. Ative a GPU: Ambiente de execução → Alterar o tipo de ambiente de execução → GPU (T4).
  4. Rode as células de cima para baixo (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.
  5. Coloque o .gguf baixado na pasta models/ do projeto local — ele aparece automaticamente no Chat (console e web).

Dica: edite a variável BASE_MODEL na 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).

Opção B — Treinar localmente (requer GPU NVIDIA com ~6–8 GB de VRAM)

1. Ambiente

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.

2. Treinar (QLoRA 4-bit)

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.

3. Converter para GGUF

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

4. Usar no app

O .gguf cai em models/ e aparece automaticamente na lista de modelos do app C#.


📄 Formato do dataset

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).


⚠️ Notas e limitações

  • Template de prompt: o 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|>).
  • Licenças: Gemma e Llama têm termos próprios de uso; Phi-3 é MIT. Revise antes de uso comercial. Os modelos do catálogo apontam para mirrors GGUF públicos no Hugging Face.
  • Modelos não versionados: arquivos .gguf/.safetensors são grandes e ficam fora do Git (veja .gitignore).

🛠️ Stack

  • .NET 10 · C#
  • LLamaSharp 0.27 (binding .NET do llama.cpp)
  • Spectre.Console (UI de terminal)
  • xUnit (testes)
  • Python: transformers, peft, trl, bitsandbytes (QLoRA)