Regularization:从背题到泛化
最烦人的训练曲线不是 loss 爆炸。 loss 爆炸至少很诚实:学习率太大、梯度溢出、数据有脏样本,问题通常摆在明面上。更麻烦的是另一种情况:train loss 一路下降,val loss 却从某个 checkpoint 开始慢慢抬头。 训练日志还在告诉你模型变好了,验证集已经在提醒你:它学偏了。 一、泛化误差与过拟合信号 训练初期,两条曲线通常会一起下降。这个阶段很好判断:模型还在学通用模式,词和词之间的搭配、图像里的边缘和纹理、分类任务里的主要判别特征,都会同时改善训练集和验证集。 真正容易误判的是分叉之后。train loss 继续下降,val loss 反而上升。训练集还在给模型发奖励,验证集已经开始扣分。 这类曲线有欺骗性。你看 optimizer,没有异常;看梯度,没有爆;看训练 loss,还在变好。问题不在模型不学习,问题在它开始学习那些只对训练集有用的东西。 1234step 1000: train loss = 2.41, val loss = 2.50step 2000: train loss = 1.92, val loss = 2.05step ...
LLM 推理显存估算
一个 Llama 2 7B 模型,FP16 权重 13GB。把它装进 A100 40GB,看上去绰绰有余。batch size 拉到 8,上下文设 4096,跑两步 OOM 了。13GB 的模型在 40GB 的卡上怎么会爆? 原因是显存的主体不止权重。KV Cache 随请求数和上下文长度线性增长,注意力中间结果随序列长度平方膨胀,框架的 workspace、通信 buffer 还要再吃一块。三块加起来,部署前不算清楚,运维线上就要在 dashboard 里看 OOM 告警。 这篇文章回答一个具体问题:给定模型、上下文长度和并发请求数,一张卡到底会占多少显存。 一、权重的显存 最直觉的部分。模型的参数量 NNN 是一个固定数字,权重显存等于参数量乘以每个参数占的字节数: WeightMem=N×bytes/param\text{WeightMem} = N \times \text{bytes/param}WeightMem=N×bytes/paramLlama 2 7B 标称 7B,实际参数量约 6.7B(含 embedding 与每一层的 attention/FFN 矩阵...
LLM 的训练与对齐
你做了一个客服机器人。用户问:“我的订单什么时候到?” 预训练模型的回答是:“我的订单什么时候到?这是一个常见的问题。订单的到达时间取决于多个因素,包括发货地、目的地、物流公司…” 用户要的是一个具体的日期,不是一篇物流科普文章。 这是每个做 LLM 应用的人都会遇到的问题:预训练模型会写文章,但不会回答问题。 你可能觉得这是 prompt 的问题,试了各种 prompt engineering 技巧,发现效果有限。根本原因不在 prompt,而在模型本身——它没有学过"什么是好的回答"。 本篇讲的就是如何让模型从"会写"变成"会答"。 一、预训练模型为什么不会回答问题 预训练的目标是预测下一个 token。这个目标让模型学会了语言的统计规律,但没有教它什么是"好的回答"。 你问它"我的订单什么时候到",它会续写"我的订单什么时候到?这是一个常见的问题…"——因为它在训练数据里见过最多的就是这种"百科全书式"的写法。 问题出在损失函数:...
Transformer 架构与注意力机制
第一篇结束时,每个 token 是一个 ddd 维向量。“cat” 和 “dog” 在语义空间里距离很近,但它们各自的向量是独立的——token iii 不知道序列里还有 token jjj。 “The animal didn’t cross the street because it was too tired.” 模型处理到 “it” 时,怎么知道 “it” 指的是 “animal” 而不是 “street”? Transformer 要解决的只有一件事:让每个 token 的最终表示融合整个序列的上下文。 一、Self-Attention 机制 给定一个长度为 NNN 的 token 序列,每个 token 对应一个 ddd 维向量。要让 tokeni_ii 的表示融合其他 token 的信息,最直接的想法是加权平均——权重由 tokeni_ii 和 tokenj_jj 的相似度决定。这就是 Self-Attention 的起点。 QKV 注意力分数 输入矩阵 X∈RN×dX \in \mathbb{R}^{N \times d}X∈RN×d(NNN 个 tok...
从文本到向量:LLM 的数据管道
你在终端里输入一段 prompt,几秒后模型返回了一段流畅的回答。 但在模型"看到"你的 prompt 之前,它已经花了数月时间阅读互联网上的万亿级文本。 这些文本——HTML、PDF、代码、论坛帖子——是如何变成模型能理解的数字的? 本文是系列第一篇,回答一个问题:如何把人类语言变成模型能吃的数字? 链路分三步:清洗原始语料 → 将文本切分为 Token → 将 Token 映射为向量。每一步的输出是下一步的输入,每一步的设计决策都直接影响模型的最终能力。 一、语料工程:从互联网到训练数据集 原始数据长什么样 Common Crawl 每月抓取数十亿网页,原始数据是 HTML。随便取一条: 1234567<html><head><title>Buy cheap shoes!!!</title></head><body> <div class="ad">CLICK HERE FOR DEALS</div> <p>The mi...
Mixed Precision:从 FP32 到 FP8
Mixed Precision:从 FP32 到 FP8 一台 A100 80GB,一个 7B 模型。参数 28GB,梯度 28GB,Adam 的 mtm_tmt 和 vtv_tvt 共 56GB——合计 112GB。一张卡放不下,还没开始训练显存就先不够了。 一、训练时的显存分布 先把 FP32 训练的显存拆开算一遍。 一个 7B 模型,参数量 N=7×109N = 7 \times 10^9N=7×109,每个参数占 4 字节(FP32),参数本身 4N≈28GB4N \approx 28\text{GB}4N≈28GB。梯度与参数同 shape,又是 28GB。 但真正的大头是优化器状态。Adam 维护两个 FP32 buffer: mt=β1⋅mt−1+(1−β1)⋅gt(一阶矩)m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t \qquad \text{(一阶矩)}mt=β1⋅mt−1+(1−β1)⋅gt(一阶矩)vt=β2⋅vt−1+(1−β2)⋅gt2(二阶矩)v_t = \beta_2 \...
Optimizer:从 SGD 到 AdamW
Optimizer:从 SGD 到 AdamW 训练神经网络就是在找一个最低点。你站在 loss 曲面的某个位置,每步沿着梯度方向往下走一点,希望最后走到谷底。 SGD 做的就是这件事: 1234for x, y in dataloader: loss = model(x, y) # 前向:算出当前位置的 loss loss.backward() # 反向:算出梯度 g_t optimizer.step() # 更新:θ = θ - lr * g_t 听起来简单。但真正跑起来,你会发现 loss 降着降着不动了——不是到了最低点,梯度还不是零,参数也还在更新,但 loss 就是不降。 问题出在哪?梯度告诉你的是"当前脚下最陡的下坡方向"。但如果这座山的形状是一个又窄又长的峡谷呢?最陡的方向指向峡谷的侧壁,而不是谷底。你每一步都往侧壁走,走到对面再弹回来——来来回回,沿谷底的有效位移极小。 这就是 SGD 的第一个问题:梯度方向和真正该去的方向之间有夹角。 一、Momentum:让方向靠谱一点 M...
KV Cache 和 Prompt Cache 的区别
KV Cache 和 Prompt Cache 的区别 如果你用过 OpenAI 的 API,可能注意过响应里偶尔会出现这么一个字段: 12345678"usage": { "prompt_tokens": 2006, "completion_tokens": 300, "total_tokens": 2306, "prompt_tokens_details": { "cached_tokens": 1920 }} 明明 prompt 一样长,但 cached_tokens 显示这 2006 个 token 里有 1920 个命中了缓存——意味着你只需要为 86 个 token 付全价,剩下的按折扣价算。 再翻翻 vLLM 或者 HuggingFace 的推理教程,又会频繁撞见另一个词:KV Cache。文档里说它是推理加速的核心,vLLM 的 PagedAttention 本质上就是在管理 KV Cach...
招新平台是如何解决高并发判题问题的?
招新平台是如何解决高并发判题问题的? 在设计一个招新平台答题系统时,有一个典型需求: 同一道题提交人数越多,得分越低(衰减计分)。同时要保证高并发下的分数正确性。 一、业务场景 & 技术挑战 典型需求 包括: 用户高并发提交题解 只有 首次正确提交 才计入计数 答题人数越多,分数越低(例如对数衰减) 分数必须计算准确,不能出现并发错误 衰减后的分数必须可靠写入数据库 系统需要具备足够的高并发吞吐能力 二、整体实现思路 用户提交答案 ↓ 后端校验答案是否正确 ↓ Redis Lua(原子): SETNX 判断是否首次正确提交 INCR 计数 返回计数 ↓ Java 计算衰减得分 ↓ MySQL 落库: - 用户得分(幂等写) - 题目统计(乐观锁 + 重试) 三、Redis的Lua脚本实现计数原子性 -- KEYS[1] = 用户是否答对标记 key -- KEYS[2] = 题目提交人数计数 key -- 防重复:首次答对才能进入计分逻辑 local firstSolved = redis.ca...
Java线程池
Java线程池 线程池是Java并发编程中的一个重要工具,通过复用已创建的线程来减少性能开销。线程池的核心类主要为ThreadPoolExecutor。 线程池的七大参数 核心线程数 : 指的是线程池中始终保持存活的线程数。即使处于空闲状态也不会销毁。 最大线程数 : 指的是线程池中能创建的最大线程数量。 当任务队列已满且当前线程数 < 最大线程数时,线程池就会继续创建线程。如果线程数已达到最大值,任务就会被拒绝。 空闲时间 : 指非核心线程在空闲状态下的存活时间。 当当前线程数 > 核心线程数时,非核心线程在空闲时间后就会被销毁。 时间单位 任务队列 : 常用三种: ArrayBlockingQueue:有界队列,适用于有资源限制的场景。 LinkedBlockingQueue:无界队列,适用于任务量较大的场景。 SynchronousQueue:不存储任务的队列,适用于直接将任务传递给线程的场景。 其它三种: PriorityBlockingQueue:无界队列,按照任务优先级排序,适用于优先级调度场景 DelayQueue:无界队列,适用于定时任务、缓存过期...



