Text Generation
Transformers
Safetensors
English
Chinese
qwen3
qwen3-8b
lora
qlora
sft
rag
faiss
dense-retrieval
agent
ppo
rlhf
rule-reward
harness-engineering
um-handbook
question-answering
chatbot
education
tensor-talk
Instructions to use TensorCat/TensorTalk with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use TensorCat/TensorTalk with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="TensorCat/TensorTalk")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("TensorCat/TensorTalk", dtype="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps
- vLLM
How to use TensorCat/TensorTalk with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "TensorCat/TensorTalk" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "TensorCat/TensorTalk", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/TensorCat/TensorTalk
- SGLang
How to use TensorCat/TensorTalk with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "TensorCat/TensorTalk" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "TensorCat/TensorTalk", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "TensorCat/TensorTalk" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "TensorCat/TensorTalk", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use TensorCat/TensorTalk with Docker Model Runner:
docker model run hf.co/TensorCat/TensorTalk
Delete UM_Handbook/(Demo Pilot)FineTune_QWEN3_UM_Handbook_en.ipynb
Browse files
UM_Handbook/(Demo Pilot)FineTune_QWEN3_UM_Handbook_en.ipynb
DELETED
|
@@ -1,1531 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "markdown",
|
| 5 |
-
"id": "ac09de66",
|
| 6 |
-
"metadata": {},
|
| 7 |
-
"source": [
|
| 8 |
-
"# Qwen3-8B UM Handbook LoRA / QLoRA Fine-tuning\n",
|
| 9 |
-
"\n",
|
| 10 |
-
"This notebook keeps the training logic from `finetune_qwen3_um_handbook_v3.py` and organizes it into separate notebook sections for DICC.\n",
|
| 11 |
-
"\n",
|
| 12 |
-
"## Workflow\n",
|
| 13 |
-
"1. Check the environment and available devices\n",
|
| 14 |
-
"2. Read and validate `SFT_QA_Training_Ready.jsonl`\n",
|
| 15 |
-
"3. Convert the QA data into prompt-completion format\n",
|
| 16 |
-
"4. Split the data into training and validation sets\n",
|
| 17 |
-
"5. Download the Qwen3-8B base model into a local directory\n",
|
| 18 |
-
"6. Select the backend automatically: **CUDA > MPS > CPU**\n",
|
| 19 |
-
"7. Use **4-bit QLoRA** on CUDA and standard LoRA on MPS / CPU\n",
|
| 20 |
-
"8. Train with `TRL SFTTrainer`\n",
|
| 21 |
-
"9. Evaluate with both loss-based and generation-based metrics\n",
|
| 22 |
-
"10. Save the LoRA adapter, merged model, `.pt` file, metrics, and predictions\n",
|
| 23 |
-
"\n"
|
| 24 |
-
]
|
| 25 |
-
},
|
| 26 |
-
{
|
| 27 |
-
"cell_type": "markdown",
|
| 28 |
-
"id": "71f10012",
|
| 29 |
-
"metadata": {},
|
| 30 |
-
"source": [
|
| 31 |
-
"## Part 1 - Install dependencies\n",
|
| 32 |
-
"\n",
|
| 33 |
-
"Run this cell only if the current DICC kernel does not already have the required packages.\n",
|
| 34 |
-
"\n"
|
| 35 |
-
]
|
| 36 |
-
},
|
| 37 |
-
{
|
| 38 |
-
"cell_type": "code",
|
| 39 |
-
"execution_count": null,
|
| 40 |
-
"id": "d091473c",
|
| 41 |
-
"metadata": {},
|
| 42 |
-
"outputs": [],
|
| 43 |
-
"source": [
|
| 44 |
-
"# %pip install -U torch transformers accelerate datasets trl peft bitsandbytes sentencepiece evaluate rouge_score bert_score sacrebleu huggingface_hub"
|
| 45 |
-
]
|
| 46 |
-
},
|
| 47 |
-
{
|
| 48 |
-
"cell_type": "markdown",
|
| 49 |
-
"id": "f44d18ff",
|
| 50 |
-
"metadata": {},
|
| 51 |
-
"source": [
|
| 52 |
-
"## Part 2 - Import libraries\n",
|
| 53 |
-
"\n",
|
| 54 |
-
"This section imports the libraries used in the training pipeline.\n",
|
| 55 |
-
"\n"
|
| 56 |
-
]
|
| 57 |
-
},
|
| 58 |
-
{
|
| 59 |
-
"cell_type": "code",
|
| 60 |
-
"execution_count": 26,
|
| 61 |
-
"id": "47a8b3f7",
|
| 62 |
-
"metadata": {},
|
| 63 |
-
"outputs": [],
|
| 64 |
-
"source": [
|
| 65 |
-
"from __future__ import annotations\n",
|
| 66 |
-
"\n",
|
| 67 |
-
"import gc\n",
|
| 68 |
-
"import json\n",
|
| 69 |
-
"import math\n",
|
| 70 |
-
"import random\n",
|
| 71 |
-
"import re\n",
|
| 72 |
-
"import time\n",
|
| 73 |
-
"from pathlib import Path\n",
|
| 74 |
-
"from typing import Dict, List\n",
|
| 75 |
-
"from pathlib import Path\n",
|
| 76 |
-
"\n",
|
| 77 |
-
"import numpy as np\n",
|
| 78 |
-
"import torch\n",
|
| 79 |
-
"from datasets import Dataset, DatasetDict\n",
|
| 80 |
-
"from huggingface_hub import snapshot_download\n",
|
| 81 |
-
"from peft import LoraConfig, PeftModel\n",
|
| 82 |
-
"import transformers\n",
|
| 83 |
-
"from transformers import (\n",
|
| 84 |
-
" AutoModelForCausalLM,\n",
|
| 85 |
-
" AutoTokenizer,\n",
|
| 86 |
-
" BitsAndBytesConfig,\n",
|
| 87 |
-
" set_seed,\n",
|
| 88 |
-
")\n",
|
| 89 |
-
"from trl import SFTConfig, SFTTrainer"
|
| 90 |
-
]
|
| 91 |
-
},
|
| 92 |
-
{
|
| 93 |
-
"cell_type": "markdown",
|
| 94 |
-
"id": "469e1466",
|
| 95 |
-
"metadata": {},
|
| 96 |
-
"source": [
|
| 97 |
-
"## Part 3 - Configuration\n",
|
| 98 |
-
"\n",
|
| 99 |
-
"This section defines project paths, model paths, output paths, and training hyperparameters.\n",
|
| 100 |
-
"\n"
|
| 101 |
-
]
|
| 102 |
-
},
|
| 103 |
-
{
|
| 104 |
-
"cell_type": "code",
|
| 105 |
-
"execution_count": 13,
|
| 106 |
-
"id": "59d68547",
|
| 107 |
-
"metadata": {},
|
| 108 |
-
"outputs": [
|
| 109 |
-
{
|
| 110 |
-
"name": "stdout",
|
| 111 |
-
"output_type": "stream",
|
| 112 |
-
"text": [
|
| 113 |
-
"PROJECT_ROOT = /scr/user/kevin2002/TensorCat/NLP/UM_Handbook\n",
|
| 114 |
-
"DATASET_ROOT = /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/Dataset/SFT_Dataset\n",
|
| 115 |
-
"DATASET_PATH = /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/Dataset/SFT_Dataset/SFT_QA_Training_Ready.jsonl\n",
|
| 116 |
-
"BASE_MODEL_DIR = /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/models/Qwen3-8B\n",
|
| 117 |
-
"OUTPUT_ROOT = /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook\n",
|
| 118 |
-
"USE_4BIT = True\n",
|
| 119 |
-
"MAX_GRAD_NORM = 1.0\n",
|
| 120 |
-
"PACKING = False\n",
|
| 121 |
-
"GRADIENT_CHECKPOINTING = True\n",
|
| 122 |
-
"LOW_CPU_MEM_USAGE = True\n",
|
| 123 |
-
"USE_FLASH_ATTENTION_2_IF_AVAILABLE = False\n"
|
| 124 |
-
]
|
| 125 |
-
}
|
| 126 |
-
],
|
| 127 |
-
"source": [
|
| 128 |
-
"# ============================================================\n",
|
| 129 |
-
"# CONFIG\n",
|
| 130 |
-
"# ============================================================\n",
|
| 131 |
-
"\n",
|
| 132 |
-
"from pathlib import Path\n",
|
| 133 |
-
"\n",
|
| 134 |
-
"WARMUP_STEPS = 20\n",
|
| 135 |
-
"\n",
|
| 136 |
-
"# Project root\n",
|
| 137 |
-
"PROJECT_ROOT = Path(\"/scr/user/kevin2002/TensorCat/NLP/UM_Handbook\")\n",
|
| 138 |
-
"\n",
|
| 139 |
-
"# Dataset paths\n",
|
| 140 |
-
"DATASET_ROOT = PROJECT_ROOT / \"Dataset\" / \"SFT_Dataset\"\n",
|
| 141 |
-
"DATASET_PATH = DATASET_ROOT / \"SFT_QA_Training_Ready.jsonl\"\n",
|
| 142 |
-
"\n",
|
| 143 |
-
"# Base model selection\n",
|
| 144 |
-
"BASE_MODEL_NAME = \"Qwen/Qwen3-8B\"\n",
|
| 145 |
-
"BASE_MODEL_LOCAL_DIR = PROJECT_ROOT / \"models\" / \"Qwen3-8B\"\n",
|
| 146 |
-
"\n",
|
| 147 |
-
"# Output paths\n",
|
| 148 |
-
"OUTPUT_ROOT = PROJECT_ROOT / \"outputs\" / \"qwen3_um_handbook\"\n",
|
| 149 |
-
"ADAPTER_OUTPUT_DIR = OUTPUT_ROOT / \"lora_adapter\"\n",
|
| 150 |
-
"MERGED_MODEL_DIR = OUTPUT_ROOT / \"merged_model\"\n",
|
| 151 |
-
"FINAL_PT_PATH = OUTPUT_ROOT / \"Qwen3-8B-Instruct_UM_Handbook.pt\"\n",
|
| 152 |
-
"METRICS_JSON_PATH = OUTPUT_ROOT / \"final_metrics.json\"\n",
|
| 153 |
-
"PREDICTIONS_JSONL_PATH = OUTPUT_ROOT / \"validation_predictions.jsonl\"\n",
|
| 154 |
-
"TRAIN_VAL_SPLIT_JSON_PATH = OUTPUT_ROOT / \"dataset_split_summary.json\"\n",
|
| 155 |
-
"\n",
|
| 156 |
-
"# Data / prompt settings\n",
|
| 157 |
-
"SYSTEM_PROMPT = (\n",
|
| 158 |
-
" \"You are an academic assistant for the Faculty of Computer Science and \"\n",
|
| 159 |
-
" \"Information Technology, Universiti Malaya. Answer questions accurately \"\n",
|
| 160 |
-
" \"and only using handbook-consistent information. If the handbook does not support \"\n",
|
| 161 |
-
" \"a claim, avoid inventing details.\"\n",
|
| 162 |
-
")\n",
|
| 163 |
-
"TRAIN_VAL_RATIO = 0.90\n",
|
| 164 |
-
"MAX_SEQ_LENGTH = 1024\n",
|
| 165 |
-
"RANDOM_SEED = 42\n",
|
| 166 |
-
"\n",
|
| 167 |
-
"# LoRA / QLoRA settings\n",
|
| 168 |
-
"USE_4BIT = True\n",
|
| 169 |
-
"LORA_R = 32\n",
|
| 170 |
-
"LORA_ALPHA = 64\n",
|
| 171 |
-
"LORA_DROPOUT = 0.05\n",
|
| 172 |
-
"LORA_TARGET_MODULES = [\n",
|
| 173 |
-
" \"q_proj\",\n",
|
| 174 |
-
" \"k_proj\",\n",
|
| 175 |
-
" \"v_proj\",\n",
|
| 176 |
-
" \"o_proj\",\n",
|
| 177 |
-
" \"gate_proj\",\n",
|
| 178 |
-
" \"up_proj\",\n",
|
| 179 |
-
" \"down_proj\",\n",
|
| 180 |
-
"]\n",
|
| 181 |
-
"\n",
|
| 182 |
-
"# Training settings\n",
|
| 183 |
-
"NUM_TRAIN_EPOCHS = 6\n",
|
| 184 |
-
"PER_DEVICE_TRAIN_BATCH_SIZE = 2\n",
|
| 185 |
-
"PER_DEVICE_EVAL_BATCH_SIZE = 2\n",
|
| 186 |
-
"GRADIENT_ACCUMULATION_STEPS = 8\n",
|
| 187 |
-
"LEARNING_RATE = 2e-4\n",
|
| 188 |
-
"WEIGHT_DECAY = 0.01\n",
|
| 189 |
-
"WARMUP_RATIO = 0.05\n",
|
| 190 |
-
"LOGGING_STEPS = 10\n",
|
| 191 |
-
"SAVE_STEPS = 50\n",
|
| 192 |
-
"EVAL_STEPS = 50\n",
|
| 193 |
-
"\n",
|
| 194 |
-
"MAX_GRAD_NORM = 1.0\n",
|
| 195 |
-
"PACKING = False\n",
|
| 196 |
-
"GRADIENT_CHECKPOINTING = True\n",
|
| 197 |
-
"LOW_CPU_MEM_USAGE = True\n",
|
| 198 |
-
"USE_FLASH_ATTENTION_2_IF_AVAILABLE = False\n",
|
| 199 |
-
"\n",
|
| 200 |
-
"# Save / display settings\n",
|
| 201 |
-
"SAVE_MERGED_MODEL = True\n",
|
| 202 |
-
"SAVE_TOKENIZER_WITH_MERGED = True\n",
|
| 203 |
-
"NUM_PRINTED_PREDICTIONS = 5\n",
|
| 204 |
-
"\n",
|
| 205 |
-
"\n",
|
| 206 |
-
"# Generation eval settings\n",
|
| 207 |
-
"MAX_NEW_TOKENS_EVAL = 192\n",
|
| 208 |
-
"NUM_EVAL_SAMPLES_FOR_GENERATION = None # None = use full validation set\n",
|
| 209 |
-
"DO_SAMPLE_EVAL = False\n",
|
| 210 |
-
"TEMPERATURE_EVAL = 0.7\n",
|
| 211 |
-
"TOP_P_EVAL = 0.9\n",
|
| 212 |
-
"\n",
|
| 213 |
-
"# Final export settings\n",
|
| 214 |
-
"SAVE_SINGLE_PT = True\n",
|
| 215 |
-
"\n",
|
| 216 |
-
"# Create the main directories early\n",
|
| 217 |
-
"for path in [PROJECT_ROOT, DATASET_ROOT, BASE_MODEL_LOCAL_DIR.parent, OUTPUT_ROOT]:\n",
|
| 218 |
-
" path.mkdir(parents=True, exist_ok=True)\n",
|
| 219 |
-
"\n",
|
| 220 |
-
"print(\"PROJECT_ROOT =\", PROJECT_ROOT)\n",
|
| 221 |
-
"print(\"DATASET_ROOT =\", DATASET_ROOT)\n",
|
| 222 |
-
"print(\"DATASET_PATH =\", DATASET_PATH)\n",
|
| 223 |
-
"print(\"BASE_MODEL_DIR =\", BASE_MODEL_LOCAL_DIR)\n",
|
| 224 |
-
"print(\"OUTPUT_ROOT =\", OUTPUT_ROOT)\n",
|
| 225 |
-
"print(\"USE_4BIT =\", USE_4BIT)\n",
|
| 226 |
-
"\n",
|
| 227 |
-
"\n",
|
| 228 |
-
"print(\"MAX_GRAD_NORM =\", MAX_GRAD_NORM)\n",
|
| 229 |
-
"print(\"PACKING =\", PACKING)\n",
|
| 230 |
-
"print(\"GRADIENT_CHECKPOINTING =\", GRADIENT_CHECKPOINTING)\n",
|
| 231 |
-
"print(\"LOW_CPU_MEM_USAGE =\", LOW_CPU_MEM_USAGE)\n",
|
| 232 |
-
"print(\"USE_FLASH_ATTENTION_2_IF_AVAILABLE =\", USE_FLASH_ATTENTION_2_IF_AVAILABLE)\n"
|
| 233 |
-
]
|
| 234 |
-
},
|
| 235 |
-
{
|
| 236 |
-
"cell_type": "markdown",
|
| 237 |
-
"id": "e9319138",
|
| 238 |
-
"metadata": {},
|
| 239 |
-
"source": [
|
| 240 |
-
"## Part 4 - Helper functions\n",
|
| 241 |
-
"\n",
|
| 242 |
-
"This section defines utility functions for paths, logging, text cleanup, device selection, dtype selection, and 4-bit control.\n",
|
| 243 |
-
"\n"
|
| 244 |
-
]
|
| 245 |
-
},
|
| 246 |
-
{
|
| 247 |
-
"cell_type": "code",
|
| 248 |
-
"execution_count": 14,
|
| 249 |
-
"id": "f4a0e4f4",
|
| 250 |
-
"metadata": {},
|
| 251 |
-
"outputs": [],
|
| 252 |
-
"source": [
|
| 253 |
-
"def ensure_dir(path: Path) -> None:\n",
|
| 254 |
-
" path.mkdir(parents=True, exist_ok=True)\n",
|
| 255 |
-
"\n",
|
| 256 |
-
"def print_banner(title: str) -> None:\n",
|
| 257 |
-
" print(\"\\n\" + \"=\" * 88)\n",
|
| 258 |
-
" print(title)\n",
|
| 259 |
-
" print(\"=\" * 88)\n",
|
| 260 |
-
"\n",
|
| 261 |
-
"def select_runtime_backend() -> str:\n",
|
| 262 |
-
" if torch.cuda.is_available():\n",
|
| 263 |
-
" return \"cuda\"\n",
|
| 264 |
-
" if hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n",
|
| 265 |
-
" return \"mps\"\n",
|
| 266 |
-
" return \"cpu\"\n",
|
| 267 |
-
"\n",
|
| 268 |
-
"def detect_compute_dtype(backend: str) -> torch.dtype:\n",
|
| 269 |
-
" if backend == \"cuda\":\n",
|
| 270 |
-
" if torch.cuda.is_bf16_supported():\n",
|
| 271 |
-
" return torch.bfloat16\n",
|
| 272 |
-
" return torch.float16\n",
|
| 273 |
-
" if backend == \"mps\":\n",
|
| 274 |
-
" return torch.float16\n",
|
| 275 |
-
" return torch.float32\n",
|
| 276 |
-
"\n",
|
| 277 |
-
"def should_use_4bit(backend: str) -> bool:\n",
|
| 278 |
-
" return USE_4BIT and backend == \"cuda\"\n",
|
| 279 |
-
"\n",
|
| 280 |
-
"def normalize_text(text: str) -> str:\n",
|
| 281 |
-
" text = text.strip()\n",
|
| 282 |
-
" text = re.sub(r\"\\s+\", \" \", text)\n",
|
| 283 |
-
" return text\n",
|
| 284 |
-
"\n",
|
| 285 |
-
"def normalize_for_exact(text: str) -> str:\n",
|
| 286 |
-
" text = text.lower().strip()\n",
|
| 287 |
-
" text = re.sub(r\"[^\\w\\s]\", \" \", text)\n",
|
| 288 |
-
" text = re.sub(r\"\\s+\", \" \", text)\n",
|
| 289 |
-
" return text\n",
|
| 290 |
-
"\n",
|
| 291 |
-
"def cleanup_memory() -> None:\n",
|
| 292 |
-
" gc.collect()\n",
|
| 293 |
-
" if torch.cuda.is_available():\n",
|
| 294 |
-
" torch.cuda.empty_cache()"
|
| 295 |
-
]
|
| 296 |
-
},
|
| 297 |
-
{
|
| 298 |
-
"cell_type": "markdown",
|
| 299 |
-
"id": "e8d9c423",
|
| 300 |
-
"metadata": {},
|
| 301 |
-
"source": [
|
| 302 |
-
"## Part 5 - Device detection and display\n",
|
| 303 |
-
"\n",
|
| 304 |
-
"This section prints the available devices, selected backend, selected dtype, and whether 4-bit loading is enabled.\n",
|
| 305 |
-
"\n"
|
| 306 |
-
]
|
| 307 |
-
},
|
| 308 |
-
{
|
| 309 |
-
"cell_type": "code",
|
| 310 |
-
"execution_count": 15,
|
| 311 |
-
"id": "64398701",
|
| 312 |
-
"metadata": {},
|
| 313 |
-
"outputs": [
|
| 314 |
-
{
|
| 315 |
-
"name": "stdout",
|
| 316 |
-
"output_type": "stream",
|
| 317 |
-
"text": [
|
| 318 |
-
"\n",
|
| 319 |
-
"========================================================================================\n",
|
| 320 |
-
"Device Detection\n",
|
| 321 |
-
"========================================================================================\n",
|
| 322 |
-
"{\n",
|
| 323 |
-
" \"selected_backend\": \"cuda\",\n",
|
| 324 |
-
" \"torch_dtype\": \"torch.bfloat16\",\n",
|
| 325 |
-
" \"use_4bit_qlora\": true,\n",
|
| 326 |
-
" \"cuda_available\": true,\n",
|
| 327 |
-
" \"mps_available\": false,\n",
|
| 328 |
-
" \"cuda_device_count\": 1,\n",
|
| 329 |
-
" \"cuda_device_name\": \"NVIDIA A100-SXM4-80GB\"\n",
|
| 330 |
-
"}\n"
|
| 331 |
-
]
|
| 332 |
-
}
|
| 333 |
-
],
|
| 334 |
-
"source": [
|
| 335 |
-
"print_banner(\"Device Detection\")\n",
|
| 336 |
-
"\n",
|
| 337 |
-
"RUNTIME_DEVICE_BACKEND = select_runtime_backend()\n",
|
| 338 |
-
"effective_dtype = detect_compute_dtype(RUNTIME_DEVICE_BACKEND)\n",
|
| 339 |
-
"effective_use_4bit = should_use_4bit(RUNTIME_DEVICE_BACKEND)\n",
|
| 340 |
-
"\n",
|
| 341 |
-
"device_info = {\n",
|
| 342 |
-
" \"selected_backend\": RUNTIME_DEVICE_BACKEND,\n",
|
| 343 |
-
" \"torch_dtype\": str(effective_dtype),\n",
|
| 344 |
-
" \"use_4bit_qlora\": effective_use_4bit,\n",
|
| 345 |
-
" \"cuda_available\": torch.cuda.is_available(),\n",
|
| 346 |
-
" \"mps_available\": hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available(),\n",
|
| 347 |
-
"}\n",
|
| 348 |
-
"\n",
|
| 349 |
-
"if torch.cuda.is_available():\n",
|
| 350 |
-
" device_info[\"cuda_device_count\"] = torch.cuda.device_count()\n",
|
| 351 |
-
" try:\n",
|
| 352 |
-
" device_info[\"cuda_device_name\"] = torch.cuda.get_device_name(0)\n",
|
| 353 |
-
" except Exception:\n",
|
| 354 |
-
" device_info[\"cuda_device_name\"] = \"Unavailable\"\n",
|
| 355 |
-
"\n",
|
| 356 |
-
"print(json.dumps(device_info, indent=2))\n",
|
| 357 |
-
"\n",
|
| 358 |
-
"if RUNTIME_DEVICE_BACKEND != \"cuda\":\n",
|
| 359 |
-
" print(\n",
|
| 360 |
-
" \"\\\\n[Info] Non-CUDA backend detected. 4-bit bitsandbytes QLoRA is disabled automatically, \"\n",
|
| 361 |
-
" \"and the training path falls back to standard LoRA on the selected backend.\"\n",
|
| 362 |
-
" )"
|
| 363 |
-
]
|
| 364 |
-
},
|
| 365 |
-
{
|
| 366 |
-
"cell_type": "markdown",
|
| 367 |
-
"id": "69b02751",
|
| 368 |
-
"metadata": {},
|
| 369 |
-
"source": [
|
| 370 |
-
"## Part 6 - Evaluation functions\n",
|
| 371 |
-
"\n",
|
| 372 |
-
"This section defines exact-match and token-level F1 scoring functions.\n",
|
| 373 |
-
"\n"
|
| 374 |
-
]
|
| 375 |
-
},
|
| 376 |
-
{
|
| 377 |
-
"cell_type": "code",
|
| 378 |
-
"execution_count": 16,
|
| 379 |
-
"id": "379c9dd7",
|
| 380 |
-
"metadata": {},
|
| 381 |
-
"outputs": [],
|
| 382 |
-
"source": [
|
| 383 |
-
"def token_f1(prediction: str, reference: str) -> float:\n",
|
| 384 |
-
" pred_tokens = normalize_for_exact(prediction).split()\n",
|
| 385 |
-
" ref_tokens = normalize_for_exact(reference).split()\n",
|
| 386 |
-
"\n",
|
| 387 |
-
" if not pred_tokens and not ref_tokens:\n",
|
| 388 |
-
" return 1.0\n",
|
| 389 |
-
" if not pred_tokens or not ref_tokens:\n",
|
| 390 |
-
" return 0.0\n",
|
| 391 |
-
"\n",
|
| 392 |
-
" common = {}\n",
|
| 393 |
-
" for token in pred_tokens:\n",
|
| 394 |
-
" common[token] = common.get(token, 0) + 1\n",
|
| 395 |
-
"\n",
|
| 396 |
-
" overlap = 0\n",
|
| 397 |
-
" ref_counts = {}\n",
|
| 398 |
-
" for token in ref_tokens:\n",
|
| 399 |
-
" ref_counts[token] = ref_counts.get(token, 0) + 1\n",
|
| 400 |
-
"\n",
|
| 401 |
-
" for token, count in common.items():\n",
|
| 402 |
-
" if token in ref_counts:\n",
|
| 403 |
-
" overlap += min(count, ref_counts[token])\n",
|
| 404 |
-
"\n",
|
| 405 |
-
" if overlap == 0:\n",
|
| 406 |
-
" return 0.0\n",
|
| 407 |
-
"\n",
|
| 408 |
-
" precision = overlap / len(pred_tokens)\n",
|
| 409 |
-
" recall = overlap / len(ref_tokens)\n",
|
| 410 |
-
" return 2 * precision * recall / (precision + recall)\n",
|
| 411 |
-
"\n",
|
| 412 |
-
"def exact_match(prediction: str, reference: str) -> float:\n",
|
| 413 |
-
" return float(normalize_for_exact(prediction) == normalize_for_exact(reference))"
|
| 414 |
-
]
|
| 415 |
-
},
|
| 416 |
-
{
|
| 417 |
-
"cell_type": "markdown",
|
| 418 |
-
"id": "d26e26fd",
|
| 419 |
-
"metadata": {},
|
| 420 |
-
"source": [
|
| 421 |
-
"## Part 7 - Dataset reading and validation functions\n",
|
| 422 |
-
"\n",
|
| 423 |
-
"This section reads the JSONL file, checks required fields, and prepares prompt-completion rows.\n",
|
| 424 |
-
"\n"
|
| 425 |
-
]
|
| 426 |
-
},
|
| 427 |
-
{
|
| 428 |
-
"cell_type": "code",
|
| 429 |
-
"execution_count": 17,
|
| 430 |
-
"id": "56287c34",
|
| 431 |
-
"metadata": {},
|
| 432 |
-
"outputs": [],
|
| 433 |
-
"source": [
|
| 434 |
-
"def load_jsonl(path: Path) -> List[Dict]:\n",
|
| 435 |
-
" rows: List[Dict] = []\n",
|
| 436 |
-
" with path.open(\"r\", encoding=\"utf-8\") as f:\n",
|
| 437 |
-
" for line_number, line in enumerate(f, 1):\n",
|
| 438 |
-
" line = line.strip()\n",
|
| 439 |
-
" if not line:\n",
|
| 440 |
-
" continue\n",
|
| 441 |
-
" obj = json.loads(line)\n",
|
| 442 |
-
" required = {\"question\", \"answer\"}\n",
|
| 443 |
-
" missing = required - set(obj.keys())\n",
|
| 444 |
-
" if missing:\n",
|
| 445 |
-
" raise ValueError(f\"Line {line_number} is missing keys: {sorted(missing)}\")\n",
|
| 446 |
-
" obj[\"question\"] = normalize_text(obj[\"question\"])\n",
|
| 447 |
-
" obj[\"answer\"] = normalize_text(obj[\"answer\"])\n",
|
| 448 |
-
" rows.append(obj)\n",
|
| 449 |
-
"\n",
|
| 450 |
-
" if not rows:\n",
|
| 451 |
-
" raise ValueError(f\"Dataset at {path} is empty.\")\n",
|
| 452 |
-
" return rows\n",
|
| 453 |
-
"\n",
|
| 454 |
-
"def build_prompt_completion_rows(rows: List[Dict]) -> List[Dict]:\n",
|
| 455 |
-
" converted: List[Dict] = []\n",
|
| 456 |
-
" for row in rows:\n",
|
| 457 |
-
" converted.append(\n",
|
| 458 |
-
" {\n",
|
| 459 |
-
" \"qa_id\": row.get(\"qa_id\", \"\"),\n",
|
| 460 |
-
" \"index_id\": row.get(\"index_id\", \"\"),\n",
|
| 461 |
-
" \"prompt\": [\n",
|
| 462 |
-
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 463 |
-
" {\"role\": \"user\", \"content\": row[\"question\"]},\n",
|
| 464 |
-
" ],\n",
|
| 465 |
-
" \"completion\": [\n",
|
| 466 |
-
" {\"role\": \"assistant\", \"content\": row[\"answer\"]},\n",
|
| 467 |
-
" ],\n",
|
| 468 |
-
" \"question\": row[\"question\"],\n",
|
| 469 |
-
" \"answer\": row[\"answer\"],\n",
|
| 470 |
-
" }\n",
|
| 471 |
-
" )\n",
|
| 472 |
-
" return converted\n",
|
| 473 |
-
"\n",
|
| 474 |
-
"def split_dataset(rows: List[Dict], train_ratio: float, seed: int) -> DatasetDict:\n",
|
| 475 |
-
" rng = random.Random(seed)\n",
|
| 476 |
-
" rows = rows.copy()\n",
|
| 477 |
-
" rng.shuffle(rows)\n",
|
| 478 |
-
"\n",
|
| 479 |
-
" split_idx = max(1, int(len(rows) * train_ratio))\n",
|
| 480 |
-
" split_idx = min(split_idx, len(rows) - 1)\n",
|
| 481 |
-
"\n",
|
| 482 |
-
" train_rows = rows[:split_idx]\n",
|
| 483 |
-
" val_rows = rows[split_idx:]\n",
|
| 484 |
-
"\n",
|
| 485 |
-
" ds = DatasetDict(\n",
|
| 486 |
-
" {\n",
|
| 487 |
-
" \"train\": Dataset.from_list(train_rows),\n",
|
| 488 |
-
" \"validation\": Dataset.from_list(val_rows),\n",
|
| 489 |
-
" }\n",
|
| 490 |
-
" )\n",
|
| 491 |
-
" return ds"
|
| 492 |
-
]
|
| 493 |
-
},
|
| 494 |
-
{
|
| 495 |
-
"cell_type": "markdown",
|
| 496 |
-
"id": "53f03f17",
|
| 497 |
-
"metadata": {},
|
| 498 |
-
"source": [
|
| 499 |
-
"## Part 8 - Read and inspect the dataset\n",
|
| 500 |
-
"\n",
|
| 501 |
-
"This section loads the dataset, creates the train / validation split, and prints one sample.\n",
|
| 502 |
-
"\n"
|
| 503 |
-
]
|
| 504 |
-
},
|
| 505 |
-
{
|
| 506 |
-
"cell_type": "code",
|
| 507 |
-
"execution_count": 18,
|
| 508 |
-
"id": "943f8ae3",
|
| 509 |
-
"metadata": {},
|
| 510 |
-
"outputs": [
|
| 511 |
-
{
|
| 512 |
-
"name": "stdout",
|
| 513 |
-
"output_type": "stream",
|
| 514 |
-
"text": [
|
| 515 |
-
"\n",
|
| 516 |
-
"========================================================================================\n",
|
| 517 |
-
"Step 1 - Validate dataset\n",
|
| 518 |
-
"========================================================================================\n",
|
| 519 |
-
"{\n",
|
| 520 |
-
" \"dataset_path\": \"/scr/user/kevin2002/TensorCat/NLP/UM_Handbook/Dataset/SFT_Dataset/SFT_QA_Training_Ready.jsonl\",\n",
|
| 521 |
-
" \"total_examples\": 388,\n",
|
| 522 |
-
" \"train_examples\": 349,\n",
|
| 523 |
-
" \"validation_examples\": 39,\n",
|
| 524 |
-
" \"seed\": 42,\n",
|
| 525 |
-
" \"train_val_ratio\": 0.9\n",
|
| 526 |
-
"}\n",
|
| 527 |
-
"\\nSample example:\n",
|
| 528 |
-
"{\n",
|
| 529 |
-
" \"qa_id\": \"qa_000001\",\n",
|
| 530 |
-
" \"index_id\": \"UMI-0001\",\n",
|
| 531 |
-
" \"prompt\": [\n",
|
| 532 |
-
" {\n",
|
| 533 |
-
" \"role\": \"system\",\n",
|
| 534 |
-
" \"content\": \"You are an academic assistant for the Faculty of Computer Science and Information Technology, Universiti Malaya. Answer questions accurately and only using handbook-consistent information. If the handbook does not support a claim, avoid inventing details.\"\n",
|
| 535 |
-
" },\n",
|
| 536 |
-
" {\n",
|
| 537 |
-
" \"role\": \"user\",\n",
|
| 538 |
-
" \"content\": \"What are the faculty objectives?\"\n",
|
| 539 |
-
" }\n",
|
| 540 |
-
" ],\n",
|
| 541 |
-
" \"completion\": [\n",
|
| 542 |
-
" {\n",
|
| 543 |
-
" \"role\": \"assistant\",\n",
|
| 544 |
-
" \"content\": \"The faculty objectives are to sustain excellence in undergraduate and postgraduate teaching, learning, and research; contribute to national development through quality research and publications; provide innovative academic programmes that respond to societal needs; and produce quality graduates with advanced knowledge and skills in computer science and information technology.\"\n",
|
| 545 |
-
" }\n",
|
| 546 |
-
" ],\n",
|
| 547 |
-
" \"question\": \"What are the faculty objectives?\",\n",
|
| 548 |
-
" \"answer\": \"The faculty objectives are to sustain excellence in undergraduate and postgraduate teaching, learning, and research; contribute to national development through quality research and publications; provide innovative academic programmes that respond to societal needs; and produce quality graduates with advanced knowledge and skills in computer science and information technology.\"\n",
|
| 549 |
-
"}\n"
|
| 550 |
-
]
|
| 551 |
-
}
|
| 552 |
-
],
|
| 553 |
-
"source": [
|
| 554 |
-
"print_banner(\"Step 1 - Validate dataset\")\n",
|
| 555 |
-
"\n",
|
| 556 |
-
"if not DATASET_PATH.exists():\n",
|
| 557 |
-
" raise FileNotFoundError(\n",
|
| 558 |
-
" f\"Dataset not found: {DATASET_PATH}\\n\"\n",
|
| 559 |
-
" f\"Place SFT_QA_Training_Ready.jsonl in DATASET_ROOT or update DATASET_PATH.\"\n",
|
| 560 |
-
" )\n",
|
| 561 |
-
"\n",
|
| 562 |
-
"set_seed(RANDOM_SEED)\n",
|
| 563 |
-
"random.seed(RANDOM_SEED)\n",
|
| 564 |
-
"np.random.seed(RANDOM_SEED)\n",
|
| 565 |
-
"\n",
|
| 566 |
-
"raw_rows = load_jsonl(DATASET_PATH)\n",
|
| 567 |
-
"converted_rows = build_prompt_completion_rows(raw_rows)\n",
|
| 568 |
-
"dataset_dict = split_dataset(converted_rows, TRAIN_VAL_RATIO, RANDOM_SEED)\n",
|
| 569 |
-
"\n",
|
| 570 |
-
"split_summary = {\n",
|
| 571 |
-
" \"dataset_path\": str(DATASET_PATH),\n",
|
| 572 |
-
" \"total_examples\": len(converted_rows),\n",
|
| 573 |
-
" \"train_examples\": len(dataset_dict[\"train\"]),\n",
|
| 574 |
-
" \"validation_examples\": len(dataset_dict[\"validation\"]),\n",
|
| 575 |
-
" \"seed\": RANDOM_SEED,\n",
|
| 576 |
-
" \"train_val_ratio\": TRAIN_VAL_RATIO,\n",
|
| 577 |
-
"}\n",
|
| 578 |
-
"\n",
|
| 579 |
-
"print(json.dumps(split_summary, indent=2, ensure_ascii=False))\n",
|
| 580 |
-
"print(\"\\\\nSample example:\")\n",
|
| 581 |
-
"print(json.dumps(converted_rows[0], indent=2, ensure_ascii=False)[:1800])"
|
| 582 |
-
]
|
| 583 |
-
},
|
| 584 |
-
{
|
| 585 |
-
"cell_type": "markdown",
|
| 586 |
-
"id": "1589862e",
|
| 587 |
-
"metadata": {},
|
| 588 |
-
"source": [
|
| 589 |
-
"## Part 9 - Save the dataset split summary\n",
|
| 590 |
-
"\n",
|
| 591 |
-
"This section writes the split summary to JSON.\n",
|
| 592 |
-
"\n"
|
| 593 |
-
]
|
| 594 |
-
},
|
| 595 |
-
{
|
| 596 |
-
"cell_type": "code",
|
| 597 |
-
"execution_count": 19,
|
| 598 |
-
"id": "61b5ae46",
|
| 599 |
-
"metadata": {},
|
| 600 |
-
"outputs": [
|
| 601 |
-
{
|
| 602 |
-
"name": "stdout",
|
| 603 |
-
"output_type": "stream",
|
| 604 |
-
"text": [
|
| 605 |
-
"Saved split summary to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/dataset_split_summary.json\n"
|
| 606 |
-
]
|
| 607 |
-
}
|
| 608 |
-
],
|
| 609 |
-
"source": [
|
| 610 |
-
"def save_json(path: Path, obj: Dict) -> None:\n",
|
| 611 |
-
" ensure_dir(path.parent)\n",
|
| 612 |
-
" with path.open(\"w\", encoding=\"utf-8\") as f:\n",
|
| 613 |
-
" json.dump(obj, f, indent=2, ensure_ascii=False)\n",
|
| 614 |
-
"\n",
|
| 615 |
-
"def save_predictions_jsonl(path: Path, rows: List[Dict]) -> None:\n",
|
| 616 |
-
" ensure_dir(path.parent)\n",
|
| 617 |
-
" with path.open(\"w\", encoding=\"utf-8\") as f:\n",
|
| 618 |
-
" for row in rows:\n",
|
| 619 |
-
" f.write(json.dumps(row, ensure_ascii=False) + \"\\\\n\")\n",
|
| 620 |
-
"\n",
|
| 621 |
-
"ensure_dir(OUTPUT_ROOT)\n",
|
| 622 |
-
"save_json(TRAIN_VAL_SPLIT_JSON_PATH, split_summary)\n",
|
| 623 |
-
"print(f\"Saved split summary to: {TRAIN_VAL_SPLIT_JSON_PATH}\")"
|
| 624 |
-
]
|
| 625 |
-
},
|
| 626 |
-
{
|
| 627 |
-
"cell_type": "markdown",
|
| 628 |
-
"id": "79e53130",
|
| 629 |
-
"metadata": {},
|
| 630 |
-
"source": [
|
| 631 |
-
"## Part 10 - Download the base model into a local directory\n",
|
| 632 |
-
"\n",
|
| 633 |
-
"This section reuses the local model if it already exists; otherwise it downloads the base model to the configured model directory.\n",
|
| 634 |
-
"\n"
|
| 635 |
-
]
|
| 636 |
-
},
|
| 637 |
-
{
|
| 638 |
-
"cell_type": "code",
|
| 639 |
-
"execution_count": 20,
|
| 640 |
-
"id": "73f9a585",
|
| 641 |
-
"metadata": {},
|
| 642 |
-
"outputs": [
|
| 643 |
-
{
|
| 644 |
-
"name": "stdout",
|
| 645 |
-
"output_type": "stream",
|
| 646 |
-
"text": [
|
| 647 |
-
"Base model already exists at: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/models/Qwen3-8B\n",
|
| 648 |
-
"Local model path: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/models/Qwen3-8B\n"
|
| 649 |
-
]
|
| 650 |
-
}
|
| 651 |
-
],
|
| 652 |
-
"source": [
|
| 653 |
-
"def download_base_model_if_needed() -> Path:\n",
|
| 654 |
-
" ensure_dir(BASE_MODEL_LOCAL_DIR)\n",
|
| 655 |
-
"\n",
|
| 656 |
-
" if (BASE_MODEL_LOCAL_DIR / \"config.json\").exists():\n",
|
| 657 |
-
" print(f\"Base model already exists at: {BASE_MODEL_LOCAL_DIR}\")\n",
|
| 658 |
-
" return BASE_MODEL_LOCAL_DIR\n",
|
| 659 |
-
"\n",
|
| 660 |
-
" print_banner(\"Downloading base model snapshot\")\n",
|
| 661 |
-
" local_path = snapshot_download(\n",
|
| 662 |
-
" repo_id=BASE_MODEL_NAME,\n",
|
| 663 |
-
" local_dir=str(BASE_MODEL_LOCAL_DIR),\n",
|
| 664 |
-
" local_dir_use_symlinks=False,\n",
|
| 665 |
-
" resume_download=True,\n",
|
| 666 |
-
" )\n",
|
| 667 |
-
" print(f\"Downloaded base model to: {local_path}\")\n",
|
| 668 |
-
" return BASE_MODEL_LOCAL_DIR\n",
|
| 669 |
-
"\n",
|
| 670 |
-
"local_model_path = download_base_model_if_needed()\n",
|
| 671 |
-
"print(f\"Local model path: {local_model_path}\")"
|
| 672 |
-
]
|
| 673 |
-
},
|
| 674 |
-
{
|
| 675 |
-
"cell_type": "markdown",
|
| 676 |
-
"id": "94be6680",
|
| 677 |
-
"metadata": {},
|
| 678 |
-
"source": [
|
| 679 |
-
"## Part 11 - Load the tokenizer and training model\n",
|
| 680 |
-
"\n",
|
| 681 |
-
"This section loads the tokenizer and model, enables 4-bit QLoRA on CUDA, and falls back to standard LoRA on MPS / CPU.\n",
|
| 682 |
-
"\n"
|
| 683 |
-
]
|
| 684 |
-
},
|
| 685 |
-
{
|
| 686 |
-
"cell_type": "code",
|
| 687 |
-
"execution_count": 21,
|
| 688 |
-
"id": "b6771a4e",
|
| 689 |
-
"metadata": {},
|
| 690 |
-
"outputs": [
|
| 691 |
-
{
|
| 692 |
-
"name": "stdout",
|
| 693 |
-
"output_type": "stream",
|
| 694 |
-
"text": [
|
| 695 |
-
"\n",
|
| 696 |
-
"========================================================================================\n",
|
| 697 |
-
"Step 4 - Load tokenizer and model\n",
|
| 698 |
-
"========================================================================================\n"
|
| 699 |
-
]
|
| 700 |
-
},
|
| 701 |
-
{
|
| 702 |
-
"data": {
|
| 703 |
-
"application/vnd.jupyter.widget-view+json": {
|
| 704 |
-
"model_id": "c8e4cf58c2d243ddb9fbf786788b5037",
|
| 705 |
-
"version_major": 2,
|
| 706 |
-
"version_minor": 0
|
| 707 |
-
},
|
| 708 |
-
"text/plain": [
|
| 709 |
-
"Loading weights: 0%| | 0/399 [00:00<?, ?it/s]"
|
| 710 |
-
]
|
| 711 |
-
},
|
| 712 |
-
"metadata": {},
|
| 713 |
-
"output_type": "display_data"
|
| 714 |
-
},
|
| 715 |
-
{
|
| 716 |
-
"name": "stdout",
|
| 717 |
-
"output_type": "stream",
|
| 718 |
-
"text": [
|
| 719 |
-
"Tokenizer and model loaded successfully.\n",
|
| 720 |
-
"Model class: Qwen3ForCausalLM\n"
|
| 721 |
-
]
|
| 722 |
-
}
|
| 723 |
-
],
|
| 724 |
-
"source": [
|
| 725 |
-
"def load_tokenizer(model_path: Path):\n",
|
| 726 |
-
" tokenizer = AutoTokenizer.from_pretrained(str(model_path), use_fast=True)\n",
|
| 727 |
-
" if tokenizer.pad_token is None:\n",
|
| 728 |
-
" tokenizer.pad_token = tokenizer.eos_token\n",
|
| 729 |
-
" tokenizer.padding_side = \"right\"\n",
|
| 730 |
-
" return tokenizer\n",
|
| 731 |
-
"\n",
|
| 732 |
-
"def load_model_for_training(model_path: Path, backend: str):\n",
|
| 733 |
-
" compute_dtype = detect_compute_dtype(backend)\n",
|
| 734 |
-
"\n",
|
| 735 |
-
" quantization_config = None\n",
|
| 736 |
-
" if should_use_4bit(backend):\n",
|
| 737 |
-
" quantization_config = BitsAndBytesConfig(\n",
|
| 738 |
-
" load_in_4bit=True,\n",
|
| 739 |
-
" bnb_4bit_use_double_quant=True,\n",
|
| 740 |
-
" bnb_4bit_quant_type=\"nf4\",\n",
|
| 741 |
-
" bnb_4bit_compute_dtype=compute_dtype,\n",
|
| 742 |
-
" )\n",
|
| 743 |
-
"\n",
|
| 744 |
-
" model_kwargs = {\n",
|
| 745 |
-
" \"pretrained_model_name_or_path\": str(model_path),\n",
|
| 746 |
-
" \"torch_dtype\": compute_dtype,\n",
|
| 747 |
-
" \"low_cpu_mem_usage\": LOW_CPU_MEM_USAGE,\n",
|
| 748 |
-
" \"trust_remote_code\": False,\n",
|
| 749 |
-
" }\n",
|
| 750 |
-
"\n",
|
| 751 |
-
" if backend == \"cuda\":\n",
|
| 752 |
-
" model_kwargs[\"device_map\"] = \"auto\"\n",
|
| 753 |
-
" if quantization_config is not None:\n",
|
| 754 |
-
" model_kwargs[\"quantization_config\"] = quantization_config\n",
|
| 755 |
-
" if USE_FLASH_ATTENTION_2_IF_AVAILABLE and backend == \"cuda\":\n",
|
| 756 |
-
" model_kwargs[\"attn_implementation\"] = \"flash_attention_2\"\n",
|
| 757 |
-
"\n",
|
| 758 |
-
" model = AutoModelForCausalLM.from_pretrained(**model_kwargs)\n",
|
| 759 |
-
"\n",
|
| 760 |
-
" if backend in {\"mps\", \"cpu\"}:\n",
|
| 761 |
-
" model = model.to(backend)\n",
|
| 762 |
-
"\n",
|
| 763 |
-
" model.config.use_cache = False if GRADIENT_CHECKPOINTING else True\n",
|
| 764 |
-
" if GRADIENT_CHECKPOINTING:\n",
|
| 765 |
-
" model.gradient_checkpointing_enable()\n",
|
| 766 |
-
"\n",
|
| 767 |
-
" return model\n",
|
| 768 |
-
"\n",
|
| 769 |
-
"print_banner(\"Step 4 - Load tokenizer and model\")\n",
|
| 770 |
-
"tokenizer = load_tokenizer(local_model_path)\n",
|
| 771 |
-
"model = load_model_for_training(local_model_path, RUNTIME_DEVICE_BACKEND)\n",
|
| 772 |
-
"\n",
|
| 773 |
-
"print(\"Tokenizer and model loaded successfully.\")\n",
|
| 774 |
-
"print(\"Model class:\", model.__class__.__name__)"
|
| 775 |
-
]
|
| 776 |
-
},
|
| 777 |
-
{
|
| 778 |
-
"cell_type": "markdown",
|
| 779 |
-
"id": "192160bb",
|
| 780 |
-
"metadata": {},
|
| 781 |
-
"source": [
|
| 782 |
-
"## Part 12 - Build the LoRA configuration and training arguments\n",
|
| 783 |
-
"\n",
|
| 784 |
-
"This section defines the LoRA settings and trainer arguments.\n",
|
| 785 |
-
"\n"
|
| 786 |
-
]
|
| 787 |
-
},
|
| 788 |
-
{
|
| 789 |
-
"cell_type": "code",
|
| 790 |
-
"execution_count": 22,
|
| 791 |
-
"id": "38a4cc45",
|
| 792 |
-
"metadata": {},
|
| 793 |
-
"outputs": [
|
| 794 |
-
{
|
| 795 |
-
"name": "stdout",
|
| 796 |
-
"output_type": "stream",
|
| 797 |
-
"text": [
|
| 798 |
-
"\n",
|
| 799 |
-
"========================================================================================\n",
|
| 800 |
-
"Step 5 - Build trainer\n",
|
| 801 |
-
"========================================================================================\n"
|
| 802 |
-
]
|
| 803 |
-
},
|
| 804 |
-
{
|
| 805 |
-
"data": {
|
| 806 |
-
"application/vnd.jupyter.widget-view+json": {
|
| 807 |
-
"model_id": "2c1da0d8fb454d9bb3ca83a8f871a8b5",
|
| 808 |
-
"version_major": 2,
|
| 809 |
-
"version_minor": 0
|
| 810 |
-
},
|
| 811 |
-
"text/plain": [
|
| 812 |
-
"Tokenizing train dataset (num_proc=1): 0%| | 0/349 [00:00<?, ? examples/s]"
|
| 813 |
-
]
|
| 814 |
-
},
|
| 815 |
-
"metadata": {},
|
| 816 |
-
"output_type": "display_data"
|
| 817 |
-
},
|
| 818 |
-
{
|
| 819 |
-
"data": {
|
| 820 |
-
"application/vnd.jupyter.widget-view+json": {
|
| 821 |
-
"model_id": "e0b96c79ace94af39e6018adea9da222",
|
| 822 |
-
"version_major": 2,
|
| 823 |
-
"version_minor": 0
|
| 824 |
-
},
|
| 825 |
-
"text/plain": [
|
| 826 |
-
"Tokenizing eval dataset (num_proc=1): 0%| | 0/39 [00:00<?, ? examples/s]"
|
| 827 |
-
]
|
| 828 |
-
},
|
| 829 |
-
"metadata": {},
|
| 830 |
-
"output_type": "display_data"
|
| 831 |
-
},
|
| 832 |
-
{
|
| 833 |
-
"name": "stdout",
|
| 834 |
-
"output_type": "stream",
|
| 835 |
-
"text": [
|
| 836 |
-
"Trainer built successfully.\n"
|
| 837 |
-
]
|
| 838 |
-
}
|
| 839 |
-
],
|
| 840 |
-
"source": [
|
| 841 |
-
"def build_peft_config() -> LoraConfig:\n",
|
| 842 |
-
" return LoraConfig(\n",
|
| 843 |
-
" r=LORA_R,\n",
|
| 844 |
-
" lora_alpha=LORA_ALPHA,\n",
|
| 845 |
-
" lora_dropout=LORA_DROPOUT,\n",
|
| 846 |
-
" bias=\"none\",\n",
|
| 847 |
-
" task_type=\"CAUSAL_LM\",\n",
|
| 848 |
-
" target_modules=LORA_TARGET_MODULES,\n",
|
| 849 |
-
" )\n",
|
| 850 |
-
"\n",
|
| 851 |
-
"def build_training_args(backend: str) -> SFTConfig:\n",
|
| 852 |
-
" bf16 = backend == \"cuda\" and torch.cuda.is_bf16_supported()\n",
|
| 853 |
-
" fp16 = backend in {\"cuda\", \"mps\"} and not bf16\n",
|
| 854 |
-
"\n",
|
| 855 |
-
" return SFTConfig(\n",
|
| 856 |
-
" output_dir=str(OUTPUT_ROOT / \"trainer_runs\"),\n",
|
| 857 |
-
" num_train_epochs=NUM_TRAIN_EPOCHS,\n",
|
| 858 |
-
" per_device_train_batch_size=PER_DEVICE_TRAIN_BATCH_SIZE,\n",
|
| 859 |
-
" per_device_eval_batch_size=PER_DEVICE_EVAL_BATCH_SIZE,\n",
|
| 860 |
-
" gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,\n",
|
| 861 |
-
" learning_rate=LEARNING_RATE,\n",
|
| 862 |
-
" weight_decay=WEIGHT_DECAY,\n",
|
| 863 |
-
" warmup_steps=WARMUP_STEPS,\n",
|
| 864 |
-
" logging_steps=LOGGING_STEPS,\n",
|
| 865 |
-
" eval_strategy=\"steps\",\n",
|
| 866 |
-
" eval_steps=EVAL_STEPS,\n",
|
| 867 |
-
" save_strategy=\"steps\",\n",
|
| 868 |
-
" save_steps=SAVE_STEPS,\n",
|
| 869 |
-
" save_total_limit=2,\n",
|
| 870 |
-
" load_best_model_at_end=True,\n",
|
| 871 |
-
" metric_for_best_model=\"eval_loss\",\n",
|
| 872 |
-
" greater_is_better=False,\n",
|
| 873 |
-
" max_grad_norm=MAX_GRAD_NORM,\n",
|
| 874 |
-
" lr_scheduler_type=\"cosine\",\n",
|
| 875 |
-
" bf16=bf16,\n",
|
| 876 |
-
" fp16=fp16,\n",
|
| 877 |
-
" gradient_checkpointing=GRADIENT_CHECKPOINTING,\n",
|
| 878 |
-
" max_length=MAX_SEQ_LENGTH,\n",
|
| 879 |
-
" packing=PACKING,\n",
|
| 880 |
-
" dataset_num_proc=1,\n",
|
| 881 |
-
" completion_only_loss=True,\n",
|
| 882 |
-
" remove_unused_columns=False,\n",
|
| 883 |
-
" report_to=\"none\",\n",
|
| 884 |
-
" seed=RANDOM_SEED,\n",
|
| 885 |
-
" optim=\"paged_adamw_8bit\" if should_use_4bit(backend) else \"adamw_torch\",\n",
|
| 886 |
-
" )\n",
|
| 887 |
-
"\n",
|
| 888 |
-
"def build_trainer(model, tokenizer, dataset_dict: DatasetDict, backend: str) -> SFTTrainer:\n",
|
| 889 |
-
" peft_config = build_peft_config()\n",
|
| 890 |
-
" training_args = build_training_args(backend)\n",
|
| 891 |
-
"\n",
|
| 892 |
-
" trainer = SFTTrainer(\n",
|
| 893 |
-
" model=model,\n",
|
| 894 |
-
" processing_class=tokenizer,\n",
|
| 895 |
-
" args=training_args,\n",
|
| 896 |
-
" train_dataset=dataset_dict[\"train\"],\n",
|
| 897 |
-
" eval_dataset=dataset_dict[\"validation\"],\n",
|
| 898 |
-
" peft_config=peft_config,\n",
|
| 899 |
-
" )\n",
|
| 900 |
-
" return trainer\n",
|
| 901 |
-
"\n",
|
| 902 |
-
"print_banner(\"Step 5 - Build trainer\")\n",
|
| 903 |
-
"trainer = build_trainer(model, tokenizer, dataset_dict, RUNTIME_DEVICE_BACKEND)\n",
|
| 904 |
-
"print(\"Trainer built successfully.\")"
|
| 905 |
-
]
|
| 906 |
-
},
|
| 907 |
-
{
|
| 908 |
-
"cell_type": "markdown",
|
| 909 |
-
"id": "b7892f31",
|
| 910 |
-
"metadata": {},
|
| 911 |
-
"source": [
|
| 912 |
-
"## Part 13 - Start training\n",
|
| 913 |
-
"\n",
|
| 914 |
-
"This section starts fine-tuning.\n",
|
| 915 |
-
"\n"
|
| 916 |
-
]
|
| 917 |
-
},
|
| 918 |
-
{
|
| 919 |
-
"cell_type": "code",
|
| 920 |
-
"execution_count": 24,
|
| 921 |
-
"id": "fee6890b",
|
| 922 |
-
"metadata": {},
|
| 923 |
-
"outputs": [
|
| 924 |
-
{
|
| 925 |
-
"name": "stdout",
|
| 926 |
-
"output_type": "stream",
|
| 927 |
-
"text": [
|
| 928 |
-
"\n",
|
| 929 |
-
"========================================================================================\n",
|
| 930 |
-
"Step 6 - Train\n",
|
| 931 |
-
"========================================================================================\n"
|
| 932 |
-
]
|
| 933 |
-
},
|
| 934 |
-
{
|
| 935 |
-
"data": {
|
| 936 |
-
"text/html": [
|
| 937 |
-
"\n",
|
| 938 |
-
" <div>\n",
|
| 939 |
-
" \n",
|
| 940 |
-
" <progress value='132' max='132' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
|
| 941 |
-
" [132/132 11:01, Epoch 6/6]\n",
|
| 942 |
-
" </div>\n",
|
| 943 |
-
" <table border=\"1\" class=\"dataframe\">\n",
|
| 944 |
-
" <thead>\n",
|
| 945 |
-
" <tr style=\"text-align: left;\">\n",
|
| 946 |
-
" <th>Step</th>\n",
|
| 947 |
-
" <th>Training Loss</th>\n",
|
| 948 |
-
" <th>Validation Loss</th>\n",
|
| 949 |
-
" </tr>\n",
|
| 950 |
-
" </thead>\n",
|
| 951 |
-
" <tbody>\n",
|
| 952 |
-
" <tr>\n",
|
| 953 |
-
" <td>50</td>\n",
|
| 954 |
-
" <td>0.300081</td>\n",
|
| 955 |
-
" <td>1.132511</td>\n",
|
| 956 |
-
" </tr>\n",
|
| 957 |
-
" <tr>\n",
|
| 958 |
-
" <td>100</td>\n",
|
| 959 |
-
" <td>0.025233</td>\n",
|
| 960 |
-
" <td>1.302579</td>\n",
|
| 961 |
-
" </tr>\n",
|
| 962 |
-
" </tbody>\n",
|
| 963 |
-
"</table><p>"
|
| 964 |
-
],
|
| 965 |
-
"text/plain": [
|
| 966 |
-
"<IPython.core.display.HTML object>"
|
| 967 |
-
]
|
| 968 |
-
},
|
| 969 |
-
"metadata": {},
|
| 970 |
-
"output_type": "display_data"
|
| 971 |
-
},
|
| 972 |
-
{
|
| 973 |
-
"name": "stdout",
|
| 974 |
-
"output_type": "stream",
|
| 975 |
-
"text": [
|
| 976 |
-
"Train metrics:\n",
|
| 977 |
-
"{\n",
|
| 978 |
-
" \"train_runtime\": 666.4624,\n",
|
| 979 |
-
" \"train_samples_per_second\": 3.142,\n",
|
| 980 |
-
" \"train_steps_per_second\": 0.198,\n",
|
| 981 |
-
" \"total_flos\": 1.4419846776152064e+16,\n",
|
| 982 |
-
" \"train_loss\": 0.26707774019715463\n",
|
| 983 |
-
"}\n",
|
| 984 |
-
"Adapter saved to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/lora_adapter\n",
|
| 985 |
-
"Train stage minutes: 11.13\n"
|
| 986 |
-
]
|
| 987 |
-
}
|
| 988 |
-
],
|
| 989 |
-
"source": [
|
| 990 |
-
"print_banner(\"Step 6 - Train\")\n",
|
| 991 |
-
"train_start_time = time.time()\n",
|
| 992 |
-
"\n",
|
| 993 |
-
"train_result = trainer.train()\n",
|
| 994 |
-
"trainer.save_model(str(ADAPTER_OUTPUT_DIR))\n",
|
| 995 |
-
"tokenizer.save_pretrained(str(ADAPTER_OUTPUT_DIR))\n",
|
| 996 |
-
"\n",
|
| 997 |
-
"train_metrics = train_result.metrics\n",
|
| 998 |
-
"print(\"Train metrics:\")\n",
|
| 999 |
-
"print(json.dumps(train_metrics, indent=2, default=str))\n",
|
| 1000 |
-
"\n",
|
| 1001 |
-
"print(f\"Adapter saved to: {ADAPTER_OUTPUT_DIR}\")\n",
|
| 1002 |
-
"print(f\"Train stage minutes: {(time.time() - train_start_time)/60:.2f}\")"
|
| 1003 |
-
]
|
| 1004 |
-
},
|
| 1005 |
-
{
|
| 1006 |
-
"cell_type": "markdown",
|
| 1007 |
-
"id": "1a64c5b7",
|
| 1008 |
-
"metadata": {},
|
| 1009 |
-
"source": [
|
| 1010 |
-
"## Part 14 - Teacher-forced loss evaluation\n",
|
| 1011 |
-
"\n",
|
| 1012 |
-
"This section computes `eval_loss` and `perplexity` on the validation set.\n",
|
| 1013 |
-
"\n"
|
| 1014 |
-
]
|
| 1015 |
-
},
|
| 1016 |
-
{
|
| 1017 |
-
"cell_type": "code",
|
| 1018 |
-
"execution_count": 27,
|
| 1019 |
-
"id": "7ceb96b4",
|
| 1020 |
-
"metadata": {},
|
| 1021 |
-
"outputs": [
|
| 1022 |
-
{
|
| 1023 |
-
"name": "stdout",
|
| 1024 |
-
"output_type": "stream",
|
| 1025 |
-
"text": [
|
| 1026 |
-
"\n",
|
| 1027 |
-
"========================================================================================\n",
|
| 1028 |
-
"Step 7 - Evaluate teacher-forced loss\n",
|
| 1029 |
-
"========================================================================================\n",
|
| 1030 |
-
"Eval loss : 1.132510781288147\n",
|
| 1031 |
-
"Perplexity : 3.1034387822556253\n",
|
| 1032 |
-
"{'eval_loss': 1.132510781288147, 'eval_runtime': 2.8459, 'eval_samples_per_second': 13.704, 'eval_steps_per_second': 7.028}\n"
|
| 1033 |
-
]
|
| 1034 |
-
}
|
| 1035 |
-
],
|
| 1036 |
-
"source": [
|
| 1037 |
-
"print_banner(\"Step 7 - Evaluate teacher-forced loss\")\n",
|
| 1038 |
-
"\n",
|
| 1039 |
-
"# Remove notebook progress callback to avoid Jupyter evaluate callback error\n",
|
| 1040 |
-
"trainer.remove_callback(transformers.utils.notebook.NotebookProgressCallback)\n",
|
| 1041 |
-
"\n",
|
| 1042 |
-
"eval_metrics = trainer.evaluate()\n",
|
| 1043 |
-
"eval_loss = float(eval_metrics.get(\"eval_loss\", float(\"nan\")))\n",
|
| 1044 |
-
"perplexity = float(math.exp(min(eval_loss, 20))) if math.isfinite(eval_loss) else float(\"nan\")\n",
|
| 1045 |
-
"\n",
|
| 1046 |
-
"print(\"Eval loss :\", eval_loss)\n",
|
| 1047 |
-
"print(\"Perplexity :\", perplexity)\n",
|
| 1048 |
-
"print(eval_metrics)"
|
| 1049 |
-
]
|
| 1050 |
-
},
|
| 1051 |
-
{
|
| 1052 |
-
"cell_type": "markdown",
|
| 1053 |
-
"id": "0da67d71",
|
| 1054 |
-
"metadata": {},
|
| 1055 |
-
"source": [
|
| 1056 |
-
"## Part 15 - Generation evaluation functions\n",
|
| 1057 |
-
"\n",
|
| 1058 |
-
"This section defines generation-based evaluation functions and metric calculation.\n",
|
| 1059 |
-
"\n"
|
| 1060 |
-
]
|
| 1061 |
-
},
|
| 1062 |
-
{
|
| 1063 |
-
"cell_type": "code",
|
| 1064 |
-
"execution_count": 33,
|
| 1065 |
-
"id": "eff9034b",
|
| 1066 |
-
"metadata": {},
|
| 1067 |
-
"outputs": [],
|
| 1068 |
-
"source": [
|
| 1069 |
-
"def format_eval_prompt(tokenizer, question: str) -> str:\n",
|
| 1070 |
-
" messages = [\n",
|
| 1071 |
-
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 1072 |
-
" {\"role\": \"user\", \"content\": question},\n",
|
| 1073 |
-
" ]\n",
|
| 1074 |
-
" return tokenizer.apply_chat_template(\n",
|
| 1075 |
-
" messages,\n",
|
| 1076 |
-
" tokenize=False,\n",
|
| 1077 |
-
" add_generation_prompt=True,\n",
|
| 1078 |
-
" )\n",
|
| 1079 |
-
"\n",
|
| 1080 |
-
"@torch.inference_mode()\n",
|
| 1081 |
-
"def generate_answers(model, tokenizer, questions: List[str], max_new_tokens: int) -> List[str]:\n",
|
| 1082 |
-
" device = next(model.parameters()).device\n",
|
| 1083 |
-
" prompts = [format_eval_prompt(tokenizer, q) for q in questions]\n",
|
| 1084 |
-
" outputs: List[str] = []\n",
|
| 1085 |
-
"\n",
|
| 1086 |
-
" for prompt in prompts:\n",
|
| 1087 |
-
" encoded = tokenizer(\n",
|
| 1088 |
-
" prompt,\n",
|
| 1089 |
-
" return_tensors=\"pt\",\n",
|
| 1090 |
-
" truncation=True,\n",
|
| 1091 |
-
" max_length=MAX_SEQ_LENGTH,\n",
|
| 1092 |
-
" )\n",
|
| 1093 |
-
" encoded = {k: v.to(device) for k, v in encoded.items()}\n",
|
| 1094 |
-
"\n",
|
| 1095 |
-
" generated = model.generate(\n",
|
| 1096 |
-
" **encoded,\n",
|
| 1097 |
-
" max_new_tokens=max_new_tokens,\n",
|
| 1098 |
-
" do_sample=False,\n",
|
| 1099 |
-
" temperature=None,\n",
|
| 1100 |
-
" top_p=None,\n",
|
| 1101 |
-
" repetition_penalty=1.05,\n",
|
| 1102 |
-
" pad_token_id=tokenizer.pad_token_id,\n",
|
| 1103 |
-
" eos_token_id=tokenizer.eos_token_id,\n",
|
| 1104 |
-
" )\n",
|
| 1105 |
-
"\n",
|
| 1106 |
-
" gen_only = generated[0][encoded[\"input_ids\"].shape[1]:]\n",
|
| 1107 |
-
" text = tokenizer.decode(gen_only, skip_special_tokens=True)\n",
|
| 1108 |
-
" outputs.append(normalize_text(text))\n",
|
| 1109 |
-
"\n",
|
| 1110 |
-
" return outputs\n",
|
| 1111 |
-
"\n",
|
| 1112 |
-
"def compute_generation_metrics(predictions: List[str], references: List[str]) -> Dict[str, float]:\n",
|
| 1113 |
-
" import evaluate\n",
|
| 1114 |
-
" import sacrebleu\n",
|
| 1115 |
-
"\n",
|
| 1116 |
-
" rouge = evaluate.load(\"rouge\")\n",
|
| 1117 |
-
" \n",
|
| 1118 |
-
"\n",
|
| 1119 |
-
" rouge_scores = rouge.compute(predictions=predictions, references=references)\n",
|
| 1120 |
-
" \n",
|
| 1121 |
-
"\n",
|
| 1122 |
-
" sacrebleu_score = sacrebleu.corpus_bleu(predictions, [references]).score\n",
|
| 1123 |
-
" chrf_score = sacrebleu.corpus_chrf(predictions, [references], word_order=2).score\n",
|
| 1124 |
-
"\n",
|
| 1125 |
-
" em = float(np.mean([exact_match(p, r) for p, r in zip(predictions, references)]))\n",
|
| 1126 |
-
" tf1 = float(np.mean([token_f1(p, r) for p, r in zip(predictions, references)]))\n",
|
| 1127 |
-
" avg_pred_len = float(np.mean([len(p.split()) for p in predictions])) if predictions else 0.0\n",
|
| 1128 |
-
" avg_ref_len = float(np.mean([len(r.split()) for r in references])) if references else 0.0\n",
|
| 1129 |
-
"\n",
|
| 1130 |
-
" metrics = {\n",
|
| 1131 |
-
" \"exact_match\": em,\n",
|
| 1132 |
-
" \"token_f1\": tf1,\n",
|
| 1133 |
-
" \"rouge1\": float(rouge_scores[\"rouge1\"]),\n",
|
| 1134 |
-
" \"rouge2\": float(rouge_scores[\"rouge2\"]),\n",
|
| 1135 |
-
" \"rougeL\": float(rouge_scores[\"rougeL\"]),\n",
|
| 1136 |
-
" \"bertscore_f1\": None,\n",
|
| 1137 |
-
" \"sacrebleu\": float(sacrebleu_score),\n",
|
| 1138 |
-
" \"chrf_pp\": float(chrf_score),\n",
|
| 1139 |
-
" \"avg_prediction_words\": avg_pred_len,\n",
|
| 1140 |
-
" \"avg_reference_words\": avg_ref_len,\n",
|
| 1141 |
-
" }\n",
|
| 1142 |
-
" return metrics"
|
| 1143 |
-
]
|
| 1144 |
-
},
|
| 1145 |
-
{
|
| 1146 |
-
"cell_type": "markdown",
|
| 1147 |
-
"id": "d9298597",
|
| 1148 |
-
"metadata": {},
|
| 1149 |
-
"source": [
|
| 1150 |
-
"## Part 16 - Run final generation evaluation on the validation set\n",
|
| 1151 |
-
"\n",
|
| 1152 |
-
"This section generates answers on the validation set, computes metrics, saves predictions, and prints a few samples.\n",
|
| 1153 |
-
"\n"
|
| 1154 |
-
]
|
| 1155 |
-
},
|
| 1156 |
-
{
|
| 1157 |
-
"cell_type": "code",
|
| 1158 |
-
"execution_count": 35,
|
| 1159 |
-
"id": "995cd0ee",
|
| 1160 |
-
"metadata": {},
|
| 1161 |
-
"outputs": [
|
| 1162 |
-
{
|
| 1163 |
-
"name": "stdout",
|
| 1164 |
-
"output_type": "stream",
|
| 1165 |
-
"text": [
|
| 1166 |
-
"\n",
|
| 1167 |
-
"========================================================================================\n",
|
| 1168 |
-
"Step 8 - Final generation evaluation on validation split\n",
|
| 1169 |
-
"========================================================================================\n",
|
| 1170 |
-
"Generation metrics:\n",
|
| 1171 |
-
"{\n",
|
| 1172 |
-
" \"exact_match\": 0.0,\n",
|
| 1173 |
-
" \"token_f1\": 0.5181110282406411,\n",
|
| 1174 |
-
" \"rouge1\": 0.5171361078676141,\n",
|
| 1175 |
-
" \"rouge2\": 0.33460021476687485,\n",
|
| 1176 |
-
" \"rougeL\": 0.45557456154376447,\n",
|
| 1177 |
-
" \"bertscore_f1\": null,\n",
|
| 1178 |
-
" \"sacrebleu\": 31.010919781258593,\n",
|
| 1179 |
-
" \"chrf_pp\": 49.920320261813664,\n",
|
| 1180 |
-
" \"avg_prediction_words\": 36.0,\n",
|
| 1181 |
-
" \"avg_reference_words\": 36.69230769230769\n",
|
| 1182 |
-
"}\n",
|
| 1183 |
-
"\n",
|
| 1184 |
-
"========================================================================================\n",
|
| 1185 |
-
"Sample validation predictions\n",
|
| 1186 |
-
"========================================================================================\n",
|
| 1187 |
-
"\n",
|
| 1188 |
-
"[Q] What courses are listed under programme core courses?\n",
|
| 1189 |
-
"[REF] The handbook presents programme core courses as a curriculum table listing the approved course codes, course titles, credit values, and semester arrangement for the programme core component.\n",
|
| 1190 |
-
"[PRED] <think> </think> The handbook presents programme core courses as a curriculum table listing the approved course codes, course titles, credit values, and semester arrangement for that part of the programme.\n",
|
| 1191 |
-
"[EM=0, TokenF1=0.8772]\n",
|
| 1192 |
-
"\n",
|
| 1193 |
-
"[Q] What responsibilities do supervisors have for guidance, feedback, and assessment in the academic project?\n",
|
| 1194 |
-
"[REF] Supervisors are responsible for assigning or confirming project titles in the ilmiah system, supervising and coaching students, meeting them regularly, verifying the progress logbook, evaluating reports, and entering marks in the ilmiah system. More broadly, the handbook frames supervisors as one of the key parties responsible for guidance, monitoring, feedback, and assessment in the academic project process.\n",
|
| 1195 |
-
"[PRED] <think> </think> Supervisors are responsible for providing regular guidance, giving feedback on progress and submissions, carrying out viva or panel-based assessment where applicable, and submitting the final report through the ilmiah system for administration to issue the mark.\n",
|
| 1196 |
-
"[EM=0, TokenF1=0.3542]\n",
|
| 1197 |
-
"\n",
|
| 1198 |
-
"[Q] What information is given about the faculty cafeteria?\n",
|
| 1199 |
-
"[REF] The cafeteria is located at the back of Block A.\n",
|
| 1200 |
-
"[PRED] <think> </think> The handbook states that the faculty cafeteria serves both staff and students and that it is open to all UM students.\n",
|
| 1201 |
-
"[EM=0, TokenF1=0.2424]\n",
|
| 1202 |
-
"\n",
|
| 1203 |
-
"[Q] What dress expectations are illustrated for official events?\n",
|
| 1204 |
-
"[REF] For official events, the poster illustrates formal attire, including suit-style clothing and traditional formal wear, to convey a neat and official appearance appropriate for formal university occasions.\n",
|
| 1205 |
-
"[PRED] <think> </think> For official events, men are expected to follow formal or semi-formal Western business attire, while women should also aim for formal or appropriate Western office or ceremonial clothing.\n",
|
| 1206 |
-
"[EM=0, TokenF1=0.3729]\n",
|
| 1207 |
-
"\n",
|
| 1208 |
-
"[Q] What courses are listed under specialization elective courses - artificial intelligence?\n",
|
| 1209 |
-
"[REF] The handbook presents the Artificial Intelligence specialization electives as a curriculum table listing the approved course codes, course titles, credit values, and semester arrangement for that specialization.\n",
|
| 1210 |
-
"[PRED] <think> </think> The specialization elective section is intended to show the elective pool available for that track. Students should use it as a selection list of approved course codes they can choose from, following the shown curriculum structure and any stated university or faculty rules for that programme.\n",
|
| 1211 |
-
"[EM=0, TokenF1=0.3467]\n"
|
| 1212 |
-
]
|
| 1213 |
-
}
|
| 1214 |
-
],
|
| 1215 |
-
"source": [
|
| 1216 |
-
"print_banner(\"Step 8 - Final generation evaluation on validation split\")\n",
|
| 1217 |
-
"\n",
|
| 1218 |
-
"final_metrics = {\n",
|
| 1219 |
-
" \"teacher_forced_eval\": eval_metrics,\n",
|
| 1220 |
-
" \"perplexity\": perplexity,\n",
|
| 1221 |
-
"}\n",
|
| 1222 |
-
"\n",
|
| 1223 |
-
"prediction_rows = []\n",
|
| 1224 |
-
"\n",
|
| 1225 |
-
"validation_questions = dataset_dict[\"validation\"][\"question\"]\n",
|
| 1226 |
-
"validation_answers = dataset_dict[\"validation\"][\"answer\"]\n",
|
| 1227 |
-
"\n",
|
| 1228 |
-
"predictions = generate_answers(\n",
|
| 1229 |
-
" model=trainer.model,\n",
|
| 1230 |
-
" tokenizer=tokenizer,\n",
|
| 1231 |
-
" questions=validation_questions,\n",
|
| 1232 |
-
" max_new_tokens=MAX_NEW_TOKENS_EVAL,\n",
|
| 1233 |
-
")\n",
|
| 1234 |
-
"\n",
|
| 1235 |
-
"generation_metrics = compute_generation_metrics(predictions, validation_answers)\n",
|
| 1236 |
-
"final_metrics[\"generation_metrics\"] = generation_metrics\n",
|
| 1237 |
-
"\n",
|
| 1238 |
-
"for i, (question, reference, prediction) in enumerate(\n",
|
| 1239 |
-
" zip(validation_questions, validation_answers, predictions)\n",
|
| 1240 |
-
"):\n",
|
| 1241 |
-
" prediction_rows.append(\n",
|
| 1242 |
-
" {\n",
|
| 1243 |
-
" \"row_id\": i,\n",
|
| 1244 |
-
" \"question\": question,\n",
|
| 1245 |
-
" \"reference_answer\": reference,\n",
|
| 1246 |
-
" \"predicted_answer\": prediction,\n",
|
| 1247 |
-
" \"exact_match\": exact_match(prediction, reference),\n",
|
| 1248 |
-
" \"token_f1\": token_f1(prediction, reference),\n",
|
| 1249 |
-
" }\n",
|
| 1250 |
-
" )\n",
|
| 1251 |
-
"\n",
|
| 1252 |
-
"save_predictions_jsonl(PREDICTIONS_JSONL_PATH, prediction_rows)\n",
|
| 1253 |
-
"\n",
|
| 1254 |
-
"print(\"Generation metrics:\")\n",
|
| 1255 |
-
"print(json.dumps(generation_metrics, indent=2, ensure_ascii=False))\n",
|
| 1256 |
-
"\n",
|
| 1257 |
-
"print_banner(\"Sample validation predictions\")\n",
|
| 1258 |
-
"for row in prediction_rows[:NUM_PRINTED_PREDICTIONS]:\n",
|
| 1259 |
-
" print(f\"\\n[Q] {row['question']}\")\n",
|
| 1260 |
-
" print(f\"[REF] {row['reference_answer']}\")\n",
|
| 1261 |
-
" print(f\"[PRED] {row['predicted_answer']}\")\n",
|
| 1262 |
-
" print(f\"[EM={row['exact_match']:.0f}, TokenF1={row['token_f1']:.4f}]\")"
|
| 1263 |
-
]
|
| 1264 |
-
},
|
| 1265 |
-
{
|
| 1266 |
-
"cell_type": "markdown",
|
| 1267 |
-
"id": "255eb7de",
|
| 1268 |
-
"metadata": {},
|
| 1269 |
-
"source": [
|
| 1270 |
-
"## Part 17 - Save metrics\n",
|
| 1271 |
-
"\n",
|
| 1272 |
-
"This section writes the current metrics to JSON.\n",
|
| 1273 |
-
"\n"
|
| 1274 |
-
]
|
| 1275 |
-
},
|
| 1276 |
-
{
|
| 1277 |
-
"cell_type": "code",
|
| 1278 |
-
"execution_count": 36,
|
| 1279 |
-
"id": "ebd241d3",
|
| 1280 |
-
"metadata": {},
|
| 1281 |
-
"outputs": [
|
| 1282 |
-
{
|
| 1283 |
-
"name": "stdout",
|
| 1284 |
-
"output_type": "stream",
|
| 1285 |
-
"text": [
|
| 1286 |
-
"Metrics saved to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/final_metrics.json\n",
|
| 1287 |
-
"Predictions saved to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/validation_predictions.jsonl\n"
|
| 1288 |
-
]
|
| 1289 |
-
}
|
| 1290 |
-
],
|
| 1291 |
-
"source": [
|
| 1292 |
-
"save_json(METRICS_JSON_PATH, final_metrics)\n",
|
| 1293 |
-
"print(f\"Metrics saved to: {METRICS_JSON_PATH}\")\n",
|
| 1294 |
-
"print(f\"Predictions saved to: {PREDICTIONS_JSONL_PATH}\")"
|
| 1295 |
-
]
|
| 1296 |
-
},
|
| 1297 |
-
{
|
| 1298 |
-
"cell_type": "markdown",
|
| 1299 |
-
"id": "429b8fea",
|
| 1300 |
-
"metadata": {},
|
| 1301 |
-
"source": [
|
| 1302 |
-
"## Part 18 - Merge the LoRA adapter and export the final model\n",
|
| 1303 |
-
"\n",
|
| 1304 |
-
"This section reloads the base model, merges the LoRA adapter, saves the merged model directory, and optionally exports a `.pt` file.\n",
|
| 1305 |
-
"\n"
|
| 1306 |
-
]
|
| 1307 |
-
},
|
| 1308 |
-
{
|
| 1309 |
-
"cell_type": "code",
|
| 1310 |
-
"execution_count": 37,
|
| 1311 |
-
"id": "720f1089",
|
| 1312 |
-
"metadata": {},
|
| 1313 |
-
"outputs": [
|
| 1314 |
-
{
|
| 1315 |
-
"name": "stdout",
|
| 1316 |
-
"output_type": "stream",
|
| 1317 |
-
"text": [
|
| 1318 |
-
"\n",
|
| 1319 |
-
"========================================================================================\n",
|
| 1320 |
-
"Step 9 - Save merged model\n",
|
| 1321 |
-
"========================================================================================\n"
|
| 1322 |
-
]
|
| 1323 |
-
},
|
| 1324 |
-
{
|
| 1325 |
-
"data": {
|
| 1326 |
-
"application/vnd.jupyter.widget-view+json": {
|
| 1327 |
-
"model_id": "00dc5873f54b4054853f7908bd366489",
|
| 1328 |
-
"version_major": 2,
|
| 1329 |
-
"version_minor": 0
|
| 1330 |
-
},
|
| 1331 |
-
"text/plain": [
|
| 1332 |
-
"Loading weights: 0%| | 0/399 [00:00<?, ?it/s]"
|
| 1333 |
-
]
|
| 1334 |
-
},
|
| 1335 |
-
"metadata": {},
|
| 1336 |
-
"output_type": "display_data"
|
| 1337 |
-
},
|
| 1338 |
-
{
|
| 1339 |
-
"data": {
|
| 1340 |
-
"application/vnd.jupyter.widget-view+json": {
|
| 1341 |
-
"model_id": "5c8f48a945a84fa5b75150c6cb3939d6",
|
| 1342 |
-
"version_major": 2,
|
| 1343 |
-
"version_minor": 0
|
| 1344 |
-
},
|
| 1345 |
-
"text/plain": [
|
| 1346 |
-
"Writing model shards: 0%| | 0/1 [00:00<?, ?it/s]"
|
| 1347 |
-
]
|
| 1348 |
-
},
|
| 1349 |
-
"metadata": {},
|
| 1350 |
-
"output_type": "display_data"
|
| 1351 |
-
},
|
| 1352 |
-
{
|
| 1353 |
-
"name": "stdout",
|
| 1354 |
-
"output_type": "stream",
|
| 1355 |
-
"text": [
|
| 1356 |
-
"Merged model saved to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/merged_model\n",
|
| 1357 |
-
"\n",
|
| 1358 |
-
"========================================================================================\n",
|
| 1359 |
-
"Saving single .pt state_dict export\n",
|
| 1360 |
-
"========================================================================================\n",
|
| 1361 |
-
"Saved .pt file to: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/Qwen3-8B-Instruct_UM_Handbook.pt\n"
|
| 1362 |
-
]
|
| 1363 |
-
}
|
| 1364 |
-
],
|
| 1365 |
-
"source": [
|
| 1366 |
-
"def load_base_model_for_merge(model_path: Path, backend: str):\n",
|
| 1367 |
-
" compute_dtype = detect_compute_dtype(backend)\n",
|
| 1368 |
-
" model_kwargs = {\n",
|
| 1369 |
-
" \"pretrained_model_name_or_path\": str(model_path),\n",
|
| 1370 |
-
" \"torch_dtype\": compute_dtype,\n",
|
| 1371 |
-
" \"low_cpu_mem_usage\": LOW_CPU_MEM_USAGE,\n",
|
| 1372 |
-
" \"trust_remote_code\": False,\n",
|
| 1373 |
-
" }\n",
|
| 1374 |
-
" if backend == \"cuda\":\n",
|
| 1375 |
-
" model_kwargs[\"device_map\"] = \"auto\"\n",
|
| 1376 |
-
" model = AutoModelForCausalLM.from_pretrained(**model_kwargs)\n",
|
| 1377 |
-
" if backend in {\"mps\", \"cpu\"}:\n",
|
| 1378 |
-
" model = model.to(backend)\n",
|
| 1379 |
-
" return model\n",
|
| 1380 |
-
"\n",
|
| 1381 |
-
"def save_single_pt_state_dict(model, path: Path) -> None:\n",
|
| 1382 |
-
" print_banner(\"Saving single .pt state_dict export\")\n",
|
| 1383 |
-
" ensure_dir(path.parent)\n",
|
| 1384 |
-
"\n",
|
| 1385 |
-
" cpu_state_dict = {}\n",
|
| 1386 |
-
" for key, value in model.state_dict().items():\n",
|
| 1387 |
-
" cpu_state_dict[key] = value.detach().cpu()\n",
|
| 1388 |
-
"\n",
|
| 1389 |
-
" torch.save(\n",
|
| 1390 |
-
" {\n",
|
| 1391 |
-
" \"model_state_dict\": cpu_state_dict,\n",
|
| 1392 |
-
" \"base_model_name\": BASE_MODEL_NAME,\n",
|
| 1393 |
-
" \"system_prompt\": SYSTEM_PROMPT,\n",
|
| 1394 |
-
" \"max_seq_length\": MAX_SEQ_LENGTH,\n",
|
| 1395 |
-
" },\n",
|
| 1396 |
-
" str(path),\n",
|
| 1397 |
-
" )\n",
|
| 1398 |
-
" print(f\"Saved .pt file to: {path}\")\n",
|
| 1399 |
-
"\n",
|
| 1400 |
-
"print_banner(\"Step 9 - Save merged model\")\n",
|
| 1401 |
-
"cleanup_memory()\n",
|
| 1402 |
-
"\n",
|
| 1403 |
-
"if SAVE_MERGED_MODEL:\n",
|
| 1404 |
-
" base_model_for_merge = load_base_model_for_merge(local_model_path, RUNTIME_DEVICE_BACKEND)\n",
|
| 1405 |
-
" merged_model = PeftModel.from_pretrained(base_model_for_merge, str(ADAPTER_OUTPUT_DIR))\n",
|
| 1406 |
-
" merged_model = merged_model.merge_and_unload()\n",
|
| 1407 |
-
"\n",
|
| 1408 |
-
" ensure_dir(MERGED_MODEL_DIR)\n",
|
| 1409 |
-
" merged_model.save_pretrained(str(MERGED_MODEL_DIR), safe_serialization=True)\n",
|
| 1410 |
-
"\n",
|
| 1411 |
-
" if SAVE_TOKENIZER_WITH_MERGED:\n",
|
| 1412 |
-
" tokenizer.save_pretrained(str(MERGED_MODEL_DIR))\n",
|
| 1413 |
-
"\n",
|
| 1414 |
-
" print(f\"Merged model saved to: {MERGED_MODEL_DIR}\")\n",
|
| 1415 |
-
"\n",
|
| 1416 |
-
" if SAVE_SINGLE_PT:\n",
|
| 1417 |
-
" save_single_pt_state_dict(merged_model, FINAL_PT_PATH)\n",
|
| 1418 |
-
"\n",
|
| 1419 |
-
" del merged_model\n",
|
| 1420 |
-
" del base_model_for_merge\n",
|
| 1421 |
-
" cleanup_memory()"
|
| 1422 |
-
]
|
| 1423 |
-
},
|
| 1424 |
-
{
|
| 1425 |
-
"cell_type": "markdown",
|
| 1426 |
-
"id": "f2973f9b",
|
| 1427 |
-
"metadata": {},
|
| 1428 |
-
"source": [
|
| 1429 |
-
"## Part 19 - End-of-training summary\n",
|
| 1430 |
-
"\n",
|
| 1431 |
-
"This section prints the final output paths.\n",
|
| 1432 |
-
"\n"
|
| 1433 |
-
]
|
| 1434 |
-
},
|
| 1435 |
-
{
|
| 1436 |
-
"cell_type": "code",
|
| 1437 |
-
"execution_count": 38,
|
| 1438 |
-
"id": "6891902c",
|
| 1439 |
-
"metadata": {},
|
| 1440 |
-
"outputs": [
|
| 1441 |
-
{
|
| 1442 |
-
"name": "stdout",
|
| 1443 |
-
"output_type": "stream",
|
| 1444 |
-
"text": [
|
| 1445 |
-
"\n",
|
| 1446 |
-
"========================================================================================\n",
|
| 1447 |
-
"Done\n",
|
| 1448 |
-
"========================================================================================\n",
|
| 1449 |
-
"Selected backend: cuda\n",
|
| 1450 |
-
"Adapter directory: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/lora_adapter\n",
|
| 1451 |
-
"Merged model directory: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/merged_model\n",
|
| 1452 |
-
"Single .pt file: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/Qwen3-8B-Instruct_UM_Handbook.pt\n",
|
| 1453 |
-
"Metrics JSON: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/final_metrics.json\n",
|
| 1454 |
-
"Predictions JSONL: /scr/user/kevin2002/TensorCat/NLP/UM_Handbook/outputs/qwen3_um_handbook/validation_predictions.jsonl\n"
|
| 1455 |
-
]
|
| 1456 |
-
}
|
| 1457 |
-
],
|
| 1458 |
-
"source": [
|
| 1459 |
-
"total_runtime_minutes = None\n",
|
| 1460 |
-
"try:\n",
|
| 1461 |
-
" # 如果 notebook 从头开始运行,这个变量就存在\n",
|
| 1462 |
-
" total_runtime_minutes = \"See notebook runtime from execution order / timestamps\"\n",
|
| 1463 |
-
"except Exception:\n",
|
| 1464 |
-
" pass\n",
|
| 1465 |
-
"\n",
|
| 1466 |
-
"final_metrics[\"completion_note\"] = \"Notebook execution completed.\"\n",
|
| 1467 |
-
"save_json(METRICS_JSON_PATH, final_metrics)\n",
|
| 1468 |
-
"\n",
|
| 1469 |
-
"print_banner(\"Done\")\n",
|
| 1470 |
-
"print(f\"Selected backend: {RUNTIME_DEVICE_BACKEND}\")\n",
|
| 1471 |
-
"print(f\"Adapter directory: {ADAPTER_OUTPUT_DIR}\")\n",
|
| 1472 |
-
"print(f\"Merged model directory: {MERGED_MODEL_DIR}\")\n",
|
| 1473 |
-
"print(f\"Single .pt file: {FINAL_PT_PATH if SAVE_SINGLE_PT else 'disabled'}\")\n",
|
| 1474 |
-
"print(f\"Metrics JSON: {METRICS_JSON_PATH}\")\n",
|
| 1475 |
-
"print(f\"Predictions JSONL: {PREDICTIONS_JSONL_PATH}\")"
|
| 1476 |
-
]
|
| 1477 |
-
},
|
| 1478 |
-
{
|
| 1479 |
-
"cell_type": "markdown",
|
| 1480 |
-
"id": "e35a1ca8",
|
| 1481 |
-
"metadata": {},
|
| 1482 |
-
"source": [
|
| 1483 |
-
"## Part 20 - Result inspection\n",
|
| 1484 |
-
"\n",
|
| 1485 |
-
"Check these files after training:\n",
|
| 1486 |
-
"\n",
|
| 1487 |
-
"### 1. `final_metrics.json`\n",
|
| 1488 |
-
"Review the overall metrics.\n",
|
| 1489 |
-
"\n",
|
| 1490 |
-
"### 2. `validation_predictions.jsonl`\n",
|
| 1491 |
-
"Inspect generated answers against the reference answers.\n",
|
| 1492 |
-
"\n",
|
| 1493 |
-
"### 3. `merged_model/`\n",
|
| 1494 |
-
"Use this directory for standard Hugging Face loading.\n",
|
| 1495 |
-
"\n",
|
| 1496 |
-
"### 4. `Qwen3-8B-Instruct_UM_Handbook.pt`\n",
|
| 1497 |
-
"This is the optional single-file export.\n",
|
| 1498 |
-
"\n"
|
| 1499 |
-
]
|
| 1500 |
-
},
|
| 1501 |
-
{
|
| 1502 |
-
"cell_type": "code",
|
| 1503 |
-
"execution_count": null,
|
| 1504 |
-
"id": "91778773",
|
| 1505 |
-
"metadata": {},
|
| 1506 |
-
"outputs": [],
|
| 1507 |
-
"source": []
|
| 1508 |
-
}
|
| 1509 |
-
],
|
| 1510 |
-
"metadata": {
|
| 1511 |
-
"kernelspec": {
|
| 1512 |
-
"display_name": "Python (TensorCat Py3.10)",
|
| 1513 |
-
"language": "python",
|
| 1514 |
-
"name": "tensorcat-py310"
|
| 1515 |
-
},
|
| 1516 |
-
"language_info": {
|
| 1517 |
-
"codemirror_mode": {
|
| 1518 |
-
"name": "ipython",
|
| 1519 |
-
"version": 3
|
| 1520 |
-
},
|
| 1521 |
-
"file_extension": ".py",
|
| 1522 |
-
"mimetype": "text/x-python",
|
| 1523 |
-
"name": "python",
|
| 1524 |
-
"nbconvert_exporter": "python",
|
| 1525 |
-
"pygments_lexer": "ipython3",
|
| 1526 |
-
"version": "3.10.14"
|
| 1527 |
-
}
|
| 1528 |
-
},
|
| 1529 |
-
"nbformat": 4,
|
| 1530 |
-
"nbformat_minor": 5
|
| 1531 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|