slime/show/video/scenes.py

415 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Chinese Manim film; all timings shown come from data/benchmarks.json."""
from pathlib import Path
import json
import os
import re
import manimpango
from manim import *
HERE = Path(__file__).resolve().parent
DATA = json.loads((HERE / 'data/benchmarks.json').read_text())
BG, INK, MUTED = '#0B1220', '#E8EEF7', '#94A3B8'
CYAN, MINT, AMBER, PANEL = '#38BDF8', '#34D399', '#FBBF24', '#172235'
# WSL may use an installed Windows font without copying/distributing it.
font_path = os.environ.get('SLIME_FONT_PATH', '/mnt/c/Windows/Fonts/msyh.ttc')
if Path(font_path).is_file():
manimpango.register_font(font_path)
fonts = manimpango.list_fonts()
FONT = next((f for f in ('Microsoft YaHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei') if f in fonts), None)
if FONT is None:
raise RuntimeError('需要中文字体:安装 Noto Sans CJK,或设置 SLIME_FONT_PATH 指向中文字体。')
def text(value, size=28, color=INK, max_width=12):
obj = Text(str(value), font=FONT, font_size=size, color=color)
if obj.width > max_width:
obj.scale_to_fit_width(max_width)
return obj
MATH_TEMPLATE = TexTemplate(
documentclass=r"\documentclass[preview]{standalone}",
preamble=r"\usepackage{amsmath}\usepackage{amssymb}",
)
def formula_tex(value, size=32, color=INK, max_width=12):
obj = MathTex(value, tex_template=MATH_TEMPLATE, font_size=size, color=color)
if obj.width > max_width:
obj.scale_to_fit_width(max_width)
return obj
def subtitle_text(value, max_width=12.4):
"""Mix Pango Chinese prose and actual MathTex for $...$ inline formulas."""
if value.count('$') % 2:
raise ValueError(f'Unbalanced inline math: {value}')
parts = VGroup()
for segment in re.split(r'(\$[^$]+\$)', value):
if not segment.strip():
continue
if segment.startswith('$') and segment.endswith('$'):
parts.add(formula_tex(segment[1:-1], 30))
else:
parts.add(text(segment.strip(), 24))
parts.arrange(RIGHT, buff=.055, aligned_edge=DOWN)
if parts.width > max_width:
parts.scale_to_fit_width(max_width)
return parts
def grid(n=5, side=.48, color=CYAN):
return VGroup(*[
Square(side_length=side, stroke_color=color, stroke_width=1.3,
fill_color=PANEL, fill_opacity=1).move_to(
[(j-(n-1)/2)*side, ((n-1)/2-i)*side, 0])
for i in range(n) for j in range(n)
])
def box(label, width=2.5, height=.65, color=CYAN):
frame = RoundedRectangle(width=width, height=height, corner_radius=.10,
stroke_color=color, fill_color=PANEL, fill_opacity=1)
return VGroup(frame, text(label, 22, color, width-.2))
class FilmBase(Scene):
def setup(self):
self.camera.background_color = BG
self.cues = []
self.chapters = []
self.subtitle = None
def chapter(self, number, title, tag):
if self.mobjects:
self.play(*[FadeOut(m) for m in list(self.mobjects)], run_time=.45)
self.subtitle = None
self.chapters.append({'title': title, 'start': float(self.time)})
heading = text(title, 34).to_edge(UP, buff=.35).align_to(LEFT*6.5, LEFT)
badge = text(number, 20, MINT).to_corner(UR, buff=.4)
footer = text(tag, 17, MUTED).move_to([0, -2.93, 0])
line = Line([-6.5, -3.16, 0], [6.5, -3.16, 0], color=PANEL)
self.play(FadeIn(heading), FadeIn(badge), FadeIn(footer), Create(line), run_time=.6)
def say(self, caption, seconds=5, *, display=None):
if self.subtitle is not None:
self.remove(self.subtitle)
self.subtitle = subtitle_text(display if display is not None else caption).move_to([0, -3.58, 0])
self.add(self.subtitle)
self.cues.append({'start': float(self.time), 'end': float(self.time + seconds), 'text': caption, 'display': display if display is not None else caption})
self.wait(seconds)
def chart(self, key, labels):
d = DATA[key]
before, after = d['before_ms'], d['after_ms']
max_width = 6.4
# Exact linear scale, including very small optimized bars.
baseline = -3.4
items = VGroup()
for i, (label, value, color) in enumerate(zip(labels, (before, after), (AMBER, MINT))):
y = .5-i*1.0
name = text(label, 24, max_width=2.5).move_to([-5, y, 0])
bar = Rectangle(width=max_width*value/before, height=.4,
stroke_width=0, fill_color=color, fill_opacity=1)
bar.move_to([baseline+bar.width/2, y, 0])
value_text = text(f'{value:.3f} ms', 23, color).next_to(bar, RIGHT, buff=.18)
items.add(name, bar, value_text)
ratio = formula_tex(rf'{before/after:.2f}\times', 46, MINT).move_to([0, -1.45, 0])
condition = text(d['workload'], 25, CYAN).move_to([0, 1.55, 0])
metric_label = {
'A': 'CPU 搜索 / GPU scan · 中位数 / 5 轮 · CPU 16 逻辑线程',
'B': 'GPU kernel · 中位数 / 5 轮 · 同组完整命中列表一致',
'C': 'GPU kernel · 中位数 / 100 轮 · 同组完整命中列表一致',
}[key]
metric = text(metric_label, 19, MUTED).move_to([0, -2.2, 0])
self.play(FadeIn(condition), LaggedStart(*[FadeIn(m) for m in items], lag_ratio=.08),
FadeIn(ratio), FadeIn(metric), run_time=1.8)
return VGroup(condition, items, ratio, metric)
def opening(self):
self.chapter('00', '寻找史莱姆区块:从暴力搜索到结构化计算', '示意图用于解释工作分配;测速来自实际程序')
g = grid(9, .40).move_to([-3.3, -.15, 0])
title = VGroup(text('同一个问题', 38, CYAN), text('换一种计算方式', 38, MINT),
text('CPU → CUDA → 复用 → 数学性质', 24, MUTED)).arrange(DOWN, buff=.4).move_to([2.3, 0, 0])
self.play(LaggedStart(*[FadeIn(c) for c in g], lag_ratio=.008), FadeIn(title), run_time=1.5)
target = [g[i*9+j] for i in range(2, 7) for j in range(2, 7)]
self.play(*[c.animate.set_fill(MINT, opacity=.75) for c in target], run_time=.8)
self.say('我们想找一个 seed,以及一片连续 5×5 的史莱姆区块。', 6, display='我们想找一个 seed,以及一片连续 $5\\times5$ 的史莱姆区块。')
self.say('先让程序算得更快,再研究随机数本身的结构。', 6)
def problem(self):
self.chapter('01', 'CPU:先把正确答案算出来', '源码:01_cpu.cpp · 原始 Java Random 语义 · 不通过便立即退出')
g = grid(5, .62).move_to([-3.2, .15, 0])
code = VGroup(*[text(s, 25, c, 6.1) for s,c in [
('for seed in seeds:', CYAN), (' for (x, z) in anchors:', INK),
(' check 5 × 5 cells', INK), (' stop on first failure', AMBER),
(' collect complete hits', MINT)]]).arrange(DOWN, aligned_edge=LEFT, buff=.26).move_to([2.5, .25, 0])
self.play(FadeIn(g), FadeIn(code), run_time=1)
self.play(g[0].animate.set_fill(MINT, opacity=.6), g[1].animate.set_fill(AMBER, opacity=.8), run_time=.8)
self.say('对每个 seed,遍历锚点,再判断锚点周围的 25 格。', 6)
self.say('一格不满足就停止,只有全部通过才记录结果。', 6)
note = text('CPU 基线也使用多线程:实测为 16 个逻辑线程', 23, MUTED).move_to([0, -2.1, 0])
self.play(FadeIn(note))
self.say('保留 CPU 实现,后面的优化都要核对相同搜索结果。', 6)
def cuda(self):
self.chapter('02', 'CUDA:每个 thread 负责一个 seed', '源码:02_cuda_seed.cuh · 图中只展示少量线程,实际启动更多线程')
workers = VGroup(*[box(f'thread {i}', 1.45, .65) for i in range(6)]).arrange(RIGHT, buff=.3).move_to([0, 1.3, 0])
seeds = VGroup(*[text(f'seed {i}', 23, MINT) for i in range(6)]).arrange(RIGHT, buff=.75).move_to([0, .2, 0])
maps = VGroup(*[grid(3, .24) for _ in range(6)]).arrange(RIGHT, buff=1.0).move_to([0, -1.1, 0])
arrows = VGroup(*[Arrow(w.get_bottom(), s.get_top(), buff=.1, color=MUTED) for w,s in zip(workers,seeds)])
self.play(FadeIn(workers), FadeIn(seeds), FadeIn(maps), Create(arrows), run_time=1.5)
for cell in range(3):
self.play(*[g[cell].animate.set_fill(MINT, opacity=.65) for g in maps], run_time=.35)
self.say('把 seed 分给 GPU 线程;每个线程仍然遍历整个坐标窗口。', 7)
self.say('相同的检查逻辑,被更多线程同时执行。', 5)
self.play(FadeOut(VGroup(workers, seeds, maps, arrows)), run_time=.6)
self.chart('A', ('CPU', 'CUDA / seed'))
self.say('A 组:CPU 搜索 3550 毫秒,GPU scan 111 毫秒,约快 32 倍。', 8)
self.say('这里比较的是搜索时间和 GPU scan,不能改拿 kernel 时间计算这个倍数。', 7)
def mapping(self):
self.chapter('03', '改变分工:固定坐标,遍历 seed', '源码:03_cuda_position.cuh · 坐标项存在线程私有数组中 · 不引入全局坐标表')
self.coordinate_identity()
left = VGroup(box('thread → seed', 3.2, color=AMBER),
text('for each (x, z)', 25), text('compute F(x+dx), G(z+dz)', 22, AMBER)).arrange(DOWN, buff=.35).move_to([-3.4, .6, 0])
right = VGroup(box('thread → (x, z)', 3.2, color=MINT),
text('precompute 5 X + 5 Z terms', 22, MINT),
text('for each seed: reuse terms', 23)).arrange(DOWN, buff=.35).move_to([3.1, .6, 0])
arrow = Arrow([-1.2, .5, 0], [1.0, .5, 0], color=CYAN)
self.play(FadeIn(left), FadeIn(right), GrowArrow(arrow), run_time=1.2)
formula = formula_tex(r'T(x+\Delta x,z+\Delta z)=F(x+\Delta x)+G(z+\Delta z)', 34, CYAN).move_to([0, -1.7, 0])
self.play(FadeIn(formula))
self.say('坐标固定之后,X 和 Z 的多项式在 seed 循环外计算。', 7, display='坐标固定之后,$X$ 和 $Z$ 的多项式在 seed 循环外计算。')
self.say('同一份坐标项,可以在许多 seed 上重复使用。', 6)
self.play(FadeOut(VGroup(left, right, arrow, formula)))
self.chart('B', ('CUDA / seed', 'CUDA / coordinate'))
self.say('B 组扩大到 4097×4097;相同任务下,从 31.599 秒降到 17.600 秒。', 8, display='B 组扩大到 $4097\\times4097$;相同任务下,从 31.599 秒降到 17.600 秒。')
self.say('这一组重新测量两个版本,只在组内比较:约快 1.80 倍。', 7)
def outer(self):
self.chapter('04', 'Outer sum:邻居也可以共享坐标项', '源码:04_cuda_outer_sum.cuh · 实际 block 为 16×16 · 示意图简化为 5×5')
self.outer_identity()
g = grid(5, .54).move_to([0, .1, 0])
xs = VGroup(*[formula_tex(rf'F_{{{i}}}', 28, CYAN).move_to([g[i*5].get_x()-.75, g[i*5].get_y(), 0]) for i in range(5)])
zs = VGroup(*[formula_tex(rf'G_{{{i}}}', 28, MINT).move_to([g[i].get_x(), g[i].get_y()+.65, 0]) for i in range(5)])
self.play(FadeIn(g), FadeIn(xs), FadeIn(zs), run_time=1)
self.play(*[g[2*5+j].animate.set_fill(CYAN, opacity=.4) for j in range(5)], run_time=.7)
self.say('固定 X 的坐标项沿一整行复用;固定 Z 的项沿一整列复用。', 7, display='固定 $X$ 的坐标项沿一整行复用;固定 $Z$ 的项沿一整列复用。')
self.play(*[g[i*5+3].animate.set_fill(MINT, opacity=.4) for i in range(5)], run_time=.7)
formula = formula_tex(r'T_{ij}=X_i+Z_j', 36, MINT, 4.5).move_to([3.9, -.1, 0])
self.play(FadeIn(formula))
self.say('把两条轴放进 shared memory,再用 outer sum 组合每格的输入。', 7)
self.play(FadeOut(VGroup(g, xs, zs, formula)))
note = text('16×16 锚点 + 5×5 图案 → 每条轴需要 20 项(包含 halo)', 22, MUTED).move_to([0, 2.1, 0])
self.add(note)
self.chart('C', ('fixed coordinate', 'tiled outer sum'))
self.say('C 组固定一个 seed;kernel 从 2.715 毫秒降到 1.598 毫秒。', 8)
self.say('重新测量 03 再比较 04,约快 1.70 倍;不同负载的倍数不能连乘。', 7)
def coordinate_identity(self):
intro = text('先看送入 Random 之前的坐标加和项', 25, MUTED).move_to([0,2.05,0])
original = formula_tex(
r'T(x,z)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)'
r'+4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)',
34).move_to([0,1.25,0])
wrap = VGroup(
formula_tex(r'\operatorname{J}_{32}(a)=((a+2^{31})\bmod 2^{32})-2^{31}', 30, MUTED),
text('32 位回绕 → 有符号解释 → 提升到 64 位', 21, MUTED),
).arrange(DOWN, buff=.15).move_to([0,-1.9,0])
self.play(FadeIn(intro), FadeIn(original), FadeIn(wrap), run_time=1.1)
self.say('把原始坐标公式写完整,并保留 Java 的 32 位回绕位置。', 7)
x_part = formula_tex(
r'F(x)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)',
36, CYAN).move_to([0,.35,0])
z_part = formula_tex(
r'G(z)=4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)',
36, MINT).move_to([0,-.55,0])
self.play(FadeIn(x_part), FadeIn(z_part), run_time=1)
self.say('前两项只依赖 X,后两项只依赖 Z,把它们分别命名为 F 和 G。', 8, display='前两项只依赖 $X$,后两项只依赖 $Z$,把它们分别命名为 $F$ 和 $G$。')
separated = formula_tex(r'T(x,z)=F(x)+G(z)', 44, AMBER).move_to(original)
self.play(Transform(original, separated), run_time=1)
self.say('没有 X 与 Z 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。', 8, display='没有 $X$ 与 $Z$ 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。')
self.say('这只是按变量重新分组,不是近似;每一项的整数回绕规则都保持原样。', 7)
self.play(FadeOut(VGroup(intro,original,x_part,z_part,wrap)), run_time=.6)
def outer_identity(self):
entry = formula_tex(r'T_{ij}=F(x_i)+G(z_j)=X_i+Z_j', 42, CYAN).move_to([0,1.8,0])
vectors = formula_tex(
r'\mathbf X=(X_0,\ldots,X_{m-1})^{\mathsf T},\qquad'
r'\mathbf Z=(Z_0,\ldots,Z_{n-1})^{\mathsf T}', 32, MUTED).move_to([0,.9,0])
identity = formula_tex(
r'\mathbf T=\underbrace{\mathbf X\mathbf 1_n^{\mathsf T}}_{\text{repeat X across columns}}'
r'+\underbrace{\mathbf 1_m\mathbf Z^{\mathsf T}}_{\text{repeat Z across rows}}',
40, MINT).move_to([0,-.2,0])
self.play(FadeIn(entry), FadeIn(vectors), FadeIn(identity), run_time=1.3)
self.say('对所有 X 和所有 Z 配对,第 i 行、第 j 列就是 X 的第 i 项加 Z 的第 j 项。', 8, display='对所有 $X$ 和所有 $Z$ 配对,第 $i$ 行、第 $j$ 列就是 $X_i+Z_j$。')
self.say('矩阵写法是:X 列向量复制到每列,Z 行向量复制到每行,再逐项相加。', 8, display='矩阵写法是:$X$ 列向量复制到每列,$Z$ 行向量复制到每行,再逐项相加。')
self.play(FadeOut(vectors), FadeOut(identity), run_time=.5)
example = formula_tex(
r'\begin{pmatrix}2&2&2\\5&5&5\end{pmatrix}'
r'+\begin{pmatrix}10&20&30\\10&20&30\end{pmatrix}'
r'=\begin{pmatrix}12&22&32\\15&25&35\end{pmatrix}',
40).move_to([0,.1,0])
note = text('数值仅用于演示广播相加;不是实测坐标项', 21, MUTED).move_to([0,-1.25,0])
contrast = text('outer sum:两条轴相加 · outer product:两条轴相乘', 24, AMBER).move_to([0,-2.0,0])
self.play(FadeIn(example), FadeIn(note), FadeIn(contrast), run_time=1)
self.say('例如 X 是二和五,Z 是十、二十、三十,就得到这张二乘三的加法表。', 8, display='例如 $X=(2,5)^{\\mathsf T}$,$Z=(10,20,30)^{\\mathsf T}$,就得到这张 $2\\times3$ 的加法表。')
self.say('两项 X 加三项 Z,只需五次轴函数计算;六个格子仍需六次相加。', 8, display='两项 $X$ 加三项 $Z$,只需五次轴函数计算;六个格子仍需六次相加。')
self.say('这就是 outer sum:所有交叉组合都相加。外积则是相乘,运算不同。', 7)
self.say('我们复用的是 Random 之前的坐标项,仍要逐格执行随机数判断。', 8)
self.play(FadeOut(VGroup(entry,example,note,contrast)), run_time=.6)
def random(self):
self.chapter('05', '研究 Random:从计算速度转向状态空间', '性质章节独立于 01–04 的测速 · seed 均指有效的 48-bit 状态等价类')
bits = VGroup(*[Rectangle(width=.19, height=.55, stroke_width=.7,
stroke_color=BG, fill_color=AMBER if i<16 else CYAN,
fill_opacity=1) for i in range(64)]).arrange(RIGHT, buff=.015).move_to([0, 1.25, 0])
labels = VGroup(text('高 16 位:不影响 Random 结果', 24, AMBER).move_to([-4.0,.3,0]),
text('低 48 位:决定内部状态', 24, CYAN).move_to([2,.3,0]))
self.play(FadeIn(bits), FadeIn(labels), run_time=1)
formula = VGroup(formula_tex(r'u=\bigl((\mathrm{seed}+T)\mathbin{\mathtt{xor}}C\bigr)\bmod 2^{48}', 34, CYAN),
formula_tex(r'v=(Au+11)\bmod 2^{48}', 34),
formula_tex(r'q=\left\lfloor\frac{v}{2^{17}}\right\rfloor', 34, MINT)).arrange(DOWN, buff=.25).move_to([0, -1.4, 0])
self.play(FadeIn(formula))
self.say('输入是 64 位,但 Java Random 的状态只有 48 位。', 7)
self.say('相差 2^48 的 seed 给出相同结果,包括拒绝重抽的路径。', 7, display='相差 $2^{48}$ 的 seed 给出相同结果,包括拒绝重抽的路径。')
self.say('这不是一个微小的计时优化,而是搜索空间里的等价关系。', 7)
self.play(FadeOut(VGroup(bits,labels,formula)))
self.lower35_proof()
def lower35_proof(self):
def page(title, equations, note=None):
heading = text(title, 27, CYAN).move_to([0, 2.15, 0])
rows = VGroup(*[formula_tex(eq, 32, color) for eq, color in equations])
rows.arrange(DOWN, buff=.32).move_to([0, .25, 0])
objects = VGroup(heading, rows)
if note:
objects.add(text(note, 21, MUTED).move_to([0, -2.25, 0]))
self.play(FadeIn(objects), run_time=.7)
return objects
first = page('先明确未知量:不是把高 13 位忽略', [
(r's=\ell+2^{35}h,\quad 0\le\ell<2^{35},\quad0\le h<8192', CYAN),
(r'u_i(h)=\bigl((s+d_i)\mathbin{\mathtt{xor}}C\bigr)\bmod2^{48}', INK),
(r'C=\mathtt{0x5E434E432},\qquad 2^{34}\le C<2^{35}', MINT),
], '固定坐标求解:s 是世界种子')
self.say('低 35 位是待求输入的一部分;固定它之后,还要求出高 13 位。', 8, display='$\\ell=s\\bmod2^{35}$ 是待求输入的低 35 位;固定它之后,还要求出高 13 位。')
self.say('为什么选 35?合并后的 XOR 常量恰好占 35 位,不会翻转更高位。', 9)
self.play(FadeOut(first))
carry = page('保留坐标进位,再分离 XOR', [
(r'(\ell+d_i)\bmod2^{48}=L_i+2^{35}H_i', INK),
(r'(s+d_i)\bmod2^{48}=L_i+2^{35}\bigl((H_i+h)\bmod8192\bigr)', CYAN),
(r'u_i(h)=\bigl(u_i(0)+2^{35}h\bigr)\bmod2^{48}', MINT),
], 'Hᵢ 包含坐标加法的进位;C 只 XOR 低位 Lᵢ')
self.say('坐标项加到低位上可能进位,我们把进位保留在 H 里面。', 8, display='坐标项加到低位上可能进位,我们把进位保留在 $H_i$ 里面。')
self.say('XOR 只改变低位,所以改变 h,相当于让初始化状态增加 h 乘 2^35。', 10, display='XOR 只改变低位,所以改变 $h$,相当于让初始化状态增加 $h\\,2^{35}$。')
self.say('这个平移来自位域分离,不能把一般的 XOR 当成线性运算。', 8)
self.play(FadeOut(carry))
output = page('一次状态推进:35 − 17 = 18', [
(r'v_i(h)=\bigl(v_i(0)+Ah\,2^{35}\bigr)\bmod2^{48}', INK),
(r'q_i(h)=\bigl(q_i(0)+Ah\,2^{18}\bigr)\bmod2^{31}', CYAN),
(r'q_i(0)=r_i+2^{18}b_i', INK),
(r'q_i(h)=r_i+2^{18}\bigl((b_i+Ah)\bmod8192\bigr)', MINT),
], '0 ≤ rᵢ < 2¹⁸;0 ≤ bᵢ < 8192;所有格子共用同一个 h')
self.say('LCG 先乘 A。next(31) 再去掉状态低 17 位,因此 2^35 变成 2^18。', 11, display='LCG 先乘 $A$。next(31) 再去掉状态低 17 位,因此 $2^{35}$ 变成 $2^{18}$。')
self.say('把首次输出拆成 r 和 b:高位 h 只改变 b,输出的低 18 位 r 保持不变。', 10, display='把首次输出拆成 $r_i$ 和 $b_i$:高位 $h$ 只改变高位部分,低 18 位 $r_i$ 保持不变。')
self.play(FadeOut(output))
modular = page('模 10 条件 = 奇偶条件 + 模 5 条件', [
(r'y_i=(b_i+Ah)\bmod8192,\qquad q_i=r_i+2^{18}y_i', INK),
(r'2^{18}\equiv0\pmod2,\qquad2^{18}\equiv-1\pmod5', CYAN),
(r'q_i\equiv0\pmod{10}\iff r_i\equiv0\pmod2\ \land\ y_i\equiv r_i\pmod5', MINT),
], '这里先求 q mod 10 = 0 的候选;Java 拒绝重抽在后面单独说明')
self.say('如果 r 是奇数,所有 h 都无法让首次输出被 10 整除。', 8, display='如果 $r_i$ 是奇数,所有 $h$ 都无法让首次输出被 10 整除。')
self.say('r 为偶数时,因为 2^18 模 5 等于负一,剩下的要求就是 y 与 r 模 5 相同。', 11, display='$r_i$ 为偶数时,因为 $2^{18}\\equiv-1\\pmod5$,剩下的要求就是 $y_i\\equiv r_i\\pmod5$。')
self.play(FadeOut(modular))
intervals = page('换变量:每个格子最多回绕一次', [
(r't=Ah\bmod8192,\qquad h=4965t\bmod8192', CYAN),
(r'y_i=(b_i+t)\bmod8192', INK),
(r'\begin{cases}t\equiv r_i-b_i\pmod5,&t<8192-b_i,\\'
r't\equiv r_i-b_i+2\pmod5,&t\ge8192-b_i.\end{cases}', MINT),
], 'A 是奇数,模 8192 可逆;回绕后 +2 来自 8192 mod 5 = 2')
self.say('把 A 乘 h 换成 t。A 是奇数,这个换元一一对应,不会丢掉高位解。', 10, display='令 $t=Ah\\bmod8192$。$A$ 是奇数,这个换元一一对应,不会丢掉高位解。')
self.say('b 加 t 最多回绕一次,所以每格只有两段固定的模 5 要求。', 9, display='$b_i+t$ 最多回绕一次,所以每格只有两段固定的模 5 要求。')
self.play(FadeOut(intervals))
example = page('数值例子:看见回绕怎样改变要求', [
(r'r=2,\quad b=8190,\quad8192-b=2', CYAN),
(r'0\le t<2:\ t\equiv2\pmod5\quad\Longrightarrow\quad\varnothing', INK),
(r'2\le t<8192:\ t\equiv4\pmod5\quad\Longrightarrow\quad t=4,9,14,\ldots', MINT),
(r't=4:\quad y=2,\quad q=2+2^{18}\cdot2=524290', AMBER),
], 'r、b 为示意数值;取 t=4 时恢复 h=3476')
self.say('例如 r 等于二、b 等于 8190,t 到二就回绕,允许的余数从二变成四。', 11, display='例如 $r_i=2$、$b_i=8190$,$t=2$ 时回绕,允许的余数从二变成四。')
self.say('取 t 等于四,输出为 524290,确实能被十整除。', 8, display='取 $t=4$,输出为 $q=524290$,确实能被十整除。')
self.play(FadeOut(example))
joint = page('多个格子:按回绕点分段,再求交', [
(r't\in[a_j,a_{j+1}),\qquad t\equiv c_i\pmod5\ \ (\forall i)', INK),
(r'c_i\ne c_k\ \Longrightarrow\ \varnothing', AMBER),
(r'c_i=c\ \ (\forall i)\ \Longrightarrow\ t=c+5k\in[a_j,a_{j+1})', MINT),
(r'\underbrace{18}_{\mathrm{parity}}+\underbrace{17}_{\mathrm{enumerate}}'
r'+\underbrace{13}_{\mathrm{solve}}=48', CYAN),
], '输入低 18 位决定首次输出奇偶;再补上中间 17 位得到低 35 位;最后求高 13 位')
self.say('排序所有格子的回绕点。每段内,只要两格要求不同余数,就能丢掉整段。', 10)
self.say('要求一致时,直接按间隔五生成 t,再恢复 h,不必逐个检查 8192 个高位。', 11, display='要求一致时,直接按间隔五生成 $t$,再恢复 $h$,不必逐个检查 8192 个高位。')
self.say('低 18 位先筛奇偶,中间 17 位目前仍需扩展;低 35 位候选的生成成本没有消失。', 10, display='低 18 位先筛奇偶,中间 17 位仍需扩展;$\\ell=s\\bmod2^{35}$ 的供给成本没有消失。')
self.play(FadeOut(joint))
boundary = page('公式精确,但候选条件有适用边界', [
(r'q<2^{31}-8:\quad\mathrm{nextInt}(10)=q\bmod10', MINT),
(r'q\ge2^{31}-8:\quad\mathrm{reject\ and\ redraw}', AMBER),
(r'2147483640\bmod10=0\quad\text{but this output is rejected}', INK),
], '精确验证能去掉假阳性,不能补回首次输出筛选漏掉的重抽成功解')
self.say('Java 会拒绝最高八个首次输出并重抽;此时不能只看首次输出模十。', 10)
self.say('所以这里是首次输出候选模型,还不能宣称穷尽了所有 Java Random 解。', 9)
def ending(self):
self.chapter('06', '优化的两条线:复用计算,利用结构', DATA['hardware'])
rows = VGroup(*[text(s, 29, c) for s,c in [
('CPU → CUDA:把独立任务同时做', CYAN),
('固定坐标 → outer sum:减少重复的坐标计算', MINT),
('Random → 高位解析:利用输出的模运算结构', AMBER),
('每个加速比,都要有同任务、同输入的对照', INK)]]).arrange(DOWN, aligned_edge=LEFT, buff=.55).move_to([0,.2,0])
self.play(LaggedStart(*[FadeIn(r) for r in rows], lag_ratio=.2), run_time=1.8)
self.say('先理解重复工作在哪里,再决定并行、复用,还是改变搜索方法。', 7)
self.say('源码、完整测试口径和原始测速记录,都放在 show 目录里。', 6)
def tear_down(self):
output = HERE / 'output'
output.mkdir(exist_ok=True)
name = self.__class__.__name__
(output / f'{name}.timeline.json').write_text(json.dumps(
{'duration': float(self.time), 'chapters': self.chapters, 'cues': self.cues}, ensure_ascii=False, indent=2)+'\n')
def stamp(t):
ms = round(t*1000)
return f'{ms//3600000:02}:{ms//60000%60:02}:{ms//1000%60:02},{ms%1000:03}'
(output / f'{name}.srt').write_text('\n\n'.join(
f'{i}\n{stamp(c["start"])} --> {stamp(c["end"])}\n{c["text"]}' for i,c in enumerate(self.cues,1))+'\n')
(output / f'{name}.narration.md').write_text('# 口播稿(与当前渲染时间轴一致)\n\n'+
'\n\n'.join(f'[{stamp(c["start"])}] {c["text"]}' for c in self.cues)+'\n')
super().tear_down()
class SlimeFilm(FilmBase):
def construct(self):
for part in (self.opening, self.problem, self.cuda, self.mapping,
self.outer, self.random, self.ending):
part()
class ParallelEpisode(FilmBase):
def construct(self):
for part in (self.opening, self.problem, self.cuda, self.mapping, self.outer):
part()
class RandomEpisode(FilmBase):
def construct(self):
for part in (self.random, self.ending):
part()