Stanford-CS336-Lecture-01 学习理解

1.资源

课件资料https://github.com/stanford-cs336/spring2025-lectures

clone下来后,将var目录移动到trace-viewer中的public目录内,将images目录也移动到public目录内,然后cd trace-viewer,以此命令npm install和npm run dev,到浏览器打开如下链接来看第一章的课件:

http://localhost:5173?trace=/var/traces/lecture_01.json

这个链接只是示例,localhost后面的端口根据自己运行实际情况而定。不同的课程有对应的json。

相关中文博客https://www.cnblogs.com/apachecn/p/19577358

其他可能参考的资料https://datawhaler.feishu.cn/wiki/SBGEw3kFfipFQbkStGocaFVHnSf

2.为什么要学Standford CS336?

  • 理解大模型底层逻辑。
  • 为你提供如何选择、处理数据,以及如何建模的直觉。
  • 纠正资源至上心态,优化算法能最大化资源利用效率。精度 = 算法效率 x 资源投入。

3.课程大纲

4.Tokenization概览

Tokenization的作用是将字符串分割为若干片段,这些片段称为token,并为这些片段逐个赋予一个数字,这个过程称为Tokenization的编码。而这些编码片段也可以通过解码得到原来的字符串。

课程将以BPE(Byte-Pair Encoding)Tokenization为例进行tokenization技术的讲解,并要求你在无AI Tools和少量Pytorch API调用的情况下实现一个BPE。

另外可以提到也有无Tokenization技术,主要通过直接读取字节来实现语言建模。然而在现在的前沿模型中,Tokenization技术已经十分常见,课程暂且不论无Tokenization的方法。

5.最大化硬件效率

在训练的时候,加快训练速度,最大化硬件资源效率的一个Trick是,最大化GPU的利用率

在训练模型的过程中,GPU的作用是负责计算和模型训练,CPU的职责是负责将模型参数、optimizer参数以及其他超参从外存运到内存,从内存运到GPU中。GPU的速度是相当快的,CPU则在进行数据传输中疲于奔命。因此常常会遇到GPU利用率低的问题,这通常是发生在GPU等待CPU数据传输的情况。所以处理好数据传输,能很好地提高训练速度,最大化硬件资源效率,即最大化硬件资源效率等价于最小化数据传输

当进行多个GPU并行训练时,数据之间的传输更加慢,最大化硬件资源效率等价于最小化数据传输依然正确。

这时候有若干并行性方法去应对这个问题,比如data parallelism,tensor parallelism等。

6.推理

推理成本>训练成本:

随着时间的推移,推理上的成本要超过训练上的成本,这是因为训练的成本通常是一次性的,而推理是按次数而言的,随着用户量增大,使用率提高,推理次数将指数级上升。

推理的两个阶段:

分为**预填充(Prefill)和解码(Decode)**阶段。预填充一次性将用户提供的prompt tokens等上下文处理完毕,包括计算每个位置的隐藏状态以及最后一个位置的logits然后存储KV-Cache,而解码则是在生成过程中逐步追加新 token,每生成一个 token,上下文就变长一点,模型需要用"旧的 KV 缓存 + 新 token"再算一次下一个 token 的分布,这个过程将会重复直到生成结束。

因此时间上来讲,预填充是很快的,耗时主要集中于解码。因此有一些手段来加速解码,比如使用小一些的模型进行推理 (模型蒸馏、模型剪枝、模型量化),或者是一些特别的策略 (先用小而快的模型连续生成一段候选 token,然后由大的模型一次性验证这段候选合理性以决定是否采纳),又或者是在系统方面进行优化(优化KV-Cache等)。

7.Scaling laws

有时候为大模型做实验时,会遇到这样的问题:在算力预算C是固定的情况下,模型参数N应该选多大,数据tokens数D应该为多少,才能最小化自己模型的困惑度?

我们通常会觉得,如果模型越大,其能表达更复杂的规律,但如果给它喂的数据太少就"吃不饱"。数据量越大,越能让模型充分地学习,但对于小模型会"吃不下"。所以存在一个"compute-optimal"的平衡点。

假设训练一个Transformer的FLOPs量大致可以写成:C≈kND

  • C:总训练FLOPs
  • N:参数量
  • D:训练tokens数
  • k:与架构细节有关的常数

左图的每一条曲线都存在一个临界值,在临界值左边,模型太小,表达能力不够,损失较高。右边则是模型太大,训练效率不足导致的损失上升。将上面公式变形后有:D / N ≈ C / (kN²)。这个公式代表着单个参数所需训练的tokens数,即在固定FLOPs下随着模型参数N变大,每个参数见到的tokens变少,因此训练效率变低。

中间的图表示给定一个算力值FLOPs,模型大概需要多少参数。而右边的图则表示给定一个算力值FLOPs,模型大概需要多少tokens来训练。

Scaling Laws for Neural Language Models 和 Training Compute-Optimal Large Language Models 这两篇文章揭示了这样一个结论:如果你想让模型在当前算力下达到最优,那N和D大概满足 D ≈ 20 N。这是一种scaling law。

然而,具体的模型和实验,有自己的scaling laws。上面两篇论文的工作旨在启发我们要在小规模实验中寻找自己模型的scaling laws,得到相关规律后预测在大规模实验和应用情境下的表现。

8.对齐

对齐方式:

简单讲一下Alignment。大模型可以分成两种,一种是基础大模型 ,另一种是指令微调大模型。一般来说,搜集大量数据,用大量显卡训练许久的基础大模型,它能够很好地完成预测下一个token的任务,但有时候他所生成的内容并非我们用户想要得到的。

比如,GPT-4从大量的网络数据中训练得来,未经对齐你就向其发送这样的字符串:"法国的首都是?"那么其实很有可能GPT-4会回复:"法国首都的经纬度是?"类似这样与回答无关的内容。这是因为它可能是从一个有关法国首都的问题清单中训练的,当你询问其中一个问题时,大模型会返回这个问题后面最后可能出现的内容。

这并非用户想要得到的回答。因此就有了指令微调大模型。基础大模型拥有了强大的预测tokens的能力,便可以用指令-回答对进行微调,以能够得到强大的问答能力。这是一种对齐方式。

另外还有其他的对齐方式,比如:微调大模型的回答风格 (回答格式、回答长度以及回答语气等),还有一些安全性的因素(监督大模型生成的有害、错误的回答)。

相关的对齐方法有监督微调 (supervised_finetuning),从反馈中学习(learning_from_feedback)。

监督微调:

又称SFT。给大模型微调的数据格式通常是一问一答:

数据除了一部分是合成数据,大多是人工标注的数据,获取成本和难度较高。

因为基础大模型已经具备了强大的tokens预测能力,因此用SFT进行微调的时候不必需要大量的数据,LIMA: Less Is More for Alignment这篇文章表明大概只需要1000对数据即可,SFT主要做的只是让大模型能够从预测tokens转向回答和完成用户的问题和要求。

从反馈中学习:

从反馈中学习指的是,给大模型一些基本的例子,在这些例子中问题的回答有两种风格,由人选择其中一个偏爱的回答来微调模型的输出偏好。

可以用一些形式验证器 (适用于代码或者数学的输出)或者训练过的验证器(训练一个LLM对抗器)对结果是否符合偏好进行验证。

9.Tokenization详解

给一个字符串"I love LLM",对每一个Unicode字符,UTF-8编码可以将其转换为8位二进制序列,得到:

01001001 00100000 01101100 01101111 01110110 01100101 00100000 01001100 01001100 01001101

这样的indices序列长度是80,即tokens长度是80。因为是8位二进制序列,因此这样的序列字节数是10,压缩比是1 / 8。也就是说每一个token只能代表 1 / 8 个字节。这样的序列太长了,太长的序列,LLM处理所需的时间就更久,我们当然希望一个token能代表尽可能多的字节信息,使得序列长度尽可能低

Byte Tokenization:

在上面的例子中,用来表示字符的token只有0和1两种。将token的种类汇总为字典,字典的大小为2。因此这种基于二进制位的tokenization方式,特点是序列长,字典小。效率更高的方式应该让序列更短,因此通常用字典大小换序列长。

将8位二进制转换为整数,得到:

73 32 108 111 118 101 32 76 76 77

一个整数代表一个字节的信息,一个token就是一个字节。整数的范围是0-255。因此字典大小为256。indices的长度是token的长度,因为像73这样的数字在字典中算一个token,所以indices长度是10。可以计算一下压缩比为1。这样的序列其实还是太长,效率还不够。

Character Tokenization:

那么可以考虑将Unicode字符表当成token字典,一个字符就是一个token。ASCll字符是单字节字符,也就是一个字节一个字符,因此全为ASCll字符的字符串,压缩比为1。对于非ASCll字符,比如emoji和中文或者其他语言,通常是多个字节一个字符,因此这时一个token能表示多个字节的信息,此时压缩比通常大于1。Unicode 字符大约有 15 万个(150K),因此token字典大概也有15万个。让我们来计算一下这里的压缩比。压缩比是一个token能表达的字节信息量。因此压缩比也是1。

然而,CharacterTokenization的问题是词表(token表)太大,实际高频使用的只有一小部分,如此巨大的词表使得embedding矩阵相当大,占显存,拖训练和推理速度。对于一些冷门的字符,其与其他高频的字符占用相同的空间大小。更何况,压缩比也不算十分高。

Word Tokenization:

这个tokenization的想法是把一整段字符串按词分开,一个词或者一个标点当作一个token,然后给这些不同的词分配整数id,当作词表的一个条目。

通常这种简单的切分方法可以用regex.findall(r"\w+|.", string)实现,string是需要切分的字符串,\w+分出一个词,点号分出一个标点。比如"I love LLM"会被分成"I"、"love"、"LLM"。

GPT-2的做法是把单词前面的空格也算在切分的单词里,比如"I love LLM"会被分成"I"、" love"、" LLM"。这样能更好处理数字、标点和空白等情况。

切分完毕后,收集训练语料的所有非重复segement,然后为它们分别分配一个整数id。

计算一下压缩比。可以知道这时token只有3个,压缩比为 10 / 3 ≈ 3.33。压缩比较前面的大大提高了。

然而,问题也很明显。由于自然语言中单词非常多,因此词表也还是很巨大 。训练时,语料都能在词表字典中找到以映射为整数喂给大模型。但是如果在测试遇到了词表字典中没见过的新词(新造词或者错别字),就会发生未登录词(OOV)问题,只能将这些词映射为统一的UNK,这会增加困惑度。

Byte-Pair Encoding(BPE) Tokenization:

BPE在Byte Tokenization基础上做了进一步优化,它在indices进行合并同类项的操作,把高频的词或词片段作为一个 token

以"the cat in the hat"为例,首先将字符串按Byte Tokenization的方式编码为字节,得到整数序列:116 104 101 32 99 97 116 32 105 110 32 116 104 101 32 104 97 116。这是保证不会发生OOV的基础,这是因为无论什么字符(包括中文、emoji),都能拆成 byte,被覆盖到。

词典范围依然是0-255,长度为256。

统计相邻token对的频率,然后选出频率最高的一对 。比如(116,104)出现了2次,(104,101)出现了2次,(32,99)出现了1次,选择频率最高的(104,101),虽然(116,104)也是最高的,但并没有什么影响。说明104和101可能是某个有意义的子单元,于是将它们进行同类项合并

为这个频率最高的Pair分配一个新整数id,然后在indices中将这个Pair替换为这个新整数。比如将(104,101)分配为256,这个数字正好是词典当前长度。然后在indices中将所有(104,101)替换为256。

重复执行以上操作多次,就能优化序列长度。比如执行3次,得到258 99 97 116 32 105 110 32 258 104 97 116,具体是将(116,104)合并为256,将(256,101)合并为257,(257,32)合并为258。

上面的例子,最终的indice长度为12,原先为18。计算一下压缩比,压缩比为 18 / 12 ≈ 1.5。

看起来不是很高?上面的合并还不算是最优的结果,毕竟只执行了3次循环。你需要训练一个BPE,使得压缩比尽可能高。

GPT-4,LLaMA等主流模型都在用BPE或其变体,BPE确实在词表大小、序列长度、鲁棒性之间做了很好的均衡。