ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从NumPy到Transformer:用Shape思维手写大模型

从NumPy到Transformer:用Shape思维手写大模型 1. 从一行 import numpy 开始为什么这是理解大模型最扎实的起点很多人学大模型第一反应是去找 Transformer 架构图、去啃 Attention 公式、去跑一个 HuggingFace 的 demo。这条路不是不行但绝大多数人会在第三步就卡住——因为当你打开一个开源模型的源码看到torch.nn.Linear、view、permute、einsum的时候如果脑子里没有清晰的张量形状概念那些代码对你来说就是天书。我自己的路径比较笨先老老实实把 NumPy 用熟再过渡到 PyTorch 的 Tensor最后才去拆 Transformer。回头看这条路反而是最稳的。原因很简单——大模型的本质就是一堆张量在特定形状下做矩阵乘法而 NumPy 是理解张量运算成本最低的工具。你不需要 GPU不需要配环境配到崩溃一行import numpy as np就能开始。这篇文章不是又一篇Transformer 从入门到精通我想做的是把这条学习路径完整地摊开从 NumPy 的 ndarray 到底比 list 快在哪到 shape 和 axis 这两个概念怎么贯穿整个深度学习再到 Transformer 里那些让人头大的维度变换到底在干什么最后落到实际跑模型时会遇到的那些坑。适合已经会一点 Python、想往大模型方向走但不知道从哪下手的人也适合已经能跑模型但说不清楚里面在发生什么的人。关键词里提到的 NumPy、LLM、Transformer、Tensor、Shape这五个词其实是一条线NumPy 是工具Tensor 是数据载体Shape 是思维方式Transformer 是架构LLM 是最终产物。把这条线捋顺了很多之前看不懂的东西会突然通。2. NumPy 的 ndarray 到底比 Python list 快在哪里2.1 内存布局决定了性能上限先看一个最直观的对比。假设你要把一百万个数字每个乘以 2import numpy as np import time n 1_000_000 py_list list(range(n)) np_array np.arange(n) # Python list 方式 start time.time() result_list [x * 2 for x in py_list] print(flist: {time.time() - start:.4f}s) # NumPy 方式 start time.time() result_array np_array * 2 print(fnumpy: {time.time() - start:.4f}s)实测下来NumPy 通常比 list 快 20 到 50 倍。这个差距不是优化得好一点而是量级上的差异。原因在于两者的内存模型完全不同。Python list 存的是指针数组——每个元素都是一个独立的 Python 对象散落在内存各处。你遍历 list 的时候CPU 要不停地跳转到不同内存地址去取对象还要处理每个对象的引用计数。而 NumPy 的 ndarray 是一块连续的、同类型的内存所有元素紧挨着排列CPU 可以一次性把一批数据读进缓存还能用 SIMD 指令并行处理。打个比方list 像是把一万本书分散放在图书馆各个角落你要一本本跑过去拿ndarray 像是把一万本书按顺序码在一个书架上你伸手就能连续取。这个差异在数据量小的时候感觉不到一旦上了百万级就是天壤之别。2.2 广播机制NumPy 最容易被低估的设计广播broadcasting是 NumPy 里我认为最值得花时间理解的概念因为它直接对应到深度学习里的 batch 运算。a np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) b np.array([10, 20, 30]) # shape (3,) result a b # [[11, 22, 33], # [14, 25, 36]]b的 shape 是(3,)a的 shape 是(2, 3)。NumPy 发现b的最后一维和a的最后一维都是 3于是自动把b复制成(2, 3)再相加。这个自动扩展就是广播。为什么这个重要因为在 Transformer 里位置编码positional encoding加到词嵌入token embedding上用的就是广播。词嵌入的 shape 是(batch_size, seq_len, d_model)位置编码的 shape 是(seq_len, d_model)两者相加时位置编码会在 batch 维度上自动广播。如果你不理解广播看到这行代码会以为哪里写错了。广播的规则其实就三条从右往左对齐两个数组的 shape每一维要么相等要么其中一个是 1要么其中一个不存在满足条件的维度会被扩展成较大的那个不满足就报错。我见过太多人在拼接多头注意力的输出时被广播规则坑到后面会专门讲。2.3 视图与拷贝一个影响显存占用的关键区分NumPy 里有一类操作返回的是视图view另一类返回的是拷贝copy。这个区别在 NumPy 阶段可能只是性能问题到了 PyTorch 阶段就是显存爆炸问题。a np.arange(12).reshape(3, 4) b a[0] # 视图改 b 会影响 a c a[0].copy() # 拷贝独立内存 d a.reshape(4, 3) # 通常是视图 e a.T # 转置视图 f a[[0, 1]] # 花式索引拷贝判断标准很简单基本切片用冒号返回视图花式索引用列表或布尔数组返回拷贝。reshape和transpose在内存连续的情况下返回视图不连续时会触发拷贝。这个知识点在 PyTorch 里对应view和reshape的区别view要求内存连续不连续就报错reshape会自动处理必要时拷贝。Transformer 源码里大量使用view和permute如果你不清楚什么时候是视图什么时候是拷贝调试维度错误时会非常痛苦。2.4 矩阵运算从 np.dot 到 运算符NumPy 里做矩阵乘法有好几种写法新手容易搞混a np.random.randn(3, 4) b np.random.randn(4, 5) c1 np.dot(a, b) # 矩阵乘法shape (3, 5) c2 a b # 同上推荐写法 c3 np.matmul(a, b) # 同上支持批量np.dot和在二维情况下等价但到了高维就不一样了。np.dot对高维数组的行为比较反直觉而np.matmul和遵循批量矩阵乘法的规则把前面的维度当作 batch 维度只对最后两维做矩阵乘法。这个规则直接对应到 Transformer 里的torch.bmmbatch matrix multiply和torch.matmul。注意力计算里的Q K.transpose(-2, -1)就是典型的批量矩阵乘法shape 从(batch, heads, seq_len, d_k)变成(batch, heads, seq_len, seq_len)。不理解这个就没法理解注意力矩阵到底在算什么。顺便说一句热词里有人搜numpy 如何求解矩阵的逆用np.linalg.inv(a)就行但要注意只有方阵且可逆才行。实际做深度学习几乎不会用到求逆因为数值不稳定但理解线性代数的这些基本操作对建立直觉有帮助。3. Shape 和 Axis贯穿 NumPy 到 Transformer 的思维主线3.1 把 shape 当成一种语言来读如果让我只选一个概念推荐给刚入门的人我会选shape。因为深度学习里 90% 的 bug 都是 shape 不匹配而 90% 的看不懂是因为没有在脑子里把 shape 念出来。我的习惯是看到任何一行张量操作先在脑子里把输入输出的 shape 念一遍。比如x torch.randn(32, 128, 512) # 读作batch 32序列长度 128特征维度 512这三个数字不是随便排的它们有明确的语义。32是一个 batch 里有 32 条样本128是每条样本有 128 个 token512是每个 token 用 512 维向量表示。当你在代码里看到x.transpose(1, 2)你就知道这是在把序列长度和特征维度换位置变成(32, 512, 128)。养成这个习惯之后读 Transformer 源码会顺畅很多。因为 Transformer 本质上就是一系列 shape 变换嵌入层把 token id 变成向量注意力层在序列维度上做交互前馈层在特征维度上做变换最后输出层把向量变回词表上的概率分布。3.2 axis 参数为什么 axis0 和 axis1 差别这么大axis是 NumPy 里另一个高频踩坑点。看这个例子a np.array([[1, 2, 3], [4, 5, 6]]) print(a.sum(axis0)) # [5, 7, 9]沿着行方向压缩得到每列的和 print(a.sum(axis1)) # [6, 15]沿着列方向压缩得到每行的和很多人第一次看到会懵为什么axis0得到的是列的和我的记忆方法是axis 指的是被消掉的那个维度。axis0消掉第 0 维行剩下的就是列axis1消掉第 1 维列剩下的就是行。这个理解方式在 softmax 里特别有用。Transformer 的注意力权重做 softmax 时是在最后一个维度上做的attn_weights torch.softmax(scores, dim-1)scores的 shape 是(batch, heads, seq_len, seq_len)dim-1表示在最后一个维度key 的方向上做归一化。也就是说对于每个 query它对所有 key 的注意力权重加起来等于 1。如果你把dim写错了softmax 就归一化到了错误的方向模型直接学不动。3.3 维度变换三件套reshape、transpose、permute在 NumPy 和 PyTorch 里维度变换主要靠这三个操作。它们的区别值得单独说清楚操作作用是否改变内存布局典型场景reshape/view改变形状不改变元素顺序视图连续时展平特征、拆分维度transpose交换两个维度视图二维转置、交换 seq 和 featurepermute任意重排所有维度视图多头注意力的维度重排reshape是最常用的。比如把(batch, seq_len, d_model)拆成(batch, seq_len, num_heads, head_dim)就是把d_model这一维拆成两个。反过来把(batch, seq_len, num_heads, head_dim)合并回(batch, seq_len, d_model)也是 reshape。transpose只能交换两个维度permute可以一次重排多个。在多头注意力里标准操作是# 输入 x: (batch, seq_len, d_model) x x.reshape(batch, seq_len, num_heads, head_dim) x x.permute(0, 2, 1, 3) # (batch, num_heads, seq_len, head_dim)为什么要 permute因为后面要做Q K.transpose(-2, -1)需要把num_heads放到 batch 后面作为独立的并行维度让每个头独立计算注意力。这个 permute 是 Transformer 里最容易写错的地方之一写错了 shape 可能还对得上但语义完全错了模型训练 loss 不降。3.4 用 shape 追踪法调试维度错误我调试维度问题有一个固定套路叫shape 追踪法在每一个关键操作前后打印 shape然后对照预期。def forward(self, x): print(finput: {x.shape}) q self.wq(x) print(fafter wq: {q.shape}) q q.reshape(B, T, self.n_heads, self.head_dim).permute(0, 2, 1, 3) print(fafter reshapepermute: {q.shape}) # ...这个方法看起来很笨但极其有效。尤其是当你从别人的代码里抄了一段不确定它期望的输入 shape 是什么的时候打印一遍就清楚了。我建议在学 Transformer 的阶段把每个模块的输入输出 shape 都手写注释在代码旁边形成肌肉记忆。4. 从 NumPy 到 Tensor迁移时最容易踩的五个坑4.1 坑一ModuleNotFoundError: No module named numpy这个报错几乎每个新手都会遇到。热词里也有人搜python 安装 numpy 库的方法和numpy 安装说明这是高频问题。最直接的原因是你当前的 Python 环境里没装 NumPy。但更常见的情况是你装了但装到了另一个 Python 环境里。比如你用pip install numpy装到了系统 Python但你的代码跑在 conda 虚拟环境里或者反过来。排查步骤# 1. 确认当前用的是哪个 python which python which pip # 2. 确认这个 python 里有没有 numpy python -c import numpy; print(numpy.__version__) # 3. 如果没有用同一个 python 的 pip 装 python -m pip install numpy关键是第二步和第三步要用同一个python。我见过太多人pip install之后还是报错就是因为pip和python指向了不同的环境。用python -m pip能保证装到当前解释器里。还有一个隐蔽情况虚拟环境没激活。VS Code 里选了错误的解释器终端里是另一个环境。这种问题不看which python是查不出来的。4.2 坑二numpy 版本不匹配引发的连锁反应热词里有numpy 版本不匹配这是个真实且烦人的问题。典型场景是你装了个新库比如某个深度学习框架它依赖特定版本的 NumPy把你原来的 NumPy 升级或降级了结果另一个库跑不了了。常见的报错长这样A module that was compiled using NumPy 1.x cannot be run in NumPy 2.x这是因为 NumPy 2.0 做了 ABI 变更用 1.x 编译的 C 扩展在 2.x 下不兼容。解决办法有两个要么把 NumPy 降回 1.x要么把依赖它的库升级到支持 2.x 的版本。# 查看当前版本 pip show numpy # 降级到 1.x pip install numpy2 # 或者升级相关库 pip install --upgrade 那个报错的库我的经验是做深度学习项目时用虚拟环境隔离并且把依赖版本写进 requirements.txt。不要在一个环境里装一堆不相关的东西否则版本冲突会让你怀疑人生。4.3 坑三Tensor 和 ndarray 的隐式转换陷阱PyTorch 的 Tensor 和 NumPy 的 ndarray 可以互相转换但转换过程中有几个坑import torch import numpy as np # ndarray - Tensor a np.array([1, 2, 3]) t torch.from_numpy(a) # 共享内存改一个另一个也变 # Tensor - ndarray t2 torch.tensor([1.0, 2.0, 3.0]) a2 t2.numpy() # 也共享内存CPU 上 # 要独立内存用 clone 或 copy t3 torch.from_numpy(a).clone()torch.from_numpy和.numpy()默认是共享内存的这在某些场景下是优点省内存但在另一些场景下是灾难你以为改了副本结果原数据也变了。更麻烦的是如果 Tensor 在 GPU 上.numpy()会直接报错必须先.cpu()。还有一个常见错误把需要梯度的 Tensor 转成 ndarray。tensor.numpy()对requires_gradTrue的 Tensor 会报错必须先.detach()。这个在写训练循环时经常遇到。4.4 坑四数据类型不一致导致的精度问题NumPy 默认的浮点类型是float64PyTorch 默认是float32。这个差异在转换时会带来精度损失更麻烦的是会带来类型不匹配的报错。a np.array([1.0, 2.0]) # float64 t torch.from_numpy(a) # 也是 float64 # 如果你把它和一个 float32 的 Tensor 相加会报错解决办法是显式指定类型a np.array([1.0, 2.0], dtypenp.float32) t torch.from_numpy(a) # float32在深度学习里float32是默认精度float16和bfloat16用于混合精度训练。如果你从 NumPy 传数据进来一定要检查 dtype否则会在某个意想不到的地方报错。4.5 坑五设备不一致——CPU 和 GPU 的 Tensor 不能直接运算这个坑在 NumPy 阶段不存在但一进入 PyTorch 就会遇到a torch.tensor([1, 2, 3]) # CPU b torch.tensor([4, 5, 6]).cuda() # GPU c a b # 报错报错信息通常是Expected all tensors to be on the same device。解决办法是把它们放到同一个设备上device torch.device(cuda if torch.cuda.is_available() else cpu) a a.to(device) b b.to(device) c a b # OK我的习惯是在代码开头定义好device然后所有新建的 Tensor 都显式.to(device)。不要依赖默认行为因为默认行为在不同版本的 PyTorch 里可能不一样。5. Transformer 里的张量形状把架构图翻译成代码5.1 输入到嵌入token id 怎么变成向量Transformer 的输入是一串 token id比如[101, 2023, 4567, ...]。第一步是查表embedding lookup把每个 id 映射成一个d_model维的向量。# 词表大小 30000d_model 512 embedding nn.Embedding(30000, 512) # 输入 token ids: (batch, seq_len) token_ids torch.randint(0, 30000, (32, 128)) # 查表后: (batch, seq_len, d_model) x embedding(token_ids) # (32, 128, 512)这一步的 shape 变化是(batch, seq_len)→(batch, seq_len, d_model)。多出来的最后一维就是每个 token 的向量表示。然后是位置编码。因为 Transformer 本身没有顺序概念注意力是集合运算必须显式加入位置信息。位置编码的 shape 是(seq_len, d_model)加到x上时通过广播自动扩展到(batch, seq_len, d_model)。5.2 多头注意力Q、K、V 的 shape 变换全流程多头注意力是 Transformer 的核心也是 shape 变换最密集的地方。我把完整流程拆开batch, seq_len, d_model 32, 128, 512 num_heads, head_dim 8, 64 # d_model num_heads * head_dim # 输入 x: (32, 128, 512) # 线性投影得到 Q, K, V q self.wq(x) # (32, 128, 512) k self.wk(x) # (32, 128, 512) v self.wv(x) # (32, 128, 512) # 拆分成多头: (32, 128, 8, 64) q q.reshape(batch, seq_len, num_heads, head_dim) k k.reshape(batch, seq_len, num_heads, head_dim) v v.reshape(batch, seq_len, num_heads, head_dim) # 调整维度顺序: (32, 8, 128, 64) q q.permute(0, 2, 1, 3) k k.permute(0, 2, 1, 3) v v.permute(0, 2, 1, 3) # 计算注意力分数: (32, 8, 128, 128) scores q k.transpose(-2, -1) / math.sqrt(head_dim) # softmax 归一化 attn torch.softmax(scores, dim-1) # 加权求和: (32, 8, 128, 64) out attn v # 合并多头: (32, 128, 512) out out.permute(0, 2, 1, 3).reshape(batch, seq_len, d_model) # 输出投影 out self.wo(out) # (32, 128, 512)这里每一步的 shape 我都标出来了。你可以看到permute出现了两次一次是把num_heads提到前面一次是把它放回去。reshape也出现了两次一次是拆分一次是合并。热词里有人搜llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么这个类比其实很准确。Query 是我在找什么Key 是我是谁Value 是我能提供什么。注意力分数就是 Query 和 Key 的匹配程度然后用这个匹配度去加权 Value。5.3 前馈网络为什么中间层要放大四倍Transformer 的每个 block 里注意力后面跟着一个前馈网络FFN。它的结构很简单ffn nn.Sequential( nn.Linear(d_model, 4 * d_model), nn.ReLU(), nn.Linear(4 * d_model, d_model) )中间层放大四倍是原始论文的选择后来大部分模型都沿用。为什么是四倍没有严格的理论证明但经验上这个比例在表达能力和计算量之间取得了不错的平衡。放大倍数太小模型容量不够太大参数爆炸且容易过拟合。从 shape 角度看FFN 是(batch, seq_len, d_model)→(batch, seq_len, 4*d_model)→(batch, seq_len, d_model)。它是在特征维度上做变换不涉及序列维度的交互。这也是为什么 FFN 可以独立地对每个位置计算而注意力必须让所有位置互相看见。5.4 残差连接和 LayerNormshape 不变但作用巨大每个子层注意力和 FFN外面都包了一层残差连接和 LayerNormx x self.attention(self.norm1(x)) x x self.ffn(self.norm2(x))残差连接要求输入输出 shape 完全一致这也是为什么d_model在整个 Transformer 里保持不变。LayerNorm 也是在最后一维上做的归一化每个 token 的特征向量。这里有个细节原始论文用的是 Post-LN先做子层再归一化后来很多模型改用 Pre-LN先归一化再做子层。Pre-LN 训练更稳定不需要 warmup现在是大势所趋。如果你自己手写 Transformer建议直接用 Pre-LN。6. 手写一个迷你 Transformer从零跑通一次前向传播6.1 为什么建议手写一遍而不是直接调库现在用 HuggingFace 的transformers库三行代码就能加载一个预训练模型。但我强烈建议每个想深入理解 LLM 的人都手写一遍迷你版 Transformer。原因有三个第一调库的时候你不需要理解 shape但手写的时候 shape 错了就报错这种被迫理解是最有效的学习方式。第二手写一遍之后你再看开源模型的源码会发现结构完全一样只是层数更多、维度更大、加了各种优化。第三面试和实际调优时对内部结构的理解是硬通货。我手写的版本大概 150 行包含嵌入、位置编码、多头注意力、FFN、编码器块和完整的模型。下面挑关键部分讲。6.2 位置编码的实现细节位置编码有两种常见实现正弦余弦原始论文和可学习嵌入。正弦余弦的公式是class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x): # x: (batch, seq_len, d_model) return x self.pe[:, :x.size(1)]这里有几个细节值得说。register_buffer把pe注册为 buffer这样它会被保存到 state_dict 里但不会被当作可训练参数。unsqueeze(0)把 shape 从(max_len, d_model)变成(1, max_len, d_model)方便广播到 batch 维度。div_term的计算用了exp(log(...))的形式这是为了避免直接计算10000^(2i/d_model)时的数值溢出。这种小技巧在实际代码里很常见值得学习。6.3 注意力掩码为什么解码器需要它编码器可以看见所有位置但解码器在生成时只能看见当前位置及之前的位置。这是通过注意力掩码实现的def generate_causal_mask(seq_len): mask torch.triu(torch.ones(seq_len, seq_len), diagonal1).bool() return mask # True 表示需要被 mask 掉的位置torch.triu取上三角diagonal1表示从对角线往上一格开始。得到的 mask 里True 的位置是未来位置。在计算注意力分数后把这些位置设成负无穷softmax 之后权重就变成 0scores scores.masked_fill(mask, float(-inf)) attn torch.softmax(scores, dim-1)为什么用负无穷而不是直接设 0因为 softmax 之后设 0 的位置仍然会有权重exp(0)1而负无穷经过exp变成 0才是真正的看不见。这个细节很多人第一次写会搞错。6.4 完整前向传播的 shape 追踪把整个模型串起来shape 的变化是这样的阶段操作输出 shape输入token ids(batch, seq_len)嵌入embedding lookup(batch, seq_len, d_model)位置编码加法广播(batch, seq_len, d_model)注意力 QKV线性投影(batch, seq_len, d_model) × 3拆多头reshape permute(batch, heads, seq_len, head_dim)注意力分数Q K^T(batch, heads, seq_len, seq_len)加权求和attn V(batch, heads, seq_len, head_dim)合并多头permute reshape(batch, seq_len, d_model)输出投影线性层(batch, seq_len, d_model)FFN两层线性 激活(batch, seq_len, d_model)输出层线性到词表(batch, seq_len, vocab_size)这张表我建议打印出来贴在显示器旁边。每次写代码卡住的时候对照这张表看当前 shape 应该是什么问题往往就找到了。7. 跑通之后从迷你模型到真实 LLM 的差距在哪7.1 参数量从百万到千亿的跨越我手写的迷你 Transformer 大概 100 万参数而 GPT-3 是 1750 亿参数。这个差距不是简单的多堆几层而是涉及一系列工程和算法上的变化。参数量主要来自几个地方d_model从 512 涨到 12288层数从 6 涨到 96词表从 3 万涨到 5 万。注意参数量和d_model是平方关系线性层的参数量是d_model^2所以d_model翻倍参数量翻四倍。这就是为什么大模型对显存的需求增长得这么快。热词里有人搜transformer 架构模型参数计算这里给个粗略公式。一个 Transformer block 的参数量大约是注意力部分4 * d_model^2Q、K、V、O 四个投影矩阵FFN 部分8 * d_model^2两个线性层中间层是 4 倍LayerNorm4 * d_model可以忽略所以每个 block 约12 * d_model^2。总参数量约12 * d_model^2 * num_layers vocab_size * d_model嵌入层。7.2 训练技巧那些让大模型能训起来的关键从迷你模型到真实 LLM中间隔着一堆训练技巧。这些技巧不是可选项而是没有就训不起来的必需品混合精度训练用float16或bfloat16做前向和反向用float32保存主权重。显存占用减半速度提升明显。但要注意梯度缩放gradient scaling否则小梯度会下溢成 0。梯度累积显存不够放一个大 batch 时把多个小 batch 的梯度累加起来再更新。效果等价于大 batch但显存占用小。梯度检查点用计算换显存。前向时不保存中间激活反向时重新计算。显存占用大幅降低但训练速度慢 20% 到 30%。分布式训练数据并行、张量并行、流水线并行各种并行策略组合使用。这部分是工程重灾区不是算法问题。7.3 推理优化模型跑起来之后的事训练完之后部署推理又是另一套东西。热词里有人搜onnx 部署 llm 模型说明大家对这个环节有需求。推理优化的核心目标是降低延迟和显存占用。常见手段包括量化把float16权重压成int8或int4显存占用降到 1/2 或 1/4精度损失可控。这是目前最主流的优化手段。KV Cache自回归生成时每次只生成一个新 token但注意力需要看到所有历史 token 的 K 和 V。如果每次都重算浪费巨大。KV Cache 把历史的 K、V 缓存起来每次只算新的。这是推理加速的标配。批处理把多个请求打包成一个 batch 一起推理提高 GPU 利用率。但要注意不同请求的序列长度不一样需要 padding 和 attention mask 处理。投机解码用一个小模型快速生成草稿大模型验证。如果草稿被接受就省了一次大模型的前向。这个技术能把推理速度提升 2 到 3 倍。7.4 从能跑到跑得好我踩过的几个坑最后分享几个我在实际跑模型时踩过的坑都是文档里不会写的坑一显存碎片化。长时间运行的服务显存会碎片化明明总显存够但就是分配不出来。解决办法是设置PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True或者定期重启服务。坑二dtype 不一致导致的静默错误。模型权重是float16但输入是float32PyTorch 会自动转换但转换过程可能丢失精度。更隐蔽的是某些操作在float16下会溢出得到inf或nan但不会报错。我的习惯是在关键位置加assert not torch.isnan(x).any()。坑三batch 内序列长度差异过大。如果 batch 里有一条超长序列和一堆短序列padding 会浪费大量计算。解决办法是按长度分桶bucketing把长度相近的样本放在一个 batch 里。坑四tokenizer 和模型不匹配。用 A 模型的 tokenizer 处理输入喂给 B 模型结果完全错乱。tokenizer 必须和模型配套不能混用。坑五生成时的重复问题。模型生成时容易陷入重复循环输出我我我我我。解决办法是加 repetition penalty或者用 top-k、top-p 采样代替贪心解码。8. 给不同阶段学习者的路径建议如果你刚开始我的建议是先把 NumPy 用熟再学 PyTorch最后碰 Transformer。不要一上来就啃论文那样容易劝退。NumPy 阶段重点掌握 ndarray 的创建、索引、广播、矩阵运算这些是后面所有内容的基础。如果你已经会 PyTorch但看不懂 Transformer 源码我的建议是手写一遍迷你版。不要抄自己一行行写shape 错了就调调通了就理解了。写完之后再去看 HuggingFace 的实现你会发现结构完全一样只是多了各种工程优化。如果你已经能跑模型但想深入理解我的建议是从 shape 入手追踪一个完整的前向传播。把每个模块的输入输出 shape 都打印出来对照架构图看。这个过程会让你对模型的理解从黑盒变成白盒。热词里有人搜新手跑 transformer 模型和transformer 手写说明这两个需求都很真实。我的经验是跑通别人的代码只能给你信心手写自己的代码才能给你理解。两者都要做但顺序不能反。从一行import numpy到一个能跑的大模型中间的距离比想象中远但路径是清晰的。每一个概念都是下一个概念的基础没有捷径但也没有死胡同。把 shape 念出来把每一步的维度变化搞清楚剩下的就是时间和练习的问题。
返回列表