{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fine-tune an open model with Unsloth (QLoRA) - starter notebook\n",
    "\n",
    "Starter file for the AssociationAI tutorial \"Fine-Tune Your Own Model with Unsloth\".\n",
    "Upload your `training.jsonl` (built in the training-data studio from the tutorial) and run the cells in order.\n",
    "\n",
    "**Data rule, read first:** run this notebook only on Q&A pairs you could publish: past FAQs, event copy,\n",
    "policy text, de-identified member questions. Never upload member PII (names, emails, phone numbers) to Colab.\n",
    "If your data cannot be de-identified, run Unsloth on a local NVIDIA GPU instead; the code below is the same.\n",
    "\n",
    "Docs this notebook follows: https://unsloth.ai/docs (fetched September 2026). Unsloth's own requirements table\n",
    "puts an 8B QLoRA fine-tune at a 6 GB VRAM minimum, and its Colab guide confirms the free tier provides a T4.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Install Unsloth\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "%%capture\n",
    "!pip install unsloth\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Check the GPU (state VRAM, not RAM)\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import torch\n",
    "props = torch.cuda.get_device_properties(0)\n",
    "print(props.name)\n",
    "print(f\"VRAM: {props.total_memory / 1e9:.1f} GB\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Upload your training.jsonl\n",
    "Built in the tutorial's training-data studio or the kit's csv-to-jsonl converter. One Alpaca-format object per line.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from google.colab import files\n",
    "uploaded = files.upload()\n",
    "print(list(uploaded.keys()))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Load the base model in 4-bit\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from unsloth import FastLanguageModel, FastModel\n",
    "import torch\n",
    "\n",
    "max_seq_length = 2048\n",
    "\n",
    "model, tokenizer = FastModel.from_pretrained(\n",
    "    model_name = \"unsloth/llama-3.1-8b-unsloth-bnb-4bit\",\n",
    "    max_seq_length = max_seq_length,\n",
    "    load_in_4bit = True,\n",
    "    load_in_8bit = False,\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Format the dataset into the Alpaca prompt Unsloth expects\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from datasets import load_dataset\n",
    "\n",
    "alpaca_prompt = \"\"\"Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n",
    "\n",
    "### Instruction:\n",
    "{}\n",
    "\n",
    "### Input:\n",
    "{}\n",
    "\n",
    "### Response:\n",
    "{}\"\"\"\n",
    "\n",
    "def formatting_prompts_func(examples):\n",
    "    instructions = examples[\"instruction\"]\n",
    "    inputs       = examples[\"input\"]\n",
    "    outputs      = examples[\"output\"]\n",
    "    texts = []\n",
    "    for instruction, input, output in zip(instructions, inputs, outputs):\n",
    "        text = alpaca_prompt.format(instruction, input, output) + tokenizer.eos_token\n",
    "        texts.append(text)\n",
    "    return {\"text\": texts}\n",
    "\n",
    "dataset = load_dataset(\"json\", data_files = \"training.jsonl\", split = \"train\")\n",
    "dataset = dataset.map(formatting_prompts_func, batched = True)\n",
    "print(len(dataset), \"training examples\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Attach the QLoRA adapters (the only weights that train)\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "model = FastLanguageModel.get_peft_model(\n",
    "    model,\n",
    "    r = 16,\n",
    "    target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n",
    "                      \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
    "    lora_alpha = 16,\n",
    "    lora_dropout = 0,\n",
    "    bias = \"none\",\n",
    "    use_gradient_checkpointing = \"unsloth\",\n",
    "    random_state = 3407,\n",
    "    max_seq_length = max_seq_length,\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Train\n",
    "Smoke test first: set `max_steps = 60` to confirm the run works, then train 1 to 3 epochs for real.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from trl import SFTTrainer, SFTConfig\n",
    "\n",
    "trainer = SFTTrainer(\n",
    "    model = model,\n",
    "    train_dataset = dataset,\n",
    "    tokenizer = tokenizer,\n",
    "    args = SFTConfig(\n",
    "        max_seq_length = max_seq_length,\n",
    "        per_device_train_batch_size = 2,\n",
    "        gradient_accumulation_steps = 4,\n",
    "        warmup_steps = 5,\n",
    "        num_train_epochs = 2,\n",
    "        learning_rate = 2e-4,\n",
    "        logging_steps = 1,\n",
    "        output_dir = \"outputs\",\n",
    "        optim = \"adamw_8bit\",\n",
    "        seed = 3407,\n",
    "    ),\n",
    ")\n",
    "trainer.train()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Save the adapters (small) and optionally the merged model (large)\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "model.save_pretrained(\"lora_model\")\n",
    "tokenizer.save_pretrained(\"lora_model\")\n",
    "# Merged 16-bit model for deployment (large download; run only if you need it):\n",
    "# model.save_pretrained_merged(\"merged_model\", tokenizer, save_method = \"merged_16bit\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Next\n",
    "Run the tutorial's ten-question scorecard: compare this model against the base model on ten held-out\n",
    "member questions before anything member-facing ships.\n"
   ]
  }
 ]
}
