pt-br/3_parameter_efficient_finetuning/notebooks/finetune_sft_peft.ipynb (515 lines of code) (raw):
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "z-6LLOPZouLg"
},
"source": [
"# Como Ajustar LLMs com Adaptadores LoRA usando Hugging Face TRL\n",
"\n",
"Este notebook demonstra como ajustar eficientemente grandes modelos de linguagem usando adaptadores LoRA (Adaptação de Baixa Classificação). LoRA é uma técnica de ajuste fino eficiente em termos de parâmetros que:\n",
"- Congela os pesos do modelo pré-treinado\n",
"- Adiciona pequenas matrizes treináveis de decomposição de classificação às camadas de atenção\n",
"- Normalmente reduz os parâmetros treináveis em ~90%\n",
"- Mantém o desempenho do modelo sendo eficiente em termos de memória\n",
"\n",
"Vamos abordar:\n",
"1. Configurar o ambiente de desenvolvimento e a configuração do LoRA\n",
"2. Criar e preparar o conjunto de dados para o treinamento do adaptador\n",
"3. Ajustar o modelo usando `trl` e `SFTTrainer` com adaptadores LoRA\n",
"4. Testar o modelo e mesclar adaptadores (opcional)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "fXqd9BXgouLi"
},
"source": [
"## 1. Configurar o ambiente de desenvolvimento\n",
"\n",
"Nosso primeiro passo é instalar os módulos do Hugging Face e o PyTorch, incluindo trl, transformers e datasets. Se você ainda não ouviu falar do trl, não se preocupe. É uma nova biblioteca baseada em transformers e datasets, que facilita o ajuste fino, o RLHF (Reinforcement Learning from Human Feedback) e o alinhamento de modelos de linguagem abertos (open LLMs).\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "tKvGVxImouLi"
},
"outputs": [],
"source": [
"# Install the requirements in Google Colab\n",
"# !pip install transformers datasets trl huggingface_hub\n",
"\n",
"# Authenticate to Hugging Face\n",
"\n",
"from huggingface_hub import login\n",
"\n",
"login()\n",
"\n",
"# for convenience you can create an environment variable containing your hub token as HF_TOKEN"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "XHUzfwpKouLk"
},
"source": [
"## 2. Carregue o conjunto de dados"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"id": "z4p6Bvo7ouLk"
},
"outputs": [
{
"data": {
"text/plain": [
"DatasetDict({\n",
" train: Dataset({\n",
" features: ['full_topic', 'messages'],\n",
" num_rows: 2260\n",
" })\n",
" test: Dataset({\n",
" features: ['full_topic', 'messages'],\n",
" num_rows: 119\n",
" })\n",
"})"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Load a sample dataset\n",
"from datasets import load_dataset\n",
"\n",
"# TODO: define your dataset and config using the path and name parameters\n",
"dataset = load_dataset(path=\"HuggingFaceTB/smoltalk\", name=\"everyday-conversations\")\n",
"dataset"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "9TOhJdtsouLk"
},
"source": [
"## 3. Ajustar uma LLM usando `trl` e o `SFTTrainer` com o LoRA\n",
"\n",
"O [SFTTrainer](https://huggingface.co/docs/trl/sft_trainer) do `trl` oferece integração com adaptadores LoRA através do módulo [PEFT](https://huggingface.co/docs/peft/en/index). As principais vantagens dessa configuração incluem:\n",
"\n",
"1. **Eficiência de Memória**: \n",
" - Apenas os parâmetros dos adaptadores são armazenados na memória da GPU\n",
" - Os pesos do modelo base permanecem congelados e podem ser carregados em precisão reduzida\n",
"\n",
" - Enables fine-tuning of large models on consumer GPUs\n",
"\n",
"2. **Recursos de Treinamento**:\n",
" - Integração nativa PEFT/LoRA com configuração mínima\n",
" - Suporte ao QLoRA (LoRA Quantizado) para uma eficiência de memória ainda melhor\n",
"\n",
"3. **Gerenciamento de Adaptadores**:\n",
" - Salvamento de pesos dos adaptadores durante checkpoints\n",
" - Recursos para mesclar adaptadores de volta ao modelo base\n",
"\n",
"Usaremos LoRA em nosso exemplo, que combina LoRA com quantização de 4 bits para reduzir ainda mais o uso de memória sem sacrificar o desempenho. A configuração requer apenas algumas etapas:\n",
"1. Definir a configuração do LoRA (rank, alpha, dropout)\n",
"2. Criar o SFTTrainer com configuração PEFT\n",
"3. Treinar e salvar os pesos dos adaptadores\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import necessary libraries\n",
"from transformers import AutoModelForCausalLM, AutoTokenizer\n",
"from datasets import load_dataset\n",
"from trl import SFTConfig, SFTTrainer, setup_chat_format\n",
"import torch\n",
"\n",
"device = (\n",
" \"cuda\"\n",
" if torch.cuda.is_available()\n",
" else \"mps\" if torch.backends.mps.is_available() else \"cpu\"\n",
")\n",
"\n",
"# Load the model and tokenizer\n",
"model_name = \"HuggingFaceTB/SmolLM2-135M\"\n",
"\n",
"model = AutoModelForCausalLM.from_pretrained(\n",
" pretrained_model_name_or_path=model_name\n",
").to(device)\n",
"tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_name)\n",
"\n",
"# Set up the chat format\n",
"model, tokenizer = setup_chat_format(model=model, tokenizer=tokenizer)\n",
"\n",
"# Set our name for the finetune to be saved &/ uploaded to\n",
"finetune_name = \"SmolLM2-FT-MyDataset\"\n",
"finetune_tags = [\"smol-course\", \"module_1\"]"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ZbuVArTHouLk"
},
"source": [
"O `SFTTrainer` oferece uma integração nativa com o `peft`, tornando extremamente fácil ajustar eficientemente LLMs, como com o LoRA, por exemplo. Precisamos apenas criar nosso `LoraConfig` e fornecê-lo ao trainer.\n",
"\n",
"<div style='background-color: lightblue; padding: 10px; border-radius: 5px; margin-bottom: 20px; color:black'> \n",
" <h2 style='margin: 0;color:blue'>Exercício: Definir parâmetros LoRA para ajuste fino</h2> \n",
" <p>Escolha um conjunto de dados do hub do Hugging Face e ajuste um modelo com ele.</p> \n",
" <p><b>Níveis de Dificuldade</b></p> \n",
" <p>🐢 Use os parâmetros gerais para um ajuste fino arbitrário.</p> \n",
" <p>🐕 Ajuste os parâmetros e revise em pesos & biases.</p> \n",
" <p>🦁 Ajuste os parâmetros e mostre a mudança nos resultados de inferência.</p> \n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "blDSs9swouLk"
},
"outputs": [],
"source": [
"from peft import LoraConfig\n",
"\n",
"# TODO: Configure LoRA parameters\n",
"# r: rank dimension for LoRA update matrices (smaller = more compression)\n",
"rank_dimension = 6\n",
"# lora_alpha: scaling factor for LoRA layers (higher = stronger adaptation)\n",
"lora_alpha = 8\n",
"# lora_dropout: dropout probability for LoRA layers (helps prevent overfitting)\n",
"lora_dropout = 0.05\n",
"\n",
"peft_config = LoraConfig(\n",
" r=rank_dimension, # Rank dimension - typically between 4-32\n",
" lora_alpha=lora_alpha, # LoRA scaling factor - typically 2x rank\n",
" lora_dropout=lora_dropout, # Dropout probability for LoRA layers\n",
" bias=\"none\", # Bias type for LoRA. the corresponding biases will be updated during training.\n",
" target_modules=\"all-linear\", # Which modules to apply LoRA to\n",
" task_type=\"CAUSAL_LM\", # Task type for model architecture\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "l5NUDPcaouLl"
},
"source": [
"Antes de iniciarmos nosso treinamento, precisamos definir os hiperparâmetros (`TrainingArguments`) que queremos usar.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "NqT28VZlouLl"
},
"outputs": [],
"source": [
"# Training configuration\n",
"# Hyperparameters based on QLoRA paper recommendations\n",
"args = SFTConfig(\n",
" # Output settings\n",
" output_dir=finetune_name, # Directory to save model checkpoints\n",
" # Training duration\n",
" num_train_epochs=1, # Number of training epochs\n",
" # Batch size settings\n",
" per_device_train_batch_size=2, # Batch size per GPU\n",
" gradient_accumulation_steps=2, # Accumulate gradients for larger effective batch\n",
" # Memory optimization\n",
" gradient_checkpointing=True, # Trade compute for memory savings\n",
" # Optimizer settings\n",
" optim=\"adamw_torch_fused\", # Use fused AdamW for efficiency\n",
" learning_rate=2e-4, # Learning rate (QLoRA paper)\n",
" max_grad_norm=0.3, # Gradient clipping threshold\n",
" # Learning rate schedule\n",
" warmup_ratio=0.03, # Portion of steps for warmup\n",
" lr_scheduler_type=\"constant\", # Keep learning rate constant after warmup\n",
" # Logging and saving\n",
" logging_steps=10, # Log metrics every N steps\n",
" save_strategy=\"epoch\", # Save checkpoint every epoch\n",
" # Precision settings\n",
" bf16=True, # Use bfloat16 precision\n",
" # Integration settings\n",
" push_to_hub=False, # Don't push to HuggingFace Hub\n",
" report_to=None, # Disable external logging\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cGhR7uFBouLl"
},
"source": [
"Agora temos todos os pedaços que precisamos para criar nosso `SFTTrainer` para então começar a treinar nosso modelo."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "M00Har2douLl"
},
"outputs": [],
"source": [
"max_seq_length = 1512 # max sequence length for model and packing of the dataset\n",
"\n",
"# Create SFTTrainer with LoRA configuration\n",
"trainer = SFTTrainer(\n",
" model=model,\n",
" args=args,\n",
" train_dataset=dataset[\"train\"],\n",
" peft_config=peft_config, # LoRA configuration\n",
" max_seq_length=max_seq_length, # Maximum sequence length\n",
" tokenizer=tokenizer,\n",
" packing=True, # Enable input packing for efficiency\n",
" dataset_kwargs={\n",
" \"add_special_tokens\": False, # Special tokens handled by template\n",
" \"append_concat_token\": False, # No additional separator needed\n",
" },\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "zQ_kRN24ouLl"
},
"source": [
"Comece treinando nosso modelo chamando a função `train ()` em nossa instância de `Trainer`. Isso iniciará o loop de treinamento e treinará nosso modelo por 3 épocas. Como estamos usando um método PEFT, salvaremos apenas os pesos do modelo adaptado e não o do modelo completo."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Tq4nIYqKouLl"
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "300e5dfbb4b54750b77324345c7591f9",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/72 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"TrainOutput(global_step=72, training_loss=1.6402628521124523, metrics={'train_runtime': 195.2398, 'train_samples_per_second': 1.485, 'train_steps_per_second': 0.369, 'total_flos': 282267289092096.0, 'train_loss': 1.6402628521124523, 'epoch': 0.993103448275862})"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# start training, the model will be automatically saved to the hub and the output directory\n",
"trainer.train()\n",
"\n",
"# save model\n",
"trainer.save_model()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "y4HHSYYzouLl"
},
"source": [
"O treinamento com Flash Attention por 3 épocas com um conjunto de dados de 15 mil amostras levou 4:14:36 em um `g5.2xlarge`. A instância custa `1,21 dólares/h`, o que nos leva a um custo total de apenas ~`5,3 dólares`.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "C309KsXjouLl"
},
"source": [
"### Mesclar Adaptador LoRA no Modelo Original\n",
"\n",
"Ao usar LoRA, treinamos apenas os pesos do adaptador mantendo o modelo base congelado. Durante o treinamento, salvamos apenas esses pesos de adaptador leves (~2-10MB) em vez de uma cópia completa do modelo. No entanto, para implantação, você talvez queira mesclar os adaptadores de volta ao modelo base para:\n",
"\n",
"1. **Implantação Simplificada**: Arquivo de modelo único em vez de modelo base + adaptadores\n",
"2. **Velocidade de Inferência**: Sem sobrecarga de computação de adaptadores\n",
"3. **Compatibilidade de Framework**: Melhor compatibilidade com serving frameworks \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from peft import AutoPeftModelForCausalLM\n",
"\n",
"\n",
"# Load PEFT model on CPU\n",
"model = AutoPeftModelForCausalLM.from_pretrained(\n",
" pretrained_model_name_or_path=args.output_dir,\n",
" torch_dtype=torch.float16,\n",
" low_cpu_mem_usage=True,\n",
")\n",
"\n",
"# Merge LoRA and base model and save\n",
"merged_model = model.merge_and_unload()\n",
"merged_model.save_pretrained(\n",
" args.output_dir, safe_serialization=True, max_shard_size=\"2GB\"\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-yO6E9quouLl"
},
"source": [
"## 3. Testar Modelo e Executar Inferência\n",
"\n",
"Após a conclusão do treinamento, queremos testar nosso modelo. Vamos carregar diferentes amostras do conjunto de dados original e avaliar o modelo baseado nessas amostras, usando um loop simples e a precisão como nossa métrica."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div style='background-color: lightblue; padding: 10px; border-radius: 5px; margin-bottom: 20px; color:black'>\n",
" <h2 style='margin: 0;color:blue'>Exercício Bônus: Carregar Adaptador LoRA</h2>\n",
" <p>Use o que você aprendeu no caderno de exemplo para carregar seu adaptador LoRA treinado para inferência.</p> \n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"id": "I5B494OdouLl"
},
"outputs": [],
"source": [
"# free the memory again\n",
"del model\n",
"del trainer\n",
"torch.cuda.empty_cache()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "P1UhohVdouLl"
},
"outputs": [],
"source": [
"import torch\n",
"from peft import AutoPeftModelForCausalLM\n",
"from transformers import AutoTokenizer, pipeline\n",
"\n",
"# Load Model with PEFT adapter\n",
"tokenizer = AutoTokenizer.from_pretrained(finetune_name)\n",
"model = AutoPeftModelForCausalLM.from_pretrained(\n",
" finetune_name, device_map=\"auto\", torch_dtype=torch.float16\n",
")\n",
"pipe = pipeline(\n",
" \"text-generation\", model=merged_model, tokenizer=tokenizer, device=device\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "99uFDAuuouLl"
},
"source": [
"Vamos testar uns exemplos de prompts e ver como o modelo reage."
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {
"id": "-shSmUbvouLl",
"outputId": "16d97c61-3b31-4040-c780-3c4de75c3824"
},
"outputs": [],
"source": [
"prompts = [\n",
" \"What is the capital of Germany? Explain why thats the case and if it was different in the past?\",\n",
" \"Write a Python function to calculate the factorial of a number.\",\n",
" \"A rectangular garden has a length of 25 feet and a width of 15 feet. If you want to build a fence around the entire garden, how many feet of fencing will you need?\",\n",
" \"What is the difference between a fruit and a vegetable? Give examples of each.\",\n",
"]\n",
"\n",
"\n",
"def test_inference(prompt):\n",
" prompt = pipe.tokenizer.apply_chat_template(\n",
" [{\"role\": \"user\", \"content\": prompt}],\n",
" tokenize=False,\n",
" add_generation_prompt=True,\n",
" )\n",
" outputs = pipe(\n",
" prompt,\n",
" )\n",
" return outputs[0][\"generated_text\"][len(prompt) :].strip()\n",
"\n",
"\n",
"for prompt in prompts:\n",
" print(f\" prompt:\\n{prompt}\")\n",
" print(f\" response:\\n{test_inference(prompt)}\")\n",
" print(\"-\" * 50)"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 0
}