已合并
【课程社区共建】新增大语言模型微调实战课程框架 #69
pescn创建于 5月12日
【课程社区共建】新增大语言模型微调实战课程框架 #69
已合并
共 10 个文件变更+1435-0
| @@ -0,0 +1,26 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# 第 01 章 · 大语言模型基础理论介绍 | ||
| 12 | + | ||
| 13 | +## 章节定位 | ||
| 14 | + | ||
| 15 | +本章面向**完全没有大模型背景**的同学,建立后续 02-05 章所需的基础知识与术语体系。 | ||
| 16 | +预期读者在学完本章后,能看懂后续章节里出现的 token、Transformer、self-attention、KV cache、SFT、LoRA、RLHF 等术语,知道它们大致在解决什么问题。 | ||
| 17 | + | ||
| 18 | +## 计划节次 | ||
| 19 | + | ||
| 20 | +| 节次 | 标题(暂定) | 状态 | | ||
| 21 | +|------|---------------|------| | ||
| 22 | +| 01.01 | 章节简介与学习路径 | 建设中 | | ||
| 23 | +| 01.02 | 从词向量到 Transformer | 建设中 | | ||
| 24 | +| 01.03 | 自注意力机制与位置编码 | 建设中 | | ||
| 25 | +| 01.04 | 预训练、SFT、RLHF 三段式流程 | 建设中 | | ||
| 26 | +| 01.05 | 推理过程与 KV cache | 建设中 | | ||
| @@ -0,0 +1,544 @@ | |||
| 1 | +{ | ||
| 2 | + "cells": [ | ||
| 3 | + { | ||
| 4 | + "cell_type": "markdown", | ||
| 5 | + "id": "16cfc8a9-8593-4b8e-8d8b-4433c547bf8f", | ||
| 6 | + "metadata": {}, | ||
| 7 | + "source": [ | ||
| 8 | + "# Qwen3 大模型指令微调实战——通过训练,让模型具备问答聊天的能力\n", | ||
| 9 | + "\n", | ||
| 10 | + "以Qwen3作为基座大模型,通过参数微调的方式,实现垂直专业领域聊天,甚至支持DeepSeek R1 / QwQ式的带推理过程的对话,是学习LLM微调的入门任务。\n", | ||
| 11 | + "\n", | ||
| 12 | + "## 使用Qwen进行微调\n", | ||
| 13 | + "\n", | ||
| 14 | + "要想将Qwen3完成微调,我们要进行三个步骤:\n", | ||
| 15 | + "\n", | ||
| 16 | + "1. 将模型加载进 NPU\n", | ||
| 17 | + "2. 下载数据集并调整成正确格式\n", | ||
| 18 | + "3. 运行模型训练\n" | ||
| 19 | + ] | ||
| 20 | + }, | ||
| 21 | + { | ||
| 22 | + "cell_type": "markdown", | ||
| 23 | + "id": "52258740", | ||
| 24 | + "metadata": {}, | ||
| 25 | + "source": [ | ||
| 26 | + "## 环境准备\n", | ||
| 27 | + "\n", | ||
| 28 | + "本 Notebook 在 CANNLab 的 Ascend NPU 环境下运行。CANNLab 默认镜像已预装 `torch` 与 `torch_npu`,下方 cell 仅安装训练所需的上层依赖。\n", | ||
| 29 | + "\n", | ||
| 30 | + "> **首次运行**:直接执行下方 cell 即可,安装完成后无需重启 kernel。\n", | ||
| 31 | + ">\n", | ||
| 32 | + "> **重跑场景**:如果环境里已经残留过旧版 `transformers` / `trl` 且本 Notebook 之前 import 过它们,安装后请点 `Kernel → Restart`,否则新版本不会生效。" | ||
| 33 | + ] | ||
| 34 | + }, | ||
| 35 | + { | ||
| 36 | + "cell_type": "code", | ||
| 37 | + "execution_count": null, | ||
| 38 | + "id": "e7ad9c46", | ||
| 39 | + "metadata": {}, | ||
| 40 | + "outputs": [], | ||
| 41 | + "source": [ | ||
| 42 | + "# 1. 安装训练所需依赖\n", | ||
| 43 | + "%pip install -q -i https://pypi.tuna.tsinghua.edu.cn/simple modelscope==1.35.4 transformers==5.5.4 trl==1.2.0 datasets==4.8.4 swanlab==0.7.15 accelerate==1.13.0\n", | ||
| 44 | + "\n", | ||
| 45 | + "# 2. 注册 Ascend NPU 后端:torch_npu 由 CANNLab 镜像预装,只需 import 即可让 PyTorch 识别 NPU\n", | ||
| 46 | + "import torch\n", | ||
| 47 | + "import torch_npu # noqa: F401\n", | ||
| 48 | + "\n", | ||
| 49 | + "print(f\"torch: {torch.__version__}\")\n", | ||
| 50 | + "print(f\"NPU 可用: {torch.npu.is_available()}, 卡数: {torch.npu.device_count()}\")" | ||
| 51 | + ] | ||
| 52 | + }, | ||
| 53 | + { | ||
| 54 | + "cell_type": "markdown", | ||
| 55 | + "id": "b779ced8-a6c8-4062-8edb-a0c4245f51a4", | ||
| 56 | + "metadata": {}, | ||
| 57 | + "source": [ | ||
| 58 | + "## 下载并加载基座模型\n", | ||
| 59 | + "\n", | ||
| 60 | + "首先我们需要从 ModelScope 中下载基座模型 Qwen3-0.6B-Base" | ||
| 61 | + ] | ||
| 62 | + }, | ||
| 63 | + { | ||
| 64 | + "cell_type": "code", | ||
| 65 | + "execution_count": null, | ||
| 66 | + "id": "49129e5c-7bd2-4eb8-81ad-de38e25a0923", | ||
| 67 | + "metadata": { | ||
| 68 | + "scrolled": true, | ||
| 69 | + "tags": [] | ||
| 70 | + }, | ||
| 71 | + "outputs": [], | ||
| 72 | + "source": [ | ||
| 73 | + "!modelscope download --model Qwen/Qwen3-0.6B-Base --local_dir ./Qwen3-0.6B-Base" | ||
| 74 | + ] | ||
| 75 | + }, | ||
| 76 | + { | ||
| 77 | + "cell_type": "markdown", | ||
| 78 | + "id": "b1df7107-87ab-465b-8071-b5d4b5f5e23a", | ||
| 79 | + "metadata": {}, | ||
| 80 | + "source": [ | ||
| 81 | + "接下来,我们可以将下载好的模型加载到 NPU 的显存中,同时,我们可以观察到 HBM 占用率的上升" | ||
| 82 | + ] | ||
| 83 | + }, | ||
| 84 | + { | ||
| 85 | + "cell_type": "code", | ||
| 86 | + "execution_count": null, | ||
| 87 | + "id": "b6b275a0-e8da-4107-a6db-a4e748299262", | ||
| 88 | + "metadata": {}, | ||
| 89 | + "outputs": [], | ||
| 90 | + "source": [ | ||
| 91 | + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", | ||
| 92 | + "\n", | ||
| 93 | + "MODEL_PATH=\"./Qwen3-0.6B-Base\" # 模型路径\n", | ||
| 94 | + "\n", | ||
| 95 | + "tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)\n", | ||
| 96 | + "model = AutoModelForCausalLM.from_pretrained(\n", | ||
| 97 | + " MODEL_PATH,\n", | ||
| 98 | + " torch_dtype=\"auto\",\n", | ||
| 99 | + " device_map=\"auto\"\n", | ||
| 100 | + ")" | ||
| 101 | + ] | ||
| 102 | + }, | ||
| 103 | + { | ||
| 104 | + "cell_type": "markdown", | ||
| 105 | + "id": "3c3e2b45-2274-4224-a287-7a9a618b3740", | ||
| 106 | + "metadata": {}, | ||
| 107 | + "source": [ | ||
| 108 | + "使用普通文本测试一下模型是否成功加载了" | ||
| 109 | + ] | ||
| 110 | + }, | ||
| 111 | + { | ||
| 112 | + "cell_type": "code", | ||
| 113 | + "execution_count": null, | ||
| 114 | + "id": "d1e961c8-69e1-4596-b300-7b7fe8562bd9", | ||
| 115 | + "metadata": {}, | ||
| 116 | + "outputs": [], | ||
| 117 | + "source": [ | ||
| 118 | + "\n", | ||
| 119 | + "# 输入的文本⬇️\n", | ||
| 120 | + "text=\"Python 是一种\" # 普通文本——Base 模型擅长续写陈述性内容\n", | ||
| 121 | + "\n", | ||
| 122 | + "# 模型推理代码⬇️\n", | ||
| 123 | + "model_inputs = tokenizer([text], return_tensors=\"pt\").to(model.device)\n", | ||
| 124 | + "generation_ids = model.generate(**model_inputs,max_new_tokens=128)\n", | ||
| 125 | + "content = tokenizer.decode(generation_ids[0], skip_special_tokens=True)\n", | ||
| 126 | + "print(content)\n" | ||
| 127 | + ] | ||
| 128 | + }, | ||
| 129 | + { | ||
| 130 | + "cell_type": "markdown", | ||
| 131 | + "id": "b1600802-2498-48d2-98e8-e0047f9f481a", | ||
| 132 | + "metadata": {}, | ||
| 133 | + "source": [ | ||
| 134 | + "使用聊天文本测试模型能力\n", | ||
| 135 | + "\n", | ||
| 136 | + "> ⚠️ **预期结果会很怪**:当前模型是 `Qwen3-0.6B-Base`,**没有经过指令微调**,不知道如何按对话格式回答。下面这个 cell 的输出多半会是杂乱的续写或重复文字——这正是接下来我们要做 SFT 训练的原因。" | ||
| 137 | + ] | ||
| 138 | + }, | ||
| 139 | + { | ||
| 140 | + "cell_type": "code", | ||
| 141 | + "execution_count": null, | ||
| 142 | + "id": "abc289bf-15c1-464f-bf9e-0ec76d74586c", | ||
| 143 | + "metadata": {}, | ||
| 144 | + "outputs": [], | ||
| 145 | + "source": [ | ||
| 146 | + "# 输入聊天文本⬇️\n", | ||
| 147 | + "messages = [\n", | ||
| 148 | + " {\"role\": \"user\", \"content\": \"什么是机器学习?\"},\n", | ||
| 149 | + "]\n", | ||
| 150 | + "\n", | ||
| 151 | + "# 模型推理代码⬇️\n", | ||
| 152 | + "text=tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True,enable_thinking=False)\n", | ||
| 153 | + "model_inputs = tokenizer([text], return_tensors=\"pt\").to(model.device)\n", | ||
| 154 | + "generation_ids = model.generate(**model_inputs,max_new_tokens=128)\n", | ||
| 155 | + "content = tokenizer.decode(generation_ids[0], skip_special_tokens=True)\n", | ||
| 156 | + "print(content)" | ||
| 157 | + ] | ||
| 158 | + }, | ||
| 159 | + { | ||
| 160 | + "cell_type": "markdown", | ||
| 161 | + "id": "5cce8b14-c1e4-4dbe-8eee-9985f4b489fc", | ||
| 162 | + "metadata": {}, | ||
| 163 | + "source": [ | ||
| 164 | + "---\n", | ||
| 165 | + "\n", | ||
| 166 | + "## 加载我们要训练的数据集\n", | ||
| 167 | + "\n", | ||
| 168 | + "Alpaca_zh 是面向中文场景的指令监督微调数据集,基于斯坦福大学 Alpaca 项目的核心结构设计,通过本地化改造适配中文语言习惯与任务需求。该数据集遵循 Alpaca 格式标准,每条数据包含必填的 instruction(任务指令)和 output(模型期望回答),以及可选的 input(任务输入)与 history(多轮对话历史)字段,其核心价值在于通过 指令-输入-输出 的强关联结构,优化大模型对中文任务的理解与执行能力。数据源包含从 Alpaca-3.5-en 翻译的 52K 指令样本,并融合中文社区优化的问答对,覆盖翻译、推理、摘要等场景。\n", | ||
| 169 | + "\n", | ||
| 170 | + "数据集下载地址:\n", | ||
| 171 | + "\n", | ||
| 172 | + "* Aplaca_zh数据集下载链接:https://www.modelscope.cn/datasets/llamafactory/alpaca_zh/summary" | ||
| 173 | + ] | ||
| 174 | + }, | ||
| 175 | + { | ||
| 176 | + "cell_type": "markdown", | ||
| 177 | + "id": "742d9f3f-12ce-4ed0-a4a3-7d65b9acbf20", | ||
| 178 | + "metadata": {}, | ||
| 179 | + "source": [ | ||
| 180 | + "下载数据集命令" | ||
| 181 | + ] | ||
| 182 | + }, | ||
| 183 | + { | ||
| 184 | + "cell_type": "code", | ||
| 185 | + "execution_count": null, | ||
| 186 | + "id": "07b03ea5-f8b7-418f-9734-63455dce201b", | ||
| 187 | + "metadata": { | ||
| 188 | + "scrolled": true | ||
| 189 | + }, | ||
| 190 | + "outputs": [], | ||
| 191 | + "source": [ | ||
| 192 | + "!modelscope download --dataset llamafactory/alpaca_zh --local_dir ./alpaca_zh" | ||
| 193 | + ] | ||
| 194 | + }, | ||
| 195 | + { | ||
| 196 | + "cell_type": "markdown", | ||
| 197 | + "id": "6852c507-2dfc-4899-83e8-21ce39e5471e", | ||
| 198 | + "metadata": {}, | ||
| 199 | + "source": [ | ||
| 200 | + "### 读取数据集" | ||
| 201 | + ] | ||
| 202 | + }, | ||
| 203 | + { | ||
| 204 | + "cell_type": "code", | ||
| 205 | + "execution_count": null, | ||
| 206 | + "id": "cd4dae53-6526-4a52-94fb-5092ab320cc2", | ||
| 207 | + "metadata": {}, | ||
| 208 | + "outputs": [], | ||
| 209 | + "source": [ | ||
| 210 | + "import datasets\n", | ||
| 211 | + "\n", | ||
| 212 | + "alpaca_data = datasets.load_dataset(\"json\", data_files=\"alpaca_zh/alpaca_data_zh_51k.json\")\n", | ||
| 213 | + "\n", | ||
| 214 | + "alpaca_data=alpaca_data[\"train\"]\n", | ||
| 215 | + "print(alpaca_data)" | ||
| 216 | + ] | ||
| 217 | + }, | ||
| 218 | + { | ||
| 219 | + "cell_type": "markdown", | ||
| 220 | + "id": "f313670d-411d-49e4-9e43-b21b66a056d4", | ||
| 221 | + "metadata": {}, | ||
| 222 | + "source": [ | ||
| 223 | + "展示一条数据" | ||
| 224 | + ] | ||
| 225 | + }, | ||
| 226 | + { | ||
| 227 | + "cell_type": "code", | ||
| 228 | + "execution_count": null, | ||
| 229 | + "id": "dde7deac-6943-44bd-9ae5-cc78d0eeb8c0", | ||
| 230 | + "metadata": {}, | ||
| 231 | + "outputs": [], | ||
| 232 | + "source": [ | ||
| 233 | + "print(alpaca_data[0][\"instruction\"])\n", | ||
| 234 | + "print(alpaca_data[0][\"input\"])\n", | ||
| 235 | + "print(alpaca_data[0][\"output\"])" | ||
| 236 | + ] | ||
| 237 | + }, | ||
| 238 | + { | ||
| 239 | + "cell_type": "markdown", | ||
| 240 | + "id": "0f4dfe4a-c925-4d31-a4f9-7c1afc8f6f1b", | ||
| 241 | + "metadata": {}, | ||
| 242 | + "source": [ | ||
| 243 | + "## 整理数据格式" | ||
| 244 | + ] | ||
| 245 | + }, | ||
| 246 | + { | ||
| 247 | + "cell_type": "markdown", | ||
| 248 | + "id": "d356e959-f0bb-47d4-a9e2-9ec9b80b88ab", | ||
| 249 | + "metadata": {}, | ||
| 250 | + "source": [ | ||
| 251 | + "将数据集拼接成我们之前模型能接受的聊天格式" | ||
| 252 | + ] | ||
| 253 | + }, | ||
| 254 | + { | ||
| 255 | + "cell_type": "code", | ||
| 256 | + "execution_count": null, | ||
| 257 | + "id": "7e67964c-d26d-4736-8ca6-5a43ecd7d5c1", | ||
| 258 | + "metadata": {}, | ||
| 259 | + "outputs": [], | ||
| 260 | + "source": [ | ||
| 261 | + "example=alpaca_data[0]\n", | ||
| 262 | + "\n", | ||
| 263 | + "user_text = example[\"instruction\"]+example[\"input\"]\n", | ||
| 264 | + "assistant_text = example[\"output\"]\n", | ||
| 265 | + "messages = [\n", | ||
| 266 | + " {\"role\": \"user\", \"content\": user_text},\n", | ||
| 267 | + " {\"role\": \"assistant\", \"content\": assistant_text},\n", | ||
| 268 | + "]\n", | ||
| 269 | + "text=tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=False)\n", | ||
| 270 | + "print(text)" | ||
| 271 | + ] | ||
| 272 | + }, | ||
| 273 | + { | ||
| 274 | + "cell_type": "markdown", | ||
| 275 | + "id": "13e4c34b-9e0d-4593-9287-ec87f6236eb8", | ||
| 276 | + "metadata": {}, | ||
| 277 | + "source": [ | ||
| 278 | + "#### 通过函数对数据集进行批量处理" | ||
| 279 | + ] | ||
| 280 | + }, | ||
| 281 | + { | ||
| 282 | + "cell_type": "code", | ||
| 283 | + "execution_count": null, | ||
| 284 | + "id": "933ed965-cde4-4b4b-af36-5b5f80cd0683", | ||
| 285 | + "metadata": {}, | ||
| 286 | + "outputs": [], | ||
| 287 | + "source": [ | ||
| 288 | + "def format_data(example):\n", | ||
| 289 | + " user_text = example[\"instruction\"]+example[\"input\"]\n", | ||
| 290 | + " assistant_text = example[\"output\"]\n", | ||
| 291 | + " messages = [\n", | ||
| 292 | + " {\"role\": \"user\", \"content\": user_text},\n", | ||
| 293 | + " {\"role\": \"assistant\", \"content\": assistant_text},\n", | ||
| 294 | + " ]\n", | ||
| 295 | + " text=tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=False,enable_thinking=False)\n", | ||
| 296 | + " return {\"text\":text}\n", | ||
| 297 | + "\n", | ||
| 298 | + "format_alpaca_data=alpaca_data.map(format_data,remove_columns=alpaca_data.column_names)" | ||
| 299 | + ] | ||
| 300 | + }, | ||
| 301 | + { | ||
| 302 | + "cell_type": "code", | ||
| 303 | + "execution_count": null, | ||
| 304 | + "id": "3d0004cc-7fe7-498c-9ba2-2c65ba529f65", | ||
| 305 | + "metadata": {}, | ||
| 306 | + "outputs": [], | ||
| 307 | + "source": [ | ||
| 308 | + "print(format_alpaca_data[0][\"text\"])" | ||
| 309 | + ] | ||
| 310 | + }, | ||
| 311 | + { | ||
| 312 | + "cell_type": "markdown", | ||
| 313 | + "id": "8a438ff3-0142-458c-8c4d-a071803628ce", | ||
| 314 | + "metadata": {}, | ||
| 315 | + "source": [ | ||
| 316 | + "### 切分数据集" | ||
| 317 | + ] | ||
| 318 | + }, | ||
| 319 | + { | ||
| 320 | + "cell_type": "code", | ||
| 321 | + "execution_count": null, | ||
| 322 | + "id": "9673b01d-8ab5-48af-847f-1340f83dccdf", | ||
| 323 | + "metadata": {}, | ||
| 324 | + "outputs": [], | ||
| 325 | + "source": [ | ||
| 326 | + "train_test_data=format_alpaca_data.train_test_split(0.01)\n", | ||
| 327 | + "train_data = train_test_data[\"train\"]\n", | ||
| 328 | + "test_data = train_test_data[\"test\"]\n", | ||
| 329 | + "\n", | ||
| 330 | + "print(\"=====训练集数量\")\n", | ||
| 331 | + "print(train_data)\n", | ||
| 332 | + "print(\"=====测试集数量\")\n", | ||
| 333 | + "print(test_data)" | ||
| 334 | + ] | ||
| 335 | + }, | ||
| 336 | + { | ||
| 337 | + "cell_type": "markdown", | ||
| 338 | + "id": "1a2b6e49-4fa1-40bc-8b91-139395c740c1", | ||
| 339 | + "metadata": {}, | ||
| 340 | + "source": [ | ||
| 341 | + "不使用lora微调⬇️ " | ||
| 342 | + ] | ||
| 343 | + }, | ||
| 344 | + { | ||
| 345 | + "cell_type": "markdown", | ||
| 346 | + "id": "e18907f1-d439-4f45-93c6-f7690122e3b5", | ||
| 347 | + "metadata": {}, | ||
| 348 | + "source": [ | ||
| 349 | + "---\n", | ||
| 350 | + "\n", | ||
| 351 | + "## 开始训练" | ||
| 352 | + ] | ||
| 353 | + }, | ||
| 354 | + { | ||
| 355 | + "cell_type": "markdown", | ||
| 356 | + "id": "462e0726-d1f2-4115-a6d2-1294ae04107c", | ||
| 357 | + "metadata": {}, | ||
| 358 | + "source": [ | ||
| 359 | + "### 构建训练参数" | ||
| 360 | + ] | ||
| 361 | + }, | ||
| 362 | + { | ||
| 363 | + "cell_type": "markdown", | ||
| 364 | + "id": "670bddb5", | ||
| 365 | + "metadata": {}, | ||
| 366 | + "source": [ | ||
| 367 | + "注意下方代码运行后,会在页面上方有一个输入框的弹窗出现,请在这里输入 SwanLab 的 API Key,然后按回车提交。可以从 https://swanlab.cn/settings 获取。如果不输入则会卡在这里无法继续运行。" | ||
| 368 | + ] | ||
| 369 | + }, | ||
| 370 | + { | ||
| 371 | + "cell_type": "code", | ||
| 372 | + "execution_count": null, | ||
| 373 | + "id": "de6cd272-9c91-4936-b5d3-4a2ce7d55ef3", | ||
| 374 | + "metadata": {}, | ||
| 375 | + "outputs": [], | ||
| 376 | + "source": [ | ||
| 377 | + "import getpass\n", | ||
| 378 | + "import swanlab\n", | ||
| 379 | + "from pathlib import Path\n", | ||
| 380 | + "from trl import SFTConfig, SFTTrainer\n", | ||
| 381 | + "\n", | ||
| 382 | + "# ====== SwanLab 项目配置 ======\n", | ||
| 383 | + "# 下方的SWANLAB_PROJ请勿修改!\n", | ||
| 384 | + "%env SWANLAB_PROJ=CANN_SwanLab\n", | ||
| 385 | + "\n", | ||
| 386 | + "# 将下方的 SWANLAB_EXP_NAME 改成自己的名字,方便看出哪个实验是自己的!!支持中文\n", | ||
| 387 | + "%env SWANLAB_EXP_NAME=Qwen3-Finetune-Instruction\n", | ||
| 388 | + "\n", | ||
| 389 | + "\n", | ||
| 390 | + "# ====== SwanLab 登录(首次运行弹出安全输入框,凭证保存后下次自动复用) ======\n", | ||
| 391 | + "def _swanlab_credential_saved() -> bool:\n", | ||
| 392 | + " \"\"\"检查本地是否已有 SwanLab 凭证(默认存储在 ~/.netrc)\"\"\"\n", | ||
| 393 | + " netrc = Path.home() / \".netrc\"\n", | ||
| 394 | + " if not netrc.exists():\n", | ||
| 395 | + " return False\n", | ||
| 396 | + " try:\n", | ||
| 397 | + " return \"swanlab.cn\" in netrc.read_text()\n", | ||
| 398 | + " except Exception:\n", | ||
| 399 | + " return False\n", | ||
| 400 | + "\n", | ||
| 401 | + "\n", | ||
| 402 | + "if _swanlab_credential_saved():\n", | ||
| 403 | + " print(\"检测到本地已有 SwanLab 凭证,跳过登录。\")\n", | ||
| 404 | + "else:\n", | ||
| 405 | + " api_key = getpass.getpass(\"请输入你的 SwanLab API Key(从 https://swanlab.cn/settings 获取): \")\n", | ||
| 406 | + " swanlab.login(api_key=api_key, save=True)\n", | ||
| 407 | + " print(\"SwanLab 登录成功!凭证已保存到 ~/.netrc,下次运行无需再输入。\")\n", | ||
| 408 | + "\n", | ||
| 409 | + "\n", | ||
| 410 | + "sft_config = SFTConfig(\n", | ||
| 411 | + " output_dir = \"./sft_qwen\", # 保存位置\n", | ||
| 412 | + " max_steps=1000, # 训练步长\n", | ||
| 413 | + " per_device_train_batch_size=4, # 批量大小\n", | ||
| 414 | + " learning_rate = 0.00001, # 学习率lr\n", | ||
| 415 | + " logging_steps=1, # 打印日志的频率\n", | ||
| 416 | + " eval_strategy=\"steps\", # 开启评估\n", | ||
| 417 | + " eval_steps=0.2, # 评估次数\n", | ||
| 418 | + " save_total_limit=2, # 设置最多保存的权重数量\n", | ||
| 419 | + " report_to=\"swanlab\" # 将日志报告给swanlab\n", | ||
| 420 | + ")" | ||
| 421 | + ] | ||
| 422 | + }, | ||
| 423 | + { | ||
| 424 | + "cell_type": "markdown", | ||
| 425 | + "id": "89a76937-ff54-43e2-8dd0-994ccacef5db", | ||
| 426 | + "metadata": {}, | ||
| 427 | + "source": [ | ||
| 428 | + "### 开启训练\n", | ||
| 429 | + "\n", | ||
| 430 | + "> ⏳ **训练耗时较长**(默认 1000 步约 30-60 分钟)。**点击运行后等当前 cell 跑完再点下一个 cell**,连续点会重复造成异常。SwanLab 训练曲线可在 [swanlab.cn](https://swanlab.cn) 实时查看。" | ||
| 431 | + ] | ||
| 432 | + }, | ||
| 433 | + { | ||
| 434 | + "cell_type": "code", | ||
| 435 | + "execution_count": null, | ||
| 436 | + "id": "02878430-0408-485b-94e8-a2a4b8cf11f7", | ||
| 437 | + "metadata": { | ||
| 438 | + "tags": [] | ||
| 439 | + }, | ||
| 440 | + "outputs": [], | ||
| 441 | + "source": [ | ||
| 442 | + "trainer = SFTTrainer(\n", | ||
| 443 | + " model=model,\n", | ||
| 444 | + " processing_class=tokenizer,\n", | ||
| 445 | + " args=sft_config,\n", | ||
| 446 | + " data_collator=None,\n", | ||
| 447 | + " train_dataset=train_data,\n", | ||
| 448 | + " eval_dataset=test_data,\n", | ||
| 449 | + " \n", | ||
| 450 | + ")\n", | ||
| 451 | + "trainer.train()\n", | ||
| 452 | + "swanlab.finish()" | ||
| 453 | + ] | ||
| 454 | + }, | ||
| 455 | + { | ||
| 456 | + "cell_type": "markdown", | ||
| 457 | + "id": "19b0634b-5586-4b50-a33f-878fbedef097", | ||
| 458 | + "metadata": {}, | ||
| 459 | + "source": [ | ||
| 460 | + "---\n", | ||
| 461 | + "\n", | ||
| 462 | + "## 保存模型" | ||
| 463 | + ] | ||
| 464 | + }, | ||
| 465 | + { | ||
| 466 | + "cell_type": "code", | ||
| 467 | + "execution_count": null, | ||
| 468 | + "id": "dc598c9e-2519-47b5-a410-20b9b1e496af", | ||
| 469 | + "metadata": {}, | ||
| 470 | + "outputs": [], | ||
| 471 | + "source": [ | ||
| 472 | + "model.save_pretrained(sft_config.output_dir, safe_serialization=True)\n", | ||
| 473 | + "tokenizer.save_pretrained(sft_config.output_dir)" | ||
| 474 | + ] | ||
| 475 | + }, | ||
| 476 | + { | ||
| 477 | + "cell_type": "markdown", | ||
| 478 | + "id": "6be412eb-96b8-4d76-b959-a7f6ec7abd61", | ||
| 479 | + "metadata": {}, | ||
| 480 | + "source": [ | ||
| 481 | + "---\n", | ||
| 482 | + "\n", | ||
| 483 | + "## 推理训练好的模型" | ||
| 484 | + ] | ||
| 485 | + }, | ||
| 486 | + { | ||
| 487 | + "cell_type": "code", | ||
| 488 | + "execution_count": null, | ||
| 489 | + "id": "965f50ab-7982-47ed-89da-c84e901ed2f9", | ||
| 490 | + "metadata": {}, | ||
| 491 | + "outputs": [], | ||
| 492 | + "source": [ | ||
| 493 | + "your_model_path=\"./sft_qwen\"\n", | ||
| 494 | + "\n", | ||
| 495 | + "tokenizer = AutoTokenizer.from_pretrained(your_model_path)\n", | ||
| 496 | + "sft_model = AutoModelForCausalLM.from_pretrained(\n", | ||
| 497 | + " your_model_path,\n", | ||
| 498 | + " torch_dtype=\"auto\",\n", | ||
| 499 | + " device_map=\"auto\"\n", | ||
| 500 | + ")" | ||
| 501 | + ] | ||
| 502 | + }, | ||
| 503 | + { | ||
| 504 | + "cell_type": "code", | ||
| 505 | + "execution_count": null, | ||
| 506 | + "id": "a87d5c37-ebca-4639-afe0-ce05ba3271fa", | ||
| 507 | + "metadata": {}, | ||
| 508 | + "outputs": [], | ||
| 509 | + "source": [ | ||
| 510 | + "messages = [\n", | ||
| 511 | + " {\"role\": \"user\", \"content\": \"你好\"},\n", | ||
| 512 | + "]\n", | ||
| 513 | + "text=tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True,enable_thinking=False)\n", | ||
| 514 | + "model_inputs = tokenizer([text], return_tensors=\"pt\").to(model.device)\n", | ||
| 515 | + "generation_ids = sft_model.generate(**model_inputs, max_new_tokens=128, eos_token_id=tokenizer.eos_token_id)\n", | ||
| 516 | + "content = tokenizer.decode(\n", | ||
| 517 | + " generation_ids[0], skip_special_tokens=True\n", | ||
| 518 | + ")\n", | ||
| 519 | + "print(content)" | ||
| 520 | + ] | ||
| 521 | + } | ||
| 522 | + ], | ||
| 523 | + "metadata": { | ||
| 524 | + "kernelspec": { | ||
| 525 | + "display_name": "base", | ||
| 526 | + "language": "python", | ||
| 527 | + "name": "python3" | ||
| 528 | + }, | ||
| 529 | + "language_info": { | ||
| 530 | + "codemirror_mode": { | ||
| 531 | + "name": "ipython", | ||
| 532 | + "version": 3 | ||
| 533 | + }, | ||
| 534 | + "file_extension": ".py", | ||
| 535 | + "mimetype": "text/x-python", | ||
| 536 | + "name": "python", | ||
| 537 | + "nbconvert_exporter": "python", | ||
| 538 | + "pygments_lexer": "ipython3", | ||
| 539 | + "version": "3.12.2" | ||
| 540 | + } | ||
| 541 | + }, | ||
| 542 | + "nbformat": 4, | ||
| 543 | + "nbformat_minor": 5 | ||
| 544 | +} | ||
| @@ -0,0 +1,30 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# 第 02 章 · 大语言模型微调 | ||
| 12 | + | ||
| 13 | +## 章节定位 | ||
| 14 | + | ||
| 15 | +本章围绕 Qwen3 系列模型,按"**原理 → 数据 / 模板 → SFT → LoRA → 工程实践**"的顺序,完整跑通一次端到端微调流程。 | ||
| 16 | +预期读者在学完本章后,能理解 Chat Template、Special Token、Loss Mask、LoRA 低秩适配、超参数选择、效果评估等工程细节,并能在昇腾 NPU 上独立完成一次面向自己业务数据的指令微调,而不只是停留在套用 LLaMA Factory 等工具。 | ||
| 17 | + | ||
| 18 | +## 计划节次 | ||
| 19 | + | ||
| 20 | +| 节次 | 标题 | 状态 | | ||
| 21 | +|------|------|------| | ||
| 22 | +| 02.01 | 章节介绍 | 建设中 | | ||
| 23 | +| 02.02 | 微调与 LoRA 原理 | 建设中 | | ||
| 24 | +| 02.03 | Chat Template 与 Special Token | 建设中 | | ||
| 25 | +| [02.04](./02.04_qwen3_instruction_sft.ipynb) | Qwen3 基座模型指令微调(SFT) | 已发布 | | ||
| 26 | +| 02.05 | Qwen3 LoRA 微调:量化因子代码生成实战 | 建设中 | | ||
| 27 | +| 02.06 | 用 ms-swift 复现 SFT / LoRA 微调(成熟工具链对照) | 建设中 | | ||
| 28 | +| 02.07 | 数据合成 | 建设中 | | ||
| 29 | +| 02.08 | 微调超参数与科学调参 | 建设中 | | ||
| 30 | +| 02.09 | 微调效果评估 | 建设中 | | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# 第 03 章 · 大语言模型强化学习 | ||
| 12 | + | ||
| 13 | +## 章节定位 | ||
| 14 | + | ||
| 15 | +在第 02 章 SFT / LoRA 的基础上,引入**偏好学习与强化学习**这一阶段,覆盖从 RLHF(PPO)到 DPO、GRPO 等更现代的方案。 | ||
| 16 | +目标是让同学理解:为什么仅有 SFT 不够、奖励信号如何注入、训练为什么更不稳定,以及在昇腾 NPU 上跑 RL 训练时的工程权衡。 | ||
| 17 | + | ||
| 18 | +## 计划节次 | ||
| 19 | + | ||
| 20 | +| 节次 | 标题(暂定) | 状态 | | ||
| 21 | +|------|---------------|------| | ||
| 22 | +| 03.01 | 章节简介与对齐范式概览 | 建设中 | | ||
| 23 | +| 03.02 | 偏好数据集与 Reward Model | 建设中 | | ||
| 24 | +| 03.03 | PPO 端到端实战 | 建设中 | | ||
| 25 | +| 03.04 | DPO / GRPO 等无 RM 方案对比 | 建设中 | | ||
| 26 | +| 03.05 | 代码可执行性作为奖励信号的案例 | 建设中 | | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# 第 04 章 · 大语言模型推理部署 | ||
| 12 | + | ||
| 13 | +## 章节定位 | ||
| 14 | + | ||
| 15 | +本章聚焦"训练好的模型如何对外提供服务"这一环节,覆盖在昇腾 NPU 上的推理引擎选型、权重量化、批处理与服务化部署。 | ||
| 16 | +预期读者在学完本章后,能在 CANNLab 上把一个微调后的 Qwen3 模型起成一个对外的 HTTP / OpenAI 兼容服务,并理解吞吐、延迟、显存之间的权衡。 | ||
| 17 | + | ||
| 18 | +## 计划节次 | ||
| 19 | + | ||
| 20 | +| 节次 | 标题(暂定) | 状态 | | ||
| 21 | +|------|---------------|------| | ||
| 22 | +| 04.01 | 章节简介与推理瓶颈分析 | 建设中 | | ||
| 23 | +| 04.02 | KV cache 与 continuous batching | 建设中 | | ||
| 24 | +| 04.03 | 昇腾上的推理引擎选型与对比 | 建设中 | | ||
| 25 | +| 04.04 | 权重量化(W8A8 / AWQ / GPTQ)实战 | 建设中 | | ||
| 26 | +| 04.05 | 推理服务化与 OpenAI 兼容接口 | 建设中 | | ||
Acontrib/tutorials/swan_llm_course/05_performance_optimization/05.02_swan_rmsnorm_acceleration.ipynb+676-0
| @@ -0,0 +1,676 @@ | |||
| 1 | +{ | ||
| 2 | + "cells": [ | ||
| 3 | + { | ||
| 4 | + "cell_type": "markdown", | ||
| 5 | + "metadata": {}, | ||
| 6 | + "source": "# 实训大作业 · Notebook:把课程提供的 SwanRmsNorm 算子接到 Qwen3-8B\n\n本 Notebook 是 [`guidance.pdf`](pdf/guidance.pdf) 学生操作指导书 的实操配套。\n\n**前提**:课程提供的 SwanRmsNorm AscendC kernel 已经在指导书 §5 编译并部署到 NPU 上。本 Notebook 只做**集成层 + 端到端实验**,分三步:\n\n1. **写集成层** — 用 `torch.autograd.Function` 包装 SwanRmsNorm 的 forward / backward,并 monkey-patch 到 Qwen3 模型里\n2. **测推理加速** — 对比 baseline / patched 的 tokens/sec,画图\n3. **跑 LoRA SFT** — 用 alpaca_zh 数据集做一轮微调,对比训练 step 时间,并把 loss + step time 用 SwanLab 记录起来\n\n**前置条件**:\n- 已经按 [guidance.pdf](pdf/guidance.pdf) §5 把课程提供的 `swan_rms_norm` AscendC kernel 编译并安装到 `~/Ascend/custom_opp`\n- 启动 Jupyter 之前已在 shell 里 `source ~/Ascend/custom_opp/vendors/xllm/bin/set_env.bash`(否则 §1 中 `import rms_norm_lib` 会失败)\n- Python 依赖与 Qwen3-8B 基座模型由下面的 §0 自动处理,无需手动准备\n\n**Notebook 内有 2 个 `# TODO` 需要你补全**——参考 [guidance.pdf](pdf/guidance.pdf) §6 完成。直接运行会在 TODO 处 `NotImplementedError`。\n\n> **注意**:你**不需要**写 AscendC kernel——那是课程已经独立实现好的。你的任务是把它接到 PyTorch 与 Qwen3 模型上。" | ||
| 7 | + }, | ||
| 8 | + { | ||
| 9 | + "cell_type": "markdown", | ||
| 10 | + "id": "env-prep-md", | ||
| 11 | + "metadata": {}, | ||
| 12 | + "source": "---\n\n## §0 环境准备\n\n本 Notebook 在 **CANNLab 的 Ascend NPU 环境**下运行。镜像默认已预装 `torch` 与 `torch_npu`,下面两步分别处理:\n\n1. **Python 上层依赖**:用 `pip` 装 `transformers` / `peft` / `datasets` / `swanlab` / `accelerate` / `modelscope`\n2. **Qwen3-8B 基座模型**:从 ModelScope 下载到 `./Qwen3-8B`(约 16GB,仅首次需要,10-20 分钟)\n\n> **首次运行**:直接顺序执行下方两个 cell 即可。\n>\n> **重跑场景**:如果环境里已经残留过旧版 `transformers` 且本 Notebook 之前 import 过它,安装后请点 `Kernel → Restart`,否则新版本不会生效。\n\n> **⚠️ 重要前提**:自定义算子 `rms_norm_lib` **无法通过 pip 安装**,必须按 [guidance.pdf](pdf/guidance.pdf) §5 在 shell 里手动编译并安装到 `~/Ascend/custom_opp`,并在启动 Jupyter 之前 `source ~/Ascend/custom_opp/vendors/xllm/bin/set_env.bash`。否则 §1 环境检查中的 `import rms_norm_lib` 会失败。" | ||
| 13 | + }, | ||
| 14 | + { | ||
| 15 | + "cell_type": "code", | ||
| 16 | + "execution_count": null, | ||
| 17 | + "id": "env-pip", | ||
| 18 | + "metadata": {}, | ||
| 19 | + "outputs": [], | ||
| 20 | + "source": [ | ||
| 21 | + "# 1. 安装 Python 上层依赖\n", | ||
| 22 | + "%pip install -q -i https://pypi.tuna.tsinghua.edu.cn/simple modelscope==1.35.4 transformers==5.5.4 peft==0.18.0 datasets==4.8.4 swanlab==0.7.15 accelerate==1.13.0" | ||
| 23 | + ] | ||
| 24 | + }, | ||
| 25 | + { | ||
| 26 | + "cell_type": "code", | ||
| 27 | + "execution_count": null, | ||
| 28 | + "id": "env-download", | ||
| 29 | + "metadata": {}, | ||
| 30 | + "outputs": [], | ||
| 31 | + "source": [ | ||
| 32 | + "# 2. 下载 Qwen3-8B 基座模型(首次约 16GB / 10-20 分钟,已下载则跳过)\n", | ||
| 33 | + "import os\n", | ||
| 34 | + "\n", | ||
| 35 | + "MODEL_DIR = './Qwen3-8B'\n", | ||
| 36 | + "if os.path.exists(MODEL_DIR) and os.listdir(MODEL_DIR):\n", | ||
| 37 | + " print(f'{MODEL_DIR} 已存在,跳过下载')\n", | ||
| 38 | + "else:\n", | ||
| 39 | + " !modelscope download --model Qwen/Qwen3-8B --local_dir ./Qwen3-8B" | ||
| 40 | + ] | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + "cell_type": "markdown", | ||
| 44 | + "metadata": {}, | ||
| 45 | + "source": [ | ||
| 46 | + "---\n", | ||
| 47 | + "\n", | ||
| 48 | + "## §1 环境检查\n", | ||
| 49 | + "\n", | ||
| 50 | + "确认 NPU 可用、torch_npu 装好、自定义算子能 import。任何一行报错都得回到指导书 §4-§5 排查。" | ||
| 51 | + ] | ||
| 52 | + }, | ||
| 53 | + { | ||
| 54 | + "cell_type": "code", | ||
| 55 | + "execution_count": null, | ||
| 56 | + "metadata": {}, | ||
| 57 | + "outputs": [], | ||
| 58 | + "source": [ | ||
| 59 | + "import time, gc, types\n", | ||
| 60 | + "import torch\n", | ||
| 61 | + "import torch_npu\n", | ||
| 62 | + "\n", | ||
| 63 | + "print(f'torch : {torch.__version__}')\n", | ||
| 64 | + "print(f'torch_npu : {torch_npu.__version__}')\n", | ||
| 65 | + "assert torch.npu.is_available(), 'NPU 不可用!'\n", | ||
| 66 | + "torch_npu.npu.set_device(0)\n", | ||
| 67 | + "print(f'device : {torch_npu.npu.get_device_name(0)}')\n", | ||
| 68 | + "\n", | ||
| 69 | + "import rms_norm_lib\n", | ||
| 70 | + "print(f'rms_norm_lib loaded: {rms_norm_lib}')\n", | ||
| 71 | + "print(f'available functions: {[x for x in dir(rms_norm_lib) if not x.startswith(\"_\")]}')" | ||
| 72 | + ] | ||
| 73 | + }, | ||
| 74 | + { | ||
| 75 | + "cell_type": "markdown", | ||
| 76 | + "metadata": {}, | ||
| 77 | + "source": [ | ||
| 78 | + "---\n", | ||
| 79 | + "\n", | ||
| 80 | + "## §2 算子精度验证\n", | ||
| 81 | + "\n", | ||
| 82 | + "在调用模型前先确认我们的算子计算正确。和 PyTorch native 实现对比,bf16 + 随机 γ 的实测最大绝对误差通常落在 **0.1-0.4** 范围内,超过 0.5 才是真有问题。\n", | ||
| 83 | + "\n", | ||
| 84 | + "我们用 Qwen3-8B 的 `hidden_size = 4096` 来测,更接近真实场景的形状。" | ||
| 85 | + ] | ||
| 86 | + }, | ||
| 87 | + { | ||
| 88 | + "cell_type": "code", | ||
| 89 | + "execution_count": null, | ||
| 90 | + "metadata": {}, | ||
| 91 | + "outputs": [], | ||
| 92 | + "source": [ | ||
| 93 | + "torch.manual_seed(42)\n", | ||
| 94 | + "\n", | ||
| 95 | + "# Qwen3-8B 的 hidden_size = 4096\n", | ||
| 96 | + "M, N = 16, 4096\n", | ||
| 97 | + "x = torch.randn(M, N, dtype=torch.bfloat16, device='npu')\n", | ||
| 98 | + "gamma = torch.randn(N, dtype=torch.bfloat16, device='npu')\n", | ||
| 99 | + "\n", | ||
| 100 | + "# 我们的算子\n", | ||
| 101 | + "y_swan = rms_norm_lib.rms_norm(x, gamma, 1e-6)\n", | ||
| 102 | + "\n", | ||
| 103 | + "# PyTorch native(同 Qwen3RMSNorm.forward)\n", | ||
| 104 | + "xf = x.to(torch.float32)\n", | ||
| 105 | + "y_torch = (gamma * (xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + 1e-6)).to(x.dtype))\n", | ||
| 106 | + "\n", | ||
| 107 | + "diff = (y_swan.float() - y_torch.float()).abs().max().item()\n", | ||
| 108 | + "print(f'shape : {tuple(y_swan.shape)}, dtype: {y_swan.dtype}')\n", | ||
| 109 | + "print(f'max |diff| : {diff:.4f} (期望 < 0.5;典型范围 0.1-0.4)')\n", | ||
| 110 | + "assert diff < 0.5, '精度不对,回去检查 kernel 实现'" | ||
| 111 | + ] | ||
| 112 | + }, | ||
| 113 | + { | ||
| 114 | + "cell_type": "markdown", | ||
| 115 | + "metadata": {}, | ||
| 116 | + "source": [ | ||
| 117 | + "---\n", | ||
| 118 | + "\n", | ||
| 119 | + "## §3 加载 Qwen3-8B\n", | ||
| 120 | + "\n", | ||
| 121 | + "用之前已经下载的 Qwen3-8B 模型。后面所有测试都会重复加载这个模型——8B 在 bf16 下 ~16GB,HBM 60GB 够用,但每次 load 大约 10-30 秒。" | ||
| 122 | + ] | ||
| 123 | + }, | ||
| 124 | + { | ||
| 125 | + "cell_type": "code", | ||
| 126 | + "execution_count": null, | ||
| 127 | + "metadata": {}, | ||
| 128 | + "outputs": [], | ||
| 129 | + "source": [ | ||
| 130 | + "from transformers import AutoModelForCausalLM, AutoTokenizer\n", | ||
| 131 | + "from transformers.models.qwen3 import modeling_qwen3\n", | ||
| 132 | + "\n", | ||
| 133 | + "MODEL_PATH = './Qwen3-8B' # ~ 自动解析当前用户家目录\n", | ||
| 134 | + "\n", | ||
| 135 | + "tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)\n", | ||
| 136 | + "if tokenizer.pad_token is None:\n", | ||
| 137 | + " tokenizer.pad_token = tokenizer.eos_token\n", | ||
| 138 | + "\n", | ||
| 139 | + "# 数一下模型里有多少个 Qwen3RMSNorm 模块\n", | ||
| 140 | + "_tmp = AutoModelForCausalLM.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16)\n", | ||
| 141 | + "n_rmsnorm = sum(1 for m in _tmp.modules() if isinstance(m, modeling_qwen3.Qwen3RMSNorm))\n", | ||
| 142 | + "n_layers = len(_tmp.model.layers)\n", | ||
| 143 | + "print(f'Qwen3-8B:')\n", | ||
| 144 | + "print(f' layers : {n_layers}')\n", | ||
| 145 | + "print(f' Qwen3RMSNorm : {n_rmsnorm} (= {n_layers} 层 × 4/层 + 1 final)')\n", | ||
| 146 | + "print(f' hidden_size : {_tmp.config.hidden_size}')\n", | ||
| 147 | + "del _tmp; gc.collect(); torch.npu.empty_cache()" | ||
| 148 | + ] | ||
| 149 | + }, | ||
| 150 | + { | ||
| 151 | + "cell_type": "markdown", | ||
| 152 | + "metadata": {}, | ||
| 153 | + "source": [ | ||
| 154 | + "---\n", | ||
| 155 | + "\n", | ||
| 156 | + "## §4 定义 SwanRmsNormFunction(autograd.Function)\n", | ||
| 157 | + "\n", | ||
| 158 | + "**TODO #1**:参考指导书 §6.2.2,补全下面的 `forward` 和 `backward`。\n", | ||
| 159 | + "\n", | ||
| 160 | + "要点:\n", | ||
| 161 | + "- **forward** 调 `rms_norm_lib.rms_norm(x, gamma, eps)`,并通过 `ctx` 保存 backward 需要的张量\n", | ||
| 162 | + "- **backward** 用 `torch_npu.npu_rms_norm_backward(grad_y, x, gamma, rstd)`,注意 `rstd` 必须是 fp32 且要从 `x` 重算\n", | ||
| 163 | + "- 返回值数量必须与 forward 形参一致——没有梯度的参数返回 `None`" | ||
| 164 | + ] | ||
| 165 | + }, | ||
| 166 | + { | ||
| 167 | + "cell_type": "code", | ||
| 168 | + "execution_count": null, | ||
| 169 | + "metadata": {}, | ||
| 170 | + "outputs": [], | ||
| 171 | + "source": [ | ||
| 172 | + "class SwanRmsNormFunction(torch.autograd.Function):\n", | ||
| 173 | + " \"\"\"\n", | ||
| 174 | + " Forward: 用我们的 SwanRmsNorm AscendC kernel\n", | ||
| 175 | + " Backward: 用 torch_npu 的 fused npu_rms_norm_backward\n", | ||
| 176 | + " \"\"\"\n", | ||
| 177 | + "\n", | ||
| 178 | + " @staticmethod\n", | ||
| 179 | + " def forward(ctx, x, gamma, eps):\n", | ||
| 180 | + " # ───────── TODO #1 (a) ─────────\n", | ||
| 181 | + " # 1) x.contiguous() 一下(kernel 假设 stride 对齐,stride 不对会 silently 算错);\n", | ||
| 182 | + " # 2) 调 rms_norm_lib.rms_norm(x_contig, gamma, eps) 算 forward;\n", | ||
| 183 | + " # 3) 用 ctx.save_for_backward(x_contig, gamma) 保存反向需要的张量;\n", | ||
| 184 | + " # 4) eps 是 float(不是 tensor),存到 ctx.eps 上。\n", | ||
| 185 | + " # 详见指导书 §6.2.2。\n", | ||
| 186 | + " raise NotImplementedError('请补全 forward')\n", | ||
| 187 | + "\n", | ||
| 188 | + " @staticmethod\n", | ||
| 189 | + " def backward(ctx, grad_y):\n", | ||
| 190 | + " x, gamma = ctx.saved_tensors\n", | ||
| 191 | + " eps = ctx.eps\n", | ||
| 192 | + " # ───────── TODO #1 (b) ─────────\n", | ||
| 193 | + " # 用 torch_npu.npu_rms_norm_backward(grad_y, x, gamma, rstd) 算梯度。\n", | ||
| 194 | + " # rstd 不在 ctx 里,需要从 x 重算(fp32 精度,否则报 dtype 错)。\n", | ||
| 195 | + " # 返回 (grad_x, grad_gamma, None) ——eps 没有梯度。\n", | ||
| 196 | + " # 详见指导书 §6.2.2。\n", | ||
| 197 | + " raise NotImplementedError('请补全 backward')\n", | ||
| 198 | + "\n", | ||
| 199 | + "\n", | ||
| 200 | + "# 简单 forward 验证\n", | ||
| 201 | + "y_check = SwanRmsNormFunction.apply(x, gamma, 1e-6)\n", | ||
| 202 | + "diff = (y_check.float() - y_torch.float()).abs().max().item()\n", | ||
| 203 | + "print(f'autograd.Function forward max |diff| = {diff:.4f}')\n", | ||
| 204 | + "assert diff < 0.5" | ||
| 205 | + ] | ||
| 206 | + }, | ||
| 207 | + { | ||
| 208 | + "cell_type": "markdown", | ||
| 209 | + "metadata": {}, | ||
| 210 | + "source": [ | ||
| 211 | + "---\n", | ||
| 212 | + "\n", | ||
| 213 | + "## §5 实现 patch_qwen3_rmsnorm(带计数)\n", | ||
| 214 | + "\n", | ||
| 215 | + "**TODO #2**:参考指导书 §6.2.3,补全下面的 patch 函数。\n", | ||
| 216 | + "\n", | ||
| 217 | + "要点:\n", | ||
| 218 | + "- 把这个 module 的 `forward` 替换为调 `SwanRmsNormFunction.apply` 的版本\n", | ||
| 219 | + "- 每个 module 有自己的 `weight`,需要用**工厂函数**捕获——避免 Python 闭包后期绑定让所有 module 都用最后一个 weight\n", | ||
| 220 | + "- 替换时用 `types.MethodType(...)` 让函数能正确接收 `self`\n", | ||
| 221 | + "- 别忘了 `n += 1` 计数" | ||
| 222 | + ] | ||
| 223 | + }, | ||
| 224 | + { | ||
| 225 | + "cell_type": "code", | ||
| 226 | + "execution_count": null, | ||
| 227 | + "metadata": {}, | ||
| 228 | + "outputs": [], | ||
| 229 | + "source": [ | ||
| 230 | + "def patch_qwen3_rmsnorm(model):\n", | ||
| 231 | + " n = 0\n", | ||
| 232 | + " for module in model.modules():\n", | ||
| 233 | + " if isinstance(module, modeling_qwen3.Qwen3RMSNorm):\n", | ||
| 234 | + " eps = module.variance_epsilon\n", | ||
| 235 | + " weight = module.weight\n", | ||
| 236 | + "\n", | ||
| 237 | + " # ───────── TODO #2 ─────────\n", | ||
| 238 | + " # 1) 写一个工厂函数 make_fw(w, e) 返回新 forward\n", | ||
| 239 | + " # 2) 用 types.MethodType 把它绑定到 module\n", | ||
| 240 | + " # 3) n += 1\n", | ||
| 241 | + " # 详见指导书 §6.2.3。\n", | ||
| 242 | + " raise NotImplementedError('请补全 patch 逻辑')\n", | ||
| 243 | + " return n\n", | ||
| 244 | + "\n", | ||
| 245 | + "\n", | ||
| 246 | + "# 验证 patch\n", | ||
| 247 | + "_test_model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16).to('npu').eval()\n", | ||
| 248 | + "n_patched = patch_qwen3_rmsnorm(_test_model)\n", | ||
| 249 | + "print(f'patched {n_patched} Qwen3RMSNorm modules (期望: {n_rmsnorm})')\n", | ||
| 250 | + "assert n_patched == n_rmsnorm, 'patch 数量不对'\n", | ||
| 251 | + "del _test_model; gc.collect(); torch.npu.empty_cache()" | ||
| 252 | + ] | ||
| 253 | + }, | ||
| 254 | + { | ||
| 255 | + "cell_type": "markdown", | ||
| 256 | + "metadata": {}, | ||
| 257 | + "source": [ | ||
| 258 | + "---\n", | ||
| 259 | + "\n", | ||
| 260 | + "## §6 推理加速测试\n", | ||
| 261 | + "\n", | ||
| 262 | + "测 Qwen3-8B 的端到端推理 tokens/sec。Baseline / Patched 各加载一次模型,避免相互污染。" | ||
| 263 | + ] | ||
| 264 | + }, | ||
| 265 | + { | ||
| 266 | + "cell_type": "code", | ||
| 267 | + "execution_count": null, | ||
| 268 | + "metadata": {}, | ||
| 269 | + "outputs": [], | ||
| 270 | + "source": [ | ||
| 271 | + "PROMPT = '请用一段话介绍量子计算的基本原理。'\n", | ||
| 272 | + "MAX_NEW = 50\n", | ||
| 273 | + "\n", | ||
| 274 | + "inputs = tokenizer(PROMPT, return_tensors='pt').to('npu')\n", | ||
| 275 | + "\n", | ||
| 276 | + "def measure_tps(model, max_new=MAX_NEW, warmup=2):\n", | ||
| 277 | + " \"\"\"测一次 generate 的 tokens/sec\"\"\"\n", | ||
| 278 | + " for _ in range(warmup):\n", | ||
| 279 | + " _ = model.generate(**inputs, max_new_tokens=max_new, do_sample=False,\n", | ||
| 280 | + " pad_token_id=tokenizer.eos_token_id)\n", | ||
| 281 | + " torch_npu.npu.synchronize()\n", | ||
| 282 | + " t0 = time.perf_counter()\n", | ||
| 283 | + " out = model.generate(**inputs, max_new_tokens=max_new, do_sample=False,\n", | ||
| 284 | + " pad_token_id=tokenizer.eos_token_id)\n", | ||
| 285 | + " torch_npu.npu.synchronize()\n", | ||
| 286 | + " dt = time.perf_counter() - t0\n", | ||
| 287 | + " n_new = out.shape[1] - inputs['input_ids'].shape[1]\n", | ||
| 288 | + " return n_new / dt, tokenizer.decode(out[0], skip_special_tokens=True)\n", | ||
| 289 | + "\n", | ||
| 290 | + "\n", | ||
| 291 | + "# Baseline\n", | ||
| 292 | + "print('===== Baseline (PyTorch native RMSNorm) =====')\n", | ||
| 293 | + "m_base = AutoModelForCausalLM.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16).to('npu').eval()\n", | ||
| 294 | + "tps_baseline, text_baseline = measure_tps(m_base)\n", | ||
| 295 | + "print(f' tokens/sec: {tps_baseline:.2f}')\n", | ||
| 296 | + "del m_base; gc.collect(); torch.npu.empty_cache()\n", | ||
| 297 | + "\n", | ||
| 298 | + "# Patched\n", | ||
| 299 | + "print('\\n===== Patched (SwanRmsNorm AscendC kernel) =====')\n", | ||
| 300 | + "m_pt = AutoModelForCausalLM.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16).to('npu').eval()\n", | ||
| 301 | + "n = patch_qwen3_rmsnorm(m_pt)\n", | ||
| 302 | + "print(f' patched {n} modules')\n", | ||
| 303 | + "tps_patched, text_patched = measure_tps(m_pt)\n", | ||
| 304 | + "print(f' tokens/sec: {tps_patched:.2f}')\n", | ||
| 305 | + "del m_pt; gc.collect(); torch.npu.empty_cache()\n", | ||
| 306 | + "\n", | ||
| 307 | + "# 总结\n", | ||
| 308 | + "speedup = tps_patched / tps_baseline\n", | ||
| 309 | + "print('\\n' + '═' * 50)\n", | ||
| 310 | + "print(f' Baseline : {tps_baseline:.2f} tokens/sec')\n", | ||
| 311 | + "print(f' Patched : {tps_patched:.2f} tokens/sec')\n", | ||
| 312 | + "print(f' Speedup : {speedup:.3f} ×')\n", | ||
| 313 | + "print('═' * 50)\n", | ||
| 314 | + "print(f'\\nGreedy decode 一致性:{text_baseline == text_patched}')" | ||
| 315 | + ] | ||
| 316 | + }, | ||
| 317 | + { | ||
| 318 | + "cell_type": "markdown", | ||
| 319 | + "metadata": {}, | ||
| 320 | + "source": [ | ||
| 321 | + "## §6.1 推理加速可视化" | ||
| 322 | + ] | ||
| 323 | + }, | ||
| 324 | + { | ||
| 325 | + "cell_type": "code", | ||
| 326 | + "execution_count": null, | ||
| 327 | + "metadata": {}, | ||
| 328 | + "outputs": [], | ||
| 329 | + "source": [ | ||
| 330 | + "import os\n", | ||
| 331 | + "import matplotlib\n", | ||
| 332 | + "matplotlib.use('Agg') # headless backend\n", | ||
| 333 | + "import matplotlib.pyplot as plt\n", | ||
| 334 | + "\n", | ||
| 335 | + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))\n", | ||
| 336 | + "\n", | ||
| 337 | + "# 左图:tokens/sec 对比\n", | ||
| 338 | + "ax1.bar(['Baseline\\n(PyTorch native)', 'Patched\\n(SwanRmsNorm)'],\n", | ||
| 339 | + " [tps_baseline, tps_patched],\n", | ||
| 340 | + " color=['#9E9E9E', '#4CAF50'])\n", | ||
| 341 | + "ax1.set_ylabel('tokens / sec')\n", | ||
| 342 | + "ax1.set_title('Qwen3-8B Inference Throughput')\n", | ||
| 343 | + "ax1.text(1, tps_patched, f'{speedup:.2f}×', ha='center', va='bottom',\n", | ||
| 344 | + " fontsize=14, fontweight='bold', color='#2E7D32')\n", | ||
| 345 | + "\n", | ||
| 346 | + "# 右图:speedup 单柱\n", | ||
| 347 | + "ax2.bar(['Speedup'], [speedup], color='#4CAF50', width=0.5)\n", | ||
| 348 | + "ax2.axhline(1.0, color='gray', linestyle='--', linewidth=1)\n", | ||
| 349 | + "ax2.set_ylabel('Speedup (×)')\n", | ||
| 350 | + "ax2.set_title('End-to-end Speedup')\n", | ||
| 351 | + "ax2.text(0, speedup, f'{speedup:.3f}×', ha='center', va='bottom',\n", | ||
| 352 | + " fontsize=14, fontweight='bold', color='#2E7D32')\n", | ||
| 353 | + "ax2.set_ylim(0, max(2, speedup * 1.2))\n", | ||
| 354 | + "\n", | ||
| 355 | + "plt.tight_layout()\n", | ||
| 356 | + "plt.savefig('notebook_inference.png', dpi=120)\n", | ||
| 357 | + "plt.show()\n", | ||
| 358 | + "print('保存到 notebook_inference.png')" | ||
| 359 | + ] | ||
| 360 | + }, | ||
| 361 | + { | ||
| 362 | + "cell_type": "markdown", | ||
| 363 | + "metadata": {}, | ||
| 364 | + "source": [ | ||
| 365 | + "---\n", | ||
| 366 | + "\n", | ||
| 367 | + "## §7 LoRA 微调:使用 alpaca_zh 数据集\n", | ||
| 368 | + "\n", | ||
| 369 | + "下面用 alpaca_zh 数据集对 Qwen3-8B 做一轮 LoRA 微调,对比 baseline / patched 的训练速度。\n", | ||
| 370 | + "\n", | ||
| 371 | + "### §7.1 下载并加载 alpaca_zh" | ||
| 372 | + ] | ||
| 373 | + }, | ||
| 374 | + { | ||
| 375 | + "cell_type": "code", | ||
| 376 | + "execution_count": null, | ||
| 377 | + "metadata": {}, | ||
| 378 | + "outputs": [], | ||
| 379 | + "source": [ | ||
| 380 | + "import os\n", | ||
| 381 | + "if not os.path.exists('alpaca_zh'):\n", | ||
| 382 | + " print('下载 alpaca_zh 数据集...')\n", | ||
| 383 | + " !modelscope download --dataset llamafactory/alpaca_zh --local_dir ./alpaca_zh\n", | ||
| 384 | + "else:\n", | ||
| 385 | + " print('alpaca_zh 已存在,跳过下载')\n", | ||
| 386 | + "\n", | ||
| 387 | + "import datasets\n", | ||
| 388 | + "alpaca = datasets.load_dataset('json', data_files='alpaca_zh/alpaca_data_zh_51k.json')['train']\n", | ||
| 389 | + "print(f'数据集样本数: {len(alpaca)}')\n", | ||
| 390 | + "print('\\n第一条样本:')\n", | ||
| 391 | + "print(f' instruction: {alpaca[0][\"instruction\"]}')\n", | ||
| 392 | + "print(f' input : {alpaca[0][\"input\"]}')\n", | ||
| 393 | + "print(f' output : {alpaca[0][\"output\"][:80]}...')" | ||
| 394 | + ] | ||
| 395 | + }, | ||
| 396 | + { | ||
| 397 | + "cell_type": "markdown", | ||
| 398 | + "metadata": {}, | ||
| 399 | + "source": [ | ||
| 400 | + "### §7.2 数据格式化\n", | ||
| 401 | + "\n", | ||
| 402 | + "把 instruction/input/output 拼成 Qwen3 chat 格式:" | ||
| 403 | + ] | ||
| 404 | + }, | ||
| 405 | + { | ||
| 406 | + "cell_type": "code", | ||
| 407 | + "execution_count": null, | ||
| 408 | + "metadata": {}, | ||
| 409 | + "outputs": [], | ||
| 410 | + "source": [ | ||
| 411 | + "def format_data(example):\n", | ||
| 412 | + " user_text = example['instruction'] + (example['input'] or '')\n", | ||
| 413 | + " assistant_text = example['output']\n", | ||
| 414 | + " messages = [\n", | ||
| 415 | + " {'role': 'user', 'content': user_text},\n", | ||
| 416 | + " {'role': 'assistant', 'content': assistant_text},\n", | ||
| 417 | + " ]\n", | ||
| 418 | + " text = tokenizer.apply_chat_template(\n", | ||
| 419 | + " messages, tokenize=False, add_generation_prompt=False, enable_thinking=False)\n", | ||
| 420 | + " return {'text': text}\n", | ||
| 421 | + "\n", | ||
| 422 | + "# 取 200 条做演示(实测 alpaca_zh 全集 51k 单 epoch 太长)\n", | ||
| 423 | + "DEMO_SIZE = 200\n", | ||
| 424 | + "demo_data = alpaca.select(range(DEMO_SIZE)).map(format_data, remove_columns=alpaca.column_names)\n", | ||
| 425 | + "print(f'演示用数据集大小: {len(demo_data)} 条')\n", | ||
| 426 | + "print(f'\\n格式化后第一条:\\n{demo_data[0][\"text\"][:300]}...')" | ||
| 427 | + ] | ||
| 428 | + }, | ||
| 429 | + { | ||
| 430 | + "cell_type": "markdown", | ||
| 431 | + "metadata": {}, | ||
| 432 | + "source": [ | ||
| 433 | + "### §7.3 构造 LoRA 模型 + 测一步训练时间(同时用 SwanLab 记录)\n", | ||
| 434 | + "\n", | ||
| 435 | + "跑 30 步 LoRA SFT,每步把 loss 和 step_time_ms 推到 SwanLab。两个变体(baseline / patched)独立 run,可以在 SwanLab 上叠加对比。\n", | ||
| 436 | + "\n", | ||
| 437 | + "> **使用 SwanLab 前**:在 shell 里跑 `swanlab login`,输入你的 API key([swanlab.cn/settings](https://swanlab.cn/settings))。如果没登录,cell 里有 try/except 兜底,跟踪会跳过但测速本身不受影响。\n", | ||
| 438 | + "\n", | ||
| 439 | + "> ⏳ **训练耗时较长**(baseline + patched 共两轮 LoRA SFT,约 10-20 分钟)。**点击运行后等 cell 跑完再点下一个**——SwanLab 训练 cell 不会自动跳转,连续点会重复触发训练。" | ||
| 440 | + ] | ||
| 441 | + }, | ||
| 442 | + { | ||
| 443 | + "cell_type": "code", | ||
| 444 | + "execution_count": null, | ||
| 445 | + "metadata": {}, | ||
| 446 | + "outputs": [], | ||
| 447 | + "source": [ | ||
| 448 | + "from peft import LoraConfig, get_peft_model\n", | ||
| 449 | + "\n", | ||
| 450 | + "BATCH_SIZE = 4\n", | ||
| 451 | + "SEQ_LEN = 512\n", | ||
| 452 | + "LORA_R = 16\n", | ||
| 453 | + "N_TRAIN_STEPS = 30 # 跑 30 步看时间趋势(前 5 步 warmup)\n", | ||
| 454 | + "WARMUP = 5\n", | ||
| 455 | + "\n", | ||
| 456 | + "def build_lora_model():\n", | ||
| 457 | + " base = AutoModelForCausalLM.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16)\n", | ||
| 458 | + " for p in base.parameters():\n", | ||
| 459 | + " p.requires_grad = False\n", | ||
| 460 | + " cfg = LoraConfig(\n", | ||
| 461 | + " r=LORA_R, lora_alpha=LORA_R*2,\n", | ||
| 462 | + " target_modules=['q_proj','k_proj','v_proj','o_proj','gate_proj','up_proj','down_proj'],\n", | ||
| 463 | + " lora_dropout=0.0, bias='none', task_type='CAUSAL_LM',\n", | ||
| 464 | + " )\n", | ||
| 465 | + " return get_peft_model(base, cfg).to('npu')\n", | ||
| 466 | + "\n", | ||
| 467 | + "def make_batches(n):\n", | ||
| 468 | + " \"\"\"取 n 个 batch(每个 batch_size 条),固定 padding 到 seq_len。\n", | ||
| 469 | + " label 里的 pad 位置设为 -100 → CrossEntropy 会忽略,loss 才能正常下降。\"\"\"\n", | ||
| 470 | + " batches = []\n", | ||
| 471 | + " for i in range(n):\n", | ||
| 472 | + " texts = [demo_data[(i * BATCH_SIZE + j) % len(demo_data)]['text']\n", | ||
| 473 | + " for j in range(BATCH_SIZE)]\n", | ||
| 474 | + " enc = tokenizer(texts, return_tensors='pt', truncation=True,\n", | ||
| 475 | + " padding='max_length', max_length=SEQ_LEN).to('npu')\n", | ||
| 476 | + " labels = enc['input_ids'].clone()\n", | ||
| 477 | + " labels[enc['attention_mask'] == 0] = -100 # 不要 train pad token\n", | ||
| 478 | + " enc['labels'] = labels\n", | ||
| 479 | + " batches.append(enc)\n", | ||
| 480 | + " return batches\n", | ||
| 481 | + "\n", | ||
| 482 | + "batches = make_batches(N_TRAIN_STEPS)\n", | ||
| 483 | + "print(f'构造 {N_TRAIN_STEPS} 个 batch, batch_size={BATCH_SIZE}, seq_len={SEQ_LEN}')\n", | ||
| 484 | + "print(f'M = batch × seq_len = {BATCH_SIZE * SEQ_LEN}')" | ||
| 485 | + ] | ||
| 486 | + }, | ||
| 487 | + { | ||
| 488 | + "cell_type": "code", | ||
| 489 | + "execution_count": null, | ||
| 490 | + "metadata": {}, | ||
| 491 | + "outputs": [], | ||
| 492 | + "source": [ | ||
| 493 | + "# ====== SwanLab 登录 + 配置 ======\n", | ||
| 494 | + "import getpass\n", | ||
| 495 | + "import swanlab\n", | ||
| 496 | + "from pathlib import Path\n", | ||
| 497 | + "\n", | ||
| 498 | + "# 项目名统一为 CANN_SwanLab,便于和前两章实验放在同一项目下\n", | ||
| 499 | + "%env SWANLAB_PROJ=CANN_SwanLab\n", | ||
| 500 | + "\n", | ||
| 501 | + "def _swanlab_credential_saved() -> bool:\n", | ||
| 502 | + " \"\"\"检查本地是否已有 SwanLab 凭证(默认存储在 ~/.netrc)\"\"\"\n", | ||
| 503 | + " netrc = Path.home() / '.netrc'\n", | ||
| 504 | + " if not netrc.exists():\n", | ||
| 505 | + " return False\n", | ||
| 506 | + " try:\n", | ||
| 507 | + " return 'swanlab.cn' in netrc.read_text()\n", | ||
| 508 | + " except Exception:\n", | ||
| 509 | + " return False\n", | ||
| 510 | + "\n", | ||
| 511 | + "if _swanlab_credential_saved():\n", | ||
| 512 | + " print('检测到本地已有 SwanLab 凭证,跳过登录。')\n", | ||
| 513 | + "else:\n", | ||
| 514 | + " api_key = getpass.getpass('请输入你的 SwanLab API Key(从 https://swanlab.cn/settings 获取): ')\n", | ||
| 515 | + " swanlab.login(api_key=api_key, save=True)\n", | ||
| 516 | + " print('SwanLab 登录成功!凭证已保存到 ~/.netrc,下次运行无需再输入。')\n", | ||
| 517 | + "\n", | ||
| 518 | + "\n", | ||
| 519 | + "swanlab_runs_logged = 0 # 累计真正记录成功的 run 数\n", | ||
| 520 | + "\n", | ||
| 521 | + "\n", | ||
| 522 | + "def measure_step_time(model, batches, warmup=WARMUP, run_name='baseline'):\n", | ||
| 523 | + " \"\"\"跑 len(batches) 个 step,返回稳定阶段平均 step time + 全部 loss。\n", | ||
| 524 | + " 每步的 loss / step_time_ms 同步推到 SwanLab。\"\"\"\n", | ||
| 525 | + " global swanlab_runs_logged\n", | ||
| 526 | + " swanlab.init(\n", | ||
| 527 | + " project=os.environ['SWANLAB_PROJ'],\n", | ||
| 528 | + " experiment_name=f'SwanRmsNorm-{run_name}',\n", | ||
| 529 | + " config=dict(\n", | ||
| 530 | + " model=MODEL_PATH, batch_size=BATCH_SIZE, seq_len=SEQ_LEN,\n", | ||
| 531 | + " lora_r=LORA_R, optimizer='AdamW', lr=2e-4,\n", | ||
| 532 | + " variant=run_name,\n", | ||
| 533 | + " ),\n", | ||
| 534 | + " reinit=True,\n", | ||
| 535 | + " )\n", | ||
| 536 | + "\n", | ||
| 537 | + " model.train()\n", | ||
| 538 | + " opt = torch.optim.AdamW(\n", | ||
| 539 | + " [p for p in model.parameters() if p.requires_grad], lr=2e-4)\n", | ||
| 540 | + "\n", | ||
| 541 | + " times, losses = [], []\n", | ||
| 542 | + " for i, batch in enumerate(batches):\n", | ||
| 543 | + " torch_npu.npu.synchronize()\n", | ||
| 544 | + " t0 = time.perf_counter()\n", | ||
| 545 | + " opt.zero_grad(set_to_none=True)\n", | ||
| 546 | + " out = model(**batch)\n", | ||
| 547 | + " out.loss.backward()\n", | ||
| 548 | + " opt.step()\n", | ||
| 549 | + " torch_npu.npu.synchronize()\n", | ||
| 550 | + " step_ms = (time.perf_counter() - t0) * 1000\n", | ||
| 551 | + " loss_v = float(out.loss.detach().item())\n", | ||
| 552 | + " times.append(step_ms)\n", | ||
| 553 | + " losses.append(loss_v)\n", | ||
| 554 | + "\n", | ||
| 555 | + " swanlab.log({\n", | ||
| 556 | + " 'loss': loss_v,\n", | ||
| 557 | + " 'step_time_ms': step_ms,\n", | ||
| 558 | + " 'is_warmup': int(i < warmup),\n", | ||
| 559 | + " }, step=i)\n", | ||
| 560 | + "\n", | ||
| 561 | + " swanlab.finish()\n", | ||
| 562 | + " swanlab_runs_logged += 1\n", | ||
| 563 | + "\n", | ||
| 564 | + " stable = times[warmup:]\n", | ||
| 565 | + " return sum(stable) / len(stable), times, losses\n", | ||
| 566 | + "\n", | ||
| 567 | + "\n", | ||
| 568 | + "# Baseline LoRA SFT\n", | ||
| 569 | + "print('===== Baseline LoRA SFT =====')\n", | ||
| 570 | + "m = build_lora_model()\n", | ||
| 571 | + "n_train = sum(p.numel() for p in m.parameters() if p.requires_grad)\n", | ||
| 572 | + "print(f' trainable params: {n_train:,}')\n", | ||
| 573 | + "t_base, times_base, losses_base = measure_step_time(m, batches, run_name='baseline')\n", | ||
| 574 | + "print(f' step time: {t_base:.2f} ms')\n", | ||
| 575 | + "print(f' loss: first={losses_base[0]:.3f} last={losses_base[-1]:.3f} (期望下降)')\n", | ||
| 576 | + "del m; gc.collect(); torch.npu.empty_cache()\n", | ||
| 577 | + "\n", | ||
| 578 | + "# Patched LoRA SFT\n", | ||
| 579 | + "print('\\n===== Patched LoRA SFT (SwanRmsNorm) =====')\n", | ||
| 580 | + "m = build_lora_model()\n", | ||
| 581 | + "patch_qwen3_rmsnorm(m)\n", | ||
| 582 | + "t_patched, times_patched, losses_patched = measure_step_time(m, batches, run_name='patched')\n", | ||
| 583 | + "print(f' step time: {t_patched:.2f} ms')\n", | ||
| 584 | + "print(f' loss: first={losses_patched[0]:.3f} last={losses_patched[-1]:.3f} (期望下降)')\n", | ||
| 585 | + "del m; gc.collect(); torch.npu.empty_cache()\n", | ||
| 586 | + "\n", | ||
| 587 | + "sft_speedup = t_base / t_patched\n", | ||
| 588 | + "print('\\n' + '═' * 50)\n", | ||
| 589 | + "print(f' Baseline step : {t_base:.2f} ms')\n", | ||
| 590 | + "print(f' Patched step : {t_patched:.2f} ms')\n", | ||
| 591 | + "print(f' SFT Speedup : {sft_speedup:.3f} ×')\n", | ||
| 592 | + "print('═' * 50)\n", | ||
| 593 | + "print(f'\\n已记录 {swanlab_runs_logged} 个 SwanLab run(baseline + patched),在 swanlab.cn 上看 loss / step_time 对比。')" | ||
| 594 | + ] | ||
| 595 | + }, | ||
| 596 | + { | ||
| 597 | + "cell_type": "markdown", | ||
| 598 | + "metadata": {}, | ||
| 599 | + "source": [ | ||
| 600 | + "### §7.4 微调加速可视化" | ||
| 601 | + ] | ||
| 602 | + }, | ||
| 603 | + { | ||
| 604 | + "cell_type": "code", | ||
| 605 | + "execution_count": null, | ||
| 606 | + "metadata": {}, | ||
| 607 | + "outputs": [], | ||
| 608 | + "source": [ | ||
| 609 | + "import os\n", | ||
| 610 | + "import numpy as np\n", | ||
| 611 | + "import matplotlib.pyplot as plt # 显式 import:即使你跳过了 §6.1,本 cell 也能跑\n", | ||
| 612 | + "\n", | ||
| 613 | + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))\n", | ||
| 614 | + "\n", | ||
| 615 | + "# 左图:每个 step 的 step time(看稳定性)\n", | ||
| 616 | + "xs = np.arange(len(times_base))\n", | ||
| 617 | + "ax1.plot(xs, times_base, 'o-', color='#9E9E9E', markersize=3, label='Baseline')\n", | ||
| 618 | + "ax1.plot(xs, times_patched, 'o-', color='#4CAF50', markersize=3, label='Patched (SwanRmsNorm)')\n", | ||
| 619 | + "ax1.axvspan(0, WARMUP - 0.5, alpha=0.15, color='red', label='warmup (excluded)')\n", | ||
| 620 | + "ax1.set_xlabel('step')\n", | ||
| 621 | + "ax1.set_ylabel('step time (ms)')\n", | ||
| 622 | + "ax1.set_title(f'LoRA SFT step time (B={BATCH_SIZE}, S={SEQ_LEN})')\n", | ||
| 623 | + "ax1.legend()\n", | ||
| 624 | + "ax1.grid(alpha=0.3)\n", | ||
| 625 | + "\n", | ||
| 626 | + "# 右图:平均 step time 对比\n", | ||
| 627 | + "ax2.bar(['Baseline', 'Patched\\n(SwanRmsNorm)'],\n", | ||
| 628 | + " [t_base, t_patched],\n", | ||
| 629 | + " color=['#9E9E9E', '#4CAF50'])\n", | ||
| 630 | + "ax2.set_ylabel('avg step time (ms)')\n", | ||
| 631 | + "ax2.set_title(f'LoRA SFT Speedup: {sft_speedup:.3f}×')\n", | ||
| 632 | + "ax2.text(1, t_patched, f'{sft_speedup:.2f}×', ha='center', va='bottom',\n", | ||
| 633 | + " fontsize=14, fontweight='bold', color='#2E7D32')\n", | ||
| 634 | + "\n", | ||
| 635 | + "plt.tight_layout()\n", | ||
| 636 | + "plt.savefig('notebook_lora_sft.png', dpi=120)\n", | ||
| 637 | + "plt.show()\n", | ||
| 638 | + "print('保存到 notebook_lora_sft.png')" | ||
| 639 | + ] | ||
| 640 | + }, | ||
| 641 | + { | ||
| 642 | + "cell_type": "markdown", | ||
| 643 | + "metadata": {}, | ||
| 644 | + "source": [ | ||
| 645 | + "---\n", | ||
| 646 | + "\n", | ||
| 647 | + "## §8 总结\n", | ||
| 648 | + "\n", | ||
| 649 | + "你完成了一条完整的「把课程提供的 AscendC kernel 接到大模型」实操链路:\n", | ||
| 650 | + "\n", | ||
| 651 | + "1. ✅ 读懂了 ~150 行 SwanRmsNorm AscendC kernel 的 fused 设计\n", | ||
| 652 | + "2. ✅ 在 NPU 上完成编译、部署\n", | ||
| 653 | + "3. ✅ **自己写**了 PyTorch 集成层:autograd.Function + monkey-patch(带工厂函数)\n", | ||
| 654 | + "4. ✅ 推理对比:tokens/sec 端到端 **~1.30-1.55×** 加速(145 个 RMSNorm 模块全部接管)\n", | ||
| 655 | + "5. ✅ LoRA SFT 对比:step time 端到端 **~1.05-1.15×** 加速,loss + step time 用 SwanLab 全记录\n", | ||
| 656 | + "\n", | ||
| 657 | + "为什么单算子 ~7× 加速但端到端只有 1.5×?这是 **Amdahl 律**——RMSNorm 在整步推理只占 25%,所以 7× 局部 → 1.5× 全局;在 SFT 一步里只占 ~10%,所以只有 1.05-1.15× 端到端。\n", | ||
| 658 | + "\n", | ||
| 659 | + "**详见指导书第九章总结与讨论。** 提交 `results/notebook_inference.png` 和 `results/notebook_lora_sft.png` 作为本次实训交付物,SwanLab 的 run 链接也可以一并提交。" | ||
| 660 | + ] | ||
| 661 | + } | ||
| 662 | + ], | ||
| 663 | + "metadata": { | ||
| 664 | + "kernelspec": { | ||
| 665 | + "display_name": "base", | ||
| 666 | + "language": "python", | ||
| 667 | + "name": "python3" | ||
| 668 | + }, | ||
| 669 | + "language_info": { | ||
| 670 | + "name": "python", | ||
| 671 | + "version": "3.12.2" | ||
| 672 | + } | ||
| 673 | + }, | ||
| 674 | + "nbformat": 4, | ||
| 675 | + "nbformat_minor": 4 | ||
| 676 | +} | ||
| @@ -0,0 +1,24 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# 第 05 章 · 性能调优 | ||
| 12 | + | ||
| 13 | +## 章节定位 | ||
| 14 | + | ||
| 15 | +本章面向**已经跑通过 02 章 SFT / LoRA 的同学**,时长约 **45–60 分钟**,作为大模型课程通往 **AscendC 算子开发课程**的过渡节。 | ||
| 16 | +预期读者在学完本章后,能用 Amdahl 律解释"为什么单算子 ~7× 加速到端到端只剩 ~1.5×",能跑通一次别人写好的 fused AscendC kernel 在 Qwen3 上的接入与提速,并理解下一步为什么要自己动手写 kernel。 | ||
| 17 | + | ||
| 18 | +## 计划节次 | ||
| 19 | + | ||
| 20 | +| 节次 | 标题 | 状态 | | ||
| 21 | +|------|------|------| | ||
| 22 | +| 05.01 | 性能调优全景与 Amdahl 律 | 建设中 | | ||
| 23 | +| [05.02](./05.02_swan_rmsnorm_acceleration.ipynb) | SwanRmsNorm AscendC 算子加速 Qwen3 微调 | 已发布 | | ||
| 24 | +| 05.03 | 章节小结:从"接算子"走向"写算子" | 建设中 | | ||
Binary files do not support preview
| @@ -0,0 +1,83 @@ | |||
| 1 | +<div align="center" style="border: 1px solid #eee; padding: 20px; border-radius: 10px; margin-bottom: 20px; max-width: 320px; margin-left: auto; margin-right: auto;"> | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +SwanLab x CANN 社区合作课程 | ||
| 6 | + | ||
| 7 | +</div> | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +# Swan LLM 大模型实战课程 | ||
| 12 | + | ||
| 13 | +## 课程作者与联系方式 | ||
| 14 | + | ||
| 15 | +- **作者**:韩翔宇(情感机器 SwanLab 实验室 AI 研究员)等 | ||
| 16 | +- **邮箱**:[pescn@115lab.club](mailto:pescn@115lab.club) | ||
| 17 | +- **课程背景**:本课程内容来源于 CANN 社区与 SwanLab 团队正在开展的**线下启航营**,面向高校同学讲解昇腾算力上的大语言模型训练、对齐、推理与加速。如果你所在的学校 / 社团 / 实验室希望接入这套教材,或者希望在你们学校落地一期启航营,欢迎通过讨论区或邮件联系。 | ||
| 18 | + | ||
| 19 | +## 课程简介 | ||
| 20 | + | ||
| 21 | +本课程面向**高校在校学生**,结合正在开展的**线下启航营**实践内容,围绕大语言模型在昇腾 NPU 上的**基础理论 → 微调 → 强化学习 → 推理部署 → 性能调优**展开。我们希望通过这套课程,帮助同学们: | ||
| 22 | + | ||
| 23 | +- 建立大模型从训练到部署的完整心智模型,而不是孤立地学某一个工具 | ||
| 24 | +- 真正理解微调(SFT / LoRA)、强化学习对齐(RLHF / DPO / GRPO)的工程实现细节 | ||
| 25 | +- 在昇腾 NPU + CANN 的真实硬件上完成训练 / 推理,体会国产算力栈下的工程权衡 | ||
| 26 | +- 通过 AscendC 自定义算子优化实战,理解大模型训练 / 推理性能瓶颈的来源及优化思路 | ||
| 27 | + | ||
| 28 | +## 适合人群 | ||
| 29 | + | ||
| 30 | +> 建议学习者具备 Python 基础和线性代数基础。但 **不要求** 事先有 Ascend C 经验 | ||
| 31 | + | ||
| 32 | +- 对大模型有兴趣,但还没真正跑通一次端到端训练 / 部署的本科生 / 研究生 | ||
| 33 | +- 已经在其他智算卡上跑过 transformers / trl,想了解昇腾 NPU 上有什么不一样的同学 | ||
| 34 | +- 想理解为什么大家都在写自定义算子,并亲手优化一次的同学 | ||
| 35 | + | ||
| 36 | +## 课程目录 | ||
| 37 | + | ||
| 38 | +| 章节 | 标题 | 内容概要 | 状态 | | ||
| 39 | +|------|------|----------|------| | ||
| 40 | +| [01](./01_llm_basic/) | 大语言模型基础理论介绍 | Transformer、自注意力、预训练 / SFT / RLHF 三段式、推理与 KV cache | 建设中 | | ||
| 41 | +| [02](./02_llm_finetune/) | 大语言模型微调 | Qwen3 全参数 SFT、LoRA 微调、Loss Mask、AST + 可执行性评估 | 已发布部分节次 | | ||
| 42 | +| [03](./03_llm_rl/) | 大语言模型强化学习 | RLHF / PPO、DPO、GRPO,以代码可执行性作为奖励信号的案例 | 建设中 | | ||
| 43 | +| [04](./04_llm_inference_deploy/) | 大语言模型推理部署 | KV cache、continuous batching、量化、推理引擎与服务化 | 建设中 | | ||
| 44 | +| [05](./05_performance_optimization/) | 性能调优 | AscendC 自定义算子接入 PyTorch、Amdahl 律、推理 / 训练端到端加速 | 已发布部分节次 | | ||
| 45 | + | ||
| 46 | +### 已发布节次速览 | ||
| 47 | + | ||
| 48 | +| 节次 | 标题 | | ||
| 49 | +|------|------| | ||
| 50 | +| [02.04](./02_llm_finetune/02.04_qwen3_instruction_sft.ipynb) | Qwen3 基座模型指令微调(SFT) | | ||
| 51 | +| [05.02](./05_performance_optimization/05.02_swan_rmsnorm_acceleration.ipynb) | SwanRmsNorm AscendC 算子加速 Qwen3 微调 | | ||
| 52 | + | ||
| 53 | +## 目录与命名规范 | ||
| 54 | + | ||
| 55 | +每个章节目录的统一约定如下: | ||
| 56 | + | ||
| 57 | +``` | ||
| 58 | +0X_<chapter_slug>/ | ||
| 59 | +├── README.md # 章节定位、节次清单、运行说明 | ||
| 60 | +├── 0X.01_<section_slug>.ipynb # 节次 Notebook | ||
| 61 | +├── 0X.02_<section_slug>.ipynb | ||
| 62 | +├── data/ # 节次配套数据集(可选) | ||
| 63 | +├── pdf/ # 节次配套 PDF(可选) | ||
| 64 | +├── images/ # 章节配图(可选) | ||
| 65 | +└── answer/ # 实操题参考答案(可选) | ||
| 66 | +``` | ||
| 67 | + | ||
| 68 | +## 运行环境 | ||
| 69 | + | ||
| 70 | +| 项目 | 推荐配置 | | ||
| 71 | +|------|----------| | ||
| 72 | +| 硬件 | 昇腾 910C / 910B | | ||
| 73 | +| 软件 | CANN ≥ 8.5、Python 3.10/3.11、PyTorch 2.x + `torch_npu` | | ||
| 74 | +| 平台 | 推荐 [CANNLab](https://gitcode.com/) | | ||
| 75 | +| 第三方 | SwanLab(实验可视化)、ModelScope(模型与数据集下载)、TRIO | | ||
| 76 | + | ||
| 77 | +## 反馈与贡献 | ||
| 78 | + | ||
| 79 | +发现 Notebook 里的 bug、对某一章节有改进建议,或希望补充占位章节的内容,欢迎在 [cann/cann-learning-hub](https://gitcode.com/cann/cann-learning-hub) 仓库提 Issue 或 PR。 | ||
| 80 | + | ||
| 81 | +--- | ||
| 82 | + | ||
| 83 | +> 如果这套课程帮到了你,欢迎给本课程项目点个 Star ⭐,分享给更多对大模型感兴趣的同学! | ||