415 lines
27 KiB
Python
415 lines
27 KiB
Python
"""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()
|