{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a href=\"https://colab.research.google.com/github/michael-borck/loco-llm/blob/main/notebooks/train_tiny_poc.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# LocoLLM: TinyLlama PoC \u2014 Train 3 Adapters in One Session\n",
    "\n",
    "This notebook trains **all 3 LocoLLM adapters** (math, code, analysis) on **TinyLlama-1.1B** in a single Colab session. It\u2019s a fast proof-of-concept so you can see the complete workflow before investing time in Qwen3-4B adapters.\n",
    "\n",
    "**Why TinyLlama?** It\u2019s small enough to train 3 adapters in ~20\u201330 minutes total on a free T4. The results won\u2019t be great \u2014 and that\u2019s the point. Seeing where a 1.1B model struggles motivates why base model selection matters.\n",
    "\n",
    "**What you get:** 3 GGUF files (~600MB each) that you can load into Ollama and test locally.\n",
    "\n",
    "**What you need:** A free Google Colab account with a T4 GPU runtime.\n",
    "\n",
    "---\n",
    "\n",
    "## How to use this notebook\n",
    "\n",
    "1. Open in Colab (click the badge above)\n",
    "2. Go to **Runtime > Change runtime type** and select **T4 GPU**\n",
    "3. Run all cells in order (**Runtime > Run all**)\n",
    "4. Download the 3 GGUF files from the final cells\n",
    "5. Load them into Ollama locally (instructions at the end)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 0: Verify GPU\n",
    "\n",
    "Make sure you have a T4 (or better) GPU assigned."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!nvidia-smi --query-gpu=name,memory.total --format=csv,noheader"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1: Install Unsloth\n",
    "\n",
    "This takes 2\u20133 minutes. Unsloth pulls in the right versions of transformers, peft, trl, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install --upgrade --no-cache-dir unsloth unsloth_zoo 2>&1 | tail -5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2: Define Helper Functions\n",
    "\n",
    "The load \u2192 LoRA \u2192 train \u2192 export cycle is the same for all 3 adapters. We define it once here, then call it 3 times with different datasets.\n",
    "\n",
    "Each call loads TinyLlama fresh (takes ~10 seconds in 4-bit). This is necessary because LoRA weights are per-task \u2014 we need a clean base model each time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "from datasets import Dataset\n",
    "from transformers import TrainingArguments\n",
    "from trl import SFTTrainer\n",
    "from unsloth import FastModel\n",
    "\n",
    "# Shared config\n",
    "MODEL_NAME = \"unsloth/tinyllama-bnb-4bit\"\n",
    "MAX_SEQ_LENGTH = 1024\n",
    "LORA_R = 16\n",
    "LORA_ALPHA = 32\n",
    "LORA_DROPOUT = 0.05\n",
    "NUM_EPOCHS = 3\n",
    "BATCH_SIZE = 2\n",
    "GRADIENT_ACCUMULATION = 4\n",
    "LEARNING_RATE = 2e-4\n",
    "QUANTIZATION_METHOD = \"q4_k_m\"\n",
    "\n",
    "\n",
    "def load_and_train_adapter(training_data, adapter_name):\n",
    "    \"\"\"Load TinyLlama, apply LoRA, train on data, return (model, tokenizer).\"\"\"\n",
    "    print(f\"\\n{'='*60}\")\n",
    "    print(f\"Training: {adapter_name}\")\n",
    "    print(f\"{'='*60}\")\n",
    "\n",
    "    # Load fresh base model\n",
    "    model, tokenizer = FastModel.from_pretrained(\n",
    "        model_name=MODEL_NAME,\n",
    "        max_seq_length=MAX_SEQ_LENGTH,\n",
    "        load_in_4bit=True,\n",
    "        full_finetuning=False,\n",
    "    )\n",
    "\n",
    "    # Apply LoRA\n",
    "    model = FastModel.get_peft_model(\n",
    "        model,\n",
    "        r=LORA_R,\n",
    "        lora_alpha=LORA_ALPHA,\n",
    "        lora_dropout=LORA_DROPOUT,\n",
    "        target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\"],\n",
    "    )\n",
    "\n",
    "    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "    total = sum(p.numel() for p in model.parameters())\n",
    "    print(f\"Trainable parameters: {trainable:,} / {total:,} ({100*trainable/total:.2f}%)\")\n",
    "\n",
    "    # Prepare dataset\n",
    "    dataset = Dataset.from_list(training_data)\n",
    "\n",
    "    def format_conversations(examples):\n",
    "        texts = []\n",
    "        for convos in examples[\"conversations\"]:\n",
    "            text = tokenizer.apply_chat_template(\n",
    "                convos, tokenize=False, add_generation_prompt=False,\n",
    "            )\n",
    "            texts.append(text)\n",
    "        return {\"text\": texts}\n",
    "\n",
    "    dataset = dataset.map(format_conversations, batched=True)\n",
    "    print(f\"Dataset ready: {len(dataset)} examples\")\n",
    "\n",
    "    # Train\n",
    "    trainer = SFTTrainer(\n",
    "        model=model,\n",
    "        tokenizer=tokenizer,\n",
    "        train_dataset=dataset,\n",
    "        args=TrainingArguments(\n",
    "            output_dir=f\"./checkpoints-{adapter_name}\",\n",
    "            per_device_train_batch_size=BATCH_SIZE,\n",
    "            gradient_accumulation_steps=GRADIENT_ACCUMULATION,\n",
    "            num_train_epochs=NUM_EPOCHS,\n",
    "            learning_rate=LEARNING_RATE,\n",
    "            lr_scheduler_type=\"cosine\",\n",
    "            warmup_ratio=0.1,\n",
    "            fp16=True,\n",
    "            logging_steps=5,\n",
    "            save_strategy=\"epoch\",\n",
    "            seed=42,\n",
    "            optim=\"adamw_8bit\",\n",
    "            report_to=\"none\",\n",
    "        ),\n",
    "        dataset_text_field=\"text\",\n",
    "        max_seq_length=MAX_SEQ_LENGTH,\n",
    "        packing=False,\n",
    "    )\n",
    "\n",
    "    print(f\"Training: {NUM_EPOCHS} epochs, effective batch size {BATCH_SIZE * GRADIENT_ACCUMULATION}\")\n",
    "    trainer.train()\n",
    "    print(f\"{adapter_name} training complete!\")\n",
    "\n",
    "    return model, tokenizer\n",
    "\n",
    "\n",
    "def export_gguf(model, tokenizer, adapter_name):\n",
    "    \"\"\"Export merged GGUF for the trained adapter.\"\"\"\n",
    "    output_dir = f\"./gguf-{adapter_name}\"\n",
    "    os.makedirs(output_dir, exist_ok=True)\n",
    "\n",
    "    print(f\"\\nExporting {adapter_name} GGUF ({QUANTIZATION_METHOD})...\")\n",
    "    model.save_pretrained_gguf(\n",
    "        output_dir,\n",
    "        tokenizer,\n",
    "        quantization_method=QUANTIZATION_METHOD,\n",
    "    )\n",
    "\n",
    "    for f in os.listdir(output_dir):\n",
    "        if f.endswith(\".gguf\"):\n",
    "            size_mb = os.path.getsize(os.path.join(output_dir, f)) / (1024 * 1024)\n",
    "            print(f\"Exported: {output_dir}/{f} ({size_mb:.0f} MB)\")\n",
    "\n",
    "    return output_dir\n",
    "\n",
    "\n",
    "def sanity_check(model, tokenizer, questions):\n",
    "    \"\"\"Run a few test questions through the trained model.\"\"\"\n",
    "    FastModel.for_inference(model)\n",
    "    for q in questions:\n",
    "        messages = [{\"role\": \"user\", \"content\": q}]\n",
    "        inputs = tokenizer.apply_chat_template(\n",
    "            messages, tokenize=True, add_generation_prompt=True, return_tensors=\"pt\",\n",
    "        ).to(model.device)\n",
    "        outputs = model.generate(inputs, max_new_tokens=256, temperature=0.0, use_cache=True)\n",
    "        response = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True)\n",
    "        print(f\"Q: {q}\")\n",
    "        print(f\"A: {response}\")\n",
    "        print(\"-\" * 60)\n",
    "\n",
    "\n",
    "print(\"Helper functions defined. Ready to train adapters!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Adapter 1: Math\n",
    "\n",
    "### Download GSM8K Data\n",
    "\n",
    "200 examples from [GSM8K](https://huggingface.co/datasets/openai/gsm8k) with step-by-step reasoning. Same data as the Qwen3 math notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "\n",
    "import requests\n",
    "\n",
    "MATH_NUM_EXAMPLES = 200\n",
    "\n",
    "url = f\"https://datasets-server.huggingface.co/rows?dataset=openai/gsm8k&config=main&split=train&offset=0&length={MATH_NUM_EXAMPLES}\"\n",
    "resp = requests.get(url, timeout=60)\n",
    "resp.raise_for_status()\n",
    "math_rows = [r[\"row\"] for r in resp.json()[\"rows\"]]\n",
    "print(f\"Downloaded {len(math_rows)} examples from GSM8K\")\n",
    "\n",
    "\n",
    "def format_answer(answer_text: str) -> str:\n",
    "    \"\"\"Convert GSM8K answer format to clean step-by-step reasoning.\"\"\"\n",
    "    parts = answer_text.split(\"####\")\n",
    "    reasoning = re.sub(r\"<<.*?>>\", \"\", parts[0]).strip()\n",
    "    final_answer = parts[1].strip() if len(parts) > 1 else \"\"\n",
    "    return f\"{reasoning}\\nThe answer is {final_answer}\"\n",
    "\n",
    "\n",
    "math_data = [\n",
    "    {\n",
    "        \"conversations\": [\n",
    "            {\"role\": \"user\", \"content\": ex[\"question\"].strip()},\n",
    "            {\"role\": \"assistant\", \"content\": format_answer(ex[\"answer\"])},\n",
    "        ]\n",
    "    }\n",
    "    for ex in math_rows\n",
    "]\n",
    "\n",
    "print(f\"Formatted {len(math_data)} math training examples\")\n",
    "print(f\"\\nSample question: {math_data[0]['conversations'][0]['content'][:100]}...\")\n",
    "print(f\"Sample answer:   {math_data[0]['conversations'][1]['content'][:100]}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Train Math Adapter\n",
    "\n",
    "~5 minutes on T4."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "math_model, math_tokenizer = load_and_train_adapter(math_data, \"math\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Math Sanity Check\n",
    "\n",
    "Quick test \u2014 TinyLlama math won\u2019t be amazing, but it should attempt step-by-step reasoning."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sanity_check(math_model, math_tokenizer, [\n",
    "    \"What is 15 + 27?\",\n",
    "    \"A store has 120 apples. They sell 45 in the morning and 30 in the afternoon. How many apples are left?\",\n",
    "    \"If a shirt costs $40 and is on sale for 25% off, what is the sale price?\",\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Export Math GGUF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "math_gguf_dir = export_gguf(math_model, math_tokenizer, \"math\")\n",
    "\n",
    "# Free memory before loading next adapter\n",
    "del math_model, math_tokenizer\n",
    "import gc; gc.collect()\n",
    "import torch; torch.cuda.empty_cache()\n",
    "print(\"Memory cleared for next adapter.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Adapter 2: Code\n",
    "\n",
    "### Download Code Data\n",
    "\n",
    "300 examples from [python_code_instructions_18k_alpaca](https://huggingface.co/datasets/iamtarun/python_code_instructions_18k_alpaca). We filter out examples with very short outputs (< 20 chars)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CODE_NUM_EXAMPLES = 300\n",
    "MIN_OUTPUT_LENGTH = 20\n",
    "\n",
    "url = f\"https://datasets-server.huggingface.co/rows?dataset=iamtarun/python_code_instructions_18k_alpaca&config=default&split=train&offset=0&length={CODE_NUM_EXAMPLES + 100}\"\n",
    "resp = requests.get(url, timeout=60)\n",
    "resp.raise_for_status()\n",
    "code_rows = [r[\"row\"] for r in resp.json()[\"rows\"]]\n",
    "print(f\"Downloaded {len(code_rows)} raw examples from python_code_instructions_18k_alpaca\")\n",
    "\n",
    "code_data = []\n",
    "for ex in code_rows:\n",
    "    if len(code_data) >= CODE_NUM_EXAMPLES:\n",
    "        break\n",
    "\n",
    "    output = (ex.get(\"output\") or \"\").strip()\n",
    "    if len(output) < MIN_OUTPUT_LENGTH:\n",
    "        continue\n",
    "\n",
    "    instruction = (ex.get(\"instruction\") or \"\").strip()\n",
    "    extra_input = (ex.get(\"input\") or \"\").strip()\n",
    "    user_msg = f\"{instruction}\\n\\n{extra_input}\" if extra_input else instruction\n",
    "\n",
    "    if not user_msg:\n",
    "        continue\n",
    "\n",
    "    code_data.append(\n",
    "        {\n",
    "            \"conversations\": [\n",
    "                {\"role\": \"user\", \"content\": user_msg},\n",
    "                {\"role\": \"assistant\", \"content\": output},\n",
    "            ]\n",
    "        }\n",
    "    )\n",
    "\n",
    "print(f\"Formatted {len(code_data)} code training examples\")\n",
    "print(f\"\\nSample question: {code_data[0]['conversations'][0]['content'][:100]}...\")\n",
    "print(f\"Sample answer:   {code_data[0]['conversations'][1]['content'][:100]}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Train Code Adapter\n",
    "\n",
    "~5 minutes on T4."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "code_model, code_tokenizer = load_and_train_adapter(code_data, \"code\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Code Sanity Check\n",
    "\n",
    "TinyLlama code output will be rough, but it should attempt Python syntax."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sanity_check(code_model, code_tokenizer, [\n",
    "    \"Write a Python function that checks if a number is prime.\",\n",
    "    \"Write a Python function to reverse a string.\",\n",
    "    \"Write a Python function that returns the factorial of a number.\",\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Export Code GGUF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "code_gguf_dir = export_gguf(code_model, code_tokenizer, \"code\")\n",
    "\n",
    "del code_model, code_tokenizer\n",
    "import gc; gc.collect()\n",
    "torch.cuda.empty_cache()\n",
    "print(\"Memory cleared for next adapter.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Adapter 3: Analysis\n",
    "\n",
    "### Download Analysis Data\n",
    "\n",
    "300 examples from [allenai/sciq](https://huggingface.co/datasets/allenai/sciq) \u2014 science reading comprehension. Each example has a passage, a question, and a correct answer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ANALYSIS_NUM_EXAMPLES = 300\n",
    "\n",
    "url = f\"https://datasets-server.huggingface.co/rows?dataset=allenai/sciq&config=default&split=train&offset=0&length={ANALYSIS_NUM_EXAMPLES + 100}\"\n",
    "resp = requests.get(url, timeout=60)\n",
    "resp.raise_for_status()\n",
    "analysis_rows = [r[\"row\"] for r in resp.json()[\"rows\"]]\n",
    "print(f\"Downloaded {len(analysis_rows)} raw examples from allenai/sciq\")\n",
    "\n",
    "analysis_data = []\n",
    "for ex in analysis_rows:\n",
    "    if len(analysis_data) >= ANALYSIS_NUM_EXAMPLES:\n",
    "        break\n",
    "\n",
    "    support = (ex.get(\"support\") or \"\").strip()\n",
    "    question = (ex.get(\"question\") or \"\").strip()\n",
    "    correct_answer = (ex.get(\"correct_answer\") or \"\").strip()\n",
    "\n",
    "    if not support or not question or not correct_answer:\n",
    "        continue\n",
    "\n",
    "    user_msg = (\n",
    "        f\"Read the following passage and answer the question.\\n\\n\"\n",
    "        f\"Passage: {support}\\n\\n\"\n",
    "        f\"Question: {question}\"\n",
    "    )\n",
    "\n",
    "    assistant_msg = (\n",
    "        f\"Based on the passage, the answer is: {correct_answer}\\n\\n\"\n",
    "        f\"The passage states that {support[:200].rstrip('.')}. \"\n",
    "        f'This tells us that the answer to \"{question}\" is {correct_answer}.'\n",
    "    )\n",
    "\n",
    "    analysis_data.append(\n",
    "        {\n",
    "            \"conversations\": [\n",
    "                {\"role\": \"user\", \"content\": user_msg},\n",
    "                {\"role\": \"assistant\", \"content\": assistant_msg},\n",
    "            ]\n",
    "        }\n",
    "    )\n",
    "\n",
    "print(f\"Formatted {len(analysis_data)} analysis training examples\")\n",
    "print(f\"\\nSample question: {analysis_data[0]['conversations'][0]['content'][:100]}...\")\n",
    "print(f\"Sample answer:   {analysis_data[0]['conversations'][1]['content'][:100]}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Train Analysis Adapter\n",
    "\n",
    "~5 minutes on T4."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "analysis_model, analysis_tokenizer = load_and_train_adapter(analysis_data, \"analysis\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Analysis Sanity Check\n",
    "\n",
    "TinyLlama should attempt passage-based reasoning, even if the quality is limited."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sanity_check(analysis_model, analysis_tokenizer, [\n",
    "    \"Read the following passage and answer the question.\\n\\nPassage: Water boils at 100 degrees Celsius at standard atmospheric pressure. At higher altitudes, the atmospheric pressure is lower, so water boils at a lower temperature.\\n\\nQuestion: Why does water boil at a lower temperature at higher altitudes?\",\n",
    "    \"Read the following passage and answer the question.\\n\\nPassage: Photosynthesis is the process by which plants convert sunlight, water, and carbon dioxide into glucose and oxygen. This process takes place primarily in the leaves of plants, within structures called chloroplasts.\\n\\nQuestion: Where does photosynthesis primarily take place?\",\n",
    "    \"Read the following passage and answer the question.\\n\\nPassage: The mitochondria are often called the powerhouse of the cell because they generate most of the cell's supply of adenosine triphosphate (ATP), which is used as a source of chemical energy.\\n\\nQuestion: What molecule do mitochondria primarily produce?\",\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Export Analysis GGUF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "analysis_gguf_dir = export_gguf(analysis_model, analysis_tokenizer, \"analysis\")\n",
    "\n",
    "del analysis_model, analysis_tokenizer\n",
    "import gc; gc.collect()\n",
    "torch.cuda.empty_cache()\n",
    "print(\"Memory cleared.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Save to Google Drive (Optional)\n",
    "\n",
    "Save all 3 GGUFs to Google Drive so they persist after the Colab session ends."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import shutil\n",
    "\n",
    "from google.colab import drive\n",
    "\n",
    "drive.mount(\"/content/drive\")\n",
    "\n",
    "DRIVE_DIR = \"/content/drive/MyDrive/locollm-adapters/tinyllama\"\n",
    "os.makedirs(DRIVE_DIR, exist_ok=True)\n",
    "\n",
    "for adapter_name, gguf_dir in [(\"math\", math_gguf_dir), (\"code\", code_gguf_dir), (\"analysis\", analysis_gguf_dir)]:\n",
    "    for f in os.listdir(gguf_dir):\n",
    "        if f.endswith(\".gguf\"):\n",
    "            src = os.path.join(gguf_dir, f)\n",
    "            dst = os.path.join(DRIVE_DIR, f\"tinyllama-{adapter_name}.gguf\")\n",
    "            print(f\"Copying {adapter_name} GGUF to Google Drive...\")\n",
    "            shutil.copy2(src, dst)\n",
    "            print(f\"  Saved to: {dst}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Download All GGUFs\n",
    "\n",
    "Download all 3 GGUF files to your local machine."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from google.colab import files\n",
    "\n",
    "for adapter_name, gguf_dir in [(\"math\", math_gguf_dir), (\"code\", code_gguf_dir), (\"analysis\", analysis_gguf_dir)]:\n",
    "    for f in os.listdir(gguf_dir):\n",
    "        if f.endswith(\".gguf\"):\n",
    "            print(f\"Downloading {adapter_name} GGUF...\")\n",
    "            files.download(os.path.join(gguf_dir, f))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Next Steps: Using Your TinyLlama Adapters Locally\n",
    "\n",
    "### 1. Create Ollama Models from the GGUFs\n",
    "\n",
    "For each downloaded GGUF, create an Ollama model:\n",
    "\n",
    "```bash\n",
    "# Create a Modelfile for each adapter\n",
    "echo 'FROM ./unsloth.Q4_K_M.gguf' > Modelfile-tinyllama-math\n",
    "echo 'FROM ./unsloth.Q4_K_M.gguf' > Modelfile-tinyllama-code\n",
    "echo 'FROM ./unsloth.Q4_K_M.gguf' > Modelfile-tinyllama-analysis\n",
    "\n",
    "# Create the Ollama models (run from your Downloads folder, or wherever the GGUFs are)\n",
    "ollama create tinyllama-math -f Modelfile-tinyllama-math\n",
    "ollama create tinyllama-code -f Modelfile-tinyllama-code\n",
    "ollama create tinyllama-analysis -f Modelfile-tinyllama-analysis\n",
    "```\n",
    "\n",
    "### 2. Chat with Your Models\n",
    "\n",
    "```bash\n",
    "# Test each adapter directly\n",
    "ollama run tinyllama-math \"What is 15 + 27?\"\n",
    "ollama run tinyllama-code \"Write a function to check if a number is prime.\"\n",
    "ollama run tinyllama-analysis \"Read the following passage and answer the question. Passage: Water boils at 100C at sea level. Question: At what temperature does water boil at sea level?\"\n",
    "```\n",
    "\n",
    "### 3. Compare with Qwen3-4B\n",
    "\n",
    "The TinyLlama adapters will be noticeably weaker than Qwen3-4B adapters. Try the same questions on both to see the difference. This is why base model selection matters!\n",
    "\n",
    "### 4. Train the \"Real\" Adapters\n",
    "\n",
    "Once you understand the workflow, train production-quality adapters on Qwen3-4B using the dedicated notebooks:\n",
    "\n",
    "- [train_math_adapter.ipynb](https://github.com/michael-borck/loco-llm/blob/main/notebooks/train_math_adapter.ipynb) \u2014 Math adapter on Qwen3-4B\n",
    "\n",
    "The Qwen3-4B adapters integrate with the full LocoLLM system (`loco chat`, `loco route`, `loco eval`)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Training Summary\n",
    "\n",
    "| Adapter | Dataset | Examples | Base Model | GGUF Size |\n",
    "|---------|---------|----------|------------|----------|\n",
    "| Math | GSM8K | 200 | TinyLlama-1.1B (4-bit) | ~600 MB |\n",
    "| Code | python_code_instructions_18k_alpaca | 300 | TinyLlama-1.1B (4-bit) | ~600 MB |\n",
    "| Analysis | allenai/sciq | 300 | TinyLlama-1.1B (4-bit) | ~600 MB |\n",
    "\n",
    "| Parameter | Value |\n",
    "|-----------|-------|\n",
    "| Method | QLoRA |\n",
    "| LoRA rank | 16 |\n",
    "| LoRA alpha | 32 |\n",
    "| Epochs | 3 |\n",
    "| Effective batch size | 8 |\n",
    "| Learning rate | 2e-4 |\n",
    "| Max sequence length | 1024 |\n",
    "| Export format | Q4_K_M GGUF |\n",
    "| Hardware | Colab T4 (16GB VRAM) |\n",
    "| Total time | ~20\u201330 minutes |"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}