计算本拍要跑什么
读取 checkpoint、channels、上一拍的 updated_channels,通过 trigger_to_nodes 反查被唤醒的节点,生成 PregelExecutableTask。不执行节点,不改状态。
一个超步(superstep)的完整生命周期。重点看三件事:Plan 只计算任务、
Execute 只把结果写进 pending buffer、Update 才真正 apply 到 channel 并生成 checkpoint。
图中每个节点都标注了 langgraph 源码位置,以及本仓库 pregel/ 示例中的对应观察点。
读取 checkpoint、channels、上一拍的 updated_channels,通过 trigger_to_nodes 反查被唤醒的节点,生成 PregelExecutableTask。不执行节点,不改状态。
线程池或事件循环并发执行任务。节点返回值只写入 checkpoint_pending_writes,同一超步内节点之间互不可见。
把本拍所有 writes 排序后 apply 到 channel,得到新的 updated_channels,保存 checkpoint,然后进入下一拍。
file:line源码位置(悬停看完整路径)
公开入口;内部转调 stream(),由流式生成器驱动整个超步循环。
读取该 thread 最新 checkpoint,反序列化并恢复 channels;建立后续拍次的起点。
get_tuple()checkpoint/memory/__init__.py:239channels_from_checkpoint()pregel/_checkpoint.py:458AsyncPregelLoop.__aenter__()pregel/_loop.py:2061区分新输入 / resume / time-travel;把输入映射成 channel writes,并保存 input checkpoint。
map_input()pregel/_io.py:81prepare_next_tasks()(丢弃未完成任务)pregel/_loop.py:1113apply_writes()(应用输入)pregel/_loop.py:1134_put_checkpoint("input")pregel/_loop.py:1179BSP 主循环。第 N 拍的 channel 更新只在第 N+1 拍可见;循环由 checkpoint 与 writes 驱动。
match_cached_writes()pregel/main.py:3056runner.tick(tasks, timeout=step_timeout)pregel/main.py:3058loop.after_tick()pregel/main.py:3075检查步数上限;调用任务准备函数;无任务则 status="done" 并结束循环。
if self.step > self.stoppregel/_loop.py:658prepare_next_tasks()pregel/_loop.py:663if not self.tasks: status="done"pregel/_loop.py:704_emit("tasks", map_debug_tasks)pregel/_loop.py:728生成本拍任务集合:PUSH(Send / call)与 PULL(被 channel 触发)两类任务合并。
updated_channels × trigger_to_nodespregel/_algo.py:474prepare_single_task()pregel/_algo.py:523prepare_push_task_send()pregel/_algo.py:937prepare_push_task_functional()pregel/_algo.py:799_xxhash_str() 生成 task idpregel/_algo.py:1403resume 时把已完成任务的 writes 归还给内存 task;失败任务按映射转交给 error_handler,而不是重跑原节点。
should_interrupt()pregel/_algo.py:156interrupt_before → GraphInterruptpregel/_loop.py:714schedule_error_handler()pregel/_loop.py:640缓存命中的任务直接写回上次结果,不再执行节点;同时发出 tasks 调试事件。
put_writes(..., cached=True)pregel/_loop.py:1783map_debug_tasks()pregel/debug.py:44已有 writes 的任务(缓存命中 / resume 恢复)跳过执行;剩余任务交给 runner。
提交本拍任务;先 yield 把控制权交回调用方,便于流式输出;再等待 Future。
FuturesDict 管理并发 Futurepregel/_runner.py:76self.submit()(run_with_retry, ...)pregel/_runner.py:261BackgroundExecutor.submit()pregel/_executor.py:54AsyncBackgroundExecutor.submit()pregel/_executor.py:142单任务无超时可走当前线程快路径;否则提交到线程池 / 事件循环。失败按 RetryPolicy 指数退避。
单任务快路径pregel/_runner.py:204_should_retry_on()pregel/_retry.py:834_start_timed_attempt()pregel/_retry.py:343_arun_with_timeout()pregel/_retry.py:422执行节点的 RunnableSeq:用户节点函数 → 状态 writer → 条件边 writer。节点函数返回值在这里产生。
PregelNode.invoke()pregel/_read.py:261BranchSpec._route() 条件边入口graph/_branch.py:146BranchSpec._finish() ends 映射graph/_branch.py:192ChannelWrite.do_write() 写 branch:to:*pregel/_write.py:106节点执行期间可通过 call()、Command、Send 产生 PUSH 任务;新任务插入 runner 的 Future 集合。
accept_push()pregel/_loop.py:1738aaccept_push()pregel/_loop.py:1994prepare_single_task((PUSH, ...))pregel/_loop.py:609_emit("tasks", ...)pregel/_loop.py:631Future 完成回调。成功、异常、中断统一转成 writes 并交给 loop;异常可路由到 error_handler。
成功 → put_writes(task.id, writes)pregel/_loop.py:1783异常 → 追加 ERRORpregel/_runner.py:598中断 → 追加 INTERRUPTpregel/_runner.py:589关键边界:只把 writes 追加到 checkpoint_pending_writes,不碰 channel;同时后台落盘 pending writes 并发流事件。
checkpoint_pending_writes.extend()pregel/_loop.py:514checkpointer_put_writes() 后台pregel/_loop.py:530output_writes() 流式事件pregel/_loop.py:1593cache.setpregel/_loop.py:1793等待所有 Future;任一任务失败则取消其余任务并向上抛;全部成功则本拍执行结束。
_should_stop_others()pregel/_runner.py:618concurrent.futures.wait()pregel/_runner.py:284asyncio.wait(FIRST_COMPLETED)pregel/_runner.py:483调用方在每个任务完成时 yield 流式块;执行与输出解耦,慢任务不会阻塞已完成任务的结果。
本拍执行结束。收集所有 task 的 writes,准备统一应用到 channel。
writes = [w for t in tasks for w in t.writes]pregel/_loop.py:739apply_writes()pregel/_loop.py:761按 task path 排序后 apply;返回 updated_channels,它决定下一拍唤醒谁。
更新 versions_seenpregel/_algo.py:262channels[chan].consume()pregel/_algo.py:290channels[chan].update(vals)pregel/_algo.py:318EMPTY_SEQ 通知未更新通道pregel/_algo.py:328channels[chan].finish()pregel/_algo.py:337若 output channel 被更新,则按 map_output_values 产生 values 流。
writes 已并入 channel;清空 buffer,并结束 replay 状态。
checkpoint_pending_writes.clear()pregel/_loop.py:799is_replaying = Falsepregel/_loop.py:801生成新 checkpoint,并异步提交保存;下一拍不必等待落盘(durability 可调)。
create_checkpoint()pregel/_checkpoint.py:268get_new_channel_versions()pregel/_utils.py:27submit(_checkpointer_put_after_previous)pregel/_loop.py:1360串行化 checkpoint 保存:等待上一个 put 和 delta writes 完成,保证存档顺序。
InMemorySaver.put()checkpoint/memory/__init__.py:430BaseCheckpointSaver.put()checkpoint/base/__init__.py:301更新完成后检查 after 断点;清理 resume 标记,准备下一拍或退出。
raise GraphInterruptpregel/_loop.py:809pop(CONFIG_KEY_RESUMING)pregel/_loop.py:811durability="sync" 时等 checkpoint 真正落盘再进入下一拍;async / exit 不等待。
loop._put_checkpoint_fut.result()pregel/main.py:3079await loop._put_checkpoint_fut(async)pregel/main.py:3551把 updated_channels 交给下一拍 Plan:有可触发节点则继续;否则 tick() 返回 False,进入退出路径。
updated_channels × trigger_to_nodespregel/_algo.py:474循环结束后上下文退出:补存最终 checkpoint 与 pending writes,读取最终 output;步数耗尽则抛 GraphRecursionError。
_put_checkpoint(final)pregel/_loop.py:1510_put_pending_writes()pregel/_loop.py:559read_channels() 最终输出pregel/_read.py把上方 27 个细节点压缩成 10 个可执行里程碑。每张卡片给出关键调用链, 并列出它覆盖的泳道节点;点击编号可定位并高亮上方对应节点。
只读 checkpoint / channels,产出本拍任务;不改 state。
读取该 thread 的最新 checkpoint,恢复 channels;新输入 / resume 被映射成 channel writes,并保存 input checkpoint。
用上一拍的 updated_channels × trigger_to_nodes 反查被唤醒节点,合并 PUSH(Send / call)与 PULL(边触发)任务。
已完成任务复用 writes;失败任务路由到 error_handler;检查 interrupt_before,并过滤已有 writes 的任务。
节点结果先落 pending writes,同一超步内互不可见。
把任务交给 PregelRunner;单任务走当前线程快路径,多任务提交到线程池(sync)或事件循环(async)。
运行节点 RunnableSeq:节点函数 → 状态 writer → 条件边 writer;节点内还可以派发 call() / Send 动态任务。
Future 完成回调 commit;成功、异常、中断统一转成 writes,追加到 checkpoint_pending_writes,不直接改 channel。
等待所有 Future;任一任务失败则取消其余任务。每完成一个任务,调用方就 yield 一个流式块。
writes 在这里才真正进入 channels,并生成新 checkpoint。
把所有 writes 排序后统一 apply;更新 versions_seen / consume / finish,返回 updated_channels,并产生 values 输出。
清空 pending buffer,创建新 checkpoint 并异步落盘;durability="sync" 时等待保存完成。
updated_channels 交给下一拍 Plan;没有可触发节点则退出,保存最终 checkpoint 并读取 output。
updated_channels 回到 Plan 的 while loop.tick(),
再经 trigger_to_nodes 唤醒下一批节点。这正是条件边“执行在 N 拍、生效在 N+1 拍”的原因。
Execute 阶段的节点写的都是 checkpoint_pending_writes,不会直接改 channel。N 拍的写入只在 N+1 拍可见。
route() 在 Execute 执行并写 branch:to:target;Update 才让它进入 updated_channels;下一拍 Plan 才唤醒 target。
Plan 依据的是 checkpoint + 上一拍的 updated_channels + trigger_to_nodes,而不是实时读取节点刚写完的值。
put_writes 是阶段边界Execute 的结果先进入 pending buffer;只有 after_tick → apply_writes 才把 buffer 合并进 channels 并保存 checkpoint。
| 本仓库示例 | 观察到的阶段 | 源码落点 |
|---|---|---|
pregel/debug_pregel.py:129traced_tick() |
Plan:本拍触发通道、计划执行的任务、task id | PregelLoop.tick() _loop.py:650 → prepare_next_tasks() _algo.py:391 |
pregel/debug_pregel.py:151traced_runner_tick() |
Execute:任务提交、并行执行、各任务 writes | PregelRunner.tick() _runner.py:177;run_with_retry() _retry.py:573 |
pregel/debug_pregel.py:99traced_after_tick() |
Update:channel 值、updated_channels、checkpoint | PregelLoop.after_tick() _loop.py:737 → apply_writes() _algo.py:233 |
pregel/conditional_edge.py:119traced_finish() |
Execute 写 branch:to:* → Update 生效 → 下一拍 Plan 唤醒 |
BranchSpec._route/_finish() graph/_branch.py:146/192;ChannelWrite.do_write() _write.py:106 |
pregel/loop_concurrency_demo.py |
SyncPregelLoop 线程池 vs AsyncPregelLoop 事件循环 | PregelRunner.tick/atick _runner.py:177/361;BackgroundExecutor.submit _executor.py:54/142 |
pregel/timeout_demo.py |
run_timeout / idle_timeout / RetryPolicy / error_handler | _start_timed_attempt _retry.py:343;_arun_with_timeout _retry.py:422;_should_retry_on _retry.py:834 |
pregel/compiled_graph.py |
channels / trigger_to_nodes / 并行分支如何决定下一拍任务 | prepare_next_tasks() _algo.py:391/474;apply_writes() _algo.py:233 |
pregel/compile_internals.py |
compile 产物:PregelNode / channels / writers,是运行时三阶段的输入 | PregelNode.invoke() _read.py:261;_first() _loop.py:915 |