P0 ↔ M0
当前空闲本地运行队列
- G1
- G2
- G3
- G4
这些演示都在浏览器本地运行,不连接付费 API。修改输入,再逐步观察状态与结果。有播放功能的示例可以暂停;每个示例都能重置。键盘可以使用 Tab 移动、Enter 或空格操作按钮。页面不会自行开始播放,也不依赖动画才能理解。
这些是可检查的教学模型,不是实际语言模型推理或 Go runtime 性能实验。每个演示都标明假设、固定规则与适用边界。相关章节内也会出现同样的演示。
固定输入 3 个 token,再逐个处理 8 个已给定的 token。对比开关缓存后的计算量,再看 KV 头数怎样改变缓存占用。
这里只数 K/V 投影的位置数,不执行语言模型,也不测量速度。
固定条件:批大小 1、12 层、每头维度 64、每元素 2 字节。改变 KV 头数不会改变本例的位置计数。
本步计算 3 个位置的 K/V,复用 0 个旧位置。最新位置的 Q 仍要读取全部 3 个位置的 K/V,才能预测下一个 token。
位置卡可横向滚动;手动滚动会暂停播放。键盘聚焦下方区域后,用左右方向键查看。
试着在 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。本页为简化教学模型。
改一段文本,看相邻符号如何按固定顺序合并,再核对每个 token 的教学 ID。这是自定义字符级 BPE,不是 GPT 分词器,也不在输入文字上重新训练规则。
初始状态:按字符拆分 · 当前 18 个 token。
每张卡是完整 token;␠ 表示空格。可横向滚动,聚焦后用左右方向键查看。
从上往下应用,不按当前文本重新排序。每条规则从左到右合并所有不重叠的匹配;窄屏可横向滚动,聚焦后用左右方向键查看。
先猜一条规则能合并几处。默认文本里,第一次合并会同时处理三个 l + o。走到最后再核对:符号数量变少了,把所有 token 拼回去,原文没有改变。
基础词表按 a–z、空格、句点、逗号、感叹号、问号排序,ID 从 0 开始;新符号按合并表追加。␠ 只是空格的显示标记。
词表保留完整符号;键盘聚焦区域后,可用左右方向键查看。
本章若采用 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 词表摘取。
沿着同一段教学脚本,比较 channel 等待与阻塞系统调用。观察谁停下、谁接着执行,以及 G 被唤醒后去了哪里。
G 是 goroutine;M 是操作系统线程;P 持有执行 Go 代码所需的调度资源。
初始:P0 有 4 个可运行 G;P1 的队列为空;全局队列有 G5。
P0 / M0 · 执行 空闲
本地队列:G1、G2、G3、G4
P1 / M1 · 执行 空闲
本地队列:空
全局:G5
等 channel:无
阻塞的 M/G:无
空闲 M:无
已完成:无
阶段条可横向滚动;手动滚动会暂停播放。键盘聚焦下方区域后,用左右方向键查看完整顺序。
初始:P0 有 4 个可运行 G;P1 的队列为空;全局队列有 G5。
空
无
无
无
对照两种场景的“等待发生”这一步。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。每一步都按公式重新计算。
从 w = 4 开始,损失为 16,梯度为 8。
曲线保留可读尺寸。窄屏可左右滑动,手动滚动会暂停播放;键盘聚焦曲线区域后,用左右方向键查看坐标与轨迹。
指标保留完整数字;如数字较长,可左右滑动,或聚焦数字后用方向键查看。
这组参数说明什么?学习率在 0 与 0.5 之间:从同一侧逐步靠近目标。
保留完整数值。窄屏可左右滑动;键盘聚焦记录区域后,用左右方向键查看。
| 步数 | 参数 w | 梯度 | 损失 |
|---|---|---|---|
| 0 | 4 | 8 | 16 |
令误差 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。
滑块与精确输入控制同一个值。输入越界会报错,不会自动截断。
先给 a、b 赋值。中间节点还没计算,所有梯度暂时为 0。
每个梯度都针对最终输出 q;反向尚未结束时,贡献可能还没收齐。
输入 a、b 分叉为乘法 u 与加法 v 汇入输出 q
前向运算顺序:u → v → q;反向运算顺序:q → v → u。节点里的梯度始终指“最终输出 q 对此节点的导数”。反向结束前,它可能还没收齐所有贡献。
指标保留完整数字;如数字较长,可左右滑动,或聚焦数字后用方向键查看。
完成反向传播后,下面会显示输入梯度与中心差分的对照。
页面实际建立依赖图、做拓扑排序、计算前向值,再按逆拓扑顺序把“收到的梯度 × 局部导数”累加给输入。改变 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。
改一组 Q、K、V,追踪某个查询如何筛掉未来位置,再把可见的值混合成输出。这里实际计算单头注意力;不包含投影、多头或 dropout。
每行一个位置,空格或逗号分列,换行或分号分行。范围 −100~100,最多 4 × 4;数字只用于教学,不是模型的语义向量。
第 1 步:点积打分。当前查询 q1 = (0.0000, 1.0000)。 逐个计算 q 与每把键 k 的点积,得到匹配分数。
表格保留完整数值。窄屏可横向滚动;键盘聚焦表格区域后,用左右方向键查看。
| 键位置 | q · k | 缩放后 | 掩码后 | 权重 A |
|---|---|---|---|---|
| 0 | 0.0000 | 待计算 | 待计算 | 待计算 |
| 1 | 1.0000 | 待计算 | 待计算 | 待计算 |
| 2 | 1.0000 | 待计算 | 待计算 | 待计算 |
使用三位置因果例,跟踪位置 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。输入数字仅为本书教学示例。
使用自造 logits,直接计算温度与 top-k 后的概率,再让你指定的 u 落进累计概率区间。这里没有运行语言模型,也不调用随机数;相同输入和 u 一定得到相同结果。
当前使用预设参数;修改输入后可随时切回。
第 1 步:读取 logits。 logits 是未归一化分数,不是概率。最大项为 ID 3。
表格可左右滑动;键盘聚焦后用左右方向键查看。
| 候选 | logit | 稳定分数 (z−max z)/τ | 过滤前概率 | 过滤后概率 | 状态 |
|---|---|---|---|---|---|
| ID 0 | 0 | 待计算 | 待计算 | 待计算 | 待过滤 |
| ID 1 | 1 | 待计算 | 待计算 | 待计算 | 待过滤 |
| ID 2 | 2 | 待计算 | 待计算 | 待计算 | 待过滤 |
| ID 3 | 3 | 待计算 | 待计算 | 待计算 | 待过滤 |
本例 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 是本例明确约定。
让 G1~G4 共用一个 chan int。你决定哪个 G 先行动:观察值去了哪里、谁在等待,再试着关闭 channel。
修改容量会创建一个新 channel,并清空本轮记录;Go 中已有 channel 的容量不能修改。
已创建容量为 1 的 chan int。选择一个 G,尝试发送、接收或关闭。
打开 · 缓冲 空 / 容量 1
G1:可继续
G2:可继续
G3:可继续
G4:可继续
发送把值交给接收者或缓冲,无法交付时等待。接收取出下一个值,同时返回 ok;关闭会保留缓冲数据并唤醒等待者,重复关闭会 panic。
第 0 步:已创建容量为 1 的 chan int。选择一个 G,尝试发送、接收或关闭。
缓冲:左侧为队首,先入先出;右侧空位可以接纳新值。
没有发送者等待。
没有接收者等待。
还没有操作。阻塞操作会保留在这里;配对或关闭后,同一条记录会更新结果。
以下是操作指导,不会替你切换容量或执行动作。请先重建 channel,再逐步尝试。
容量 0:G1 发送 7;再选 G2 接收。
先观察 G1 阻塞,再观察两者配对,len 始终为 0。容量 1:G1 发送 7;G2 发送 8;G3 接收。
G3 得到 7,G2 的 8 才进入缓冲,缓冲仍满。容量 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,源码链接随版本更新)。
把整数键 (a,b) 按字典序排好:先比 a,a 相同才比 b。修改数据或条件,观察一个连续候选区间如何定位、怎样过滤,再与全表检查核对结果。
这是内存有序数组上的机制模拟,用二分查找展示边界定位;不是 B+ 树页访问或真实 MySQL 执行计划,不报告毫秒和加速倍数。
五个示例只修改查询条件,保留你编辑的数据。可继续微调下面的条件。
先按 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, +∞)。 ±∞ 是定位边界的标记,不是数据值。
用默认数据选“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 联合索引、多列索引的范围定位。二分步骤为教学模型自行实现。
softmax 后的一行权重原本和为 1。训练时,dropout 会独立丢弃其中的项,再把保留项乘以 1/(1−p)。换一个掩码,看看这一行的和怎样变化。
位从左到右对应权重 0、1、2……;1 表示保留,0 表示丢弃。这里手动选掩码,不调用随机数;依次查看掩码也不等于随机抽样。
第 1 步:检查原权重。 原权重之和为 1.000000。训练模式有效 p=0.5。
表格可左右滑动;键盘聚焦后用左右方向键查看。
| 位置 | 原权重 a | 掩码 m | m × a / (1−p) | 概率加权期望 |
|---|---|---|---|---|
| 0 | 0.200000 | 待选择 | 待计算 | 待计算 |
| 1 | 0.300000 | 待选择 | 待计算 | 待计算 |
| 2 | 0.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 官方文档。掩码枚举与教学数字由本例提供。
给投影后的张量每个元素一个唯一编号,再跟着它走过 reshape、transpose 和合并。编号只用来识别元素,不是 Q、K、V 的实际数值。本例用 D 表示总投影宽度,对应正文的 Dₒ。
教学规模:B≤2、T≤4、D≤12、H≤4,且 D 必须能被 H 整除。所有下标从 0 开始。
第 1 步:投影后的张量。轴 [B, T, D],形状 [1, 3, 4]。元素 e2 现在的坐标是 [0, 0, 2]。 它原来属于批次 0、位置 0、特征 2。
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 实验室 · 抓取与索引信号
先禁止抓取,再加 noindex,能让页面从搜索结果消失吗?亲手改一项设置,沿着证据找答案。
学习目标:分清页面设置、爬虫读到的信号和真实索引状态。这是有限教学案例,不会请求真实网站,也不预测收录。
标签写了,但抓取入口被挡住。
只放开抓取,观察 noindex 能否被读到。
正常公开页面,也不等于已经收录。
声明原文偏好,最终选择仍待验证。
每次只推进一步,看哪些信息真正变成了证据。
先预测:你配置的信号,爬虫这次能读到哪些?逐步按钮只推进这个假设案例,不请求任何真实网站。
未读取的设置,不计入已知证据。
走完取证步骤后,这里会给出本次判断,以及还需要核对的证据。
robots.txt 管抓取;noindex 要先被读到,才表达排除索引的要求;canonical 为重复页面表达首选地址。正常返回 200、没有禁止标记,仍不保证收录。
假设 URL 已被发现,robots.txt 文件有效且适用规则已经解析,允许抓取时安排这一次假设请求。200 表示真实、完整、受支持格式的公开 HTML,不是软 404。noindex 与 canonical 均在有效 HTML 中,不依赖 JavaScript。
步骤顺序只用于解释依赖,不代表搜索引擎内部严格串行运行。本实验不解析 robots 路径、请求头冲突、重定向、登录、缓存、渲染、站点地图、抓取预算、内容质量或历史状态。真实系统可能重试、并行或使用旧记录;“已读取”只描述案例,不代表线上验证。
robots.txt 与 noindex 都不保护私人内容。需要保密时,应先设置身份验证与访问控制,不应为了让爬虫读标签而公开敏感资料。
文档核对:2026-10-14。步骤与预设为教学案例,不是 Google 内部算法。
GO 实验室 · CONTEXT
点一下数据库的“取消”,看看内部等待会不会一起变化,再对比父级和 HTTP。
这是虚拟演示,没有运行 Go。它只展示退出信号,不判断任务是否真的结束。
切换案例或修改预算会从头开始。所有节点都在 t=0 创建,单位为 ms,0 表示立即到期。
○ Done 未关闭截止 400 ms
○ Done 未关闭截止 150 ms
○ Done 未关闭截止 150 ms
○ Done 未关闭截止 300 ms
先试着取消数据库,看看哪些节点会跟着变。
取消只向下传,不影响父级或兄弟。已关闭表示收到退出信号,不是任务完成。
每步前进 50 ms;途中到期的节点按各自 deadline 结算。取消不推进时间,也会暂停自动演练。内部等待由数据库用 WithCancel 派生,继承它的 deadline。
上面看变化,下面查每个节点的 deadline、Err 和信号时刻。
派生自:Background(根,不参与计数)
nil未取消,也不代表任务仍在运行。
派生自:请求父级
nil未取消,也不代表任务仍在运行。
派生自:数据库分支
nil未取消,也不代表任务仍在运行。
派生自:请求父级
nil未取消,也不代表任务仍在运行。
选“先取消一个分支”,点击“取消数据库分支”。内部等待收到信号,父级与 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。
文档核对:2026-10-14。页面用 JavaScript 演示有限规则;Node 测试验证教学模型,不等于真实 Go 执行。
请求 A、B 各想预留一件商品。你决定谁先执行下一步,观察“检查有库存”和“真正扣减”之间的空隙,再用 CAS 循环比较同一次交错。
第 0 步 · 初始库存为 1。两个请求都还没读取;选一个请求推进。
当前 / 下一步Load 读取库存
当前 / 下一步Load 读取库存
每次只推进所选请求的一步。Load 和 Add / CAS 各自是原子操作;中间的检查只读本请求的局部 n。按 Tab 选按钮,Enter 或空格执行。
尚未执行。预设自动完成的步骤也会完整显示在这里。
进行中:尚未超卖还有 2 个请求没结束
试试“交错:都通过检查”,再分别推进 A、B 扣减。 对比“串行”预设:一次安全结果不能证明所有交错都安全。
这是确定性的教学交错,浏览器没有运行 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,也不该一起删掉。
教学词表:问=2、答=3、好=5、吗=6、是=7;空白只用于分隔。这里按字查表,不运行 BPE 或聊天模板。BOS、EOS 与 PAD 由页面添加。
先保留角色和真实位置。EOS 与 PAD 都用 ID 4,但只有 EOS 是真实结束标记;这一步还没有移位。
token 和数字保持完整。窄屏可在各区域左右滑动,或聚焦后用方向键查看。
上方展示截断前的原序列。源 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]
| 位置 j | 输入 X[j] | 目标 Y[j] | 目标角色 | labels[j] | mask | 目标概率 p | 原始 −ln p | 计入损失 | 原因 |
|---|---|---|---|---|---|---|---|---|---|
| 0 | BOS · 1 | 问 · 2 | 提示 | -100 | 0 | 0.513519 | 0.666468 | 0.000000 | 提示不计分 |
| 1 | 问 · 2 | 答 · 3 | 回答 | 3 | 1 | 0.279708 | 1.274009 | 1.274009 | 计入目标 |
| 2 | 答 · 3 | EOS · 4 | 真实结束 | 4 | 1 | 0.049930 | 2.997130 | 2.997130 | 监督真实 EOS |
| 3 | EOS · 4 | PAD · 4 | 右补齐 | -100 | 0 | 0.125000 | 2.079442 | 0.000000 | 补齐不计分 |
看最后一个真实 token 的边界:X 仍可能是 EOS,但它要预测的 Y 已经是 PAD。判断这一项要不要计分,必须看目标位置。
L = 4.271139 / 2 = 2.135569
| 忽略规则 | 有效目标数 | 真实 EOS 数 | 损失和 | 均值 |
|---|---|---|---|---|
| 按真实补齐位置 | 2 | 1 | 4.271139 | 2.135569 |
| 按 PAD ID 全部忽略 | 1 | 0 | 1.274009 | 1.274009 |
按 PAD ID 忽略会漏掉 1 个真实 EOS。即使平均损失更小,也只是少评分了一个目标,不能据此说模型更会回答。
loss mask 控制哪些输出位置计入损失;attention mask 控制能读取哪些输入。提示标签设成 −100,不会从输入中删去提示,回答仍可依赖它。上面的“输入有效位置”只标出真实输入与补齐,因果注意力还会限制读取未来位置。
−100 只表示“这个标签不计分”,不是词表 ID,不能送进 embedding。这里的 X 始终是非负的词表 ID。
页面使用 8 类固定 logits。四行按预测位置 j 循环使用,先做 softmax,再取真实目标的概率并计算 −ln p;没有训练模型,也没有根据文本生成预测。这组数只用来核对移位、掩码和分母,不表示语言能力。
下面是未经 softmax 的分数,不是概率;未做类别加权或标签平滑。PyTorch 的 CrossEntropyLoss 接受 logits,本例的手算与默认类别索引目标的计算口径对应。
| j mod 4 | 0 · PAD(独立 ID) | 1 · BOS | 2 · 问 | 3 · 答 | 4 · EOS | 5 · 好 | 6 · 吗 | 7 · 是 |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 2 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | -1 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
本实验显式构造 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。页面没有运行语言模型。
切换案例或修改输入会从第 0 步开始。
提示已经正确左填充,attention mask 已给定。这里只检查后续停止协议,不改变提示布局。
| 请求 | 提示 token | attention mask | 初始 finished |
|---|---|---|---|
| 行 1 | PADPADq | [0, 0, 1] | false |
| 行 2 | PADEOSq | [0, 1, 1] | false |
| 行 3 | PADrs | [0, 1, 1] | false |
还没有生成新 token。固定提示都非空;提示中的 EOS 和 PAD 不会触发本轮停止。
宽表可在框内横向滚动;用 Tab 聚焦后,可按左右方向键查看完整 token。
| 步骤 | 本步候选 | 步前 finished | 实际追加 | 步后 finished | 全批结束? |
|---|---|---|---|---|---|
| 第 0 步:还没有追加新 token | |||||
下面只展示返回缓冲区的新生成部分;固定提示保留在前面。每行列数相同,包含首次 EOS;它之后的 PAD 只负责补齐。下方有效回答取首次 EOS 前的生成片段。
第 0 步 · 继续生成
| 请求 | 新生成 token |
|---|---|
| 行 1 | 尚未生成 |
| 行 2 | 尚未生成 |
| 行 3 | 尚未生成 |
第 0 步 · 继续生成
| 请求 | 新生成 token |
|---|---|
| 行 1 | 尚未生成 |
| 行 2 | 尚未生成 |
| 行 3 | 尚未生成 |
等待第一个新生成的 EOS。
token 右下角是 ID。EOS 首次结束符;PAD 已结束后的补齐。
先试“逐行遇到 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 由步前 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 对照测试,使用随机初始化的小模型和强制候选,不下载权重或语料,也不评估文本质量。测试是否通过,以对应提交的运行记录为准。
先看哪些接收现在能进行,再亲手选择一个合法分支。试试读完缓冲、关闭 channel 和赋 nil,观察就绪集合怎样改变。
A:缓冲里的值可以立即取出。 缓冲:7。修改后已重建。
B:nil 的通信永远不能进行。 缓冲:空。修改后已重建。
初始值范围 −999~999。修改配置会创建新 channel 并清空记录;已关闭的 channel 不能重新打开。
A、Done 同时就绪,任选其中一条都合法。书写顺序不决定优先级,Done 也不会自动抢先。
还没有进入等待。
Go 规范要求从能进行的通信中均匀伪随机地选择一个。本例让你指定一个合法结果,便于对照;点击次数不代表随机频率、有限时间内的公平性或 CPU 耗时。
A:打开,缓冲值 7。缓冲 7。选择一个外部事件,看看这里怎样变。
当前缓冲:7。缓冲里的值可以立即取出。
B:nil。缓冲 空。选择一个外部事件,看看这里怎样变。
当前缓冲:空。nil 的通信永远不能进行。
关闭会让取消分支就绪;不强制替你选择它,也不自动结束本例。
本轮还没有触发 Done。 当前就绪:A、Done。
送入值、关闭和触发 Done 都代表其他 G 的外部事件。赋 nil 是接收方在两次 select 之间执行的赋值,不是关闭原 channel。
进入 select 时,各接收分支的 channel 操作数已经求值。本例不让正在等待的同一个 G 跳到后面的赋值;先让本次接收完成,再把变量赋 nil,影响下一次 select。这里没有其他接收者抢走数据。
第 0 步:尚未执行。先看上方就绪集合,再选择一个合法分支。
下方保留最近一次已完成的 select 结果;外部事件或赋 nil 不会冒充一次接收。
还没有操作;就绪预览和预设配置不计步骤。
这是 JavaScript 接收分支教学模型,浏览器没有运行 Go。A、B 各自最多缓冲一个 int,Done 是只关闭、不发送值的 chan struct{}。这里只展示通信是否能进行,不模拟发送表达式求值、超时或完整运行时调度。
nil 不会就绪;打开且空需要外部事件;关闭后先读完缓冲,再重复得到 0、false。无通信就绪时,有 default 就继续,没有就等待。多个通信同时就绪时,只执行其中一个,取消分支没有书写优先权。
依据:Go 规范:Select statements;Receive operator(核对日期:2026-10-14)。
训练实验室 · 有效 token 与梯度累积
把同一组有效目标拆成两个 microbatch(微批次),比较按 token 加权与按批次等权的损失、梯度和一次更新。默认例中,两种算法会把参数推向相反方向。
模型很小:所有输入 x=1,所以预测值是 w。每个有效目标使用半平方误差 ℓ=½(w−y)²,梯度是 w−y。这是可手算的标量例子,不是语言模型的交叉熵。
这一轮共有 4 个有效目标、2 个非空微批次。有效 token 数决定每个微批次在整体目标中的权重。
数字显示 6 位小数,计算使用未舍入值。各表与公式可左右滑动;键盘聚焦后可用方向键查看。
| 微批次 | 有效数 n | 损失和 S | 均值 S/n | 梯度和 | 均值梯度 |
|---|---|---|---|---|---|
| A | 1 | 2.000000 | 2.000000 | -2.000000 | -2.000000 |
| B | 3 | 1.500000 | 0.500000 | 3.000000 | 1.000000 |
token 加权:L = Σ Sᵣ / N = Σ (nᵣ / N) × Lᵣ
非空批等权:L_equal = Σ Lᵣ / K_nonempty
逐 token 梯度:∂ℓ/∂w = w − y
N 是这一轮所有有效目标的总数;K_nonempty 只数非空微批次。求和中的 Lᵣ 只对非空批定义。空批既不计算 0/0,也不作为“零均值”加入等权平均。
| 微批次 | token 权重 n/N | 加权梯度贡献 | 加权累计 | 非空批等权 | 等权梯度贡献 | 等权累计 |
|---|---|---|---|---|---|---|
| A | 0.250000 | -0.500000 | -0.500000 | 0.500000 | -1.000000 | -1.000000 |
| B | 0.750000 | 0.750000 | 0.250000 | 0.500000 | 0.500000 | -0.500000 |
当前查看:按有效 token 加权。所有损失与梯度均在更新前的同一个 w 处计算。
w_new = 0.000000 − 0.100000 × (0.250000) = -0.025000
| 路线 | 平均损失 | 梯度 | 更新后的 w |
|---|---|---|---|
| 合并为一个大 batch | 0.875000 | 0.250000 | -0.025000 |
| 按有效 token 加权 | 0.875000 | 0.250000 | -0.025000 |
| 微批次均值等权 | 1.250000 | -0.500000 | 0.050000 |
按有效 token 加权与合并整批一致;微批次均值等权改变了目标权重。短批中的单个 token 可能因此占更大比重。
若目标是“所有有效 token 等权”,就应把各批损失和相加,再除以全轮有效目标数。等价做法是:每个非空批的均值乘以 n/N,再反向传播。只除以微批次数,通常得到另一个加权目标。
本实验在开始时清零累积梯度;两批都使用同一个 w,没有在中途更新参数。只有所有贡献收齐后,才用同一学习率做一次普通 SGD。逐批先更新再计算下一批,已经改变了求导点,不能拿来验证这里的等价关系。
这里固定有效目标与逐目标损失,没有 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。
改动补齐位置,对比三种取列规则。先找最后一个真实输入,再观察它对应的整条 logits;不要把有效个数当成列号。
列号从 0 开始。1 是真实输入位置,0 是补齐;数量与位置是两种信息。
列号和分数不拆行。窄屏可横向滑动表格,或聚焦后按方向键。
| 请求行 | 列 0 | 列 1 | 列 2 | 列 3 | 有效个数 |
|---|---|---|---|---|---|
| 行 1 | 0补齐位置 | 0补齐位置 | 1真实位置 | 1真实位置最后有效✓ 当前取列 | 2 |
| 行 2 | 1真实位置 | 1真实位置 | 1真实位置 | 1真实位置最后有效✓ 当前取列 | 4 |
| 行 | 最右有效列 | 个数减一 | 末列 |
|---|---|---|---|
| 1 | 3 · 取列一致 | 1 · 错取补齐 | 3 · 取列一致 |
| 2 | 3 · 取列一致 | 3 · 取列一致 | 3 · 取列一致 |
取列一致:读取第 3 列,这是一个真实输入位置。
logits = [-2, -2, -2, 2, -2, -2, -2, -2]
贪心候选 ID:3。这里只让每列偏好不同的 ID,便于看清读错列的后果。
取列一致:读取第 3 列,这是一个真实输入位置。
logits = [-2, -2, -2, 2, -2, -2, -2, -2]
贪心候选 ID:3。这里只让每列偏好不同的 ID,便于看清读错列的后果。
mask [0, 0, 1, 1] 的有效个数是 2,个数减一得到 1;但最右边的 1 在第 3 列。右补齐 [1, 1, 0, 0] 恰好符合“个数减一”的前提,不能把这个公式搬到左补齐。
本例固定 8 个候选,每列 j 只有候选 j 的 logit 为 2,其余为 −2。分数是教学数据,不是模型预测;attention mask 表示可用输入位置,不会替调用者把 logits 的取列操作改正确。
Transformers 4.57.1 的普通生成路径读取 logits 的最后一列。因此非空、标准左补齐可让每行最后列都是最后真实输入。仅改成“最右有效列”不等于已修复右补齐的整个生成循环;还要遵守模型的位置编码、后续 mask 与缓存约定。本例不模拟这些部分。
全空行的最大有效位置不存在。PyTorch 普通负索引 −1 会指向末列,并不会自动表达“无结果”;本实验先拒绝空行,再读取任何分数。中间缺口预设只检验位置算法,不推荐把任意稀疏 mask 当成合法的生成提示。
不操作也能读懂:默认第一行最右有效列为 3;“有效个数减一”错取第 1 列,“直接取末列”在此左补齐前提下取对。第二行没有补齐,三条规则都取第 3 列。
依据:Transformers 4.57.1 左补齐说明;固定版本生成实现。核对:2026-10-14。
250 章媒介适配初筛 · 模型边界与依据 · 验证记录