不要提前执行 Python:把逻辑调用与物理工作拆开
Pysolate 的 Prepare–Linearize–Materialize 模型,以及它为何比“边生成边执行”更通用
一段程序规定了应该发生什么、在什么逻辑位置发生;它并不总是规定每一份物理工作必须到那个位置才开始。
Pysolate 想利用的,正是这两者之间的缝隙。
在由模型生成 Python、再调用远程工具、数据集、文件系统或其他 Host capability 的程序里,最显眼的性能问题往往不是本地的 a + b,而是大量高延迟边界操作:一次模型调用可能需要十几秒,一次搜索可能需要数秒,一次数据加载可能需要更久。最直观的优化方案是“代码来一行就执行一行”:模型刚生成 search(...),运行时立刻执行它;等后续代码继续生成,再接着运行。
这条路很诱人,也很危险。
一段尚未结束的 Python source 并不是一个已经成立的程序。它后面可能出现语法错误,可能把前面的调用放进尚未闭合的控制流,可能改变参数来源,可能在 try/finally 中重新定义异常行为,也可能产生不应提前发布的副作用。更根本地说,一旦 Host 开始执行任意 Python 前缀,它就必须维护部分解释器状态、部分控制栈、异常边界、对象身份、可变堆和后续 continuation。优化器最终会变成第二个 Python runtime,或者把“生成代码”悄悄改造成一种新的流式语言。
Pysolate 选择另一条路:
- 流式阶段不执行 Python。
- 完整 source seal 后,仍只进行一次真实、同步、fresh 的 CPython 执行。
- Host 可以根据已出现的 source facts,提前开始某些外部物理工作。
- Python 到达原调用位置时,再决定这份提前工作能否代表当前逻辑调用。
- 真正需要结果时,才同步取得普通 Python value。
这不是“让 Python 变成异步语言”,而是:
本文把这套思路形式化为一个三阶段模型:
简称 PLM:Prepare–Linearize–Materialize。
需要先说明:下文是一套面向 Pysolate 的设计模型和可实现方案,不表示当前实现已经覆盖文中所有优化。它的价值首先在于给现有的 source-time pre-dispatch、Host-side pending work、prepared input、缓存和未来的 AST 调度提供一个统一而可证明的语义框架。
一、为什么不尝试“一行一行执行”
1. 不完整的 source 不是一个安全的执行边界
假设模型已经生成:
result = send_email(address, body)
但后面还没有生成完。之后可能是:
if dry_run:
...
也可能是:
raise ValueError("do not send")
还可能发现整个函数最终存在语法错误。仅凭前缀,无法知道这个调用在完整程序中是否可达、是否受某个异常边界保护、是否应该发生。
对只读调用,这个问题看起来轻一些:
data = read_dataset("sales.csv")
但即使它没有写外部世界,它也可能:
- 读取一个会变化的资源;
- 消耗 quota;
- 使用之后会被撤销的 authority;
- 返回依赖当前工作目录或 transaction 的内容;
- 失败,而原程序本应在某个特定位置观察到这个异常。
所以,“看起来没有副作用”并不等于“随时执行都相同”。
2. 部分执行会把 Host 推向一个不干净的位置
若 Host 真正执行 Python 前缀,它需要回答:
- 一个未完成
if的控制状态存在哪里? - 一个 future result 参与
__add__、__getitem__、descriptor、异常和对象 identity 时怎么办? - 后续 source 到来后如何恢复 continuation?
- prefix 已经修改了 Guest heap,后来 source 又语法失败,如何回滚?
- 一个请求提前发出后,后续代码决定不走该 branch,副作用如何撤销?
- 多个逻辑 run 是否共享部分解释器状态?
- 如何保证最终执行仍是 fresh private state?
这些都不是小的调度问题,而是在重新定义 Python 的执行语义。
3. 更好的哲学:不要移动“程序发生了什么”,只移动“完成它所需的物理工作”
考虑:
x = remote_read(key)
这行代码至少包含两件不同的事:
- 逻辑事件:程序在这里调用
remote_read(key),在这里获得值或抛出异常; - 物理工作:建立连接、认证、排队、远程计算、传输、反序列化。
传统同步执行把两者绑在同一位置。Pysolate 的核心判断是:
逻辑调用位置必须保留,但有些物理工作不必等到那里才开始。
因此,优化不是:
提前执行一段 Python
而是:
提前生产一个可能在未来被逻辑调用采用的物理候选结果
这一区分是全文最重要的心智模型。
二、两条时间轴:逻辑顺序与物理时间
程序里有两种完全不同的“先后”。
1. 逻辑程序点
记为:
它表示源程序和 CPython 控制流中的位置。例如:
a = A()
x = f(a)
b = B(x)
逻辑上一定有:
这条顺序描述 Python 的语义:B 是否存在、它的参数是什么、异常如何传播,都由这条逻辑轴决定。
2. 物理墙钟时间
记为:
它描述:
- 请求何时被发出;
- provider 何时开始工作;
- 结果何时完成;
- Python 何时阻塞等待;
- 最终何时返回。
Pysolate 允许某个逻辑位于 的操作,其物理准备在更早的时间 开始:
其中 是 CPython 到达原逻辑调用点的物理时间。
关键不变量是:
可以把它画成:
逻辑程序轴
ℓ0 -------- ℓ1 -------- ℓ2 -------- ℓ3
H_A H_B
│ │
│ 原始语义位置 │
│ │
物理时间轴
p0 -- prepare A -- prepare B -------- linearize A -- materialize A -- ...
这就是“logical ownership / physical work separation”的最简数学版本。
三、隐藏世界状态:为什么 read-only 仍然依赖时间
一个 Host call 通常被写成:
其中 是显式参数。但这在语义上并不完整。真实操作还依赖一个程序没有显式写出的全局环境:
其中:
- :文件、数据库、Git、市场、远程服务等外部世界;
- :当前 authority 和 capability;
- :cwd、环境变量、事务、session、配置;
- :quota、nonce、rate limit 或其他资源状态。
因此,一个 Host operation 应写为:
而不是只有 。
1. “只读”只是效果属性
stock_price("AAPL") 可以是 read-only,因为它不修改市场:
但它仍可能满足:
所以:
这一区分会统一解释缓存、预取、版本验证和 snapshot。
2. 操作通常只依赖世界的一小部分
定义操作的语义依赖 footprint:
例如:
read_git_file(commit_hash, path)
可能只依赖由 commit_hash 标识的 immutable Git object;市场、当前 cwd 和其他文件变化都与它无关。
定义两个上下文对于该调用等价:
当且仅当它们在 上不可区分。
若 是确定性的,并且:
则:
这就是提前结果可以直接复用的核心条件。
四、从最简单的例子开始:issue 左移,collect 右移
考虑:
a = tool_A() # 10s
b = tool_B() # 20s
c = a + b
d = tool_D() # 20s
return c + d
1. 普通同步执行
若三个调用顺序发生:
A: 10s
B: 20s
D: 20s
总时间近似:
2. 最小 split-phase 变换
先暂时使用二阶段写法:
_hA = issue(tool_A)
_hB = issue(tool_B)
_hD = issue(tool_D)
a = collect(_hA)
b = collect(_hB)
c = a + b
d = collect(_hD)
return c + d
其中:
issue非阻塞,Host 开始工作并返回 opaque handle;collect是普通同步调用,未完成就阻塞,完成后返回普通 Python value。
Python 没有 await,也没有看到 Future[int]。
三个请求几乎同时开始,因此理想总时间是:
3. 剩余等待量
对调用 ,定义:
- :physical issue time;
- :physical finish time;
- :materialize/collect time。
其剩余阻塞为:
若耗时为 ,则:
所以:
这直接说明:
- 越靠左,等待不增加;
- 越靠右,等待不增加。
但这只是性能关系。能不能移动,仍由语义依赖决定。
五、为什么二阶段还不够:加入 Linearize
若操作依赖会变化的 ,单纯写:
candidate = issue(H, args)
...
value = collect(candidate)
会产生一个问题:
这个调用到底读取的是 issue 时的世界,原调用位置的世界,还是 collect 时的世界?
例如:
price = stock_price("AAPL")
pure_local_work()
return price
提前请求可能在 读到 100;原调用点在 ;真正 collect 在 。三个时间可能不同。
因此更干净的 normal form 是三阶段。
1. Prepare
candidate = prepare(H, frozen_args)
形式上:
它产生一个 candidate,可以包括:
pending request
candidate result
version / ETag
snapshot ID
lease
authority epoch
provider session
Prepare 允许做物理工作,但在抽象语义中必须静默:
即它不能提前向 Python抛异常、发布逻辑写操作或改变 Guest-visible state。
2. Linearize
当 CPython 到达原始调用位置:
job = linearize_or_start(candidate, H, actual_args)
形式上:
它决定:
- candidate 是否真的代表当前逻辑调用;
- 参数、authority、版本和 source site 是否匹配;
- 若无效,是否必须在这里启动 canonical operation;
- 若是写操作,是否只认领 prepared work,等待之后 commit。
典型规则:
Linearize 固定在原调用的逻辑位置。
3. Materialize
value = materialize(job)
形式上:
它:
- 未完成则同步等待;
- 已完成则立即返回;
- 返回普通 Python value;
- 或在这个语义位置抛出错误。
所以完整原则是:
六、核心正确性契约
对原始同步操作:
PLM 协议必须满足:
其中:
直观地说:
无论物理准备多早开始,只要系统在原逻辑调用点验证、采用或重新启动正确的操作,并且只在合法位置交付结果,Guest 观察到的行为就应等价于在该逻辑点进行一次普通同步调用。
如果 是 nondeterministic relation,则不要求结果等于某个唯一值,而要求:
即 PLM 产生的结果必须是原操作在该上下文中允许的结果之一。
Candidate validator 的 soundness
设 candidate 为:
其中 是版本、lease 或 snapshot evidence。Validator 必须满足:
它可以不 complete:有些其实有效的 candidate 被保守拒绝,只会损失性能。它不能不 sound:不能把过期候选错当成当前逻辑结果。
七、stock price:时间隐变量下的完整例子
考虑:
price = stock_price("AAPL")
情况一:provider 只支持“当前价格”
Prepare 在 得到:
到 没有版本、snapshot 或可验证证据。此时:
系统不能把 100 当成当前逻辑调用的结果,必须在 重新查询。
但 Prepare 仍然可能提前完成:
DNS
TLS
连接池建立
认证
provider route
request object allocation
这说明物理阶段还可以进一步细分:
越不依赖当前世界的阶段,越能向左移动。
情况二:provider 返回版本序号
Prepare 得到:
在 ,系统执行一个可线性化的 current-version check。若仍为 42,则 candidate 在这个逻辑点可采用;若已是 43,则重新请求。
这里 version check 是候选结果与当前世界之间的证明。
情况三:调用绑定 snapshot
price = stock_price("AAPL", snapshot=s)
此时语义是:
而不是随 wall-clock 变化的 current read。只要 snapshot immutable,结果就可以:
- source-time pre-dispatch;
- 缓存;
- 跨 run 复用 immutable backing;
- 延迟 materialize。
这也说明:
Cache 与 early issue 在语义上是同一种东西:它们都是“在别的物理时间产生的候选结果”,由 linearize 判断能否代表当前逻辑调用。
八、复杂控制流:Host 不判断 branch,CPython 仍是唯一语义执行器
考虑:
user = get_user("alice")
if user["premium"]:
score = normalize(user["score"]) # 普通 Python,3s
recs = search(user["topic"]) # 15s
if score > 10 and len(recs) > 3:
quote = price(recs[0]["sku"]) # 10s
else:
quote = fallback("premium") # 6s
else:
quote = fallback("basic") # 6s
tax = tax_table("UK") # 20s
return render(quote, tax)
假设:
get_user需要 8 秒;search需要 15 秒;price需要 10 秒;tax_table需要 20 秒;- 两个
fallback都是安全、便宜、可丢弃的 read。
1. Source streaming 阶段
Host 读到:
user = get_user("alice")
参数 source-closed,因此:
prepare get_user("alice")
看到:
search(user["topic"])
此时不能执行 user["topic"],因为这需要真正的 Python object 和 __getitem__ 语义。Host 只记录该 call site,不发请求。
看到:
fallback("premium")
fallback("basic")
若 contract 允许 control speculation,可以准备两个候选。
看到:
tax_table("UK")
参数 source-closed,立刻准备。
整个 streaming 阶段:
- 没有创建 Python 局部变量;
- 没有计算
user["topic"]; - 没有判断
premium; - 没有运行
normalize; - 没有执行任何 branch。
2. Sealed 后的概念性 AST 变换
_p_user = prepare_or_reuse(GET_USER, "alice")
_p_tax = prepare_or_reuse(TAX, "UK")
_p_fp = prepare_or_reuse(FALLBACK_PREMIUM, "premium")
_p_fb = prepare_or_reuse(FALLBACK_BASIC, "basic")
_j_user = linearize_or_start(
_p_user, GET_USER, "alice"
)
user = materialize(_j_user)
if user["premium"]:
# 现在 user 是真正的 Python dict。
_p_search = prepare(SEARCH, user["topic"])
_j_search = linearize_or_start(
_p_search, SEARCH, user["topic"]
)
# search 在 Host 后台运行;CPython 正常做本地计算。
score = normalize(user["score"])
recs = materialize(_j_search)
if score > 10 and len(recs) > 3:
_p_price = prepare(PRICE, recs[0]["sku"])
_j_price = linearize_or_start(
_p_price, PRICE, recs[0]["sku"]
)
quote = materialize(_j_price)
else:
_j_fp = linearize_or_start(
_p_fp, FALLBACK, "premium"
)
quote = materialize(_j_fp)
discard(_p_fb)
else:
_j_fb = linearize_or_start(
_p_fb, FALLBACK, "basic"
)
quote = materialize(_j_fb)
discard(_p_fp)
_j_tax = linearize_or_start(
_p_tax, TAX_TABLE, "UK"
)
tax = materialize(_j_tax)
return render(quote, tax)
3. 这里到底发生了什么
get_user和tax_table在 source generation 时已经开始;user到达原调用点后才被 materialize 成普通 dict;- CPython 自己判断
premium; - 进入 branch 后,
search参数终于 concrete,于是立刻发出; - search 运行期间,CPython 做
normalize; - 到
len(recs)前必须 materialize search; - 内层 branch 仍由 CPython 判断;
price参数直到recs完成后才存在,因此不能更早发;- fallback 候选可被提前准备,但只有实际路径会被 linearize;
- tax 很可能在最后使用时早已完成。
整个执行自然形成多个 wave:
source-time prepare wave
↓
materialize user
↓
CPython 决定外层 branch
↓
runtime prepare search
↓
CPython 做独立本地工作
↓
materialize search
↓
CPython 决定内层 branch
↓
prepare / adopt price 或 fallback
↓
materialize
没有 Python scheduler,也没有 Host-side Python graph evaluator。
九、数据依赖:为什么有些 critical path 无法消除
考虑:
a = tool_A() # 10s
x = a + 1
b = tool_B(x) # 20s
d = tool_D() # 20s
return b + d
Pysolate 可以在最早时准备:
A
D
但 B 的参数依赖:
x = a + 1
按设计,Host 不执行这段 Python。因此:
- materialize
A; - CPython 计算
x; - 才能 prepare/linearize
B。
关键路径仍是:
D 可以与这条链并行,所以总时间接近:
这不是优化器失败,而是明确的 semantic staging barrier:
若未来希望在 A 完成的一瞬间,即使主 Python thread 正在做其他工作,也自动计算 x 并启动 B,就必须引入至少一种额外机制:
- Python continuation scheduler;
- 另一个 Python execution context;
- Host-side expression IR;
- 更激进的本地代码重排。
这些都不属于最小、clean 的 PLM 模型。
十、副作用:不是“能不能提前发”,而是“哪一阶段可以提前”
副作用不是一个布尔标签。一个逻辑操作往往可以拆成多个物理阶段,其中有些静默,有些不可提前。
1. 四类 operation contract
Immutable / snapshot read
读取 content hash
读取固定 Git commit
读取固定 dataset version
结果不依赖变化中的当前世界,可以完整 Prepare。
Validated read
读取有 ETag/version 的对象
读取带 snapshot token 的数据库结果
Prepare 产生候选和证据;Linearize 验证,不通过则重做。
Prepare–commit effect
例如:
send_email(to, body)
可以提前:
解析地址
生成 MIME
上传临时附件
建立连接
完成 spam / policy 检查
但真正“发送”必须在原逻辑点 commit。
形式上:
这里 commit 通常不能向右越过其他逻辑效果。
Non-stageable
若调用本身:
- 立即产生不可撤销效果;
- 消耗 one-shot nonce;
- 结果依赖 current state 且无法验证;
- 会改变其他调用的结果;
- 无法安全取消或丢弃;
则它不能提前,退化为普通同步调用。
2. Speculation 的真正条件
一个调用藏在未知 branch 中:
if cond:
x = H()
要在 cond 未知时 Prepare,必须保证:
与未执行一样,在逻辑上不可观察。
若最终不进入 branch:
discard(candidate)
必须满足:
所以“read-only”仍不够。一次 read 可能消耗 quota、占用唯一 lease、触发审计或影响 provider 的后续行为。所有这些若对程序可观察,都属于 effect footprint。
3. Authority 也属于语义状态
提前工作不能绕过权限。
一个 candidate 至少应绑定:
logical run
capability identity
authority epoch
tool identity
normalised argument digest
source seal
dynamic occurrence
对写操作,Linearize 或 Commit 时通常需要重新检查 authority。若 capability 在 Prepare 后被 revoke,候选结果不得自动发布。
这使得:
物理工作可以早做、复用或共享;逻辑授权、发布和 mutable state 仍然属于每个 logical run。
十一、异常:Materialize 不能无条件右移
考虑:
a = H() # 可能失败
send_email()
return a
若变成:
_p = prepare(H)
_j = linearize_or_start(_p, H)
send_email()
a = materialize(_j)
return a
当 H 失败时:
- 原程序不会发邮件;
- 变换后已经发了邮件。
因此不等价。
1. Materialize sinking 的可交换条件
设:
而 是紧随其后的语句。只有当:
时,才能把 跨过 。
记为:
若:
则可通过相邻交换得到:
2. 一个保守实现允许跨越什么
可以跨越的典型语句:
已证明 pure
不会抛异常
不读取结果变量
不观察局部变量绑定
不改变 operation 的 temporal validity
不改变 authority / transaction / cwd
必须停止的 barrier:
可观察 I/O 或外部效果
可能抛异常的未知调用
try / except / finally
with / transaction
return / yield
时间与随机读取
locals / frame introspection
线程或 signal 边界
结果的第一次 strict use
因此,第一版实现完全可以:
只做 Prepare 左移,Materialize 保持原 call site。
大部分并发收益已经存在。Materialize sinking 是更强、更受限的后续优化。
十二、其他工作如何理解这类问题
Pysolate 并不是在真空中出现。它与 futures、dataflow、lazy evaluation、async/await、partial evaluation、speculation 和缓存都有亲缘关系。但它把边界放在了不同的位置。
1. Futures / Promises:让“尚未完成的值”进入语言
Multilisp 的 future 允许一个表达式并发求值,后续在需要具体值时隐式或显式等待;Liskov 与 Shrira 的 promises 则面向异步远程过程调用,把请求发出与取得返回值分离。[1][2]
典型心智模型是:
a: Future[int] = async_A()
b: Future[int] = async_B()
c = touch(a) + touch(b)
它的核心对象是:
Pysolate 的区别是:
- 用户 Python 中不出现
Future[T]; - 普通变量在 materialize 后仍是普通 concrete value;
- Host 不要求普通 Python operator 对 Future 做 lifting;
- source streaming 阶段可以在 Python 尚未开始执行前准备工作;
- final CPython 仍然是同步语义。
Pysolate 借用了“调用与等待分离”,但把 Future 限制为编译器和 Host 的内部 handle,而不是扩展 Guest value domain。
2. Python async / await:显式协程与 event loop
Python 原生异步模型要求 coroutine 在 await 处挂起,把控制权交还 event loop;event loop 在 Task 等待 Future 时调度其他任务。[3]
这适合开发者主动编写异步应用:
a_task = asyncio.create_task(A())
b_task = asyncio.create_task(B())
a, b = await asyncio.gather(a_task, b_task)
Pysolate 刻意不采用这个用户模型:
- 输入是普通同步 Python;
- 不要求
async def; - 不插入用户可见
await; - 不需要 final interpreter 内的 cooperative event loop;
- Host requests 在 Python thread 之外自行推进;
- Python 只在
materialize的同步 ABI 上阻塞。
换句话说,asyncio 让程序显式管理并发;PLM 让编译器移动边界工作的物理阶段。
3. APPL:Future proxy 与按需同步
APPL 是一个很接近的对照。它通过 AST transpilation 集成 Python 与 LLM calls,并使用 StringFuture、BooleanFuture 等对象延迟同步:字符串拼接可以继续组合 future,len、str 或布尔控制流等 strict context 才会 materialize。APPL 的论文明确把自动并行化建立在 asynchronous semantics 和 Future objects 上。[4]
它的模型可以概括为:
调用返回 Future-like Python object
Future 参与有限的 Python 操作
在 first strict use 时同步
Pysolate 的核心差别在于:
- Streaming phase 不执行 Python。
- 不让 Future proxy 渗入普通 Python 值语义。
- 复杂控制流仍由 sealed 后的一次同步 CPython 决定。
- 提前工作必须通过 temporal、authority 和 effect contract 在原逻辑点被采用。
- 目标不局限于 LLM generation result,也适用于任意 Host-owned capability。
因此 APPL 更接近“带透明 Future 的 Python 扩展”,Pysolate 更接近“同步 Python 外围的 split-phase physical-work overlay”。
4. Dataflow:让整个程序成为依赖图
Dataflow runtime 通常把 computation 表示为图:节点在输入依赖满足后触发。Program Dependence Graph 则显式表示程序的数据依赖和控制依赖,可用于优化和代码移动。[5]
若把示例完全 dataflow 化:
A ─┐
├─ add ─┐
B ─┘ ├─ add -> result
D ─────────┘
调度器可以直接沿 critical path 执行。
Pysolate 不做完整 dataflow runtime:
a+b不成为 Host node;- Host 不解释 Python operator;
- 普通 heap、异常和动态 dispatch 不进入 Host graph;
- PDG/CFG 只用于证明某个 Host phase 是否能移动;
- 最终语义仍由 CPython 执行。
因此更准确的结构是:
普通 Python program
+
Host physical-work overlay
而不是:
Python -> 完整计算图 -> Host 执行图
5. Lazy evaluation:延迟计算,而 Pysolate 提前工作
Call-by-need 延迟一个表达式的求值,直到它的值被需要,并通过 sharing 避免重复计算。Launchbury 的语义用显式 heap bindings 描述这种共享和按需求值。[6]
PLM 与 lazy evaluation 有一种镜像关系:
Lazy evaluation:
尽晚开始计算
在需要时求值
PLM:
尽早开始物理工作
在需要时才交付/实体化结果
可以把 Pysolate 称为:
physically eager, logically strict, materially deferred
即物理上积极,逻辑上仍保持原同步顺序,materialization 可以延迟。
6. Partial evaluation 与 multi-stage programming:提前执行静态程序
Partial evaluation 根据已知输入提前计算程序的静态部分,生成 residual program;multi-stage programming 则显式区分代码生成和执行阶段。[7]
Pysolate 的差别非常关键:
- 它不在 streaming 阶段求值普通 Python;
- 不尝试 constant-fold 任意
a+b; - 不生成一个替代 Python semantics 的 residual evaluator;
- 它只提前执行由 Host contract 明确定义的物理阶段。
当然,prepared dataset folding 或把已验证输入替换为常量,与 partial evaluation 有交集。但那是额外的 source transformation,不是 PLM 正确性的必要组成部分。
7. Speculative execution 与 prefetch:提前做可能需要的工作
CPU branch speculation、compiler prefetch 和数据库预取都试图在需求确定前启动工作。PLM 与它们共享:
提前做
可能浪费
错误路径丢弃
关键路径缩短
区别在于 Pysolate 的 speculation 被 effect 和 authority contract 包围:候选结果不能因为“只是优化”就提前产生逻辑效果。
8. Linearizability:给异步物理过程一个逻辑生效点
Linearizability 要求一个并发操作看起来像在 invocation 与 response 之间某个瞬间原子发生。[8]
PLM 借用这个心智模型,但场景并不完全相同:
- Prepare 甚至可以早于逻辑 invocation;
- 因而 Prepare 本身不能算作操作已经发生;
- 到原 call site 时,Linearize 负责证明候选结果能在这里被合法认领;
- 若不能,就从这里启动 canonical operation。
这个中间点把“提前物理结果”重新锚定到原 Python 语义。
9. Effect systems:代码移动必须知道它会触碰什么
Effect systems 用静态信息描述表达式可能产生的 effects,从而支持安全并行化、重排和隔离推理。[9]
PLM 的工具 metadata 本质上是一种面向 Host boundary 的 effect/temporal system:
读什么
写什么
是否 immutable
是否可验证
是否可 speculation
何时检查 authority
异常是否稳定
是否支持 prepare/commit
它不需要为完整 Python 建立精确 effect type system;未知即 barrier,仍可获得保守正确性。
10. Leases、version 与缓存一致性
分布式系统中的 leases 用有期限的保证支持缓存一致性。[10] 在 PLM 中,lease 可以直接变成 candidate validity evidence:
value
version
valid_until
authority_epoch
这使“时间 ”不再只是一个模糊风险,而成为可验证的协议条件。
11. LLM program runtimes
LMQL 把 prompt、constraint 和控制流编译成高效 inference procedure;SGLang 提供结构化 LLM program frontend,并在 runtime 侧做并行控制和 KV-cache reuse。[11][12]
这些系统主要优化 LLM program semantics 或 serving substrate。PLM 的抽象更低、更横向:
- 它不要求 Host operation 是模型生成;
- 不要求知道 provider 内部 KV cache;
- 只要求 operation 可以被拆成安全的 physical phases;
- 同一模型可用于 search、Git、dataset、object store、remote compute 和 prepare/commit effect。
十三、为什么这套设计是广泛通用的
PLM 的适用范围不是由“工具类型”决定,而是由一个可分解契约决定:
只要存在这样的 factorisation,就可以优化。
1. 它统一了 pre-dispatch 与 cache
Source-time request:
现在开始,之后采用
Cache:
以前完成,现在采用
二者都是 candidate:
Linearize 不关心它是刚开始、已经完成,还是来自更早的缓存;只关心它是否代表当前逻辑 operation。
2. 它统一了 warm preparation 与请求执行
一个 operation 可以分成:
runtime / interpreter preparation
connection preparation
argument-independent provider setup
argument-dependent request
time-sensitive read
validation
deserialisation
publication
每层拥有不同依赖 footprint,因此能移动到不同位置。
3. 它不依赖完整静态图
即使 branch、loop 或参数只在运行时确定,CPython 到达新 frontier 后仍可发出下一批 work。无需一开始知道完整 DAG。
4. 它允许保守退化
任何证明失败:
不 Prepare
不 speculation
不 sink Materialize
直接回到:
value = synchronous_host_call(...)
所以正确性不依赖优化成功。
5. 它适合 Host-owned authority boundary
若 Guest 本来就不能直接访问网络、Git、文件或 provider,而只能通过 typed Host capability,Host boundary 天然提供:
统一拦截点
工具身份
参数序列化
effect metadata
authority check
trace / replay
cancellation
result ownership
这使 PLM 比对任意 Python library call 做猜测更现实。
十四、形式化:一个最小核心语言
不需要尝试一次证明完整 CPython。可以定义一个小语言,再把实现限制映射到 admitted subset。
1. Baseline language
表达式:
语句:
其中:
- 是普通本地 Python computation 的抽象;
- 是显式 Host operation。
程序状态:
包括:
- :Guest local/heap state;
- :逻辑外部上下文;
- :Host 私有 physical state。
2. PLM 扩展
加入:
其中 candidate 与 job 都是 Guest 不可伪造的内部 token。
3. Labelled transitions
区分内部事件:
和可见事件:
定义:
删除物理内部事件。
4. Trace refinement 定理
对 admitted program ,若:
- Prepare 对逻辑状态 silent;
- validator sound;
- invalid candidate 在 Linearize 时 fallback;
- untaken candidate 可 silent discard;
- Materialize 返回对应 job 的结果或异常;
- phase movement 尊重 data/control/effect/exception/temporal dependencies;
- 无法证明的 site 保留 baseline call;
则:
这表示:变换程序的每个可见行为,都能由原同步程序解释。
在 deterministic、snapshot-fixed、无 real-time observation 的更强条件下,可加强为 trace equality:
5. 为什么用 refinement,而不是要求相同 wall-clock
如果程序读取真实时间:
start = time.time()
...
任何让程序更快的优化都会改变它看到的时间。
同样,若 stock_price() 真正读取现实市场,优化后程序更早到达原 call site,本来就可能看到不同市场状态。
因此,不能对任意 wall-clock-observing Python 声称 lockstep equivalence。更准确的 theorem 是:
对同一外部 history,隐藏内部 Prepare 事件后,PLM 的逻辑 operation 可以在原 call site 合法 linearize,并产生 baseline semantics 允许的 trace。
若要求严格复现,则必须:
- 虚拟化时间;
- 固定 snapshot;
- 记录/replay external inputs;
- 或把 time/random/signal 设为 optimization barrier。
十五、证明结构
全局证明可由几个局部引理组成。
引理一:Silent Prepare insertion
若:
只改变 Host 私有状态 ,不改变 ,也不产生 visible label,则插入 Prepare 是 stuttering step:
引理二:Valid adoption
由 validator soundness:
所以采用 candidate 不会产生 baseline 不允许的结果。
引理三:Invalid fallback
若 candidate 无效,Linearize 从当前逻辑上下文启动 canonical ,因此直接恢复 baseline semantics。
引理四:Untaken speculation
若 Prepare 与 Discard 都 silent,未进入 branch 的 candidate 只改变 ,隐藏内部事件后不可见。
引理五:Materialize commutation
若:
则:
通过归纳可把 Materialize 安全地向右移动到任意连续可交换区间末端。
合成
AST transformation 可分解为有限次:
插入 Prepare
原调用替换为 Linearize + Materialize
移动 Prepare
移动 Materialize
插入 Discard
每一步保持 trace refinement;由传递性得到全程序性质。
十六、把程序变成 PLM dependence graph
优化器不需要构造完整 Python dataflow graph,只需在普通 CFG/PDG 上为每个 Host call 创建:
1. 基础边
参数定义 ───────────────> P_h 或 L_h
P_h ───────────────────> L_h
L_h ───────────────────> M_h
M_h ───────────────────> 结果 consumers
若参数 source-time 已知,定义可指向 ;若参数只能由 CPython 产生,则只能在 前准备。
2. Control edge
逻辑调用必须受原 branch 控制:
若不允许 speculation:
若允许 silent speculation,可以删除到 的 control edge,但绝不能删除到 的边。
这精确表达:
physical preparation 可以跨越未知控制流;logical occurrence 不可以。
3. Effect edge
若一个语句会改变 operation 的世界 footprint、authority 或 validity,则:
若连 Prepare 都受影响,则边也指向 。
4. Exception edge
若前一个 operation 的失败会阻止后一个逻辑调用:
可以提前 Prepare ,但不能提前让 产生逻辑效果。
5. Temporal order edge
两个 read 也不一定可交换:
a = current_price()
b = current_price()
若它们没有共同 snapshot,原程序要求逻辑顺序:
这说明传统 read/read independence 在带时间世界中不充分。
十七、形式化的优化目标
在理想无限资源下,固定依赖图的 latency 下界由 critical path 决定;work/span 模型用总工作量与最长依赖链刻画并行计算。[13]
PLM 的最简单调度原则:
即:
Prepare 尽早,Linearize 原位,Materialize 在依赖允许下尽晚。
但真实系统还有:
分支命中概率
candidate 过期概率
provider 成本
rate limit
内存
并发 quota
取消成本
未知 latency
所以最优调度是 policy,而不是一个固定拓扑序。
可以定义:
实际不必求全局最优。以下几个简单优化已经足够有效。
十八、简单而有效的优化一:Source-closed Prepare
若 streaming analyzer 看到:
tax = tax_table("UK")
并证明:
tool identity 已知
参数由 literal / sealed constant 构成
authority 已知
Prepare silent
就立刻创建:
candidate = prepare(site, TAX_TABLE, ("UK",))
final runtime 到原位置时:
job = linearize_or_start(
candidate,
TAX_TABLE,
actual_args=("UK",),
)
tax = materialize(job)
必须动态检查:
source seal
site fingerprint
tool identity
actual args digest
authority epoch
logical run
temporal evidence
不匹配就 fallback。
这是一项高收益、低复杂度优化,因为 runtime guard 把大量静态证明变成“猜错只失去性能”。
十九、简单而有效的优化二:基本块内 Prepare hoisting
原代码:
a = A()
x = pure_local_1()
b = B()
y = pure_local_2()
c = C()
return combine(a, b, c, x, y)
若 A/B/C 的参数已知,变为:
_pA = prepare(A)
_pB = prepare(B)
_pC = prepare(C)
_jA = linearize_or_start(_pA, A)
a = materialize(_jA)
x = pure_local_1()
_jB = linearize_or_start(_pB, B)
b = materialize(_jB)
y = pure_local_2()
_jC = linearize_or_start(_pC, C)
c = materialize(_jC)
return combine(a, b, c, x, y)
即使 Materialize 完全不移动,三个调用也已并行。
编译器只需:
- A-normalize 保持 Python 左到右求值;
- 在基本块内向前扫描;
- Prepare 跨过已知不会影响参数、authority 或 effect 的语句;
- 遇到未知调用、异常边界或 mutation 就停止。
二十、简单而有效的优化三:局部 Materialize sinking
原代码:
a = read_immutable()
x = pure_noexcept()
y = another_pure_noexcept(x)
return use(a, y)
若 read_immutable:
total
noexcept
immutable result
则:
_p = prepare(read_immutable)
_j = linearize_or_start(_p, read_immutable)
x = pure_noexcept()
y = another_pure_noexcept(x)
a = materialize(_j)
return use(a, y)
Materialize 与本地计算 overlap。
实现上使用一个保守 commutation predicate:
can_sink(M, statement) =
statement.pure
and statement.noexcept
and not reads_result
and not observes_locals
and not changes_context
未知即 false。
二十一、简单而有效的优化四:分支 speculation
原代码:
if cond:
x = fallback("premium")
else:
x = fallback("basic")
若两个调用:
Prepare silent
结果 immutable
cost 低
可取消/丢弃
则可在 cond 未知时准备二者,最终只 linearize 一边。
Expected benefit 可粗略估计为:
只在:
时 speculation。
不需要复杂 ML scheduler。初版可以采用:
每个 run 最多 N 个 speculative jobs
每个 tool 最多 M 个
只允许低成本 immutable read
二十二、简单而有效的优化五:Just-in-time Prepare
加入时间 validity 后,“越早越好”不再总正确。
假设:
- operation latency 为 ;
- candidate 完成后有 时间的语义 lease;
- 预计逻辑调用发生在 。
希望:
以便调用前完成;又希望:
以便调用时仍有效。
因此合适的 issue window 是:
例子:
- immutable candidate:,立即发;
- latency 20 秒、lease 5 秒:在预计调用前约 20–25 秒发;
- 无 validity guarantee:只提前 transport preparation,不提前最终 current read。
这是一项简单但很有辨识度的 temporal scheduling 优化。
二十三、简单而有效的优化六:Demand promotion 与取消
Host job priority:
正在被 materialize 等待
>
已进入真实 control path
>
same-path source-time prepare
>
未知 branch speculation
当 Python 调用:
materialize(job)
Host 把 job 提升为最高优先级。
branch 确定后,未选择候选:
discard(candidate)
尽快取消,避免 speculative work 占用关键路径的 provider quota。
二十四、简单而有效的优化七:Singleflight 与 Cache 统一
如果多个 logical runs 请求:
同一 immutable operation
同一参数
同一 authority-compatible context
Host 可以共享一份 physical work:
one physical candidate
├── private adoption for run A
├── private adoption for run B
└── cache backing
但必须保持:
logical handle per run
private result ownership
mutable data private copy / COW
cancellation per run
authority checked independently
这正是“shared work, separate sandboxes”的一般化:
二十五、实现轮廓
1. Compiler pipeline
sealed source
↓
parse AST
↓
标记 Host capability calls
↓
A-normalisation
↓
CFG / control dependence / exception region
↓
def-use / mutation / effect facts
↓
创建 P-L-M 节点
↓
Prepare hoisting
↓
Materialize sinking
↓
AST rewrite
↓
verifier
↓
同步 CPython
Streaming analyzer 使用更保守的 prefix facts,只处理 source-closed site。
2. 最小 ABI
candidate = __pysolate_prepare__(
site_token,
tool_id,
frozen_args,
contract_token,
)
job = __pysolate_linearize__(
site_token,
tool_id,
actual_args,
candidate,
)
value = __pysolate_materialize__(job)
__pysolate_discard__(candidate)
实际可融合:
- 无 candidate:Prepare 与 Linearize 合并;
- 不做 sinking:Linearize 与 Materialize 合并;
- non-stageable:三者全部退化成普通同步 Host call。
3. Host job state
CREATED
↓
PREPARING
├── PENDING
├── CANDIDATE_READY
├── TENTATIVE_FAILED
└── CANCELLED
LINEARIZED
├── ADOPTED
├── VALIDATING
└── CANONICAL_RUNNING
MATERIALIZED
├── RETURNED
└── RAISED
4. Tool contract
temporal:
IMMUTABLE
SNAPSHOT
VERSIONED
LEASED
CURRENT
WALLCLOCK_OBSERVING
effect:
SILENT_READ
PREPARE_COMMIT
NON_STAGEABLE
speculation:
NEVER
SAME_PATH
BUDGETED
failure:
STABLE
RETRY_AT_LINEARIZE
VALIDATE_AT_LINEARIZE
authority:
BIND_AT_PREPARE
RECHECK_AT_LINEARIZE
RECHECK_AT_COMMIT
5. Verifier
至少验证:
每个 candidate/job 只能由内部 ABI 创建
Materialize 的定义支配所有结果使用
speculation 只用于允许的 contract
Prepare 参数与 runtime actual args 可校验
不跨越禁止的 exception/effect/authority region
循环动态 occurrence 不错误复用
未采用 candidate 有 discard/cancellation 路径
traceback/source mapping 保留
Verifier 失败时回退整段原始同步执行。
二十六、我们明确不覆盖什么
一个好的抽象不仅要说能做什么,也要明确不做什么。
1. 不覆盖任意本地 Python constant folding
例如:
x = expensive_pure_python(a)
PLM 不会在 streaming 阶段执行它,也不会自动 partial-evaluate 它。
下面这些属于传统 compiler / partial evaluation 领域:
常量折叠
函数 specialization
loop unrolling
dead local computation elimination
NumPy graph fusion
本地 pure function memoization
它们可与 PLM 组合,但不是同一机制。
2. 不覆盖完整的 Future propagation
PLM 不把:
a + b
a["id"]
obj.method()
变成 Future graph,也不自动生成 Future[Future[T]]。
因此,参数经过任意本地 Python 计算后,后续 Host call 必须等 CPython 真正算出参数。
3. 不覆盖本地 CPU computation 的并行调度
若:
a = A() # 1s
cpu_work() # 10s
x = a + 1
b = B(x) # 20s
A 在 1 秒完成,但主 Python thread 正在做 cpu_work(),B 要到 10 秒后才启动。
要在第 1 秒自动启动 B,需要:
continuation scheduler
第二 Python context
线程/进程并行
合法 code motion
Host expression IR
这超出最小 PLM。
4. 不覆盖任意跨循环迭代并行化
for x in xs:
y = tool(x)
逐迭代 split 很容易;把所有迭代批量 fan-out 会改变:
异常时机
资源峰值
变量绑定
部分副作用顺序
break/continue
迭代器行为
需要单独 loop transformation 与证明。
5. 不覆盖不可验证的 current read
若 operation 的语义是“真正调用瞬间的当前状态”,且 provider 不提供:
snapshot
version
lease
validation
那么最终值不能安全 pre-read。最多提前连接、认证等时间不敏感阶段。
6. 不覆盖任意不可撤销副作用
真正的:
发送邮件
扣款
删除资源
发布 commit
不能因为 branch speculation 提前发生。除非 provider 支持明确的 prepare/commit 或补偿协议。
7. 不覆盖所有 CPython 反射语义
以下能力会观察 AST 变换或变量绑定时机:
locals()
inspect.currentframe()
sys.settrace()
eval()
exec()
再加上:
Guest threads
signals
real wall-clock
identity-sensitive mutable object
arbitrary native extension
它们必须被禁止、虚拟化或当成 optimization barrier。
8. 不保证找到全局最优 schedule
真实 latency、branch probability、provider contention 和 temporal invalidation 都未知。全局最优是 online stochastic scheduling 问题。
PLM 提供的是:
规范化的合法调度空间
安全的局部变换
保守的 greedy policy
不是一个万能 oracle。
二十七、目前难以做到、但值得研究的优化
1. 受限的 Python continuation
允许少量已证明 pure 的 Python operator 在 Host Future 完成后运行:
A result
↓
pure projection / arithmetic
↓
B request
这可以缩短 data-dependent chain,但会引入一个小型 continuation runtime。关键研究问题是:如何把范围限制在不重新实现 Python 的程度。
2. Typed projection IR
只支持:
JSON field
tuple index
primitive arithmetic
comparison
formatting
它可以让:
B(a["id"])
在 A 完成后由 Host 自动启动。但 Python 的 __getitem__ 并不总等于 JSON projection,因此必须只对 typed Host result 生效。
3. 跨基本块的 global scheduling
利用完整 PDG、dominance、post-dominance 和 effect summary,把 Prepare 更远地 hoist、Materialize 更远地 sink。收益可能更高,但异常和反射边界会显著增加证明复杂度。
4. Profile-guided temporal scheduling
通过历史 trace 学习:
call site 到达时间
branch reach probability
provider latency distribution
candidate invalidation probability
再选择 just-in-time Prepare 时机。
5. Speculative branch portfolio
在成本预算下选择最值得提前的 branch,而不是全发:
subject to:
6. 自动推导 prepare/commit
对某些 side-effecting API,自动把请求分解为:
construct
upload temporary
validate
commit
但这通常需要 provider-specific protocol,不可能只靠 Python AST 推断。
7. 与本地 partial evaluation 组合
若以后加入纯函数证明、typed arrays 或 restricted numerical sublanguage,可以同时:
- 提前 Host work;
- fold prepared input;
- 生成 residual Python;
- 保持 final fresh execution。
这会把 PLM 扩展成更完整的 staged optimizer,但应作为独立层,而不是混入最小语义核心。
结语:不是更早执行程序,而是更聪明地安排工作
Pysolate 最容易被误解成:
“模型生成一行,我们就执行一行。”
但更准确、更强的说法是:
我们从不在 source streaming 阶段执行 Python;我们只利用 source 已经暴露的事实,提前生产可能被未来逻辑调用采用的物理工作。
程序的控制流、对象语义、异常和中间状态仍由 sealed 后的一次同步 CPython 执行决定。Host 只拥有:
物理候选
版本证据
pending job
authority contract
effect protocol
三阶段 normal form 将这个思想压缩为:
或者更短:
它的通用性来自一个简单事实:现代程序中越来越多的耗时不发生在本地算术里,而发生在受控的系统边界上。只要这个边界能够把“准备”“生效”和“交付”分开,程序就不必为了获得并行性而改变自己的同步心智模型。
这也许是 Pysolate 最重要的设计哲学:
参考文献
[1] Barbara Liskov and Liuba Shrira. Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems. PLDI, 1988.
[2] Robert H. Halstead Jr. Multilisp: A Language for Concurrent Symbolic Computation. ACM TOPLAS, 1985.
[3] Yury Selivanov. PEP 492 — Coroutines with async and await syntax;Python asyncio 官方文档。
[4] Honghua Dong et al. APPL: A Prompt Programming Language for Harmonious Integration of Programs and Large Language Model Prompts. ACL, 2025;初版 arXiv:2406.13161.
[5] Jeanne Ferrante, Karl J. Ottenstein, and Joe D. Warren. The Program Dependence Graph and Its Use in Optimization. ACM TOPLAS, 1987.
[6] John Launchbury. A Natural Semantics for Lazy Evaluation. POPL, 1993.
[7] Neil D. Jones, Carsten K. Gomard, and Peter Sestoft. Partial Evaluation and Automatic Program Generation. 1993;Walid Taha and Tim Sheard. Multi-stage Programming with Explicit Annotations. 1997.
[8] Maurice P. Herlihy and Jeannette M. Wing. Linearizability: A Correctness Condition for Concurrent Objects. ACM TOPLAS, 1990.
[9] John M. Lucassen and David K. Gifford. Polymorphic Effect Systems. POPL, 1988.
[10] Cary G. Gray and David R. Cheriton. Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency. SOSP, 1989.
[11] Luca Beurer-Kellner, Marc Fischer, and Martin Vechev. Prompting Is Programming: A Query Language for Large Language Models. PLDI, 2023.
[12] Lianmin Zheng et al. SGLang: Efficient Execution of Structured Language Model Programs. NeurIPS, 2024.
[13] Robert D. Blumofe and Charles E. Leiserson. Scheduling Multithreaded Computations by Work Stealing. JACM, 1999.