同一个最终状态,为什么需要两次写出?

持续状态、最终状态与直接输出,为什么不是同一种缓冲。

Posted by Bruce Lee on 2026-06-03

系列目录 · 资源与调度 · Read in English

“最终隐藏状态已经是图输出,为什么还要保存一次?”

“因为图输出交给调用者,持久化状态交给下一次调用。”

“那把两次保存串起来不就行了?”

听起来像一次很普通的公共子表达式复用。可状态型计算里,两个目的地拥有不同生命周期;把它们误写成一条链,可能把值的意义和副作用混在一起。

先把三个对象分开

对一个循环单元,可用教学关系描述:

1
(sequence_result, final_state) = recurrent(input, initial_state)

sequence_result 是本轮序列结果,final_state 是最后一步状态,persistent_slot 是跨调用保留状态的位置。三者不应该因为形状相似,就被当作同一类对象。

最终状态可能同时服务两件事:更新持久化位置,以及作为本轮图输出返回。于是自然的依赖图是扇出:

1
2
3
                   ┌─ 写入持久化位置
final_state ────────┤
└─ 写入图输出位置 → 返回调用者

容易出错的形式则是:

1
final_state → 状态写回的结果 → 图输出写回

第二种形式是否正确,取决于写回操作返回值的严格定义。如果返回的是目的地别名、带存储空间注解的值,或者仅用于维持副作用的占位对象,那么它不一定能无条件替代原始 final_state。

提交历史给出的不是一条直线

这里必须警惕标题诱导。两份标题相同的提交,实际改动不同。

一份方案尝试接受动态初始状态:当初始状态不是常量时,只允许某类单向情形,并要求它的类型与最终状态一致。同时,它检查最终状态是否已经只有一个存储用户,据此决定是否额外创建内部存储。

另一份同标题方案没有保留这套动态输入扩展,而是始终建立内部状态写回,同时让原有消费者继续直接使用原始最终状态。其伴随测试重点也变为“内部状态写回与图输出写回都直接读取最终状态”。

后续的一份修改把动态初始状态路径移除,改回常量初始化要求,并采用上述直接扇出方式。对所比较的两个终点,Git 文件树差异为空,但这只能说明那些终点内容一致,不说明中间尝试不存在,也不证明它们都已进入当前工作分支。

这段历史最值得写进博客的,不是“成功支持了动态状态”这样的口号,而是:状态入口策略与状态出口策略是两件事,出口正确并不自动代表入口的所有形式都受支持。

为什么“已经有一个 Store”不够

一个值只有一个存储用户,不能告诉我们这个存储写到了哪里。它可能是图输出缓冲,也可能是跨调用状态槽。二者即便字节内容相同,所有权、地址稳定性、更新时机和回收条件也可能不同。

如果只按用户数量决定省掉状态持久化,就把一个结构属性误当成语义属性。更好的问题是:“这条写回是否满足持久化协议?”答案应该来自目的地身份、效果描述或明确的 lowering 约定。

从已阅读的最终测试,可以确认它检查了内部状态写回目的地址与初始状态存储地址的一致性,并检查图输出写回直接消费最终状态,而非消费内部写回的结果。这些是 IR 结构和地址关系的断言;它们不是多轮推理数值已经验证的证明。

状态持久化为什么可能只在第二次运行暴露

在教学情形中,第一轮使用初始化状态,计算结果正确;若内部状态槽没有更新,第二轮仍从初始化状态开始。单次推理测试完全可能看不出问题。

还可以构造更隐蔽的情况:图输出确实拿到了正确最终状态,但运行时并没有把这个输出重新送回状态入口。开发者看到输出文件正确,以为持久化成立;实际下一轮读取的仍是旧位置。

反过来,如果运行时已经显式把输出作为下一轮输入,而编译器又偷偷更新内部状态,系统可能出现两套状态管理方式。公开接口应清楚说明由谁管理状态、是否允许重置、不同实例能否并发,而不是把这些语义藏进某个占位标记。

动态初始状态是另一套合同

支持动态状态至少需要核对方向、批次、隐藏维度、元素类型和量化参数。只检查张量大小相同并不充分;同样数量的元素可能对应不同方向或不同布局。

如果输入状态与输出状态量化不同,回送时可能需要重定标。若运行时把状态保存在固定地址,还要讨论别名写入是否覆盖尚未读完的输入。双向循环的最终状态含义又与单向流式推理不同,不能把一个单向实验性路径的结论扩展到所有形式。

历史中出现过一条受限动态路径,随后被移除,恰好提醒我们:能力声明必须落在具体版本和明确测试上。

多一次写回,是不是浪费

若两个目的地确实不同,两次写回可能都有必要。若最终状态恰好已经位于持久化槽,内部动作也可能只是表达效果与依赖,不一定意味着额外复制同样的字节。应查看 lowering 后的存储别名和真实搬运指令,不能只数高层 Store 节点。

有条件的优化包括让图输出直接引用持久化状态,或让最终序列片段与最终状态共享存储。但这要求调用者不会在下一轮更新之前仍持有旧输出视图,否则“省一次复制”会改变调用者观察到的数据。

因此性能评估至少要同时记录写出字节量、复制次数、状态槽寿命和调用者持有输出的时间。孤立地比较 Store 节点数量,会把语义缺失奖励为更高效。

保存完成,也需要发生在下一次读取之前

即使两条写回都读取了正确的最终状态,跨调用顺序仍然不能省略。假设本轮返回动作只保证图输出准备好,却没有等待内部状态写回完成;调用者立即启动下一轮,下一轮仍可能与尚未完成的状态更新竞争。

因此状态持久化要同时满足地址关系和值的完成关系。教学合同可以写成:本轮状态写回完成,先于下轮初始状态读取。它可以由调用同步、设备事件或运行时串行执行保证,但应该能指出由哪一层负责。

再把场景改为两个线程共用一个模型实例。若它们共享状态槽,互相穿插的调用可能形成一条没有人预期的状态序列;若业务希望两条独立流,就需要独立状态实例。单纯把输出张量复制两份,未必已经把内部持久化存储隔离。

这些问题不意味着每个循环算子都必须自己实现线程隔离,而是提醒接口设计者把责任边界说清楚。

写测试时可以故意让状态写回比图输出慢一些,再立即启动下一轮。若使用独立教学模拟器,这种延迟差很容易注入,也能让“两个输出都正确”与“状态已经可供下轮使用”成为可区分的事实。

回归矩阵与实验计划

情形 要确认的问题
最终状态不作为图输出 内部状态是否仍持久化
最终状态同时作为图输出 两个目的地是否读取正确原值
显式外部状态输入 是否被支持,或明确诊断不支持
连续调用两轮 第二轮是否使用第一轮最终状态
两个独立实例 状态槽是否互相隔离
重置后再次调用 初始状态是否按合同恢复
多方向或类型不一致 是否避免误套单向规则

提交中的结构检查覆盖了其中部分出口关系;多轮行为、实例隔离与性能比较是建议实验。

一个独立实验可以采用极小的教学递推式 h_next = 2*h + x,先不引入复杂门函数。只要两轮结果能够区分“持久化”和“重新初始化”,就能检验状态管线。待管线成立,再替换成真正的循环单元参考实现,分离状态管理错误与算术误差。

当你看到“同一个值写了两次”,值得先画出两个目的地属于谁。状态系统里,重复的外观,有时正是两份不同承诺留下的痕迹。


返回系列目录 · 上一篇 · 下一篇


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !