系列目录 · 资源与调度 · Read in English
“这个张量的最后一个用户已经过去了,可以回收。”
“最后一个用户只是换了个形状。真正读数据的算子,还在另一条队列里排队。”
如果给内存分配器颁发“过度热心奖”,这类提前复用很有竞争力。它严格按 IR 出场顺序办事,看到直接用户都处理完了,立刻把空间交给下一个写入者。唯一的问题是:用户消失了,读者没有消失。
有三个容易混淆的结束时刻
第一个时刻是编译器遍历完一条 IR。第二个时刻是设备命令已经发射。第三个时刻是设备真正完成读取。对于异步计算,这三个时刻可能相隔很远。
视图又增加了一层间接关系。一个 reshape 或不产生实际搬运的布局视图,可以返回新值,但仍指向同一片存储。它不需要一条设备命令,因此也没有“完成事件”可以等待。可是视图的后续消费者会读取原来的数据。
重新设计一条简单链:
1 | 读入 A → 取子区域 B → 视图 C → 视图 D → 计算 U |
若只检查 B 的直接用户,看到 C 不产生运行时依赖,就可能将其跳过。于是分配器认为 B 已经无人使用,让 E 覆盖 B 的地址。等 U 真正开始读取,拿到的已经是 E。
因此“零拷贝”不是“零寿命”。省掉一次搬运,通常意味着更多值共享同一份存储,活性分析必须承担额外的别名传播责任。
修复为什么只有几行,含义却很大
已核实的修改把局部存储复用时的直接用户检查,替换为沿视图链寻找运行时消费者的检查。已有辅助过程会先解析组内别名,使用 visited 集合避免重复遍历,遇到产生设备依赖的用户就验证其与下一次写入之间的先后关系;遇到不产生运行时依赖的用户,则继续沿其结果向后追踪。
这个变化同时作用于普通分配拥有者和共享分配的所有成员。后一点尤其重要:一块存储可能对应多个切片、拼接别名或输出成员。证明拥有者本身“没人用了”,不能替代证明所有别名的真正读者都结束了。
修复规模小,是因为遍历机制已经存在;缺失的是在正确的释放决策点调用它。这里可推广的经验是:某段安全检查写得再完善,若资源释放路径绕过它,系统仍然不安全。
用 happens-before,而不是文本先后顺序
如果 U 和 E 在同一条严格有序队列,U 排在 E 前面,队列语义可能足以证明复用安全。如果二者属于不同引擎,就需要跨引擎依赖建立完成顺序。
公开的教学判定可以写成:
1 | safe_to_reuse(storage, next_writer): |
这里刻意写的是 completion(reader),不是“编译器先遇见 reader”。如果只把 IR 编号比较替代完成关系,异步队列仍会穿透这层检查。
反过来,无条件为每次复用插入全局屏障也不是免费方案。它能建立很强的顺序,却可能让原本互不依赖的任务一起停下来。更理想的策略是在已有关系足够时复用,不足时保留独立空间;必要时再评估增加细粒度同步是否划算。
视图链应该追到哪里停
到达实际运行时读者以后,可以检查其完成关系,不必继续把所有下游结果都当成原存储的别名。因为实际计算通常生成新的存储,后续用户是否还读取旧地址,需要由具体算子的别名语义决定。
这也暴露一个建模边界:不能把“没有运行时依赖”普遍等同于“纯视图”。有些操作可能属于宿主控制、返回边界、未知方言或尚未建模的副作用。已阅读的辅助检查对不认识的用户采取保守处理,而不是轻率地放行。
visited 集合同样不是“遇到环就证明安全”。它负责避免遍历重复,正确性仍依赖合法 IR、别名解析和操作分类。教学实现应清楚区分“已经访问,避免重复”与“语义已经验证”。
为什么回归用例要绕两次弯
相关测试不是只有“产生一个张量,再使用一次”这么短。它构造两个数据分支,每条分支都经过视图链,然后分别计算,最后合并。检查重点是两条分支的存储必须保持不同,而各自的视图继续共享本分支地址。
这样的形状恰好暴露旧判断的盲点:直接用户看起来已经结束,真正消费者却藏在多级视图后面。如果把测试简化到没有视图,旧实现也可能通过,从而失去回归价值。
值得区分的是,地址模式测试证明编译器没有选择那个危险复用方案,并不等于已经在真实异步设备上观察到所有时序安全。后者还依赖指令依赖和设备完成语义,应由另一层验证负责。
内存变多,是否就是性能退化
修复提前释放以后,内存峰值可能变高。这未必是退化,而可能是原先的峰值统计低估了真实需求。错误复用不能作为优化基线。
如果新峰值触发容量限制,可以考虑三条路线:调整调度,让读者更早完成;把部分视图物化,换取独立寿命;或者插入足够细的同步,让空间能够安全复用。三者分别交换调度自由、搬运开销和并行度,不能只看静态字节数选方案。
还有一个常被忽视的编译时间问题:从每次释放点反复遍历长视图链,可能造成重复工作。建议测量链长、分支数和别名集合大小,再决定是否缓存真实消费者集合。缓存必须在 IR 变化或调度变化时正确失效,否则只是把旧答案存得更快。
部分重叠别名,会不会把问题再推进一步
设一块存储有前半段视图和后半段视图。后半段的读者已经结束,前半段还在运行。一个按整块分配管理的保守分配器,会继续保留整块区域;一个支持子区间管理的分配器,则可能安全复用后半段。
两者都可能正确,差别在证明粒度。若系统只记录“这些值共享同一个拥有者”,却没有精确记录偏移和覆盖范围,就不能仅凭张量形状猜测两个视图互不重叠。步长视图、转置和不连续切片会让直觉更加不可靠。
本文讨论的修复检查所有分配成员的消费者,属于以已有别名集合为基础加强安全判断。它没有据此证明实现了任意字节区间的精细回收。公开讲原理时保留这个限制,可以避免读者把一处正确性修复误读成完整的别名分析系统。
遇到峰值上升时,可以先打印教学形式的“存储—别名—真实消费者”三层图,标出哪个读者阻止释放。若阻塞来自长视图链,就优化分析效率;若来自真实跨引擎长尾,就调整调度;若来自整个大块只有很小片段仍活跃,再考虑子区间分配。三种原因都表现为内存占用较高,解决手段却完全不同。
回归与实验的最小集合
| 用例 | 应保持的事实 | 失败信号 |
|---|---|---|
| 一层与多层视图 | 最终读者决定存储寿命 | 视图越多,错误复用越早 |
| 两个跨引擎分支 | 未建立完成顺序前不能覆盖 | 两个活跃分支被分到同地址 |
| 同队列串行读写 | 可以利用明确顺序复用 | 过度保守导致不必要峰值 |
| 多个别名成员 | 每个成员的读者都被计入 | 仅拥有者安全,其他别名仍活跃 |
| 未知用户或返回边界 | 保守保留或明确建模 | 未识别操作被当作已完成 |
| 分支汇合与长链 | 遍历终止、结果稳定 | 重复访问造成编译时间异常 |
可以再设计一个完全独立的教学模拟器:为每个队列给定不同延迟,枚举交错执行;每次读操作记录期待的数据版本,每次写操作递增存储版本。若读者看到错误版本,就得到一个具体的提前复用反例。
面对一个“可以立刻回收”的张量,最好的追问不是“它还有几个用户”,而是“还有谁,通过哪条别名链,在什么完成关系下读取这片字节”。
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 !