Skip to content

交互实验:把机制一步步拆开 ​

这些演示都在浏览器本地运行,不连接付费 API。修改输入,再逐步观察状态与结果。有播放功能的示例可以暂停;每个示例都能重置。键盘可以使用 Tab 移动、Enter 或空格操作按钮。页面不会自行开始播放,也不依赖动画才能理解。

这些是可检查的教学模型,不是实际语言模型推理或 Go runtime 性能实验。每个演示都标明假设、固定规则与适用边界。相关章节内也会出现同样的演示。

KV cache:少算旧 K/V,仍然读取历史 ​

KV CACHE · 计算与复用

每来一个 token,哪些工作要重做?

固定输入 3 个 token,再逐个处理 8 个已给定的 token。对比开关缓存后的计算量,再看 KV 头数怎样改变缓存占用。

这里只数 K/V 投影的位置数,不执行语言模型,也不测量速度。

01

设置 · 同一步对比两种做法

每层 KV 头数
查询头数固定为 12

固定条件:批大小 1、12 层、每头维度 64、每元素 2 字节。改变 KV 头数不会改变本例的位置计数。

02

过程 · 先算提示词,再逐个加入新位置

  1. Prefill当前 · 处理 3 个提示词位置
  2. Decode待开始 · 每次加入 1 个 token
步骤 0 / 8

本步计算 3 个位置的 K/V,复用 0 个旧位置。最新位置的 Q 仍要读取全部 3 个位置的 K/V,才能预测下一个 token。

本步新算 K/V3位置数 · 每层需要做投影的位置
本步复用 K/V0位置数 · 从前一步缓存中取得
最新 Q 的读取范围3位置数 · 缓存不省掉历史 K/V 的读取
当前上下文中的 K/V
实线:本步计算;虚线:缓存复用

位置卡可横向滚动;手动滚动会暂停播放。键盘聚焦下方区域后,用左右方向键查看。

  • 位置 1新算 K/V
  • 位置 2新算 K/V
  • 位置 3新算 K/V
03

结果 · 分开看重复计算与缓存空间

累计计算的 K/V 位置数3每层 · 从 Prefill 到当前步骤累计
缓存张量占用108.0 KiB全部 12 层 · 只计跨步骤保留的 K/V

试着在 Decode 阶段切换缓存。上下文长度不变,新算的位置数会改变。再切换 KV 头数,观察缓存占用;这里没有把位置数换算成速度。

内存怎么算?这个演示省略了什么?

稠密、未量化、完整上下文的缓存有效数据量为:2 × B × L × Hkv × T × D × s 字节。2 表示 K 与 V;批大小 B=1,层数 L=12,KV 头数 Hkv=12,已缓存长度 T=3,每头维度 D=64,每元素 s=2 字节。查询头数固定为 12。1 KiB=1024 字节。

数值不含模型权重、Q、注意力工作区、内存分配对齐或预分配空位。关闭缓存时,“0”只表示没有跨步骤保留的 KV cache,计算仍需要临时 K/V 内存。MLA、滑动窗口、量化缓存、分页管理等不能直接照搬这组假设。

不使用缓存的基线在每一步重新运行整个前缀。图中只展示最新位置的 attention 读取范围;它没有把整个模型的 FLOPs、耗时或显存画成等比例。缓存减少重复计算,但不消除对历史 K/V 的读取。

不操作也能读懂:prefill 建立缓存;decode 只补上新 K/V,再让新 Q 读取历史与当前 K/V。

依据:Transformers:How caching works。本页为简化教学模型。

BPE:token 怎样从字符合并出来 ​

BPE · 固定规则下的编码

从字符到 token:逐条应用合并规则

改一段文本,看相邻符号如何按固定顺序合并,再核对每个 token 的教学 ID。这是自定义字符级 BPE,不是 GPT 分词器,也不在输入文字上重新训练规则。

01

输入 · 给固定规则一段文本

02

过程 · 一条规则,合并所有匹配位置

规则 0 / 7当前:按字符拆开

初始状态:按字符拆分 · 当前 18 个 token。

当前 token 序列

每张卡是完整 token;␠ 表示空格。可横向滚动,聚焦后用左右方向键查看。

  • lID 11
  • oID 14
  • wID 22
  • eID 4
  • rID 17
  • ␠ID 26
  • lID 11
  • oID 14
  • wID 22
  • eID 4
  • sID 18
  • tID 19
  • ␠ID 26
  • lID 11
  • oID 14
  • wID 22
  • eID 4
  • rID 17
当前 token 数18按当前规则进度得到的符号数量
本步合并处数0尚未应用合并规则
固定合并表

从上往下应用,不按当前文本重新排序。每条规则从左到右合并所有不重叠的匹配;窄屏可横向滚动,聚焦后用左右方向键查看。

  1. 1l + o → lo教学 ID 31待应用
  2. 2lo + w → low教学 ID 32待应用
  3. 3e + r → er教学 ID 33待应用
  4. 4low + er → lower教学 ID 34待应用
  5. 5s + t → st教学 ID 35待应用
  6. 6e + st → est教学 ID 36待应用
  7. 7low + est → lowest教学 ID 37待应用
03

解释 · token 不必是完整单词

先猜一条规则能合并几处。默认文本里,第一次合并会同时处理三个 l + o。走到最后再核对:符号数量变少了,把所有 token 拼回去,原文没有改变。

完整教学词表与边界

基础词表按 a–z、空格、句点、逗号、感叹号、问号排序,ID 从 0 开始;新符号按合并表追加。␠ 只是空格的显示标记。

词表保留完整符号;键盘聚焦区域后,可用左右方向键查看。

  • ID 0: a
  • ID 1: b
  • ID 2: c
  • ID 3: d
  • ID 4: e
  • ID 5: f
  • ID 6: g
  • ID 7: h
  • ID 8: i
  • ID 9: j
  • ID 10: k
  • ID 11: l
  • ID 12: m
  • ID 13: n
  • ID 14: o
  • ID 15: p
  • ID 16: q
  • ID 17: r
  • ID 18: s
  • ID 19: t
  • ID 20: u
  • ID 21: v
  • ID 22: w
  • ID 23: x
  • ID 24: y
  • ID 25: z
  • ID 26: ␠
  • ID 27: .
  • ID 28: ,
  • ID 29: !
  • ID 30: ?
  • ID 31: lo
  • ID 32: low
  • ID 33: er
  • ID 34: lower
  • ID 35: st
  • ID 36: est
  • ID 37: lowest

本章若采用 UTF-8 字节级 BPE,其基础单元和训练流程与本演示不同;此处只帮助观察“按既定规则合并”这一步。不做大小写转换或其他规范化;合并规则不跨空格。未知字符会明确报错,不伪造 token ID。真实 tokenizer 可能使用字节基础词表、预切分、特殊 token 和不同的合并规则;不能用这里的计数估算 API 账单。

不操作也能读懂:l + o → lo,lo + w → low,e + r → er,low + er → lower。token 是词表中的符号,不必等于一个字或一个完整单词。

依据:Hugging Face:BPE 的训练与编码。本例规则为明确列出的教学规则,并非从真实 GPT 词表摘取。

Go GMP:谁在等待,谁还能继续执行 ​

GO GMP · 执行、队列与等待

G 在等,M 和 P 就一定停下来吗?

沿着同一段教学脚本,比较 channel 等待与阻塞系统调用。观察谁停下、谁接着执行,以及 G 被唤醒后去了哪里。

G 是 goroutine;M 是操作系统线程;P 持有执行 Go 代码所需的调度资源。

01

输入 · 只改变等待方式

固定教学条件2 个 P · 5 个 G本例不可调。每一步的执行、窃取和唤醒顺序由脚本预先指定。
02

过程 · 跟着一个 G 看资源关系

步骤 0 / 9 · 初始队列等待下一步

初始:P0 有 4 个可运行 G;P1 的队列为空;全局队列有 G5。

P0 / M0 · 执行 空闲
本地队列:G1、G2、G3、G4

P1 / M1 · 执行 空闲
本地队列:空

全局:G5
等 channel:无
阻塞的 M/G:无
空闲 M:无
已完成:无

阶段条可横向滚动;手动滚动会暂停播放。键盘聚焦下方区域后,用左右方向键查看完整顺序。

  1. 当前0 · 初始队列
  2. 待进行1 · 执行 G1
  3. 待进行2 · 窃取任务
  4. 待进行3 · 等待发生
  5. 待进行4 · 继续执行
  6. 待进行5 · 唤醒 G1
  7. 待进行6 · G3 完成
  8. 待进行7 · G4 完成
  9. 待进行8 · G2 完成
  10. 待进行9 · 全部完成

初始:P0 有 4 个可运行 G;P1 的队列为空;全局队列有 G5。

当前执行与队列
这是本步快照,不是时间轴上的耗时比例
P0 ↔ M0
当前空闲
正在执行空闲M 持有这个 P,才能执行用户 Go 代码
本地运行队列
  • G1
  • G2
  • G3
  • G4
P1 ↔ M1
当前空闲
正在执行空闲M 持有这个 P,才能执行用户 Go 代码
本地运行队列

空

全局运行队列
已就绪,等待调度
  • G5
挂起等待的 G
等待 channel 条件

无

阻塞的 M/G
此处展示阻塞系统调用

无

已经完成
0 / 5 个 G · 本轮完成数量

无

03

观察 · 等待的到底是谁?

对照两种场景的“等待发生”这一步。channel 场景里,G1 挂起,M0 仍与 P0 配对;系统调用场景里,M0 / G1 一起阻塞,本例让 M2 接管 P0。接管并非所有系统调用都会立即发生。

哪些是真实机制,哪些是教学选择?

真实机制:执行用户 Go 代码的 M 需要 P;P 的数量等于当前 GOMAXPROCS;就绪 G 可在本地或全局运行队列中;运行时会窃取其他 P 的任务;G 等待 channel 与 M 阻塞于系统调用不是同一件事。

教学选择:固定两个 P;人为指定每一步先执行哪个 G、何时窃取与唤醒。窃取步骤取约一半待运行 G,先运行一个;真实实现还有 runnext、全局队列公平性检查、随机探测、netpoll、计时器、抢占、GC 与线程停放等路径。

系统调用场景显示“P 已被接管”的结果,并不声称任意调用都会立即释放 P。G1 恢复后在本例进入全局队列,实际去向取决于恢复路径。不能据此推断程序输出顺序、吞吐量或线程数。P 也不等于物理 CPU 核心。

不操作也能读懂:可运行的 G 等待调度;M 持有 P 才执行 Go 代码;G 挂起可让 M 去跑其他 G;阻塞系统调用可能需要把 P 交给其他 M。

依据:Go runtime:G、M、P;调度源码 proc.go(核对日期:2026-10-14,源码链接随 Go 版本更新)。

梯度下降:学习率怎样改变每一步 ​

训练实验室 · 梯度下降

方向对了,步子太大会怎样?

只训练一个参数 w,让它靠近目标 t。调节步长,再亲手更新一次,看损失是否真的下降。

L(w) = (w − t)² · g = 2(w − t) · w ← w − ηg。每一步都按公式重新计算。

01

选一种走法,比较结果

02

输入 · 步长与目标

决定沿负梯度走多远。0 表示停在原地。
训练开始前,参数所在的位置。
损失曲线的最低点;到达这里时梯度为零。
滑块与精确输入控制同一个参数
03

过程 · 沿曲线更新参数

已更新 0 / 16 次

从 w = 4 开始,损失为 16,梯度为 8。

曲线保留可读尺寸。窄屏可左右滑动,手动滚动会暂停播放;键盘聚焦曲线区域后,用左右方向键查看坐标与轨迹。

-4.72目标 04.72L(w),图顶约 22.2784w
曲线是损失,虚线连接更新轨迹,大圆点是当前位置。坐标范围随已经走过的点调整;判断是否发散请看数值,不能只看图的高度。

指标保留完整数字;如数字较长,可左右滑动,或聚焦数字后用方向键查看。

当前参数 w4目标 0
当前损失 L16离目标距离的平方
下次使用的梯度8g = 2(w − t)
误差倍数 1 − 2η0.6绝对值小于 1 才会缩短距离

这组参数说明什么?学习率在 0 与 0.5 之间:从同一侧逐步靠近目标。

逐步数值记录(已完成 0 次更新)

保留完整数值。窄屏可左右滑动;键盘聚焦记录区域后,用左右方向键查看。

从起点到当前步骤,显示值经过舍入
步数参数 w梯度损失
04816
04

解释 · 方向之外,还要看步长

为什么这个例子能判断是否收敛?

令误差 e = w − t,一次更新后 e 新 = (1 − 2η)e,损失因此乘以 (1 − 2η)²。对非零初始误差,0 < η < 1 时逐步收敛;η=1 保持距离;η>1 放大距离。起点已经等于目标时,梯度为零,任何允许的学习率都不移动。

这是一个可完整算清的标量二次函数,运行的是真实梯度下降规则。神经网络有许多参数,损失曲面也不同;这里的学习率分界不能直接套到网络训练。没有训练数据、随机小批量、动量或 Adam。JavaScript 使用有限精度浮点数,极小时可能出现舍入或下溢。

不操作也能读懂:负梯度给出局部下降方向;学习率决定走多远。把 η 改成 1.2,可以看到“方向正确但损失越走越大”。

依据:PyTorch:梯度与参数更新;收敛分界由本页的二次函数直接推导。外部文档核对于 2026-10-14。

反向传播:分支梯度为什么要相加 ​

训练实验室 · 反向传播

一个输入走两条路,梯度该怎么收?

a、b 都被使用两次。先算前向数值,再倒着传梯度,看看第二条路径如何加到已有结果上。

分叉图:u = a × b,v = a + b,q = u × v。

01

输入 · 从两条路径开始

同时进入乘法 u 与加法 v。修改后从零梯度重算。完整前向预览:q=-6。两条路都用到了 a;下方逐步演算从零梯度开始。
另一条输入,同样会收到来自两条路径的贡献。完整前向预览:q=-6。修改 b 会同时改变 u 和 v。

滑块与精确输入控制同一个值。输入越界会报错,不会自动截断。

02

过程 · 先前向,再反向

步骤 0 / 7 · 输入

先给 a、b 赋值。中间节点还没计算,所有梯度暂时为 0。

节点前向值累计梯度
a-20
b30
u待计算0
v待计算0
q待计算0

每个梯度都针对最终输出 q;反向尚未结束时,贡献可能还没收齐。

展开依赖图和本步链式法则

输入 a、b 分叉为乘法 u 与加法 v 汇入输出 q

  1. 输入节点 · a输入 a前向值:-2累计 ∂q/∂a:0输入节点
  2. 输入节点 · b输入 b前向值:3累计 ∂q/∂b:0输入节点
  3. 运算节点 · uu = a × b前向值:待计算累计 ∂q/∂u:0依赖 a、b
  4. 运算节点 · vv = a + b前向值:待计算累计 ∂q/∂v:0依赖 a、b
  5. 运算节点 · qq = u × v前向值:待计算累计 ∂q/∂q:0依赖 u、v

前向运算顺序:u → v → q;反向运算顺序:q → v → u。节点里的梯度始终指“最终输出 q 对此节点的导数”。反向结束前,它可能还没收齐所有贡献。

03

指标 · 两种方法,交叉检查

指标保留完整数字;如数字较长,可左右滑动,或聚焦数字后用方向键查看。

输出 q待计算前向完成后才有值
输入 a 的最终梯度待完成收齐两条路径的贡献
输入 b 的最终梯度待完成收齐两条路径的贡献

完成反向传播后,下面会显示输入梯度与中心差分的对照。

04

解释 · 多条路径,贡献相加

这是反向传播算法,还是预设动画?

页面实际建立依赖图、做拓扑排序、计算前向值,再按逆拓扑顺序把“收到的梯度 × 局部导数”累加给输入。改变 a、b 后会重新演算,不是播放固定数字。图很小,只包含标量加法与乘法,没有执行 PyTorch 或训练神经网络。

每次重新演算都会把整个图的梯度清零。这里的累加发生在一次反向传播的不同路径之间;不要与多个批次之间保留梯度混淆。micrograd 原库和 PyTorch 的参数梯度通常需要显式清零。

中心差分是独立检查,不参与反向求导。对这个图,固定另一个输入后,函数至多是二次多项式,所以中心差分在精确算术下没有截断误差;实际仍有浮点误差。一般非线性函数还会有截断误差。

不操作也能读懂:a=−2、b=3 时,u=−6、v=1、q=−6。a 收到 −6 和 3,合计 −3;b 收到 −6 和 −2,合计 −8。覆盖任何一条贡献都会算错。

依据:micrograd 固定版本源码中的累加与逆拓扑遍历;PyTorch:自动微分。核对于 2026-10-14。

注意力:从分数、掩码到加权结果 ​

ATTENTION · 从分数到输出

让一个查询走完注意力的五步

改一组 Q、K、V,追踪某个查询如何筛掉未来位置,再把可见的值混合成输出。这里实际计算单头注意力;不包含投影、多头或 dropout。

01

先选一个要观察的问题

02

调输入,看同一条计算链怎样改变

每行一个位置,空格或逗号分列,换行或分号分行。范围 −100~100,最多 4 × 4;数字只用于教学,不是模型的语义向量。

03

沿着五步,查看真实中间结果

1 / 5 · 修改参数会重新演算

第 1 步:点积打分。当前查询 q1 = (0.0000, 1.0000)。 逐个计算 q 与每把键 k 的点积,得到匹配分数。

当前查询q1向量 (0.0000, 1.0000)
掩码后可见的键待掩码共 3 把键;屏蔽项权重为 0
当前行权重之和待归一化softmax 后检查,不用四舍五入的显示值相加

表格保留完整数值。窄屏可横向滚动;键盘聚焦表格区域后,用左右方向键查看。

查询 1 的计算明细(显示 4 位小数,计算不截断)
键位置q · k缩放后掩码后权重 A
00.0000待计算待计算待计算
11.0000待计算待计算待计算
21.0000待计算待计算待计算
04

带着一个问题再试一次

试一试:怎样验证因果性?

使用三位置因果例,跟踪位置 1,走到最后一步。只改 K、V 的最后一行,位置 1 的输出应保持不变;关闭因果掩码后再比较。零分数例的权重三行分别是 (1,0,0)、(½,½,0)、(⅓,⅓,⅓)。掩码发生在 softmax 前,不能直接删掉概率而不重新归一化。

这里只演示等长序列的因果掩码,不模拟 padding 或缓存偏移;关闭掩码时 Q 与 K 可以不等长。一般注意力中 dᵥ 也不必等于 dₖ。所有屏蔽行都没有有效概率分布,本例保留对角线以避免这种情形。

不操作也能读懂:两键例中 q=(1,0)、k₀=(1,0)、k₁=(0,1)、V=(10,20)。缩放后权重约 (0.6698,0.3302),输出约 13.3024。

公式依据:Attention Is All You Need,§3.2。输入数字仅为本书教学示例。

生成采样:温度和 top-k 如何改变概率 ​

SAMPLING · 从分数到选择

同一组 logits,为什么会选出不同 token?

使用自造 logits,直接计算温度与 top-k 后的概率,再让你指定的 u 落进累计概率区间。这里没有运行语言模型,也不调用随机数;相同输入和 u 一定得到相同结果。

01

选择场景,设定分布

当前使用预设参数;修改输入后可随时切回。

小温度更集中;0 进入贪心。精准输入接受任意有限非负温度,不受快捷滑块上限约束。
0 ≤ u < 1。手动移动只演示区间选择,没有调用随机数。
02

沿着概率,找到选择

第 1 步:读取 logits。 logits 是未归一化分数,不是概率。最大项为 ID 3。

表格可左右滑动;键盘聚焦后用左右方向键查看。

公式计算的概率,显示 6 位小数;不是重复抽样的频数
候选logit稳定分数 (z−max z)/τ过滤前概率过滤后概率状态
ID 00待计算待计算待计算待过滤
ID 11待计算待计算待计算待过滤
ID 22待计算待计算待计算待过滤
ID 33待计算待计算待计算待过滤
并列、温度 0 和数值边界

本例 top-k 严格保留 k 项;分数相同则优先保留原 ID 较小的项。比如 [3,2,2,0]、k=2 留下 ID 0、1。按第 k 大分数设阈值的另一种实现会保留前三项;两种策略的支持集不同。

正温度不改变候选排序。τ=0 在本例定义为确定性贪心,并列最大时选最小 ID;它不同于 τ→0⁺ 时在并列最大项间均分概率。贪心分支忽略 u 和 top-k 的过滤结果,但仍要求输入的 k 合法。

温度不能为负,top-k 必须是候选数以内的正整数,u 不能等于 1。有限精度下极小的概率可能下溢为 0;页面使用减最大值的稳定算法,不会用极大指数直接求和。这里不模拟 top-p、EOS、上下文截断或多步生成。

不操作也能读懂:[0,1,2,3]、τ=0.8、top-k=2 时只剩 ID 2、3,概率约 0.222700 与 0.777300。u=0.5 会选中 ID 3;u=0.1 会选中 ID 2。调温度不更新模型权重,也不保证回答更正确。

参数含义参考:Transformers 生成配置。严格 k 的并列规则、τ=0 分支和手动 u 是本例明确约定。

查看逐章覆盖进展

Go channel:谁在等待,关闭后还剩什么 ​

GO CHANNEL · 一次交错,一次观察

这次操作会完成、等待,还是 panic?

让 G1~G4 共用一个 chan int。你决定哪个 G 先行动:观察值去了哪里、谁在等待,再试着关闭 channel。

01

新建 channel · 先决定容量

清空本轮操作,重新开始

修改容量会创建一个新 channel,并清空本轮记录;Go 中已有 channel 的容量不能修改。

02

本步动作 · 由谁来做什么?

已创建容量为 1 的 chan int。选择一个 G,尝试发送、接收或关闭。

打开 · 缓冲 空 / 容量 1

G1:可继续

G2:可继续

G3:可继续

G4:可继续

这三个动作分别做什么?

发送把值交给接收者或缓冲,无法交付时等待。接收取出下一个值,同时返回 ok;关闭会保留缓冲数据并唤醒等待者,重复关闭会 panic。

03

观察 · 值在哪里,谁在等待?

第 0 步:已创建容量为 1 的 chan int。选择一个 G,尝试发送、接收或关闭。

channel 生命周期未关闭关闭不等于缓冲已清空
缓冲已有值 · len0只计缓冲,不计等待发送者
缓冲容量 · cap10 表示无缓冲,必须配对

缓冲:左侧为队首,先入先出;右侧空位可以接纳新值。

  1. 空位空槽 · 队首位置

等待发送

没有发送者等待。

等待接收

没有接收者等待。

  • G1 · 本步执行者可继续
  • G2可继续
  • G3可继续
  • G4可继续
操作结果(0 次)

还没有操作。阻塞操作会保留在这里;配对或关闭后,同一条记录会更新结果。

按点击顺序回看事件(0 步)
  1. 第 0 步:已创建容量为 1 的 chan int。选择一个 G,尝试发送、接收或关闭。
04

再试三组 · 先预测,再手动执行

以下是操作指导,不会替你切换容量或执行动作。请先重建 channel,再逐步尝试。

练习 01 · 配对
没有缓冲,值去哪里?

容量 0:G1 发送 7;再选 G2 接收。

先观察 G1 阻塞,再观察两者配对,len 始终为 0。
练习 02 · 队列
缓冲满了,谁先进去?

容量 1:G1 发送 7;G2 发送 8;G3 接收。

G3 得到 7,G2 的 8 才进入缓冲,缓冲仍满。
练习 03 · 关闭
两个 0,为何意义不同?

容量 1:G1 发送 0 后关闭;用 G2 连续接收两次。

第一次 ok=true,第二次 ok=false。也可先让几个 G 等待,再由另一个 G 关闭。
模型边界:确定的是本次交错,不是调度承诺

这是 JavaScript 教学状态机,没有运行 Go。每次点击选择一个普通 channel 操作;本例按进入等待队列的顺序处理配对,把通信结果与相关唤醒合并成一步。“可继续”只表示本例已记下结果,不表示真实 G 立即获得 CPU,也不保证它先返回。

缓冲按本例已经发生的发送顺序出队。不同 G 谁先到达、谁先恢复,不由 G 的编号决定;不要据此推断所有 Go 程序的输出顺序或调度公平性。这里只演示 make 得到的非 nil channel,不包含 select、取消、真实并行或完整的 panic/recover 控制流。为便于查看,每轮最多记录 200 步;到达上限后需重建,不会悄悄删掉旧记录。

close 唤醒等待接收者,让它们得到 0, false;它也唤醒等待发送者,但结果是 panic,并不会替它们完成发送。真实运行时在等待者恢复执行后体现这些结果。关闭者必须协调好发送生命周期;关闭成功本身不能证明这样做安全。

槽位显示的是逻辑队列,读取后会左移以便观察;并非 Go 运行时环形缓冲区的物理内存布局。len 也是本步快照,不能用来保证后续并发操作安全。

不操作也能读懂:无缓冲要配对;缓冲满了发送会等,空且未关闭时接收会等;close 保留已有值;读空后返回 0, false;关闭后发送和重复关闭会 panic。

依据:Go 规范:channel、接收、发送、关闭、panic/recover;runtime/chan.go 的等待队列与唤醒路径(核对日期:2026-10-14,源码链接随版本更新)。

联合索引:定位区间之后还要过滤什么 ​

COMPOSITE INDEX · 定位、扫描、核对

联合索引:哪些条目能跳过,哪些还得检查?

把整数键 (a,b) 按字典序排好:先比 a,a 相同才比 b。修改数据或条件,观察一个连续候选区间如何定位、怎样过滤,再与全表检查核对结果。

这是内存有序数组上的机制模拟,用二分查找展示边界定位;不是 B+ 树页访问或真实 MySQL 执行计划,不报告毫秒和加速倍数。

01

选一个问题 · 条件怎样圈住候选?

五个示例只修改查询条件,保留你编辑的数据。可继续微调下面的条件。

02

输入 · 数据和两列条件

a 的条件(第一列)

a = 2 AND b >= 20。先固定 a,再用 b 的条件定位组内边界;重复的 (a,b) 会全部保留。

b 的条件(第二列)

a = 2 AND b >= 20。先固定 a,再用 b 的条件定位组内边界;重复的 (a,b) 会全部保留。

此按钮会替换数据,并重置查询。
03

过程 · 先定位边界,再逐条检查

步骤 0 / 15

先按 a 升序排列,同一 a 内再按 b 升序排列。输入行号只是教学标识。定位边界后,逐条检查候选。

先定位候选区间,再逐条核对条件。

已比较 0 次 · 已检查 0 行
保留 0 行 · 过滤 0 行

展开完整边界、计数与全部条目

全部条件:a = 2 AND b >= 20

先固定 a,再用 b 的条件定位组内边界;重复的 (a,b) 会全部保留。

用于构造区间的列:a、b。区间内仍需筛选的列:无额外条件。本模型在扫描时复核全部条件。

键范围: (2, 20) ≤ (a,b); (a,b) ≤ (2, +∞)。 ±∞ 是定位边界的标记,不是数据值。

候选位置 [起点, 终点)[?, ?)终点位置不包含在区间内
二分定位的键比较次数0仅统计已经发生的边界比较
已检查的候选条目0不与定位比较相加估算耗时
已过滤 / 已保留0 / 0每个候选仍复核全部条件
  1. 位置 0 · 行 #9(1, 10)边界待定位
  2. 位置 1 · 行 #2(1, 20)边界待定位
  3. 位置 2 · 行 #5(1, 30)边界待定位
  4. 位置 3 · 行 #4(2, 10)边界待定位
  5. 位置 4 · 行 #6(2, 20)边界待定位
  6. 位置 5 · 行 #8(2, 20)边界待定位
  7. 位置 6 · 行 #1(2, 30)边界待定位
  8. 位置 7 · 行 #11(2, 40)边界待定位
  9. 位置 8 · 行 #3(3, 10)边界待定位
  10. 位置 9 · 行 #10(3, 20)边界待定位
  11. 位置 10 · 行 #7(3, 30)边界待定位
  12. 位置 11 · 行 #12(4, 20)边界待定位
试一试:后面的列还有用吗?

用默认数据选“a 范围+b 等值”。a ≥ 2 圈住 9 个候选,b = 20 再过滤掉 5 个,保留 4 行。换成“a 等值+b 范围”,定位后只检查 4 个候选。选择“查重复键”,两条 (2,20) 都应返回。

示例按钮只改条件,保留你编辑的数据;要复现这些数字,先恢复默认数据与条件。试着把 ≥ 换成 >,观察边界等值条目如何被排除。

适用边界与真实数据库的区别

只处理两列非空整数、AND 连接、等值和单段范围;不解析任意 SQL,不处理 NULL、字符串排序规则、隐式转换、OR、IN、多区间、降序索引或 skip scan。同键按输入行号稳定排列,只为便于比较。

采用固定的“等值前缀+首个范围”策略;即使闭区间上下限相同,也按范围处理,不做优化器化简。没有 a 条件时扫描整个索引,是此模型的策略,不能据此断言真实数据库不能用索引。

索引在每次输入变化后重新排序,排序与构建代价不计入查找步骤。这里不模拟回表、覆盖、索引条件下推、MVCC、页 I/O、写入维护或优化器选路,不能替代 EXPLAIN 与实测。

不操作也能读懂:索引 (a,b) 先聚拢相同 a,再排列 b;a=2 且 b≥20 可以形成局部连续区间。只有 b≥20 时,命中分散在各个 a 分组中。

机制依据:MySQL 8.4 联合索引、多列索引的范围定位。二分步骤为教学模型自行实现。

Dropout:单次权重和为什么不等于期望 ​

DROPOUT · 单次结果与期望

这一行没有加到 1,dropout 就算错了吗?

softmax 后的一行权重原本和为 1。训练时,dropout 会独立丢弃其中的项,再把保留项乘以 1/(1−p)。换一个掩码,看看这一行的和怎样变化。

01

同一行权重,两种运行模式

保留项乘以 1/(1−p)。训练 p=1 会报错,不会自动截断。
02

选一个掩码,逐项计算

位从左到右对应权重 0、1、2……;1 表示保留,0 表示丢弃。这里手动选掩码,不调用随机数;依次查看掩码也不等于随机抽样。

第 1 步:检查原权重。 原权重之和为 1.000000。训练模式有效 p=0.5。

表格可左右滑动;键盘聚焦后用左右方向键查看。

同一位置的权重怎样变化(显示 6 位小数)
位置原权重 a掩码 mm × a / (1−p)概率加权期望
00.200000待选择待计算待计算
10.300000待选择待计算待计算
20.500000待选择待计算待计算
试一试,以及本例的边界

默认权重 [0.2,0.3,0.5]、p=0.5:掩码 101 的行和为 1.4,010 为 0.6,001 为 1,000 为 0。把 p 改成 0.2,再比较各掩码的概率。切换评估模式,有效 p=0,权重保持原样。

本例只讨论固定权重上的独立 dropout。它不模拟训练、梯度或训练效果,也不把 dropout 当作因果遮挡。原先为 0 的权重仍为 0。本例排除训练 p=1,因为这里使用的 1/(1−p) 公式会除零;这是教学范围限制,不是声称 PyTorch 不接受 p=1。

不操作也能读懂:保留时的放大,抵消了丢弃造成的期望损失。要检查 softmax 行和,应在 dropout 前检查;评估模式则关闭 dropout。

算法语义依据:PyTorch nn.Dropout 官方文档。掩码枚举与教学数字由本例提供。

多头张量:换了轴,元素去了哪里 ​

MULTI-HEAD · 跟踪每一个元素

拆成多个头以后,同一个元素去了哪里?

给投影后的张量每个元素一个唯一编号,再跟着它走过 reshape、transpose 和合并。编号只用来识别元素,不是 Q、K、V 的实际数值。本例用 D 表示总投影宽度,对应正文的 Dₒ。

01

设定形状,选择跟踪对象

教学规模:B≤2、T≤4、D≤12、H≤4,且 D 必须能被 H 整除。所有下标从 0 开始。

02

五步布局,编号始终不变

第 1 步:投影后的张量。轴 [B, T, D],形状 [1, 3, 4]。元素 e2 现在的坐标是 [0, 0, 2]。 它原来属于批次 0、位置 0、特征 2。

当前形状 · [B, T, D][1, 3, 4]轴的顺序是布局的一部分
每头宽度 · d=D/H2总宽度均分到各头
唯一元素总数12每一步保持数量和身份

e2:原坐标 [b,t,c] = [0,0,2];h = floor(c/d) = 1,r = c mod d = 0。

张量可左右滑动;键盘聚焦后用左右方向键查看。双线框标记当前跟踪元素。

按 B → T → D 的顺序展示;每组固定前 2 个下标,最后一维从 0 排列。可点任意编号跟踪它。

[0, 0, :]

[0, 1, :]

[0, 2, :]

共 12 个唯一元素;每一步数量相同,身份没有丢失。这是逻辑坐标展示,不承诺实际内存连续,也不表示发生了数据复制。

试一试:形状相同,元素就一定相同吗?

默认 [B,T,D]=[1,3,4]、H=2、d=2。e2 的坐标依次是 [0,0,2] → [0,0,1,0] → [0,1,0,0] → [0,0,1,0] → [0,0,2]。比较中间两轴,就能看到 T、H 的交换与还原。

合并阶段打开错误对照:e2 会错到 [0,1,2];点选 e6,则会看到它从正确坐标 [0,1,2] 错到 [0,2,0]。再把 D 改为 5,会因无法均分成两个头而停止计算。这里未模拟 batch 广播、不同 Q/K/V 宽度、缓存或 GPU 性能。

不操作也能读懂:reshape 把特征下标 c 拆成 h 和 r;transpose 交换 T、H 两个轴。合并前必须先换回,才能把同一位置的各头拼接起来。

操作语义依据:PyTorch 官方 reshape 与 transpose。reshape 可能返回视图或副本,不应依赖其中一种;本例只检验元素的逻辑顺序。

SEO:配置的信号,爬虫读到了吗 ​

SEO 实验室 · 抓取与索引信号

配置了,不等于爬虫读到了

先禁止抓取,再加 noindex,能让页面从搜索结果消失吗?亲手改一项设置,沿着证据找答案。

学习目标:分清页面设置、爬虫读到的信号和真实索引状态。这是有限教学案例,不会请求真实网站,也不预测收录。

01 选择一个起点

切换案例会重新演练

标签写了,但抓取入口被挡住。

只放开抓取,观察 noindex 能否被读到。

正常公开页面,也不等于已经收录。

声明原文偏好,最终选择仍待验证。

02 输入 · 页面设置

这些是你配置的内容,不代表爬虫已经取得。

“对应原文”假设两页确实重复,目标正常公开且没有 noindex。这里不比较两页质量。

03 过程 · 逐步取证

每次只推进一步,看哪些信息真正变成了证据。

步骤 0 / 4准备开始
  1. 设置当前
  2. 发现待检查
  3. 抓取尝试待检查
  4. 读取信号待检查
  5. 判断边界待检查

先预测:你配置的信号,爬虫这次能读到哪些?逐步按钮只推进这个假设案例,不请求任何真实网站。

到这一步,实际取得了什么?

未读取的设置,不计入已知证据。

已知 URL
尚未进入发现步骤
抓取许可
尚未检查
实际取得的 HTTP 状态
尚未请求
noindex
尚未检查
canonical
尚未检查
Google 选定的规范版本
未知,本实验不作选择
真实索引状态
未知,需核对实际记录

04 结果 · 证据能说明什么

走完取证步骤后,这里会给出本次判断,以及还需要核对的证据。

记住这三个区别

robots.txt 管抓取;noindex 要先被读到,才表达排除索引的要求;canonical 为重复页面表达首选地址。正常返回 200、没有禁止标记,仍不保证收录。

动手比较:只改一项,为什么结果就变了?
  1. 只放开抓取。先用默认“拦抓+noindex”走到最后,再把抓取许可改成“允许”。noindex 没变,变化的是爬虫终于能读到它。修改设置会暂停播放并从头开始。
  2. 比较两个标签。选“重复版本指向原文”,走到最后。规范版本为什么仍是未知?因为 canonical 只是偏好。再勾选 noindex,观察两种不同目的的设置。
  3. 区分两类错误。把 HTTP 从 200 改成 503,再改成 404。暂时服务故障与资源不存在,需要不同的修复证据。
前提与不模拟的部分

假设 URL 已被发现,robots.txt 文件有效且适用规则已经解析,允许抓取时安排这一次假设请求。200 表示真实、完整、受支持格式的公开 HTML,不是软 404。noindex 与 canonical 均在有效 HTML 中,不依赖 JavaScript。

步骤顺序只用于解释依赖,不代表搜索引擎内部严格串行运行。本实验不解析 robots 路径、请求头冲突、重定向、登录、缓存、渲染、站点地图、抓取预算、内容质量或历史状态。真实系统可能重试、并行或使用旧记录;“已读取”只描述案例,不代表线上验证。

robots.txt 与 noindex 都不保护私人内容。需要保密时,应先设置身份验证与访问控制,不应为了让爬虫读标签而公开敏感资料。

机制依据:Google 官方文档

文档核对:2026-10-14。步骤与预设为教学案例,不是 Google 内部算法。

Go context:谁收到取消,谁仍可继续 ​

GO 实验室 · CONTEXT

取消一个分支,谁会收到信号?

点一下数据库的“取消”,看看内部等待会不会一起变化,再对比父级和 HTTP。

这是虚拟演示,没有运行 Go。它只展示退出信号,不判断任务是否真的结束。

01 输入 · 设置同一条请求的预算

切换案例或修改预算会从头开始。所有节点都在 t=0 创建,单位为 ms,0 表示立即到期。

02 点一下,看信号传到哪里

时刻 0 ms未关闭 4已关闭 0
请求父级

○ Done 未关闭截止 400 ms

├ 数据库

○ Done 未关闭截止 150 ms

↳ 内部等待随数据库

○ Done 未关闭截止 150 ms

├ HTTP

○ Done 未关闭截止 300 ms

先试着取消数据库,看看哪些节点会跟着变。

取消只向下传,不影响父级或兄弟。已关闭表示收到退出信号,不是任务完成。

时间怎样推进?

每步前进 50 ms;途中到期的节点按各自 deadline 结算。取消不推进时间,也会暂停自动演练。内部等待由数据库用 WithCancel 派生,继承它的 deadline。

03 想看具体数值?展开核对

上面看变化,下面查每个节点的 deadline、Err 和信号时刻。

请求父级 · Err = nil

派生自:Background(根,不参与计数)

请求父级
○ 未收到退出信号
申请的截止时间
t=400 ms
有效 deadline
t=400 ms
更早约束来自
请求父级
Done()
未关闭
Err()
nil
信号发生时刻
尚未发生

未取消,也不代表任务仍在运行。

数据库分支 · Err = nil

派生自:请求父级

数据库分支
○ 未收到退出信号
申请的截止时间
t=150 ms
有效 deadline
t=150 ms
更早约束来自
数据库分支
Done()
未关闭
Err()
nil
信号发生时刻
尚未发生

未取消,也不代表任务仍在运行。

数据库内部等待 · Err = nil

派生自:数据库分支

数据库内部等待
○ 未收到退出信号
申请的截止时间
不另设,继承父级
有效 deadline
t=150 ms
更早约束来自
数据库分支
Done()
未关闭
Err()
nil
信号发生时刻
尚未发生

未取消,也不代表任务仍在运行。

HTTP 分支 · Err = nil

派生自:请求父级

HTTP 分支
○ 未收到退出信号
申请的截止时间
t=300 ms
有效 deadline
t=300 ms
更早约束来自
HTTP 分支
Done()
未关闭
Err()
nil
信号发生时刻
尚未发生

未取消,也不代表任务仍在运行。

查看本轮操作记录(1 条)
  1. 操作 0 · 所有节点在 t=0 创建。先预测:数据库先到期时,父级和 HTTP 的 Done 会关闭吗?

04 接下来可以这样试

先取消数据库

选“先取消一个分支”,点击“取消数据库分支”。内部等待收到信号,父级与 HTTP 不变;再取消父级,比较其余节点。

给子级更多时间

选“父级预算更早”。数据库申请 400 ms,实际 deadline 仍是 150 ms。把父预算改为 175,再逐步看精确的信号时刻。

比较取消与到期

同一节点先手动取消,Err 是 Canceled;先到期则是 DeadlineExceeded。已经确定的 Err 不会被后续取消改写。

取消不等于等待 goroutine 结束(join)。CancelFunc 不等待任务退出;代码仍需响应 Done,并用 WaitGroup、完成 channel 等确认结束。树中的四个 context 也不等于四个 goroutine。

这个模型简化了什么?

所有预算都从同一个虚拟 t=0 起算。现实中 WithTimeout 从各自创建时刻起算;这里为了直接比较,更像给每个节点设置相对同一起点的 WithDeadline。更改输入会重建整棵树,不会修改已创建 context 的 deadline。

每一步展示取消传播完成后的状态。真实 Done 的关闭可能在 cancel 返回后异步发生,计时器与 goroutine 也有调度延迟;这里的精确虚拟时刻不是实时性保证。相同 deadline 的节点按父级在前显示,不代表运行顺序。

手动取消不删除既有 deadline,Err 一旦非 nil 就保持不变。本实验不运行任务,不判断 I/O 是否停止、是否有副作用,也不统计资源是否清理完成。真实代码仍应接住并调用每个派生 context 的 cancel。

机制依据:Go 官方文档

文档核对:2026-10-14。页面用 JavaScript 演示有限规则;Node 测试验证教学模型,不等于真实 Go 执行。

Go atomic:操作都原子,为什么仍会超卖 ​

GO ATOMIC · 一件商品,两个请求

每次读写都是原子的,为什么还会超卖?

请求 A、B 各想预留一件商品。你决定谁先执行下一步,观察“检查有库存”和“真正扣减”之间的空隙,再用 CAS 循环比较同一次交错。

01

输入 · 选实现,再设初始库存

要守住的约束成功预留数 ≤ 初始库存等价地,共享库存始终 ≥ 0。单次 Add 原子性不等于完整预留操作的原子性。
保留当前实现与有效库存,清空事件和请求状态。
从一个明确的交错开始
02

过程 · 选择下一个请求

第 0 步 · 初始库存为 1。两个请求都还没读取;选一个请求推进。

当前共享库存 stock1只有 Add 或成功的 CAS 会改变它
请求 A
尚未返回
  1. 读取n = stock.Load()
  2. 检查n > 0?
  3. 扣减Add(−1)
  4. 返回true / false
本请求保存的局部 n尚未读取共享库存变了,这个局部值不会自动跟着变。
最近一次检查
待检查
已尝试扣减
0 次

当前 / 下一步Load 读取库存

请求 B
尚未返回
  1. 读取n = stock.Load()
  2. 检查n > 0?
  3. 扣减Add(−1)
  4. 返回true / false
本请求保存的局部 n尚未读取共享库存变了,这个局部值不会自动跟着变。
最近一次检查
待检查
已尝试扣减
0 次

当前 / 下一步Load 读取库存

每次只推进所选请求的一步。Load 和 Add / CAS 各自是原子操作;中间的检查只读本请求的局部 n。按 Tab 选按钮,Enter 或空格执行。

事件日志 · 0 步,按实际点击顺序

尚未执行。预设自动完成的步骤也会完整显示在这里。

03

结果 · 没有 data race,也可能做错生意

进行中:尚未超卖还有 2 个请求没结束

成功预留0 / 2预留操作最终返回 true 的请求数
拒绝预留0 / 2重新检查到无库存才返回 false;CAS 失败本身不算拒绝
超卖件数0成功预留数超出初始库存的部分

试试“交错:都通过检查”,再分别推进 A、B 扣减。 对比“串行”预设:一次安全结果不能证明所有交错都安全。

模型边界与对应的 Go 写法

这是确定性的教学交错,浏览器没有运行 Go。按钮指定了一个合法的操作顺序;预设也按写明的顺序执行。它不模拟运行时调度概率、CPU 耗时或吞吐量。

拆开模式对应先读取 n := stock.Load(),检查 n > 0,再执行 stock.Add(-1)。CAS 模式对应循环读取、检查,并调用 stock.CompareAndSwap(n, n-1);只有 CAS 成功才返回 true,CAS 失败回到循环开头。

这里的共享库存访问全部对应 atomic.Int64 操作,局部 n 属于各自请求。data race 与业务竞态是不同问题;race 检测不负责报告这种超卖。涉及库存、订单和预留量等多个字段时,通常应考虑用同一把锁保护完整约束。这个实验不证明任意无锁算法都正确或更快。

不操作也能读懂:库存 1,A、B 都先读到 1;拆开扣减会得到 −1。换成 CAS,一个成功后,另一个 CAS 失败,重读到 0,再检查并拒绝,最终库存为 0。

依据:Go 内存模型:原子操作;sync/atomic:CompareAndSwap(核对日期:2026-10-14)。

训练目标:这一格究竟在预测谁 ​

数据实验室 · 标签与损失

下一格是目标,这一格该不该计分?

保留提示、回答和补齐的角色,逐格对齐输入与目标,再算真正参与平均的损失。EOS 与 PAD 即使同 ID,也不该一起删掉。

01

输入 · 先分清角色

勾选:都用 4。取消:EOS 用 4,PAD 用 0。词表始终保留 8 个类别。
0–19。在 X/Y 对齐后,从左侧保留相同数量的位置。

教学词表:问=2、答=3、好=5、吗=6、是=7;空白只用于分隔。这里按字查表,不运行 BPE 或聊天模板。BOS、EOS 与 PAD 由页面添加。

02

过程 · 角色跟着目标走

步骤 1 / 4

先保留角色和真实位置。EOS 与 PAD 都用 ID 4,但只有 EOS 是真实结束标记;这一步还没有移位。

token 和数字保持完整。窄屏可在各区域左右滑动,或聚焦后用方向键查看。

  1. 原位置 0BOSID 1开始源 mask 0
  2. 原位置 1问ID 2提示源 mask 0
  3. 原位置 2答ID 3回答源 mask 1
  4. 原位置 3EOSID 4真实结束源 mask 1
  5. 原位置 4PADID 4右补齐源 mask 0

上方展示截断前的原序列。源 mask 先按角色和监督范围建立;目标 mask 取它的下一格,并跟 X/Y 一起截断。错误对照会改用 ID 判断补齐。

X = [1, 2, 3, 4]

Y = [2, 3, 4, 4]

labels = [-100, 3, 4, -100]

loss mask = [0, 1, 1, 0]

输入有效位置 = [1, 1, 1, 1]

固定 logits → softmax → 目标概率 → −ln p。自然对数,数值显示 6 位小数。
位置 j输入 X[j]目标 Y[j]目标角色labels[j]mask目标概率 p原始 −ln p计入损失原因
0BOS · 1问 · 2提示-10000.5135190.6664680.000000提示不计分
1问 · 2答 · 3回答310.2797081.2740091.274009计入目标
2答 · 3EOS · 4真实结束410.0499302.9971302.997130监督真实 EOS
3EOS · 4PAD · 4右补齐-10000.1250002.0794420.000000补齐不计分

看最后一个真实 token 的边界:X 仍可能是 EOS,但它要预测的 Y 已经是 PAD。判断这一项要不要计分,必须看目标位置。

03

指标 · 只除以有效目标数

有效目标数2分母,不含 −100 位置
有效损失和4.271139每个有效目标的 −ln p 相加
平均 token 损失2.135569损失和 ÷ 有效目标数

L = 4.271139 / 2 = 2.135569

两种规则使用同一组目标和 logits
忽略规则有效目标数真实 EOS 数损失和均值
按真实补齐位置214.2711392.135569
按 PAD ID 全部忽略101.2740091.274009

按 PAD ID 忽略会漏掉 1 个真实 EOS。即使平均损失更小,也只是少评分了一个目标,不能据此说模型更会回答。

04

解释 · 不计分,仍能作为上下文

loss mask 控制哪些输出位置计入损失;attention mask 控制能读取哪些输入。提示标签设成 −100,不会从输入中删去提示,回答仍可依赖它。上面的“输入有效位置”只标出真实输入与补齐,因果注意力还会限制读取未来位置。

−100 只表示“这个标签不计分”,不是词表 ID,不能送进 embedding。这里的 X 始终是非负的词表 ID。

这些损失是怎么算出来的?

页面使用 8 类固定 logits。四行按预测位置 j 循环使用,先做 softmax,再取真实目标的概率并计算 −ln p;没有训练模型,也没有根据文本生成预测。这组数只用来核对移位、掩码和分母,不表示语言能力。

下面是未经 softmax 的分数,不是概率;未做类别加权或标签平滑。PyTorch 的 CrossEntropyLoss 接受 logits,本例的手算与默认类别索引目标的计算口径对应。

类别轴始终有 8 项。共用 ID 时,PAD 也使用类别 4。
j mod 40 · PAD(独立 ID)1 · BOS2 · 问3 · 答4 · EOS5 · 好6 · 吗7 · 是
000200000
100010000
20000-1000
300000000
和框架接口怎样对应?

本实验显式构造 X=ids[:-1]、Y=ids[1:],用于展示原理。Hugging Face GPT2LMHeadModel 的 labels 会在模型内部移位,调用时要遵守它的接口约定。本页不调用模型,也不把这里的已移位数组当作它的调用示例。

截断在对齐后同时裁剪 X、Y 和 mask。真实角色由构造时的元数据保留,不从 token 文本中猜。真实数据还需要正确的聊天模板、分词与批处理;本实验没有覆盖完整训练流程。

不操作也能读懂:原序列 [BOS, 问, 答, EOS, PAD] 的 ID 是 [1, 2, 3, 4, 4]。只训练回答时,移位后的 labels 为 [−100, 3, 4, −100]。提示“问”作为输入保留;回答“答”和真实 EOS 共两个目标参与平均。

依据:PyTorch 2.6.0:CrossEntropyLoss;Transformers 4.57.1:GPT-2 labels 接口。核对于 2026-10-14。

批量生成:一行结束,整批要停吗 ​

GENERATION · 逐行停止

一行结束了,其他行还要继续吗?

为每行写好候选 token,逐步检查 finished、EOS 和 PAD。右侧的错误做法遇到任意一行 EOS 就停,看看它截断了谁。

这是独立教学扩展。原书第 14.5 节的代码只支持一条提示,且返回值不含新 EOS;本实验使用另一种明确协议:保留首次 EOS,再给已结束行补 PAD。页面没有运行语言模型。

01 输入 · 给每行安排候选

切换案例或修改输入会从第 0 步开始。

固定提示:每行非空,输入里的 EOS 不算生成结束

提示已经正确左填充,attention mask 已给定。这里只检查后续停止协议,不改变提示布局。

请求提示 tokenattention mask初始 finished
行 1PADPADq[0, 0, 1]false
行 2PADEOSq[0, 1, 1]false
行 3PADrs[0, 1, 1]false

02 过程 · 先补齐,再更新停止状态

  1. 检查步前的 finished,已结束行把本步候选换成 PAD。
  2. 把这一列追加到所有行。新生成的 EOS 也保留。
  3. 遇到 EOS 或达到长度上限的行设 finished;全部 finished 才停。
行 1 · 继续生成还没追加 token;提示中的 EOS 不触发停止。
行 2 · 继续生成还没追加 token;提示中的 EOS 不触发停止。
行 3 · 继续生成还没追加 token;提示中的 EOS 不触发停止。

还没有生成新 token。固定提示都非空;提示中的 EOS 和 PAD 不会触发本轮停止。

查看整批计数
已追加的新 token 列数0上限 3,不含提示
finished 为 true 的行0共 3 行,结束原因见下方
整批状态继续生成已结束行不会恢复生成
查看逐步记录(0 步)

宽表可在框内横向滚动;用 Tab 聚焦后,可按左右方向键查看完整 token。

每一列都按行 1 → 行 3 排列
步骤本步候选步前 finished实际追加步后 finished全批结束?
第 0 步:还没有追加新 token

03 结果 · 矩形缓冲区和有效回答分开看

下面只展示返回缓冲区的新生成部分;固定提示保留在前面。每行列数相同,包含首次 EOS;它之后的 PAD 只负责补齐。下方有效回答取首次 EOS 前的生成片段。

正确:逐行停止

第 0 步 · 继续生成

请求新生成 token
行 1尚未生成
行 2尚未生成
行 3尚未生成
错误:一行 EOS 就停整批

第 0 步 · 继续生成

请求新生成 token
行 1尚未生成
行 2尚未生成
行 3尚未生成

等待第一个新生成的 EOS。

token 右下角是 ID。EOS 首次结束符;PAD 已结束后的补齐。

行 1
finished
false
有效回答(token)
尚未生成
结束原因
尚未停止
停止步骤
尚未停止
行 2
finished
false
有效回答(token)
尚未生成
结束原因
尚未停止
停止步骤
尚未停止
行 3
finished
false
有效回答(token)
尚未生成
结束原因
尚未停止
停止步骤
尚未停止

04 解释 · 别把填充当成新回答

先试“逐行遇到 EOS”:候选按列是 [EOS, x, y]、[a, EOS, z]、[a, b, EOS],实际追加会变成 [EOS, x, y]、[PAD, EOS, z]、[PAD, PAD, EOS]。行 1 后面的 a 不会让它复活。

再试“先到长度上限”:两步后,行 3 的有效回答是 y z,结束原因为长度上限。没有生成的 EOS 不能补算进去;到上限也不需要额外追加一列 PAD。

PAD 和 EOS 的 ID 相同,为什么还要分角色?

是否补 PAD 由步前 finished 决定。未结束行选到 EOS 的 ID 时,当前位置是首次结束符;已结束行追加相同 ID 时,当前位置只是补齐。仅凭最后返回的数字,不能替代位置与状态记录。

如果手动把 PAD 写进未结束行的候选,PAD 与 EOS 不同 ID 时,它不会触发 EOS 停止,也仍属于生成片段;共用 ID 时,它就是 EOS,会触发停止。这与提示中已有的 EOS、PAD 不同:提示不会被当作本轮新生成的结束符。本实验显示 token 片段,不模拟 tokenizer 如何隐藏特殊 token。

机制依据与测试边界

本实验选用 Transformers 4.57.1 的逐行停止语义,限定为普通逐 token 生成、EOS 和最大新 token 数两个停止条件。每行 finished 表示后续不再生成有效 token,可以由 EOS 或长度上限触发。

核对日期:2026-10-14。网页是 JavaScript 教学模型;仓库另有真实 Transformers generate 的 CPU 对照测试,使用随机初始化的小模型和强制候选,不下载权重或语料,也不评估文本质量。测试是否通过,以对应提交的运行记录为准。

Go select:现在有哪些分支可以执行 ​

GO SELECT · 先看集合,再选分支

数据和取消都到了,select 会选谁?

先看哪些接收现在能进行,再亲手选择一个合法分支。试试读完缓冲、关闭 channel 和赋 nil,观察就绪集合怎样改变。

01

配置 · 每次修改都重建本轮

非 nil 时是容量 1 的 chan int。
nil 与“打开但为空”是不同状态。

初始值范围 −999~999。修改配置会创建新 channel 并清空记录;已关闭的 channel 不能重新打开。

保留初始配置,清空真实步骤记录
02

选择 · 集合里有什么,才可以做什么

就绪通信集合A、Done只含 A、B、Done;∅ 表示空集
本次可选分支A、Donedefault 不是通信分支
select 状态可选分支查看集合不会执行或消耗数据

A、Done 同时就绪,任选其中一条都合法。书写顺序不决定优先级,Done 也不会自动抢先。

Tab 选择按钮,Enter 或空格执行;一次只完成一个分支。
为什么不自动随机选择?

Go 规范要求从能进行的通信中均匀伪随机地选择一个。本例让你指定一个合法结果,便于对照;点击次数不代表随机频率、有限时间内的公平性或 CPU 耗时。

03

改变条件 · 让等待继续,或停用一个输入

A · 外部事件
B · 外部事件
Done · 已关闭

关闭会让取消分支就绪;不强制替你选择它,也不自动结束本例。

送入值、关闭和触发 Done 都代表其他 G 的外部事件。赋 nil 是接收方在两次 select 之间执行的赋值,不是关闭原 channel。

等待中为什么不能赋 nil?

进入 select 时,各接收分支的 channel 操作数已经求值。本例不让正在等待的同一个 G 跳到后面的赋值;先让本次接收完成,再把变量赋 nil,影响下一次 select。这里没有其他接收者抢走数据。

04

结果 · 哪个分支执行,收到了什么?

第 0 步:尚未执行。先看上方就绪集合,再选择一个合法分支。

下方保留最近一次已完成的 select 结果;外部事件或赋 nil 不会冒充一次接收。

最近执行分支尚未执行已完成 0 次 select
接收值 · value—default 没有接收值
是否来自一次发送 · ok—关闭且空才返回 false
实际动作步数0含外部事件、进入等待和赋 nil
真实步骤日志 · 0 步,按实际操作顺序

还没有操作;就绪预览和预设配置不计步骤。

模型边界与不操作时的结论

这是 JavaScript 接收分支教学模型,浏览器没有运行 Go。A、B 各自最多缓冲一个 int,Done 是只关闭、不发送值的 chan struct{}。这里只展示通信是否能进行,不模拟发送表达式求值、超时或完整运行时调度。

nil 不会就绪;打开且空需要外部事件;关闭后先读完缓冲,再重复得到 0、false。无通信就绪时,有 default 就继续,没有就等待。多个通信同时就绪时,只执行其中一个,取消分支没有书写优先权。

依据:Go 规范:Select statements;Receive operator(核对日期:2026-10-14)。

梯度累积:每个微批次应该有多大权重 ​

训练实验室 · 有效 token 与梯度累积

一批只有一个目标,另一批有三个,该各占一半吗?

把同一组有效目标拆成两个 microbatch(微批次),比较按 token 加权与按批次等权的损失、梯度和一次更新。默认例中,两种算法会把参数推向相反方向。

01

输入 · 改变有效目标数

0–8 个有效目标;每个目标都使用这个 y 值。
0–8 个有效目标;不计补齐或已被 mask 忽略的位置。

模型很小:所有输入 x=1,所以预测值是 w。每个有效目标使用半平方误差 ℓ=½(w−y)²,梯度是 w−y。这是可手算的标量例子,不是语言模型的交叉熵。

02

过程 · 先累加,最后更新

步骤 1 / 4 · 按有效 token 加权

这一轮共有 4 个有效目标、2 个非空微批次。有效 token 数决定每个微批次在整体目标中的权重。

数字显示 6 位小数,计算使用未舍入值。各表与公式可左右滑动;键盘聚焦后可用方向键查看。

微批次 A · 1 个有效目标
  1. A1y=2.000000预测=0.000000
微批次 B · 3 个有效目标
  1. B1y=-1.000000预测=0.000000
  2. B2y=-1.000000预测=0.000000
  3. B3y=-1.000000预测=0.000000
每批内部的 loss sum、count、mean 与梯度
微批次有效数 n损失和 S均值 S/n梯度和均值梯度
A12.0000002.000000-2.000000-2.000000
B31.5000000.5000003.0000001.000000

token 加权:L = Σ Sᵣ / N = Σ (nᵣ / N) × Lᵣ

非空批等权:L_equal = Σ Lᵣ / K_nonempty

逐 token 梯度:∂ℓ/∂w = w − y

N 是这一轮所有有效目标的总数;K_nonempty 只数非空微批次。求和中的 Lᵣ 只对非空批定义。空批既不计算 0/0,也不作为“零均值”加入等权平均。

固定 w 不变:每批贡献先按权重缩放,再加进累积梯度
微批次token 权重 n/N加权梯度贡献加权累计非空批等权等权梯度贡献等权累计
A0.250000-0.500000-0.5000000.500000-1.000000-1.000000
B0.7500000.7500000.2500000.5000000.500000-0.500000
03

指标 · 与同一个大 batch 对照

当前查看:按有效 token 加权。所有损失与梯度均在更新前的同一个 w 处计算。

有效目标总数 N42 个非空微批次
所有目标的损失和3.500000在两条路线中都使用同一组目标
当前路线的平均损失0.875000先核对分母与权重,再比较数字
当前路线的累积梯度 g0.250000梯度下降沿 −g 方向更新
一次更新后的 w-0.025000w_new = w − η × g
两条路线的梯度差0.750000绝对差;不能用 loss 较小代替检查

w_new = 0.000000 − 0.100000 × (0.250000) = -0.025000

相同目标、相同参数点、相同学习率,只比较一轮一次更新
路线平均损失梯度更新后的 w
合并为一个大 batch0.8750000.250000-0.025000
按有效 token 加权0.8750000.250000-0.025000
微批次均值等权1.250000-0.5000000.050000

按有效 token 加权与合并整批一致;微批次均值等权改变了目标权重。短批中的单个 token 可能因此占更大比重。

04

解释 · 累积的应是同一个目标

若目标是“所有有效 token 等权”,就应把各批损失和相加,再除以全轮有效目标数。等价做法是:每个非空批的均值乘以 n/N,再反向传播。只除以微批次数,通常得到另一个加权目标。

本实验在开始时清零累积梯度;两批都使用同一个 w,没有在中途更新参数。只有所有贡献收齐后,才用同一学习率做一次普通 SGD。逐批先更新再计算下一批,已经改变了求导点,不能拿来验证这里的等价关系。

什么时候可以与一个大 batch 对得上?

这里固定有效目标与逐目标损失,没有 dropout、跨样本统计、权重衰减或随机抽样。线性求和让按 token 加权的梯度与合并整批一致,实际浮点加法顺序仍可能带来微小差异。页面用解析梯度,CPU 契约测试另用 PyTorch autograd 核对。

同样的加权原则也适用于逐 token 交叉熵,但这不意味着本页运行了语言模型训练。这里不模拟分布式通信、混合精度、梯度裁剪或调度器,也不处理完整训练循环的尾批状态。

不操作也能读懂:w=0 时,A 的 1 个目标 y=2,损失和为 2、梯度和为 −2;B 的 3 个目标 y=−1,损失和为 1.5、梯度和为 3。整体梯度为 (−2+3)/4=0.25,批均值等权却为 (−2+1)/2=−0.5。学习率 0.1 时,一次更新分别得到 −0.025 与 0.05。

依据:Accelerate 1.10.1:变长 token 的梯度累积;PyTorch 2.6.0:backward 的梯度累加。核对于 2026-10-14。

Padding:最后一个真实输入在哪一列 ​

生成输入 · 位置与数量

有两个有效 token,为什么要读第 3 列?

改动补齐位置,对比三种取列规则。先找最后一个真实输入,再观察它对应的整条 logits;不要把有效个数当成列号。

01

输入 · 数量相同,位置可以不同

02

过程 · 先比较列号,再读取分数

步骤 1 / 4

列号从 0 开始。1 是真实输入位置,0 是补齐;数量与位置是两种信息。

列号和分数不拆行。窄屏可横向滑动表格,或聚焦后按方向键。

完整矩形布局 · 每行 4 列
请求行列 0列 1列 2列 3有效个数
行 10补齐位置0补齐位置1真实位置1真实位置最后有效✓ 当前取列2
行 21真实位置1真实位置1真实位置1真实位置最后有效✓ 当前取列4
这里只比较索引,不比较语言质量
行最右有效列个数减一末列
13 · 取列一致1 · 错取补齐3 · 取列一致
23 · 取列一致3 · 取列一致3 · 取列一致
03

结果 · mask 不会替你改索引

非空请求行2存在至少一个真实位置
取列一致的行2 / 2当前规则与最右有效列一致
拒绝的空行0没有 logits 的正确读取位置
行 1

取列一致:读取第 3 列,这是一个真实输入位置。

logits = [-2, -2, -2, 2, -2, -2, -2, -2]

贪心候选 ID:3。这里只让每列偏好不同的 ID,便于看清读错列的后果。

行 2

取列一致:读取第 3 列,这是一个真实输入位置。

logits = [-2, -2, -2, 2, -2, -2, -2, -2]

贪心候选 ID:3。这里只让每列偏好不同的 ID,便于看清读错列的后果。

04

解释 · 找对列,只解决这一步

mask [0, 0, 1, 1] 的有效个数是 2,个数减一得到 1;但最右边的 1 在第 3 列。右补齐 [1, 1, 0, 0] 恰好符合“个数减一”的前提,不能把这个公式搬到左补齐。

本例固定 8 个候选,每列 j 只有候选 j 的 logit 为 2,其余为 −2。分数是教学数据,不是模型预测;attention mask 表示可用输入位置,不会替调用者把 logits 的取列操作改正确。

与真实 generate 的关系

Transformers 4.57.1 的普通生成路径读取 logits 的最后一列。因此非空、标准左补齐可让每行最后列都是最后真实输入。仅改成“最右有效列”不等于已修复右补齐的整个生成循环;还要遵守模型的位置编码、后续 mask 与缓存约定。本例不模拟这些部分。

空行、缺口与负索引

全空行的最大有效位置不存在。PyTorch 普通负索引 −1 会指向末列,并不会自动表达“无结果”;本实验先拒绝空行,再读取任何分数。中间缺口预设只检验位置算法,不推荐把任意稀疏 mask 当成合法的生成提示。

不操作也能读懂:默认第一行最右有效列为 3;“有效个数减一”错取第 1 列,“直接取末列”在此左补齐前提下取对。第二行没有补齐,三条规则都取第 3 列。

依据:Transformers 4.57.1 左补齐说明;固定版本生成实现。核对:2026-10-14。

延伸阅读 ​

250 章媒介适配初筛 · 模型边界与依据 · 验证记录

保留各书来源与许可 · 示例运行条件以正文为准