第 2 课你手里有一把尺子:量两个向量像不像,量出来是一个数。第 3 课表上挂着 20 个字, 每个字 8 个数。现在问一句最朴素的话——这 20 个字里,谁跟谁像?连自己算上,这张表是 400 个格子;把句子换成 1,024 个 token, 同一张表会涨到 1,048,576 个格子。这一课我们就干一件事: 把「一个点积」升级成「一次算一整批点积」。
第 2 课的尺子一次只量一对向量。要给 20 个字排一张「谁像谁」的表, 就是把 190 对统统量一遍——写两个循环,不难,也不慢。
所以问题不在「算不算得完」,而在怎么把这件事一次交出去。 显卡不会一个数一个数地帮你算;它最擅长的是「同样一个动作,成千上万份一起做」。 我们需要的是一种能把一整批点积打包成一个动作的写法。
现在把「行点列」这条唯一的规则钉死。看一个能手算的例子: A 是 2 行 3 列, B 是 3 行 2 列, 结果 C 是 2 行 2 列。 点右边结果表里的任意一格,看它是怎么来的:
比如 C 的 (1, 0) 格:A 第 1 行是 4, 5, 6, B 第 0 列是 7, 9, 11, 一一相乘再相加,得到 139。 这就是「矩阵乘法」的全部:把一堆点积排成一张表,一次性报出来。
2 行 3 列的方阵,写成 [2, 3], 读作「2 行 3 列」。第 3 课那张嵌入矩阵就是 [20, 8]——20 行(20 个 token),每行 8 个数。行是「哪一个」,列是「哪一维」,这个方向感后面每一课都会用到。
矩阵乘法写成 A · B 或 A @ B(代码里写 A @ B)。 它不满足交换律:A · B 和 B · A 通常不是一回事, 甚至连形状都不一样。这一点第 3 站你会亲眼看到。
规则一句话讲完了,可它藏着一个前提:「行点列」要求两串数一样长。左边那一行有几个数,右边那一列就必须有几个数——否则点积根本配不成对。
这条要求翻译成形状,就是整件事最要紧的一句话: 左边有几列,右边就得有几行。下面这台机器,四个数字随便你改:
[m, k] @ [k, n]——中间那个 k 必须相等,这是唯一的条件。[m, n]。只抄左边几行、右边几列。m × k × n 次,加法 m × n × (k−1) 次。 我们的表是 3,200 次乘法;换成真实模型就是 3,758,096,384 次。最后一句话值得停一下。整个 AI 硬件产业,几乎都是被「格子互相不认识」这件事养活的。如果每一格都必须等上一格算完(像第 3 课 BPE 那样,一步接一步), 再多的核心也帮不上忙;正因为这 400 个格子彼此独立, 才能几千个核心一起开工、一次算完。
回到开头那个问题:20 个字,谁跟谁像。 每个字是 8 个数,所以嵌入矩阵 E 是 [20, 8]。 要的不是 190 对里的某一对,而是整张 20×20 的表—— 第 (i, j) 格就是「第 i 个字」和「第 j 个字」的点积。
最直接的念头:把 E 和自己乘一下。E · E 是[20, 8] 乘 [20, 8]—— 停一下,能乘吗?
[20, 8] 乘 [20, 8]:左边 8 列、右边 20 行,两个数不相等,乘不了。可是我们想要的明明就是「E 里两行做点积」——最小的一步修补是什么?所以相似度表的写法就是 E · Eᵀ。一次乘法,3,200 次乘法、2,800 次加法,400 个点积全部到位。 下面这张 20×20 的表就是这么算出来的——先按「一个一个算」跑一遍,再按「一次算完」跑一遍,感受差别:
整张表 400 个数里,最大的(自己和自己除外)是「减」和「加」= 3.46—— 而且这两句话反过来也成立。互相是对方最像的一个,一共 4 对:
没人教过它「加」和「减」是一家人 —— 它只是在 4000 道题里反复见到这两个字出现在同一类位置, 慢慢把它们的向量挪到了一起。这张表是第 3 课那 30 万步训练的成绩单。
横着看第 12 行、第 13 行,你会看到一个不显眼但很要紧的事实: 模型自己把「加」和「减」放得很近。 这两行的向量分别是:
减 = [0.02, -0.31, 1.02, -0.86, -0.26, -0.78, -0.80, 0.49]加 = [-0.29, -0.10, 0.84, -0.92, -0.02, -0.39, -1.29, 0.91]
没人告诉过它这两个字是一家人。它只是在第 3 课那 300,000 步「猜下一个字」里, 反复见到「加」和「减」出现在同一类位置,慢慢把它们的向量挪到了同一个方向。点积 3.46 是全表非对角里最大的一个, 而且这两句话反过来也成立:全表一共 4 对这样的「互相最像」。
另一头也很有意思:最不像的一对是「加」和「等」, 点积 -5.24——负数。 意思是这两个字的向量大致朝着相反的方向。
Transformer 里那句「第 i 个 token 要看一眼第 j 个 token,得多少分」, 算的就是这张表。名字换了而已:Q 乘 K 的转置,[T, 128] · [128, T] → [T, T], 一共 28 个头,每个头的宽度 128,28 × 128 = 3,584。 合起来就是 [1,024, 3,584] · [3,584, 1,024],3,758,096,384 次乘法——正是第 2 站那台形状计算器算出来的数。
这 117 万倍是怎么来的?拆开看就两件事: token 多了 51.2 倍,可表是 T × T 的,要平方 → 2621.44 倍; 每个向量的宽度再多 448 倍。两下乘起来正好是 1,174,405.12。
「长文本为什么贵」的答案就藏在这个平方里。一句话从 1,024 个 token 变成 2,048 个,这张表要算的乘法会变成 4 倍,而不是 2 倍。 后面讲注意力、讲 KV Cache、讲各种省显存的花招,算的都是这笔账。
第 3 课留了一个尾巴:把 token 的编号变成向量,说的是「查嵌入矩阵的那一行」。 可为什么是「查」?如果老老实实按矩阵乘法的规则算一遍,会得到什么?
one-hot 是只有一格是 1、其余全 0 的行向量。 用它去乘 [20, 8] 的嵌入矩阵, 按规则得做 160 次乘法——其中大部分是「某一行 × 0」:
nn.Embedding 这一层在干的事。 真实词表有 151,936 行,一次要乘 151,936 次;而取行是 0 次乘法。 两条路结果完全一样,所以没人真去乘 —— 但你必须知道它们是同一件事。不过真实训练里,一次要查的绝不止一个 token —— 一句话 10 个 token 就是 10 行, 一个批次 32 句就是 320 行。既然 one-hot 里能写好几个 1, 那一次查好几个字行不行?行 —— 但你要先看清楚它给你的到底是什么:
到这儿,矩阵已经会算了。可还有一个问题没交代:第 3 课训出来的那两张矩阵,平时睡在哪儿?你下载一个模型,下到的到底是什么?
答案朴素得有点无聊:就是一个文件,里面一个数挨一个数地排在一条线上。第 2 课说过「模型文件就是带形状标签的大数字数组」—— 现在这句话有证据了。下面这个 1448 字节的文件, 装的就是你自己训出来的那两张矩阵(一共 320 个数), 用真实的 safetensors 格式写的:
值得记一笔的是那 3 个补位空格:目录 JSON 本来是 157 字节,补到 160 恰好是 8 的倍数。这不是洁癖—— 真实硬件读文件喜欢从整齐的地址开始,不对齐就慢。 这种「看着没用、其实救命」的细节,在系统里到处都是。
[起点, 终点)),所以想用哪块就只读哪块, 不用把整份权重都搬进内存。加载大模型时这一条能省下几个 GB。到这里,「矩阵」这件事就齐了:怎么算(第 1、2 站)、算什么(第 3 站)、 怎么用(第 4 站)、存在哪(第 5 站)。下一课开始,我们就要用这些零件去拼第一个真正的机制—— 但在那之前,还需要给矩阵补上一样它现在完全没有的东西。
矩阵乘法没有新规则,它就是第 2 课那把点积尺子排成了一张表: 结果的第 (i, j) 格 = 左边第 i 行 · 右边第 j 列。 能不能乘只看一件事——左边几列 = 右边几行;结果的形状只看两头, 代价是 m × k × n 次乘法。 它的真正价值不在省乘法,而在「一整批一起交出去」: 结果里每一格互相不认识,所以显卡能几千个核心同时开工。 一个字从编号变成向量(one-hot 乘嵌入矩阵),数学上是矩阵乘、 实际干的是取一行;而模型训练完,这些矩阵就一个挨一个地 躺在 safetensors 文件里,前面挂一段 JSON 目录说明谁长什么样。
[20,8] · [8,20] → [20,20]。