Day 04 · CUDA 编程入门 (1)
从 Host 到 Device:理解 kernel 启动、Grid/Block/Thread 线程层级与 Global/Shared Memory 存储模型,动手实现 vector add 与 naive GEMM。
LLM 数学基础 40 天学习路线
从线性代数到 Transformer 实现:覆盖向量空间、矩阵微积分、概率论、信息论、优化算法的 40 天数学学习路线图。
Day 02 · Linux / 容器基础回顾
深入 Linux 隔离原语 cgroups 与 namespaces,掌握 Docker GPU 容器的启动链路,动手编写 CUDA Dockerfile 并运行第一个 GPU 容器。
Day 03 · GPU 硬件与体系结构
拆解 GPU 微架构:从 SM、Warp 调度到 Tensor Core,理解 HBM-L2-SMEM 存储层级与算术强度,对比 A100/H100/H20 代际演进。
AI Infra 60 天系统学习路线
从 GPU 编程入门到 LLM 训推一体平台:覆盖 CUDA、PyTorch 内部机制、分布式训练、推理引擎、平台调度的全栈 60 天学习路线图。
Day 01 · AI Infra 全景与学习环境
AI Infra 60 天学习计划的起点:梳理从用户 prompt 到 GPU kernel 的完整链路,搭建开发环境,理解推理与训练基础设施的全景图。
个人用 SKILLS
绘图 SKILL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 # Paper-tone Diagram 学术克制 × 手工纸感 × 编辑设计。回避现代 SaaS 风(饱和蓝、纯白底、阴影泛滥、圆角滥用),把"信息"当作"印刷品"来呈现。适用流程图 / 架构图 / pipeline 示意图,中文友好,可输出 HTML、Excalidraw、SVG。 气质关键词:**安静、考究、可读、不喧哗**。 --- ## 1 · Color Tokens(固定不可改) ``` --ivory: #FAF9F5 主背景(代替纯白) --paper: #FFFFFF 卡片背景 --slate: #141413 主文字(几乎黑但偏暖) --clay: #D97757 唯一强调色 — 关键路径 / 序号 / 斜体 --clay-d: #B85C3E 强调暗色,备用 --oat: #E3DACC 暖灰 — hover / 下划线 --olive: #788C5D 第二色,用量 ≈ clay 的 1/5 --g100: #F0EEE6 最浅灰 — 徽章背景 --g200: #E6E3DA 边框 / 分隔 --g300: #D1CFC5 标准描边 --g500: #87867F 次要文字、Mono 标签 --g700: #3D3D3A 正文次级 ``` 调色法则:**没有冷灰**(所有灰带土黄底);全页只有 clay 一种亮色,占比 < 20%;不用纯黑纯白;olive 仅作分类区分,用量约 clay 的 1/5。 --- ## 2 · Typography(固定) ``` --serif: ui-serif, Georgia, "Times New Roman", Times, serif; --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace; ``` | 用途 | 字体 | 字号 | 字重 | 字距 | |---|---|---|---|---| | H1 | serif | clamp(38, 5.4vw, 62)px | **500** | -0.018em | | H2 章节 | serif | 27px | 500 | -0.012em | | 卡片标题 | serif | 19px | 500 | -0.008em | | 正文 lede | sans | 16.5px | 400 | — | | 卡片描述 | sans | 13.5px | 400 | — | | Mono 标签 | mono | 10–13px | 400/600 | 0.08–0.12em | 铁律:serif 标题字重统一 **500**(不要 600/700);H1 用负字距 -0.018em + `max-width:17ch`;斜体词用 `<em>` + clay 色,是页面唯一的"装饰修辞"。 --- ## 3 · Layout ``` .wrap { max-width:1120px; margin:0 auto; padding:0 32px 140px; } ``` - 章节间距 **72px**;header 顶部 80px / 底部 56px;footer 顶部 100px - **50px 悬挂缩进** — 章节标题不缩,介绍与卡片网格 `margin-left:50px` - 卡片网格 `grid-template-columns: repeat(auto-fill, minmax(316px, 1fr)); gap:20px` --- ## 4 · Borders / Radius / Motion - 描边 **1.5px**(HTML) / **1px**(Excalidraw,无 1.5 选项) - 圆角:5px(SVG 内)、6px(tag)、10–14px(卡片)、999px(胶囊) - 动效时长 **120–150ms**(纸面回弹感) - 只用 transform + 颜色 + 阴影,**不缩放、不旋转、不淡入** - 默认 ease,不用 cubic-bezier --- ## 5 · 通用结构 — Vertical Pipeline 流程图 每张图按这个骨架组织: 1. **masthead** — eyebrow(▬ + Mono 大写)+ H1(serif 500,带 `<em>` 斜体)+ lede(sans 16.5)+ meta strip(Mono 横条罗列关键技术栈) 2. **section header** — 序号(Mono / clay / 34px 宽)+ 标题(serif 27)+ count(灰胶囊) 3. **节点垂直流** — 每节点 = mono tag + serif title + sans sub 4. **节点间 vertical arrow** — strokeWidth=1, color=g500, 长度 ~42px 5. **右侧 mono 注释** — 前置 24×1.5px clay 短横线 + Mono 11px 灰字 6. **大型节点用 panel** — slate border,内嵌 4 列子模块网格,关键子模块用 clay border;子模块再下沉到深层硬件栈(深色块用 slate 背景 + ivory 文字) 7. **底部 legend** — 色彩与符号说明 8. **footer** — 衬线斜体格言 + 仓库链接 节点状态 mapping: - `entry` — bg=g100, border=slate - 普通 service — bg=transparent, border=slate - `accent`(关键路径) — border=clay - `exit` — bg=oat, border=clay - `dark`(硬件层) — bg=slate, 文字=ivory, 副=oat --- ## 6 · Variant A — HTML 单文件 CSS 起手必备: ```css :root{ --ivory:#FAF9F5; --paper:#FFFFFF; --slate:#141413; --clay:#D97757; --clay-d:#B85C3E; --oat:#E3DACC; --olive:#788C5D; --g100:#F0EEE6; --g200:#E6E3DA; --g300:#D1CFC5; --g500:#87867F; --g700:#3D3D3A; --serif: ui-serif, Georgia, serif; --sans: system-ui, -apple-system, sans-serif; --mono: ui-monospace, "SF Mono", Menlo, monospace; } body{ background:var(--ivory); color:var(--slate); font-family:var(--sans); line-height:1.55; } .wrap{ max-width:1120px; margin:0 auto; padding:0 32px 140px; } .eyebrow{ font-family:var(--mono); font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--g500); } .eyebrow::before{ content:""; display:inline-block; width:24px; height:1.5px; background:var(--clay); margin-right:12px; vertical-align:middle; } h1{ font-family:var(--serif); font-weight:500; font-size:clamp(38px, 5.4vw, 62px); letter-spacing:-0.018em; line-height:1.06; max-width:17ch; } h1 em{ font-style:italic; color:var(--clay); } ``` 节点卡片: ```css .node{ background:var(--paper); border:1.5px solid var(--g300); border-radius:14px; padding:18px 22px; transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease; } .node:hover{ transform:translateY(-3px); box-shadow:0 10px 30px rgba(20,20,19,.10); border-color:var(--slate); } .node.accent{ border-color:var(--clay); } .node.entry { background:var(--g100); } .node.exit { background:var(--oat); border-color:var(--clay); } .annot{ font-family:var(--mono); font-size:11.5px; color:var(--g500); display:flex; align-items:center; } .annot::before{ content:""; width:32px; height:1.5px; background:var(--clay); margin-right:12px; } ``` 垂直箭头(SVG): ```html <svg width="14" height="46" viewBox="0 0 14 46"> <line x1="7" y1="2" x2="7" y2="34" stroke="#87867F" stroke-width="2.5" stroke-linecap="round"/> <polyline points="2,32 7,42 12,32" fill="none" stroke="#87867F" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> ``` --- ## 7 · Variant B — Excalidraw (.excalidraw) JSON 文件骨架: ```json { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [...], "appState": { "gridSize": null, "viewBackgroundColor": "#FAF9F5" }, "files": {} } ``` 每个 element 必备字段:`id, type, x, y, width, height, angle, strokeColor, backgroundColor, fillStyle, strokeWidth, strokeStyle, roughness, opacity, groupIds, frameId, roundness, seed, version, versionNonce, isDeleted, boundElements, updatedAt, link, locked` 铁律: - **`roughness: 0`** 全画布禁用手绘抖动(学术风核心) - `strokeWidth: 1`(Excalidraw 没有 1.5) - 圆角矩形:`"roundness": {"type": 3}` - `fillStyle: "solid"`(不用 hachure 斜线填充) - 字体: 统一使用 Comic Shanns 字体,font-family: 8 推荐用 Python 脚本生成,因为 JSON 重复字段多。最小工厂: ```python import json, time, random random.seed(42) elements = [] def base(): return { "id": f"id-{random.randint(10**8, 10**12)}", "angle": 0, "strokeColor": "#141413", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": [], "frameId": None, "roundness": None, "seed": random.randint(1, 10**9), "version": 1, "versionNonce": random.randint(1, 10**9), "isDeleted": False, "boundElements": None, "updatedAt": int(time.time() * 1000), "link": None, "locked": False, } def rect(x, y, w, h, *, stroke="#141413", bg="transparent", sw=1, rounded=True): el = base(); el.update({"type":"rectangle", "x":x, "y":y, "width":w, "height":h, "strokeColor":stroke, "backgroundColor":bg, "strokeWidth":sw, "roundness": {"type":3} if rounded else None}) elements.append(el); return el def text(x, y, content, *, size=14, font=2, color="#141413", w=None, align="left"): if w is None: cn = sum(1 for c in content if ord(c) > 127) w = int(cn*size + (len(content)-cn)*size*0.55) + 6 el = base(); el.update({"type":"text", "x":x, "y":y, "width":w, "height":int(size*1.25), "text":content, "fontSize":size, "fontFamily":font, "textAlign":align, "verticalAlign":"top", "containerId":None, "originalText":content, "lineHeight":1.25, "baseline":int(size*0.9), "strokeColor":color}) elements.append(el); return el def arrow(x1, y1, x2, y2, *, color="#87867F", sw=1): dx, dy = x2-x1, y2-y1 el = base(); el.update({"type":"arrow", "x":x1, "y":y1, "width":max(abs(dx),1), "height":max(abs(dy),1), "points":[[0,0],[dx,dy]], "lastCommittedPoint":None, "startBinding":None, "endBinding":None, "startArrowhead":None, "endArrowhead":"arrow", "strokeColor":color, "strokeWidth":sw}) elements.append(el); return el def line(x1, y1, x2, y2, *, color="#87867F", sw=1, dashed=False): dx, dy = x2-x1, y2-y1 el = base(); el.update({"type":"line", "x":x1, "y":y1, "width":max(abs(dx),1), "height":max(abs(dy),1), "points":[[0,0],[dx,dy]], "lastCommittedPoint":None, "startBinding":None, "endBinding":None, "startArrowhead":None, "endArrowhead":None, "strokeColor":color, "strokeWidth":sw, "strokeStyle": "dashed" if dashed else "solid"}) elements.append(el); return el # ... 调用 rect/text/arrow 组装画布,最后: data = {"type":"excalidraw", "version":2, "source":"https://excalidraw.com", "elements":elements, "appState":{"gridSize":None, "viewBackgroundColor":"#FAF9F5"}, "files":{}} json.dump(data, open("out.excalidraw","w"), ensure_ascii=False, indent=2) ``` 布局参考(垂直流程图): - 主轴中心 X = 700,卡片宽 380,普通节点高 78 - 节点之间 arrow 长度 42 - 大面板宽 540~560,内嵌 4 列子模块(每个 ~120 宽,gap 12) - 大面板的"04 · ENGINE"标签:在 border 上浮一个 IVORY 背景的小矩形 + clay Mono 文字 - 右侧详细注释面板 X = 主面板右边缘 + 60,宽 320 - 注释条目格式:Mono clay 小标题(`§ NN · 主题`)+ sans g700 多行正文 --- ## 8 · Variant C — SVG 缩略图 放在卡片缩略图区(.thumb,132px 高,灰底)。 约定: - `stroke-width: 2.5` - `stroke-linecap: round` - 圆角 `rx="4"` 或 `5` - 一张图只用 3–5 个色阶,clay 占比 < 20% - 几何元素:`<rect>`、`<circle>`、`<line>`,**不画曲线** class 命名: - `.st` 描边 (g500) / `.fl` 填充 (g300) / `.cl` clay 填充 / `.ol` olive 填充 - `.oa` oat 填充 + g500 描边 / `.sl` slate 填充 / `.wh` 白填充 + g500 描边 - `.ln` 灰线 / `.lc` clay 线 / `.da` 虚线 (dasharray 4 4) --- ## 9 · 输出前自检 Checklist 每张图都必须满足: 1. ☐ 背景 `#FAF9F5` 米白,**不用纯白** 2. ☐ 强调色只有 `#D97757` 粘土橙,占比 < 20%,无第二亮色 3. ☐ 大标题用 serif(Excalidraw 例外:Helvetica 大字号 字重 500) 4. ☐ Mono 标签全部大写 + 宽字距(eyebrow / 序号 / 文件名 / 注释) 5. ☐ 章节缩进 50px(标题不缩,正文与卡片缩) 6. ☐ 描边 1.5px(HTML)/ 1px(Excalidraw),圆角 10–14px 7. ☐ 没有阴影泛滥(只在 hover 时柔和阴影) 8. ☐ 没有冷灰(灰一律带土黄底) 9. ☐ Hover 只动 150ms(translateY(-3px) + 颜色 + 阴影) 10. ☐ 大量留白(下边距 ≥ 100px,章节间距 72px) 11. ☐ Excalidraw 必须 `roughness: 0` 12. ☐ 大量中文标注时,字号略放大,行高 ≥ 1.5,留白略多 --- ## 10 · 命名与产物建议 - HTML:`{topic}-pipeline.html` / `{topic}-architecture.html` - Excalidraw:`{topic}.excalidraw`(中文文件名也支持) - 生成脚本:`gen_{topic}.py`,参数化坐标 / 文字 / 配色,便于反复调整 > 这套设计的精神:把"信息"当作"印刷品",而不是"界面"。少即是多,但少不是空 — 是经过裁剪的丰富。
AI 数学基础
线性代数 向量与向量空间 - 向量与向量空间 - 什么是向量 - 向量是一个有方向的量 - 几何视角:空间中从原点出发的一支箭头,方向和长度都有意义 - 代数视角:一组有序的数字 - 示例 -  - 这是一个三维向量,每个数字对应一个坐标轴上的分量。 - 词嵌入 - 一个词(token)会被映射成一个几百甚至几千维的向量 - 比如 "猫" 这个词可能被表示成一个 768 维的向量——你可以把它想象成 768 个坐标轴上的一个点 - 语义相近的词在这个空间里位置接近 - 向量的基本运算 - 向量加法 - 两个向量相加,对应分量分别相加 -  - 几何意义: 把 $\vec{b}$ 的起点接到 $\vec{a}$ 的终点,结果是合向量。 - 在 LLM 里的例子:有研究发现,词向量之间存在近似的语义关系 - vec("国王")−vec("男人")+vec("女人")≈vec("女王") - 标量乘法 - 用一个数(标量)乘以向量,每个分量都乘以这个数 -  - 几何意义:把向量拉伸或压缩 - 点积 - 定义 - 两个同维度向量的点积 -  - 示例 -  - 几何意义 - 点积有另一种等价写法 -  - 其中 $\theta$ 是两向量之间的夹角 - 关键洞察 - 点积可以衡量两个向量"有多相似"。 - 这正是 Attention 机制里 Query 和 Key 做点积的本质——算两个向量的相关程度 - 向量范数(Norm) - 范数衡量向量的"长度"或"大小" - L2 范数(最常用) -  - 就是我们熟悉的欧几里得距离 -  - L1 范数 -  - 各分量绝对值之和,在正则化(防止过拟合)中常用。 - 余弦相似度 - 把点积和范数组合起来,就得到余弦相似度——LLM 里衡量语义相似性的核心工具 -  -  - 余弦相似度只关心方向,不关心长度 - 两个词的向量可能长度不同,但如果方向相同,说明它们在语义上是一致的 - 向量空间 - 满足以下条件的集合叫向量空间 - 元素(向量)之间可以相加,可以被标量乘 - 在这个集合里做加法和乘法,结果还在这个集合里 - 最关键的概念是维度(Dimension):向量空间需要多少个基向量来描述其中所有的点 - GPT-2 用 768 维的向量空间来表示词义,GPT-3 用 12288 维。维度越高,理论上能编码的信息越丰富 - Numpy 代码 - ```python import numpy as np # ─── 1. 创建向量 ─────────────────────────────────────── a = np.array([1, 2, 3], dtype=float) b = np.array([4, -1, 2], dtype=float) print("向量 a:", a) print("向量 b:", b) # ─── 2. 向量加法与标量乘法 ───────────────────────────── print("\n向量加法 a + b:", a + b) print("标量乘法 2 * a:", 2 * a) # ─── 3. 点积 ────────────────────────────────────────── dot_product = np.dot(a, b) print("\n点积 a · b:", dot_product) # 手动验证 manual_dot = sum(a[i] * b[i] for i in range(len(a))) print("手动计算点积:", manual_dot) # ─── 4. L2 范数 ──────────────────────────────────────── norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) print("\n‖a‖₂ =", norm_a) print("‖b‖₂ =", norm_b) # ─── 5. 余弦相似度 ───────────────────────────────────── cos_sim = dot_product / (norm_a * norm_b) print("\n余弦相似度(a, b):", cos_sim) # ─── 6. 模拟词嵌入:哪个词和"猫"最相似? ────────────── # 用随机向量模拟(实际中是模型学习出来的) np.random.seed(42) embedding_dim = 8 # 简化为 8 维演示 word_vectors = { "猫": np.random.randn(embedding_dim), "狗": np.random.randn(embedding_dim), "汽车": np.random.randn(embedding_dim), "小猫": np.random.randn(embedding_dim), } # 手动让"猫"和"小猫"更相似 word_vectors["小猫"] = word_vectors["猫"] + np.random.randn(embedding_dim) * 0.3 def cosine_similarity(v1, v2): return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) query = word_vectors["猫"] print("\n与"猫"的余弦相似度:") for word, vec in word_vectors.items(): if word != "猫": sim = cosine_similarity(query, vec) print(f" {word}: {sim:.4f}") ``` 矩阵基本运算 - 矩阵基本运算 - 矩阵是什么 - 是数字排列成的二维表格,用行数 x 列数描述它的形状(称为"维度"或"shape") -  - 这是一个 2×3 矩阵(2 行 3 列)。用 $A_{ij}$ 表示第 i 行第 j 列的元素,比如 $A_{12} = 2$ - 矩阵的本质是线性变换。 - 描述的是"把一个向量变成另一个向量"的规则。 - 矩阵与向量的乘法 - 计算规则 - 矩阵 $A(m \times n)$ 乘以向量 $\vec{x}$(n 维),结果是一个 m 维向量 -  - 结果的每一行,就是矩阵那一行与向量做点积。 - 具体例子 -  - 一个 3×2 的矩阵,把一个 2 维向量变成了 3 维向量。 - 维度变了——这正是"变换"的含义。 - 几何直觉 -  - 该矩阵可以把任意 2D 向量逆时针旋转 90 度 -  - 原来朝右的向量,变成了朝上的向量。矩阵就是这样编码变换规则的。 - 矩阵 A 乘以向量 $\vec{x}$ 等于把 $\vec{x}$ 所在的空间拉伸、旋转、投影到另一个空间。 - LLM 中的应用 - Transformer 里,每个 token 的向量 $\vec{x}$ 乘以权重矩阵 $W_Q$,得到 Query 向量 - $\vec{q} = W_Q\vec{x}$ - 这就是一次线性变换——把输入向量投影到"Query 空间" - 矩阵乘法 - 计算规则 - 两个矩阵相乘:$A(m\times k) \times B (k\times n) = C(m\times n)$ - 关键约束 - A 的列数必须等于 B 的行数(都是 k)。 - 结果矩阵 C 的第 i 行第 j 列 -  - 即 A 的第 i 行与 B 的第 j 列做点积。 - 具体例子 -  - 矩阵乘法的本质:变换的复合 - 单独看计算规则很枯燥。真正重要的是它的意义 - 矩阵 AB 表示"先做变换 B,再做变换 A"。 - 就像函数复合 $f(g(x))$,矩阵乘法是在组合两个线性变换。 - 神经网络的多层结构,本质上就是多个矩阵变换串联在一起,每一层都对数据做一次变换。 - 矩阵乘法的重要性质 - 不满足交换律 - $AB \neq BA$(大多数情况下) - 先旋转再缩放,和先缩放再旋转,结果不同——顺序很重要。 - 满足结合律 - $(AB)C=A(BC)$ - 这让我们可以灵活选择计算顺序(影响效率,不影响结果) - 转置 - 把矩阵的行和列互换,记作 $A^T$ -  - $A_{ij}^T=A_{ji}$ - 转置的重要性质 - $(AB)^T=B^TA^T$ - $(\vec{a}^T\vec{b}) = \vec{a}\cdot \vec{b}$ - 转置把向量点积统一进了矩阵符号 - LLM 的直接应用 - Attention 公式里的 $QK^T$ 就是对 K 做转置后再和 Q 相乘,目的是让每个 Query 向量和每个 Key 向量都算一次点积,得到注意力分数矩阵。 - 逆矩阵 - 对于方阵(行数=列数) A,如果存在矩阵 $A^{-1}$ 使得 $AA^{-1}=A{-1}A=I$ - 其中 I 是单位矩阵,对角线全是 1,其余是 0,那么 $A^{-1}$ 叫做 A 的逆矩阵 -  - 单位矩阵就像数字里的 1,乘以任何矩阵都不改变它:$AI=IA=A$ - 几何直觉:A 把空间做了某种变换,$A^{-1}$ 就是吧这个变换撤销,还原回去 - 注意:不是所有方阵都有逆矩阵。如果矩阵把空间"压扁"了(比如把三维压成二维),就无法恢复,这样的矩阵不可逆 - 在 LLM 里,矩阵是怎么用的 - Transformer 输入是一个序列,比如 4 个 token,每个 token 用 8 维向量表示,整体表示成矩阵 X,形状(4x8) - 要生成 Query、Key、Value,分别乘以三个权重矩阵 -  - 这三个矩阵乘法,就是在把输入向量投影到三个不同的子空间,分别用于"我在找什么"(Q)、"我有什么"(K)、"我的内容是什么"(V)。 - 注意力分数:$score=QK^T$ - Q 是 $4\times d_k$,$K^T$ 是 $d_k\times 4$,结果是 $4\times 4$ 的矩阵 - 结果是每个 token 和其他每个 token 的相关程度 - 整条公式 $QK^T$ 的每一个元素,就是一对 Query 和 Key 向量的点积 - Numpy 实战 - ```python import numpy as np # ─── 1. 矩阵与向量的乘法 ────────────────────────────── A = np.array([[1, 2], [3, 4], [5, 6]]) # 3×2 矩阵 x = np.array([1, -1]) # 2 维向量 result = A @ x # @ 是矩阵乘法运算符 print("A @ x =", result) # 应得 [-1, -1, -1] # ─── 2. 矩阵乘法 ────────────────────────────────────── B = np.array([[1, 2], [3, 4]]) C = np.array([[5, 6], [7, 8]]) print("\nB @ C =\n", B @ C) print("C @ B =\n", C @ B) print("交换律不成立:", np.allclose(B @ C, C @ B)) # False # ─── 3. 转置 ────────────────────────────────────────── M = np.array([[1, 2, 3], [4, 5, 6]]) print("\n原矩阵 shape:", M.shape) # (2, 3) print("转置后 shape:", M.T.shape) # (3, 2) print("转置:\n", M.T) # ─── 4. 模拟 Attention 里的 QK^T ────────────────────── np.random.seed(0) seq_len = 4 # 4 个 token d_model = 8 # 每个 token 8 维 d_k = 4 # Query/Key 的维度 # 输入序列矩阵:4 个 token,每个 8 维 X = np.random.randn(seq_len, d_model) # 权重矩阵(实际中是训练得到的) W_Q = np.random.randn(d_model, d_k) W_K = np.random.randn(d_model, d_k) # 计算 Q 和 K Q = X @ W_Q # shape: (4, 4) K = X @ W_K # shape: (4, 4) # 计算注意力分数矩阵 scores = Q @ K.T # shape: (4, 4) print("\nQ shape:", Q.shape) print("K^T shape:", K.T.shape) print("注意力分数矩阵 shape:", scores.shape) print("注意力分数矩阵:\n", scores.round(2)) # 每个元素 scores[i][j] 就是第 i 个 token 对第 j 个 token 的注意力分数 print("\ntoken 0 对所有 token 的注意力分数:", scores[0].round(2)) ``` 线性变换的几何直觉 - 线性变换的几何直觉 - 核心洞察:矩阵的列=基向量的去向 - 什么是基向量 - 二维平面最自然的一组基向量 -  - 任何 2D 向量都可以写成基向量的组合 -  - 矩阵的读法 -  - $\hat{i}$ 经过 A 变换后,落在 $\begin{bmatrix} 2 \\ 1 \end{bmatrix}$ - $\hat{j}$ 经过 A 变换后,落在 $\begin{bmatrix} -1 \\ 3 \end{bmatrix}$ - 线性变换有一个神奇的性质:它保留了向量加法和数乘。 - 只要你知道基向量被变到哪里,整个空间所有向量的去向就都确定了 - 这就是矩阵能用一组数字描述整个变换的原因 - 矩阵×向量,本质是用向量的分量作为系数,对矩阵的列做线性组合 -  - 几种常见的线性变换 - 缩放 -  - $\hat{i}$ 拉到 [2,0], $\hat{j}$ 拉到 [0,2],整个空间被均匀放大 2 倍 -  - 不均匀缩放,横向拉伸 3 倍,纵向不变。 - 旋转 - 逆时针旋转角度 $\theta$ -  - 错切 (Shear) -  - $\hat{i}$ 不动,$\hat{j}$ 从 [0,1] 移动到 [1,1]。整个空间像被协推的扑克牌 - 投影 (Projection) -  - $\hat{i}$ 不动,$\hat{j}$ 压扁到原点,整个 2D 平面被投影到 $x$ 轴,降了一维 - 这个变换是不可逆的,一旦把平面压扁成一条线,无法恢复 - 反射 (Reflection) -  - $\hat{i}$ 翻到 [-1,0],整个空间沿 y 轴镜像翻转 - 行列式:变换的缩放因子 - 行列式 $det(A)$ 是一个数 - 这个变换把单位面积或体积放大缩小了多少倍 - 几何意义 - $\hat{i}$ 和 $\hat{j}$ 围成的单位正方形,面积是 1 - 变换后,这两个向量会围成一个平行四边形,这个平行四边形的面积就是 $|det(A)|$ - 二阶行列式公式 -  -  - det(A) = 0 -> 矩阵不可逆 -> 变换降低了维度 - 秩 (Rank):变换后的输出维度 - 秩 (rank(A)) 是另外一个数 - 回答:变换后,所有向量落到的空间有几维 - 直观理解 -  - [2,4] = 2x[1,2],两列共线,无论 $\hat{i}$ 和 $\hat{j}$ 落到哪里,它们都在一条直线上 - 整个 2d 平面被压成了一条 1D 直线,rank(A) = 1 - 如果两列线性无关(不共线),秩为 2,变换后仍然是 2D 平面,满秩 - 满秩 vs 不满秩 -  - 秩的另一种表述 - rank(A) = A 的列向量中线性无关的最大个数 - 也等于行向量中线性无关的最大个数,行秩=列秩 - 线性相关与无关 - 一组向量是线性相关的,意味着其中某个向量可以由其他向量的线性组合得到 - n 个线性无关的向量,才能"撑起"一个 n 维空间 - 如果你有 100 个向量,但它们都共线(线性相关),实际上只描述了一条 1D 直线 - 连接到 LLM - 为什么 LLM 用高维向量 - GPT-3 用 12288 维的词向量 - 维度越高,能容纳的"线性无关方向"越多,理论上能编码越丰富的语义特征 - 情感、词性、领域、时态、语气……每个特征可以是一个独立的方向。 - 低秩的危险 - 如果训练出的某个权重矩阵秩很低,意味着它实际上只在少数几个方向上有效,浪费了表达能力 - 这是"模型坍缩"的一种数学表现 - LoRA 的精妙之处 - LoRA(Low-Rank Adaptation)微调的核心思想:微调时新增的参数变化矩阵 $\delta W$ 通常是低秩的 - 如果 $\delta W$ 是 1024x1024 的矩阵,参数量是约 100 万。但如果它的秩只有 8,可以分解成:$\delta W = A\cdot B$ - 其中 A 是 1024 x 8,B 是 8 x 1024,参数量降到约 1.6 万 - Attention 的几何意义 - $Q=XW_Q$,这是在用矩阵 $W_Q$ 把输入向量变换到 Query 空间 - $W_Q$ 的秩决定了 Query 空间的自由度—能从输入里提取多少个独立的"问题"。 - 可视化代码 - ```python import numpy as np import matplotlib.pyplot as plt def plot_transformation(matrix, title): """可视化一个 2x2 矩阵的变换效果""" # 原始基向量 i_hat = np.array([1, 0]) j_hat = np.array([0, 1]) # 变换后 i_new = matrix @ i_hat j_new = matrix @ j_hat # 画一个网格点 x = np.linspace(-2, 2, 9) y = np.linspace(-2, 2, 9) X, Y = np.meshgrid(x, y) points = np.stack([X.ravel(), Y.ravel()]) transformed = matrix @ points fig, axes = plt.subplots(1, 2, figsize=(10, 5)) # 原空间 axes[0].scatter(points[0], points[1], c='lightblue', s=10) axes[0].arrow(0, 0, *i_hat, head_width=0.1, color='red', label='i_hat') axes[0].arrow(0, 0, *j_hat, head_width=0.1, color='green', label='j_hat') axes[0].set_title("变换前") axes[0].set_xlim(-3, 3); axes[0].set_ylim(-3, 3) axes[0].grid(True); axes[0].axhline(0); axes[0].axvline(0) axes[0].set_aspect('equal') # 变换后空间 axes[1].scatter(transformed[0], transformed[1], c='lightcoral', s=10) axes[1].arrow(0, 0, *i_new, head_width=0.1, color='red') axes[1].arrow(0, 0, *j_new, head_width=0.1, color='green') axes[1].set_title(f"变换后\ndet={np.linalg.det(matrix):.2f}, " f"rank={np.linalg.matrix_rank(matrix)}") axes[1].set_xlim(-5, 5); axes[1].set_ylim(-5, 5) axes[1].grid(True); axes[1].axhline(0); axes[1].axvline(0) axes[1].set_aspect('equal') plt.suptitle(title); plt.tight_layout(); plt.show() # ─── 试试不同的变换 ─────────────────────────────── # 1. 缩放 plot_transformation(np.array([[2, 0], [0, 1.5]]), "缩放:x 方向 2 倍,y 方向 1.5 倍") # 2. 旋转 45 度 theta = np.pi / 4 plot_transformation( np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]), "旋转 45 度" ) # 3. 错切 plot_transformation(np.array([[1, 1], [0, 1]]), "错切") # 4. 投影到 x 轴(注意 det=0, rank=1) plot_transformation(np.array([[1, 0], [0, 0]]), "投影到 x 轴(降维!)") # 5. 反射 plot_transformation(np.array([[-1, 0], [0, 1]]), "沿 y 轴反射") ``` 特征值与特征向量 - 特征值与特征向量 - 变换中不变的方向 - 一个矩阵作用于空间,所有向量都被旋转、拉伸、扭曲。绝大多数向量变换后,方向都改变了 - 但有些特殊的向量——它们经过变换后,方向不变,只是被拉长或缩短 - 这种"在变换中保持方向"的向量,就叫做特征向量(eigenvector)。它被拉伸的倍数,叫做特征值(eigenvalue) - 形式化定义 - 特征值方程 - $A\vec{v}=\lambda\vec{v}$ - 矩阵 A 作用在向量 $\vec{v}$ 上,结果等于把 $\vec{v}$ 缩放 $\lamda$ 倍 - $\lamda$ 是特征值(一个标量,可以是正、负、零,甚至复数) - 几何含义 -  - 关键洞察:特征向量给出的是矩阵变换的"主轴"。沿着这些主轴,复杂的矩阵变换简化成了简单的"拉伸" - 怎么求特征值 - 推导 -  - 行列式为 0 意味着矩阵把空间压扁了,存在一些非零向量被压成了零向量——那些被压扁的方向就是特征方向 - 实际计算 - 求特征值 -  - 求特征向量 -  - 特征分解 - 如果 $n\times n$ 矩阵 A 有 n 个线性无关的特征向量,可以写成 $A=PDP^{-1}$ - 公式解析 - $P$ 是把特征向量按列排起来的矩阵 - $D$ 是对角矩阵,对角线上是对应的特征值 - $P^{-1}$ 是 $P$ 的逆矩阵 - 几何含义 - 任何复杂的线性变换 A,都可以分解成三步简单操作: - $P^{-1}$: 把空间转一下,让特征方向对齐到坐标轴 - $D$: 沿坐标轴各方向独立缩放(因为 D 是对角矩阵) - $P$: 把空间转回来 - 整个复杂变换,本质上就是沿着特征方向的简单缩放 - 矩阵的 n 次幂 -  - 计算量从 100 次矩阵乘法降到 n 次标量幂运算 - 这是 PageRank、马尔可夫链稳态分析的核心计算技巧 - PCA:特征值的经典应用 - PCA(主成分分析,Principal Component Analysis)是降维最常用的算法。它的数学本质就是特征分解 - 问题背景 - 假设你有 1000 个数据点,每个是 768 维向量 - 可视化、分析都很困难 - 能不能找到 2 个最重要的"方向",把数据投影到这两个方向上,保留尽可能多的信息 - PCA 的答案:找数据协方差矩阵的特征向量,按特征值大小排序,取前 k 个 - 为什么有效 - 协方差矩阵 C 描述了数据在各维度的"散布情况" - 它的特征向量给出了数据散布最大的方向——这些就是"主成分" - 特征值越大,说明数据沿那个方向变化越剧烈,包含的信息越多 - 如果你的 1000 个数据点几乎都集中在一条直线附近 - 那条直线就是第一主成分(最大特征值对应的特征向量) - 用它一个维度就能描述大部分信息 - PCA 的步骤 - 把数据矩阵 X 中心化:每一列减去该列均值 - 计算协方差矩阵:$C=\frac 1{n-1}X^TX$ - 对 C 做特征分解:$C=PDP^{-1}$ - 选 D 中最大的 k 个特征值对应的特征向量,组成投影矩阵 W - 降维后的数据: $X_{new}=XW$ - 连接到 LLM - 词嵌入的可视化 - GPT 的 768 维词向量没法直接看。 - 用 PCA 把它降到 2D 或 3D,就能在散点图上观察 - "国王""女王""公主"这些词是否聚在一起? - 这是分析模型语义的常用手段。 - 模型权重分析 - Transformer 的注意力矩阵分析中,研究者会看注意力权重矩阵的特征值分布 - 如果特征值过于集中(少数几个特征值很大,其他都很小) - 说明模型在过度关注某些方向,可能存在退化现象。 - RoPE 位置编码的内在结构 - 旋转位置编码(RoPE)本质上是用一组旋转矩阵作用在 Query 和 Key 上 - 这些旋转矩阵的特征值(在复数域)正是 $e^{i\theta}$,对应频率的位置信息 - 模型压缩的根基 - 特征分解的更一般形式 SVD是各种模型压缩、剪枝、低秩近似的数学基础 - Numpy 代码实现 - ```python import numpy as np import matplotlib.pyplot as plt # ─── 1. 求特征值和特征向量 ──────────────────────────── A = np.array([[3, 1], [0, 2]], dtype=float) eigenvalues, eigenvectors = np.linalg.eig(A) print("特征值:", eigenvalues) # [3., 2.] print("特征向量(按列):\n", eigenvectors) # 验证 A v = λ v v0 = eigenvectors[:, 0] # 第一个特征向量 λ0 = eigenvalues[0] print("\nA @ v0 =", A @ v0) print("λ0 * v0 =", λ0 * v0) print("两者相等:", np.allclose(A @ v0, λ0 * v0)) # ─── 2. 验证特征分解 A = P D P^(-1) ──────────────── P = eigenvectors D = np.diag(eigenvalues) A_reconstructed = P @ D @ np.linalg.inv(P) print("\n重构 A:\n", A_reconstructed) print("与原 A 相等:", np.allclose(A, A_reconstructed)) # ─── 3. 用特征分解快速算 A^10 ────────────────────── A10_naive = np.linalg.matrix_power(A, 10) A10_eigen = P @ np.diag(eigenvalues**10) @ np.linalg.inv(P) print("\n两种方法算 A^10 是否一致:", np.allclose(A10_naive, A10_eigen)) # ─── 4. 手写 PCA 并可视化 ─────────────────────────── np.random.seed(42) # 生成一些 2D 数据:长椭圆形分布 n = 200 mean = [0, 0] cov = [[3, 2], [2, 2]] data = np.random.multivariate_normal(mean, cov, n) # Step 1: 中心化 data_centered = data - data.mean(axis=0) # Step 2: 协方差矩阵 C = (data_centered.T @ data_centered) / (n - 1) # Step 3: 特征分解 eigvals, eigvecs = np.linalg.eigh(C) # eigh 用于对称矩阵,更稳定 # Step 4: 按特征值从大到小排序 idx = np.argsort(eigvals)[::-1] eigvals = eigvals[idx] eigvecs = eigvecs[:, idx] print("\n协方差矩阵的特征值:", eigvals) print("第一主成分(最大特征值方向):", eigvecs[:, 0]) print("第二主成分:", eigvecs[:, 1]) # 可视化:原数据 + 两个主成分方向 plt.figure(figsize=(8, 8)) plt.scatter(data[:, 0], data[:, 1], alpha=0.5, label='数据') # 画两个主成分方向(用特征值缩放表示重要程度) origin = data.mean(axis=0) for i, (val, vec) in enumerate(zip(eigvals, eigvecs.T)): plt.arrow(*origin, *(vec * np.sqrt(val) * 2), head_width=0.15, color=['red', 'green'][i], label=f'PC{i+1} (λ={val:.2f})', linewidth=2) plt.axis('equal') plt.grid(True) plt.legend() plt.title("PCA:特征向量指出数据散布最大的方向") plt.show() # ─── 5. 降维:把 2D 数据投影到 1D ───────────────── # 只用第一主成分 W = eigvecs[:, 0:1] # shape (2, 1) data_1d = data_centered @ W print(f"\n降维前 shape: {data.shape}") print(f"降维后 shape: {data_1d.shape}") # 计算保留的"信息比例" info_kept = eigvals[0] / eigvals.sum() print(f"用 1 维保留了 {info_kept:.1%} 的信息") ``` SVD (奇异值分解) - SVD (奇异值分解) - 为什么需要 SVD - 特征分解 $A = PDP^{-1}$ 有两个限制 - 只能用于方阵(行数=列数) - 不是所有的方阵都能分解(要求有 n 个线性无关的特征向量) - 任何 $m\times n$ 的矩阵都能做 SVD 分解,没有任何限制 - SVD 公式 - 对任意矩阵 $A$ ($m\times n$) $A=U\Sigma V^T$ -  - 奇异值通常按从大到小排列:$\sigma_1 \ge \sigma_2 \ge ... \ge \sigma _r \ge 0$, r 是矩阵的秩 - 几何解释 - 矩阵代表线性变换。SVD 说的是——任何线性变换,都可以分解成"旋转 → 缩放 → 旋转"三步 - 把向量 $\vec{x}$ 应用矩阵 $A$: $A\vec{x} = U\Sigma V^T\vec{x}$ - $V^T\vec{x}$: 对 $\vec{x}$ 做 一次旋转/反射 - $\Sigma(\cdot)$: 沿坐标轴各方向独立缩放(缩放倍数=奇异值) - $U(\cdot)$: 再做一次旋转/反射 - 与特征分解的对比 -  - 奇异值的含义 - 衡量矩阵的重要方向 - 最大的奇异值 $\sigma_1$ 对应矩阵最"主要"的变换方向;后面的奇异值一次描述次要方向。 - 决定矩阵的秩 - 非零奇异值的个数 = 矩阵的秩 - 衡量矩阵的"信息含量" - 如果一个矩阵的奇异值快速衰减,说明矩阵的信息几乎全在前两个方向上,后面的方向可以安全丢弃 - SVD 与 LoRA - 问题背景 - 微调一个 7B 参数的 LLM,要更新所有参数代价极大 - LoRA 的观察:微调时的参数变化 $\Delta W$ 通常是低秩的——也就是说,虽然 $\Delta W$ 是个大矩阵,它真正有效的"主方向"很少 - LoRA 的做法 - 不直接学习 $\Delta W$,而是学习两个小矩阵 A 和 B $\Delta W \approx BA$ - 如果 $\Delta W$ 是 4096x4096,参数量是 1700 万 - 如果用 LoRA 设秩为 8,则 A 是 8x4096,B 是 4096x8,参数量约 6.5 万,减少 250 倍 - SVD 在 LLM 中的其他应用 - 词嵌入分析 - 对一个 $50000\times 768$ 的词嵌入矩阵做 SVD,看奇异值衰减情况,可以判断词嵌入空间的有效维度 - Attention 矩阵分析 - 研究 Transformer 注意力权重矩阵的奇异值分布,可以诊断模型是否出现"注意力坍缩"——所有注意力都集中在少数几个 token 上 - 模型压缩 - 把训练好的权重矩阵做 SVD,丢弃小奇异值,用低秩矩阵替代,可以压缩模型大小 - 推荐系统的根基 - 经典的协同过滤推荐算法,本质就是对"用户-物品评分矩阵"做 SVD 低秩近似 - NumPy 代码实战 - ```python import numpy as np import matplotlib.pyplot as plt # ─── 1. 基本 SVD 分解 ───────────────────────────────── np.random.seed(42) A = np.random.randn(5, 3) # 一个 5x3 的非方阵 U, sigma, VT = np.linalg.svd(A, full_matrices=False) print("A shape:", A.shape) print("U shape:", U.shape) # (5, 3) print("奇异值:", sigma) # 长度为 3 的数组 print("V^T shape:", VT.shape) # (3, 3) # 验证 A = U Σ V^T A_reconstructed = U @ np.diag(sigma) @ VT print("\n重构误差:", np.linalg.norm(A - A_reconstructed)) # ─── 2. 验证 U 和 V 是正交矩阵 ──────────────────────── print("\nU^T @ U =\n", (U.T @ U).round(4)) # 应近似为单位矩阵 print("\nV @ V^T =\n", (VT @ VT.T).round(4)) # 应近似为单位矩阵 # ─── 3. 低秩近似:用 SVD 压缩图像 ───────────────────── # 创建一个简单的"图像"(一个矩阵) np.random.seed(0) image = np.zeros((50, 50)) # 加几个明显的结构 image[10:20, 10:40] = 1 image[25:35, 15:35] = 0.7 image[40:48, 5:45] = 0.5 image += np.random.randn(50, 50) * 0.1 # 对图像做 SVD U, sigma, VT = np.linalg.svd(image) # 用不同 k 值重构 fig, axes = plt.subplots(1, 5, figsize=(15, 3)) axes[0].imshow(image, cmap='gray') axes[0].set_title(f"原图\n所有 50 个奇异值") axes[0].axis('off') for i, k in enumerate([1, 3, 10, 30]): image_k = U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :] axes[i+1].imshow(image_k, cmap='gray') info_kept = (sigma[:k]**2).sum() / (sigma**2).sum() axes[i+1].set_title(f"k={k}\n信息保留 {info_kept:.1%}") axes[i+1].axis('off') plt.suptitle("SVD 低秩近似:用前 k 个奇异值还原矩阵") plt.tight_layout() plt.show() # ─── 4. 奇异值衰减图 ────────────────────────────────── plt.figure(figsize=(8, 4)) plt.plot(sigma, 'o-') plt.yscale('log') plt.xlabel('奇异值序号') plt.ylabel('奇异值大小(对数尺度)') plt.title('奇异值衰减情况') plt.grid(True) plt.show() # ─── 5. 模拟 LoRA:用低秩分解近似一个权重矩阵 ─────── # 假设原始权重矩阵 W = np.random.randn(1024, 1024) * 0.01 # 假设这是"微调后的变化"——故意构造成低秩的 np.random.seed(1) B_true = np.random.randn(1024, 8) A_true = np.random.randn(8, 1024) delta_W_true = B_true @ A_true # 真实秩为 8 的变化 # 用 SVD 提取最佳秩-8 近似 U, sigma, VT = np.linalg.svd(delta_W_true) k = 8 delta_W_approx = U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :] print("\n--- LoRA 模拟 ---") print(f"原始 ΔW 参数量: {1024 * 1024:,}") print(f"LoRA 形式 (B + A) 参数量: {1024 * k + k * 1024:,}") print(f"压缩比: {1024 * 1024 / (1024 * k + k * 1024):.1f}x") print(f"近似误差: {np.linalg.norm(delta_W_true - delta_W_approx):.6f}") ``` - SVD vs 特征分解 vs PCA 总结 -  - 有趣的关系 - 对对称正定矩阵,特征分解和 SVD 等价 - PCA 可以通过对中心化数据矩阵 X 直接做 SVD 实现,比先算协方差矩阵更稳定
AIInfra 学习
GPU 体系结构 + CUDA 入门 + Profiling AIInfra 全景 + 学习内容 - AIInfra 全景 & 学习内容 - 理论部分 - AIInfra 是什么 - AI Infra 不是一个单一的东西,而是一整套支撑「让模型在合适的硬件上、以合适的成本、稳定地训练和服务用户」的技术栈 -  - 一条 prompt 的完整生命周期 -  - 训练 vs 推理的关键差异 -  - 动手部分 - 环境检查清单 - ```bash # 1. 操作系统 uname -a # 期望 Linux x86_64,推荐 Ubuntu 22.04 cat /etc/os-release # 2. GPU & 驱动 nvidia-smi # 看到 GPU 型号、驱动版本、CUDA 版本 nvidia-smi topo -m # 看 GPU 间互联拓扑(NVLink/PCIe) # 3. CUDA Toolkit nvcc --version # 没装就先装,建议 12.x # 4. Python 环境(推荐 uv 或 conda) python --version # 3.10 / 3.11 都可 pip --version ``` - 装 PyTorch + transformers - ```bash # 用 conda 建一个干净环境 conda create -n aiinfra python=3.10 -y conda activate aiinfra # PyTorch(按你的 CUDA 版本选,下面以 CUDA 12.1 为例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 推理常用库 pip install transformers accelerate sentencepiece ``` - 验证 ```python # check.py import torch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) print("device count:", torch.cuda.device_count()) print("device name:", torch.cuda.get_device_name(0)) ``` - 跑通第一个 LLM 推理 - 选一个小模型(千万别第一次就拉 70B),比如 Qwen2.5-0.5B-Instruct 或 TinyLlama-1.1B: - ```python # first_infer.py import time, torch from transformers import AutoTokenizer, AutoModelForCausalLM model_id = "Qwen/Qwen2.5-0.5B-Instruct" # 国内可换镜像 tok = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float16, device_map="cuda" ) prompt = "用一句话解释什么是 KV Cache。" inputs = tok(prompt, return_tensors="pt").to("cuda") torch.cuda.synchronize() t0 = time.time() out = model.generate(**inputs, max_new_tokens=128, do_sample=False) torch.cuda.synchronize() t1 = time.time() print(tok.decode(out[0], skip_special_tokens=True)) print(f"\n耗时 {t1-t0:.2f}s, 生成 {out.shape[1]-inputs.input_ids.shape[1]} tokens") ``` - 监控 ```bash watch -n 0.5 nvidia-smi # 观察显存占用、SM 利用率 ``` Linux 容器回顾 - Linux/容器基础回顾 - 理论部分 - Linux 进程隔离三件套 -  - 需要记住的几个 namespace:pid、net、mnt、uts、ipc、user - GPU 不在 namespace 隔离范围内 —— 这就是为什么需要 nvidia-container-toolkit - NUMA、CPU 亲和性、与 GPU 的关系 - 一台 8 卡 GPU 服务器,CPU 和 GPU 之间的距离不是均等的 - ``` [ NUMA Node 0 ] [ NUMA Node 1 ] CPU0–47 CPU48–95 ├─ GPU0 (PCIe) ├─ GPU4 ├─ GPU1 ├─ GPU5 ├─ GPU2 ├─ GPU6 └─ GPU3 └─ GPU7 ``` - 如果你的数据加载进程在 NUMA 0,但用的是 GPU 4,每条数据都要跨 NUMA 走 UPI,吞吐立即掉一截 - ```bash numactl --hardware # 看 NUMA 拓扑 numactl --cpunodebind=0 --membind=0 python train.py # 绑到 NUMA 0 taskset -c 0-15 python ... # 绑到具体 CPU 核 nvidia-smi topo -m # 看 GPU 与 CPU 的拓扑距离 (PIX/PHB/NODE/SYS) ``` - 训练任务的 dataloader worker 必须和它要喂的 GPU 在同一个 NUMA node 上 - 容器与 GPU:为什么需要 nvidia-container-toolkit - 普通容器只能隔离 CPU/内存/网络,看不到 GPU 设备 - 驱动在宿主机,CUDA Toolkit 在容器里 - 宿主机驱动版本必须 ≥ 容器里 CUDA 编译要求的最低版本 -  - 动手部分 - 操作 namespace 和 cgroup - 直观感受「容器不过是一组 namespace + cgroup 的进程」 - ``` # 1. 用 unshare 起一个独立 PID namespace 的 shell sudo unshare --pid --fork --mount-proc bash ps aux # 只看到几个进程!这就是 PID namespace exit # 2. 看自己进程的 namespace ls -l /proc/self/ns/ # 3. cgroup v2:看当前 shell 的资源限制 cat /proc/self/cgroup cat /sys/fs/cgroup/memory.max 2>/dev/null || echo "no limit" ``` - 安装 nvidia-container-toolkit - ```bash distribution=$(. /etc/os-release; echo $ID$VERSION_ID) curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \ | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker ``` - 验证 ```bash docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi ``` - 写自己的 AI Infra 开发镜像 - ```dockerfile # 基础镜像:CUDA 12.4 + cuDNN + Ubuntu 22.04 FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04 ENV DEBIAN_FRONTEND=noninteractive \ LANG=C.UTF-8 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 # 系统依赖 RUN apt-get update && apt-get install -y --no-install-recommends \ python3.10 python3-pip python3.10-venv \ git curl wget vim numactl htop \ build-essential ninja-build \ && rm -rf /var/lib/apt/lists/* RUN ln -sf /usr/bin/python3.10 /usr/bin/python && \ ln -sf /usr/bin/pip3 /usr/bin/pip # Python 依赖 RUN pip install --no-cache-dir \ torch==2.4.0 torchvision \ --index-url https://download.pytorch.org/whl/cu124 RUN pip install --no-cache-dir \ transformers accelerate sentencepiece \ jupyterlab ipython \ nvitop py-spy WORKDIR /workspace CMD ["bash"] ``` - 构建 + 运行 ```bash docker build -t aiinfra-dev:0.1 . # 跑起来,挂载工作目录、暴露 jupyter 端口 docker run --rm -it \ --gpus all \ --shm-size=8g \ -v $PWD:/workspace \ -p 8888:8888 \ aiinfra-dev:0.1 ``` - NUMA 绑定 - 多卡机器 ```bash # 先看拓扑 nvidia-smi topo -m numactl --hardware # 把推理脚本绑到 GPU0 同 NUMA 的 CPU 上 CUDA_VISIBLE_DEVICES=0 numactl --cpunodebind=0 --membind=0 \ python first_infer.py ``` GPU 硬件与体系结构 - GPU 硬件与体系结构 - 理论部分 - 一张图看懂 GPU - 以 NVIDIA H100 (Hopper) 为例 -  - H100 一共有 132 个 SM(消费卡 4090 是 128 SM,A100 是 108 SM) - SM 是 GPU 的核,所有的 CUDA kernel 都在 SM 上执行 - 算力:FP16/BF16 Tensor Core ≈ 989 TFLOPS,FP8 ≈ 1979 TFLOPS - 显存带宽:HBM3 ≈ 3 TB/s - L2 Cache:50 MB(A100 是 40MB) - NVLink:单卡对外 900 GB/s(A100 600 GB/s,4090 没 NVLink) - SM 内部 -  -  - 关键洞察 - Warp Divergence:一个 warp 里 32 个线程如果走不同分支(if/else),GPU 会 顺序执行两条路径,性能直接腰斩。这就是为什么 GPU 不擅长复杂控制流 - Tensor Core 是深度学习的命门 - CUDA Core 一条指令算 1 个 FMA(乘加) - Tensor Core 一条指令算 数百个 FMA - 所以 H100 标称的 989 TFLOPS 几乎全来自 Tensor Core,CUDA Core 只有 ~67 TFLOPS - 如果你的 kernel 没用上 Tensor Core,就只用了 GPU 7% 的算力 - 显存层级:从 Register 到 HBM - GPU 是带宽为王的设备 -  - 核心结论 - 访问 HBM 比访问 Register 慢 500 倍 - 所以高性能 kernel 的核心套路就是:把数据从 HBM 搬到 Shared Memory,重复使用 - 这正是 FlashAttention 的核心思想 -
福建旅游规划
福建旅游规划