Tensors
一切数据的基本单元
CS336 · 第 2 课
训练一个 70B 模型到底要花多少钱、多长时间?答案可以用"餐巾纸数学"(napkin math)算出来——你只需要会乘法和理解几个关键数字。这一课建立整个课程的方法论:从第一性原理出发,用资源核算驱动每一个工程决策。
Spring 2026 官方来源:Lecture 2 trace · 课程日程
本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。
上一课建立了全课框架:理解源于构建,效率驱动一切。从这一讲开始,每个工程选择都要回答两个问题:模型状态能否放进显存,完成训练需要多少计算时间?PyTorch tensor、operation、module 和 training loop 是实施这些选择的工具,不是彼此孤立的 API 清单。
模型与 token 规模 → dtype 决定 bytes → operation shape 决定 FLOPs → 6PD 估算训练计算 → MFU 修正硬件峰值 → arithmetic intensity 判断 compute / memory bottleneck → PyTorch 构建块落实核算。
一切数据的基本单元
计算与内存的代价
参数化模型的抽象
端到端训练流程
贯穿始终的是资源核算——对两种核心资源保持持续追踪:
我们不会在这课讲 Transformer 架构本身(Assignment 1 会深入),而是用更简单的模型来演示原理——这些原理在任何规模都适用。
面对“训练要多久”“模型能否放下”“kernel 为什么慢”,不要先找一个经验数字。先列出对象、单位、计算路径和边界条件,再从最小矩阵乘法推到真实 Transformer。
Prerequisite refresher:shape 描述每个轴有多少元素;dtype 决定每元素 bytes;forward 产生输出与 loss;backward 用 chain rule 求梯度;optimizer 再用梯度更新权重。FLOP 是一次浮点运算,FLOP/s 才是速度。本文用“显存(GPU memory)”泛指 GPU 上可用的内存;讨论带宽时再明确写 HBM。
Training
tokens → forward → loss → backward → gradients → optimizer → new weights
显存
├─ model states(通常随 P 缩放)
│ ├─ parameters / master parameters
│ ├─ gradients
│ └─ optimizer moments m, v
├─ activations(随 batch、sequence、layers、hidden shape 缩放)
├─ temporary tensors / CUDA workspace
└─ communication buffers / allocator fragmentation
Inference
weights + KV cache + temporary activations
(没有 backward、gradients、Adam states)
先不要算 70B。如果连一个 [100,256] @ [256,512] 的 forward/backward shape 与 FLOPs 都不能手推,直接套 6PD 只会得到没有解释力的数字。
Tensor 是 PyTorch 中存储所有数据的基本结构——参数、梯度、优化器状态、数据、激活值,一切皆是 tensor。
import torch
# 创建方式
x = torch.tensor([[1., 2, 3], [4, 5, 6]]) # 从数据
x = torch.zeros(4, 8) # 全零
x = torch.ones(4, 8) # 全一
x = torch.randn(4, 8) # 随机初始化
x = torch.empty(4, 8) # 未初始化(最快)
PyTorch tensor 本质上是一个指针加上描述如何寻址元素的元数据(stride):
x = torch.tensor([
[0., 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
])
# stride 描述在存储中跳过多少元素到达下一个维度
x.stride(0) # = 4 (到下一行,跳过 4 个元素)
x.stride(1) # = 1 (到下一列,跳过 1 个元素)
# 元素 [r, c] 的索引 = r * stride(0) + c * stride(1)
# 例如 [1, 2] → 1*4 + 2*1 = 6
很多操作只是提供 tensor 的不同视图(view),不做拷贝:
x = torch.tensor([[1., 2, 3], [4, 5, 6]])
# 切片 - view,共享存储
y = x[0] # [1., 2, 3],共享存储
y = x[:, 1] # [2, 5],共享存储
# reshape(可能 view,可能拷贝)
y = x.view(3, 2) # view,共享存储
# transpose - view,共享存储
y = x.t()
y.is_contiguous() # False!非连续
# 非连续 tensor 不能再 view,需要先 contiguous()
y.view(2, 3) # RuntimeError!
y.contiguous().view(2, 3) # OK,但会拷贝
性能陷阱:Views 是零开销的,但 contiguous() 需要拷贝数据。理解何时需要拷贝对性能优化至关重要。
餐巾纸估算要先把模型中的对象换算成 bytes。参数、梯度、optimizer state 和 activation 都是 tensor;dtype 决定每个元素占多少空间,也决定动态范围和可使用的硬件路径。
| 对象 | 为什么存在 | 是否天然是 per-parameter state |
|---|---|---|
| parameter | forward 使用的可学习权重 | 是 |
| gradient | loss 对每个参数的导数,optimizer update 使用 | 是 |
| first moment \(m\) | Adam 的梯度指数移动平均 | 是 |
| second moment \(v\) | Adam 的平方梯度指数移动平均 | 是 |
| FP32 master parameter | 某些 mixed-precision recipe 用于稳定地累积小更新 | 实现相关,未必独立存在 |
| activation | backward 需要的中间结果 | 否;取决于 batch、sequence、层与 checkpointing |
因此 16 bytes/param 或 18 bytes/param 都只是某个 precision + optimizer + framework recipe 的账本。真正方法是逐项写“元素数 × dtype bytes × 是否复制/分片”,不能背一个常数。
Layer 1 · model-state capacity upper bound。假设一种 recipe 为每参数保存 2-byte BF16 weight、4-byte FP32 master weight、4-byte gradient、两个 4-byte Adam moments,共 18 bytes/parameter:
aggregate HBM = 8 × 80 GB = 640 GB
upper bound = 640×10^9 / 18 ≈ 35.6B parameters
Layer 2 · 能否使用 aggregate memory。DDP 在每张 GPU 都复制完整 model states,单卡 80GB 才是主要上限,不能把 640GB 当成一个池。ZeRO/FSDP 对 optimizer states、gradients、parameters 做不同程度 sharding,才可能逐步接近 aggregate capacity。
Layer 3 · real training。即使 model states 完全分片,activations、temporary tensors、communication buffers、allocator fragmentation、CUDA workspace 仍占空间;sequence length、microbatch size、checkpointing 策略都会改变余量。
结论:“35.6B”只能叫 theoretical model-state capacity upper bound,不能改写成“8 张 H100 可以训练 35.6B 模型”。
| 类型 | 字节 | 指数位 | 尾数位 | 动态范围 | 精度 |
|---|---|---|---|---|---|
| float32 | 4 | 8 | 23 | ±3.4×10³⁸ | 高 |
| float16 | 2 | 5 | 10 | ±6.5×10⁴ | 中 |
| bfloat16 | 2 | 8 | 7 | ±3.4×10³⁸ | 低 |
| fp8 (E4M3) | 1 | 4 | 3 | ±448 | 很低 |
| NVFP4 | 0.5(不含 block scale) | 编码依实现 | 4-bit 离散值 | 依赖分块缩放 | 极低 |
# float16 的问题:小数值下溢
x = torch.tensor([1e-8], dtype=torch.float16)
print(x) # tensor([0.]) ← 下溢了!
# bfloat16 没问题
x = torch.tensor([1e-8], dtype=torch.bfloat16)
print(x) # tensor([1.0000e-8]) ← 保留了
直觉:bfloat16 把有效精度从 float16 的 10 位尾数降到 7 位,但保留了与 fp32 相同的 8 位指数,因此动态范围更适合训练。代价是舍入误差更大;“低精度一定有正则化效果”并不是通用保证,数值稳定性仍取决于算子、归一化、累加精度和优化器实现。
H100 支持两种 FP8 格式:
这是 NVIDIA Transformer Engine 常见的 hybrid recipe,不是硬性规则;实际训练还需要缩放、amax 统计,并通常以更高精度累加。
Spring 2026 官方 trace 进一步讨论了 NVIDIA 在 2025 年公开的 NVFP4:每个值只有 4 bit,并为小块数据保存独立 scale。这样扩大了局部动态范围,但同一块中的值共享尺度,因此表示能力与误差会强烈依赖分块方式。
# 官方 trace 给出的 NVFP4 离散值直觉
values = [-6, -4, -3, -2, -1.5, -1, -0.5,
0, 0.5, 1, 1.5, 2, 3, 4, 6]
# 真实存储成本不能只写成 0.5 byte/value:
# 还要计入每个 block 的 scale,以及可能保留的高精度副本/累加器。
不要把“4 bit”直接等同于“训练显存缩小 8×”。 参数、梯度、激活、optimizer state 与累加器不一定都使用 NVFP4;分块 scale 和量化元数据也有成本。资源核算必须按实际 recipe 分项计算。
GPT-3 的一个 FFN 矩阵有多大?
GPT-3 的 d_model = 12288, d_ff = 4 × 12288 = 49152
一个矩阵: 12288 × 49152 × 4 bytes (fp32)
= 2,415,919,104 bytes ≈ 2.3 GB
GPT-3 有 96 层 → 一个模型光参数就 175 GB(fp32)
内存核算回答“能否放下”,但不能回答“需要多久”。接下来把 operation shape 转成 FLOPs,再把整次训练归约到参数量和 token 数。
FLOP(Floating-Point Operation)是一次浮点加法或乘法。注意区分:
在 dense Transformer 中,参数相关计算主要来自矩阵乘法。先看具体 shape:
X: [100, 256]
W: [256, 512]
Y = X @ W
Y: [100, 512] # 内维 256 消去,外维保留
给定 dY: [100, 512]
dW = X.T @ dY
= [256,100] @ [100,512]
= [256,512] # 必须与 W 同 shape
dX = dY @ W.T
= [100,512] @ [512,256]
= [100,256] # 必须与 X 同 shape
每个输出元素是长度 256 的 dot product:256 次乘法与 255 次加法。资源核算把它近似记作 \(2×256\) FLOPs;这一步的“2”来自乘与加,不是来自 forward/backward。
forward Y: 2 × 100 × 256 × 512 = 26,214,400 FLOPs
backward dW: 2 × 256 × 100 × 512 = 26,214,400 FLOPs
backward dX: 2 × 100 × 512 × 256 = 26,214,400 FLOPs
所以这一线性层:backward ≈ 2 × forward
不是经验魔法,而是 backward 有两个同量级 matmul。
Exact identity矩阵相乘的合法 shape 与 \(dW\)、\(dX\) 的 shape。
Napkin-math approximation把长度 \(D\) 的 dot product 记作 \(2D\) FLOPs,以及忽略 bias、activation、normalization 等较小项。
现在只看一个 token穿过所有参数主导的线性层。令 \(P\) 为被该 token 使用的 dense parameter count:
one token forward ≈ 2P # 每个权重参与一次 multiply-add
one token backward ≈ 4P # weight-gradient 与 input-gradient 两条 matmul
one token training ≈ 6P
D 个实际处理的 training tokens:
training compute ≈ 6P × D = 6PD
\(D\) 是训练过程中实际 processed token count。它不是数据文件的 GB 数,也不必等于 unique dataset token count。100B-token dataset 训练 3 epochs,若每个 token 每轮都使用,则 \(D=300B\)。这里用 \(D\) 而不是 \(T\),避免把总数据 token 数与单个 sequence length 混淆。
固定 D = 1,000,000 processed tokens
方案 A: 1,000 tokens/step × 1,000 steps = 1,000,000
方案 B: 2,000 tokens/step × 500 steps = 1,000,000
两者一阶 parameter-related compute 都约为 6P × 1,000,000。
Batch size 改变的是如何把同一批 token 分组,而不是 token 总数。因此它不单独出现在总 FLOPs 的一阶式中;但它会改变 GPU utilization、activation memory、optimizer step 次数、优化动力学与多卡通信形态,所以“总 FLOPs 相同”绝不等于“训练时间和最终模型相同”。
P = 70×10^9 parameters
D = 15×10^12 processed tokens
C ≈ 6PD
= 6 × 70×10^9 × 15×10^12
= 6.3×10^24 FLOPs
若 1024 张 H100 的所选精度对应峰值按 989 TFLOP/s 计,并假设端到端 MFU 为 50%,模型有效吞吐约为 \(989×10^{12}×1024×0.5\) FLOP/s,理想化时间约 144 天。硬件峰值与 MFU 都是条件,不是模型本身的常数。
Napkin-math approximation6PD 适合参数相关线性层主导的 dense Transformer 预训练。长上下文、小模型、embedding/output 占比高、稀疏/MoE、weight tying 或 activation checkpoint recomputation 都会使精确账本偏离它;通信、数据加载和故障时间本来就不是 FLOPs。
| 操作 | FLOPs | 和 matmul 对比 |
|---|---|---|
| 矩阵乘法 (m×n × n×p) | 2mnp | 基准 |
| 逐元素运算 (m×n) | O(mn) | 在大 dense matmul 的 FLOPs 账本中常为低阶项 |
| 加法 (m×n + m×n) | mn | FLOPs 少,但仍可能受 memory traffic 限制 |
| LayerNorm | O(mn) | FLOPs 低不等于 wall time 可忽略 |
| Softmax | O(mn) | 需结合 shape、fusion 与数据搬运判断 |
总 FLOPs 除以 GPU 峰值只能得到理想下界。真实 kernel 会受到数据搬运、shape、通信和软件开销影响,因此需要 MFU 把理论峰值与端到端训练吞吐连接起来。
MFU = 实际 FLOP/s / 硬件峰值 FLOP/s
更准确地说,MFU 是模型按约定 FLOPs 口径得到的有效 FLOP/s占对应 dtype、dense/sparse 模式下理论峰值的比例。分子不是 GPU 上发生过的所有指令。
监控面板中的 GPU utilization 常表示采样窗口内 GPU 是否有 kernel 在执行,不说明 Tensor Core 每个周期完成了多少有效模型 FLOPs。因此完全可能:
GPU utilization = 100%
MFU = 40%
GPU 可能一直在运行低效率 kernel、等待/搬运 memory、处理通信,或矩阵 shape 没有充分利用 Tensor Core。前者回答“GPU 忙不忙”,后者回答“模型有效计算离选定硬件峰值多远”。
MFU 的损失可能来自:
| GPU | fp32 (TFLOP/s) | bf16 (TFLOP/s) | HBM 带宽 |
|---|---|---|---|
| A100 | 19.5 | 312 | 2 TB/s |
| H100 (稠密) | 67.5 | 989 | 3.35 TB/s |
| H100 (稀疏) | — | 1979 | 3.35 TB/s |
import timeit
import torch
# 大矩阵乘法,bf16
B, D, K = 16384, 32768, 8192
x = torch.ones(B, D, device='cuda', dtype=torch.bfloat16)
w = torch.randn(D, K, device='cuda', dtype=torch.bfloat16)
# 预热(JIT 编译、缓存预热)
for _ in range(5):
_ = x @ w
torch.cuda.synchronize()
# 正式计时
num_trials = 5
start = timeit.default_timer()
for _ in range(num_trials):
y = x @ w
torch.cuda.synchronize()
elapsed = (timeit.default_timer() - start) / num_trials
flops = 2 * B * D * K
actual_flops_per_sec = flops / elapsed
promised_flops_per_sec = 989e12 # H100 bf16
mfu = actual_flops_per_sec / promised_flops_per_sec
print(f"MFU: {mfu:.2%}") # 通常 40-60%
Hardware / implementation-specific“40–60%”或“≥50% 很好”只能在指定模型、精度、并行策略与硬件上作为经验参照。低 MFU 可能来自 memory bandwidth,也可能来自通信、bubble、data stall、小 shape 或 kernel launch 开销;不能只凭 MFU 给出单一病因。
MFU 告诉我们“离峰值还有多远”,arithmetic intensity 进一步解释“为什么”:同样的 GPU,有些 operation 受计算单元限制,有些 operation 先耗尽 HBM bandwidth。
对于矩阵乘法 Y = XW(X: B×D, W: D×F):
读 X: B × D × 2 bytes (bf16)
读 W: D × F × 2 bytes
写 Y: B × F × 2 bytes
总传输: 2BD + 2DF + 2BF bytes
FLOPs: 2BDF
算术强度 = 2BDF / (2BD + 2DF + 2BF)
≈ B (当 D, F >> B 时)
每个硬件有一个ridge point——计算和内存带宽的平衡点:
H100 ridge point = 989 TFLOP/s / 3.35 TB/s ≈ 295
如果算术强度 > 295 → 在该简化 roofline 下更可能 compute-bound
如果算术强度 < 295 → 在该简化 roofline 下更可能 memory-bound
在 D,F >> B 且 traffic 模型成立时,B > 295 才可能跨过 ridge point
Compute-bound 与 memory-bound 不是“好/坏”标签,而是当前 workload 的性能上限分别由计算峰值还是带宽决定。优化方向也不同:前者减少 FLOPs 或提高矩阵单元效率,后者减少 bytes 或增加数据复用。
当并发规模很小、每次只生成一个 token 时,线性层接近矩阵—向量乘法:读取大量权重,却只为少量 token 做计算,因此权重复用不足,往往更容易落入 memory-bound 区域。
因此推理优化既要减少权重与 KV 的数据移动,也要通过 batching 提高复用;具体瓶颈仍需结合 batch、context、dtype、kernel 与硬件实测。
# 传统写法——你能一眼看出在算什么吗?
scores = query @ key.transpose(-2, -1) # -2, -1 是什么维度?
x = x.view(x.size(0), x.size(1), num_heads, -1) # -1 是多少?
from einops import einsum
# attention scores: Q @ K^T
scores = einsum(query, key,
'batch seq1 head dim, batch seq2 head dim -> batch head seq1 seq2'
)
# 不再需要 transpose,维度命名让意图一目了然
from einops import rearrange
# 把 hidden 拆成 heads × head_dim
x = rearrange(x, 'batch seq (heads dim) -> batch heads seq dim', heads=8)
# 合并回去
x = rearrange(x, 'batch heads seq dim -> batch seq (heads dim)')
from jaxtyping import Float
import torch
# 默认主要用于类型标注;配合 jaxtyped + typeguard/beartype 可运行时检查
x: Float[torch.Tensor, "batch seq heads dim"] = torch.ones(2, 3, 8, 64)
# 简单模型: y = 0.5 * (x @ w - 5)^2
x = torch.tensor([1., 2, 3])
w = torch.tensor([1., 1, 1], requires_grad=True)
# 前向
pred_y = x @ w # = 6
loss = 0.5 * (pred_y - 5).pow(2) # = 0.5
# 反向——自动计算 d(loss)/d(w)
loss.backward()
print(w.grad) # [1, 2, 3] (= x * (pred_y - 5))
对于每个线性层 Y = XW:
前向: Y = X @ W → 2BDF FLOPs
反向:
dL/dX = dL/dY @ W^T → 2BDF FLOPs
dL/dW = X^T @ dL/dY → 2BDF FLOPs
总计: 6BDF = 3 × 前向
重要:反向传播需要保存前向的中间值(activations)。这就是为什么训练比推理费内存——你不仅要存参数,还要存 backward 需要的所有中间结果。
不恰当的初始化会导致梯度爆炸或消失:
# 不稳定的初始化
w = nn.Parameter(torch.randn(input_dim, output_dim))
# output 的元素大小 ~ sqrt(input_dim) → 随维度增长!
# Xavier/Glorot 初始化
w = nn.Parameter(torch.randn(input_dim, output_dim) / np.sqrt(input_dim))
# output 的元素大小 ~ 1(与维度无关)
# 更安全的截断版本
nn.init.trunc_normal_(w, std=1/np.sqrt(input_dim), a=-3, b=3)
class Linear(nn.Module):
def __init__(self, input_dim, output_dim):
super().__init__()
self.weight = nn.Parameter(
torch.randn(input_dim, output_dim) / np.sqrt(input_dim)
)
def forward(self, x):
return x @ self.weight
class Cruncher(nn.Module):
"""简单深度线性模型,用于演示。"""
def __init__(self, dim, num_layers):
super().__init__()
self.layers = nn.ModuleList([Linear(dim, dim) for _ in range(num_layers)])
self.final = Linear(dim, 1)
def forward(self, x):
for layer in self.layers:
x = layer(x)
return self.final(x).squeeze(-1)
model = Cruncher(dim=64, num_layers=2)
print(f"参数数: {sum(p.numel() for p in model.parameters())}")
# 64*64 + 64*64 + 64 = 8256
for step in range(num_steps):
# 1. 获取数据
x, y = get_batch(batch_size=B)
# 2. 前向传播
pred_y = model(x)
loss = F.mse_loss(pred_y, y)
# 3. 反向传播
loss.backward()
# 4. 参数更新
optimizer.step()
# 5. 清除梯度(重要!否则梯度会累积)
optimizer.zero_grad() # 当前 PyTorch 默认 set_to_none=True
set_to_none=True 通常有更低的内存写入开销,但它也改变语义:没有收到梯度的参数会保持 grad is None,优化器通常会跳过该参数的更新;显式零梯度则可能执行一次“梯度为 0”的更新。当前 PyTorch 中 zero_grad() 已默认采用 set_to_none=True。
| 优化器 | 公式 | 内存 |
|---|---|---|
| SGD | θ -= lr × g | 0 |
| Momentum | θ -= lr × (βm + g) | 1× params |
| AdaGrad | θ -= lr / √(Σg²) × g | 1× params |
| RMSProp | θ -= lr / √(EMA(g²)) × g | 1× params |
| AdamW | Adam 更新与 weight decay 解耦:先按矩估计更新,再单独衰减参数 | 2× params |
# 对于参数量 P,batch 大小 B,序列长度 L,维度 D,层数 L_num
# 假设 float32 + AdamW
parameters: 4P (fp32 参数)
gradients: 4P (fp32 梯度)
optimizer_state: 8P (m + v, 各 4P)
activations: ~4 × B × D × L_num × L_seq (取决于具体结构)
总计(不含 activations): 16P bytes
# 常见 bf16 参数 + fp32 主参数/梯度/Adam 状态核算:
mixed_precision_total: 2P + 4P + 4P + 8P = 18P bytes
# 注意:框架可能不保留独立 master 参数,梯度 dtype 也可能不同,
# 所以 18P 是预算模型,不是所有实现的固定常数。
# 1. 对标准 .npy 文件使用 mmap_mode 懒加载
data = np.load('data.npy', mmap_mode='r')
# 2. pin_memory → 异步 CPU→GPU 传输
x = x.pin_memory().to(device, non_blocking=True)
# 3. 多个 worker 预取
loader = DataLoader(dataset, num_workers=4, pin_memory=True,
prefetch_factor=2, persistent_workers=True)
# 保存
checkpoint = {
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'step': step,
'loss': loss.item(),
}
torch.save(checkpoint, f'checkpoint_{step}.pt')
# 加载
checkpoint = torch.load('checkpoint.pt')
model.load_state_dict(checkpoint['model'])
optimizer.load_state_dict(checkpoint['optimizer'])
混合精度:用低精度做前向计算(快、省内存),用高精度存参数和做更新(稳)。
import torch
for x, y in loader:
# bf16 指数范围与 fp32 相同,通常不需要 GradScaler
with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
# 前向用 bf16——激活值只占一半内存
output = model(x)
loss = criterion(output, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
如果使用 float16,梯度更容易下溢,通常应使用 torch.amp.GradScaler("cuda")。对 bf16 无条件套用 GradScaler 会掩盖两种格式在动态范围上的关键差异。
# 无混合精度 (fp32):
参数: 4P, 梯度: 4P, 优化器: 8P → 16P bytes
# 混合精度 (bf16 前向 + fp32 主参数):
bf16 参数: 2P
fp32 主参数: 4P
fp32 梯度: 4P
fp32 优化器: 8P
bf16 激活值: ~half → 显著节省!
总计参数相关: ~18P(稍多,但激活值节省巨大)
问题 1:训练一个 P 参数的模型,处理 T 个 token,总 FLOPs 约为?
问题 2:为什么训练 LLM 通常用 bfloat16 而不是 float16?
问题 3:在本课简化假设(D、F ≫ B)下,H100 上该矩阵乘法何时更可能 compute-bound?
问题 4:为什么训练比推理需要更多内存?
问题 5:当前 PyTorch 中,optimizer.zero_grad(set_to_none=True) 与 optimizer.zero_grad() 的关系是?
X:[100,256]、W:[256,512]、Y=X@W。请先写 \(Y\)、\(dY\)、\(dW\)、\(dX\) 的 shape,再数三个 matmul 的 FLOPs。
答案:\(Y,dY:[100,512]\),\(dW:[256,512]\),\(dX:[100,256]\);forward、\(dW\)、\(dX\) 各约 26.2M FLOPs,所以本例 backward 约为 forward 两倍。
若 \(dW\) 或 \(dX\) shape 不对应原 tensor,说明你在背转置位置,还没有用“梯度必须与被求导对象同 shape”校验。
答案:若每轮完整处理,6PD 中 \(D=300B\) processed tokens。Dataset 文件有多少 GB 与这个式子没有直接等价关系。
若答 100B,你把 unique corpus size 当成了训练实际处理量;若答文件 bytes,你混淆了 storage unit 与 token count。
答案:固定 \(D\) 时,batch 翻倍通常让 steps 约减半;每 step FLOPs 翻倍、step 数减半,一阶总量抵消。但 activation memory、utilization、通信粒度与 optimizer updates 都改变。
若只看单 step 得出总 FLOPs 翻倍,说明你没有把“每步成本 × 步数”放在同一账本。
答案:能。GPU 可一直执行 kernel,却因 memory、通信、低效 shape 或未充分使用 Tensor Core,只完成峰值 40% 的模型有效 FLOP/s。
若答“不能”,说明你把“有 kernel 在忙”误当成“有效数学吞吐达到峰值”。
答案:先问并行策略能否分片 model states(DDP 不能把显存简单相加,FSDP/ZeRO 才能接近);再问 activations、buffers、fragmentation、workspace、sequence 与 batch 还要多少显存。
若直接回答“能训 35.6B”,说明你把 model-state capacity upper bound 当成了 end-to-end feasibility。
下一课的 RMSNorm、SwiGLU、RoPE、GQA 与 FlashAttention 不只是“现代组件清单”。你要继续追问:它改变了哪一个 tensor shape、哪一项 FLOPs、哪一份 activation 或 KV memory,以及为什么不采用更直觉的替代方案。