系列目录 · 资源与调度 · Read in English
“我只是生成一个稍大的输出,编译器却说全局内存用完了。”
“它知道运行时会给多少内存吗?”
“它知道一个默认数字。”
这段对话的危险之处在于,那个默认数字往往存在很久,以至于看起来像硬件真理。它可能最初服务于某个部署环境,后来却被一路复制成所有模型必须遵守的编译期上限。
三个不同的问题,被挤进了一个 capacity
地址分配至少面对三类约束。第一类是存储容量:运行时实际分配了多少字节。第二类是地址格式:指令、描述符和运行时字段能表示多大的地址。第三类是编译器自身整数运算:对齐、求和和类型转换会不会溢出。
它们可能恰好给出相近数字,但语义不同。如果编译器只是规划相对偏移,而最终存储由加载器决定,那么把某个固定物理容量塞进通用全局地址池,可能会错误拒绝本来可以部署的产物。
另一方面,取消容量上限也不意味着地址无限。一个地址即使能放进主机的宽整数,如果设备搬运描述符只有较窄字段,仍然不可编码。
相关提交明确把全局地址规划与旧的物理容量元数据脱钩,同时保留局部有限存储的容量约束。公开讨论必须保留这层区别:这是约束分类的修正,不是“编译器让硬件内存变大了”。
一个教学例子
假设编译器规划一个 11 KiB 的逻辑区间,运行时可以提供 20 KiB,而编译器里遗留的默认预算是 8 KiB。如果按旧预算拒绝,问题在于把运行时资源条件误当成固定编译条件。
再假设某个描述符的地址格式只能表示到 M,那么任意分配仍要满足明确的地址范围协议。被检查的实现还要求对齐后的排他结束地址可表示,因此教学条件写成:
1 | start <= M |
注意最后一个地址和排他结束地址是两种边界。如果硬件只需要编码最后一个有效字节,协议可能允许另一种上界;若运行时或元数据还要保存结束偏移,就必须考虑排他结束地址。这是接口约定,不能凭习惯偷偷改成 start + size - 1。
所阅读的测试专门覆盖接近边界的大小,表明这里的边界选择是有意验证的行为。
为什么先减再比较
一种看起来直观的写法是:
1 | if start + size > limit: |
但如果加法先溢出,得到一个很小的数,比较就可能错误通过。更稳妥的结构是先检查 start,再判断 size 是否超过剩余空间。
对齐也一样。常见写法 (value + alignment - 1) / alignment * alignment 在接近整数上界时,前面的加法可能先出错。被核实的改动引入受检查的对齐逻辑:拒绝零对齐,计算所需 padding,然后检查 value 是否允许再加这段 padding。
用重新设计的小数字演算:对齐单位为 10,当前值为 27,需要补 3,结果为 30。若最大可表示值是 29,不能先得到 30 再转换回窄类型,而应在加法前拒绝。这个例子故意不使用二的幂,提醒读者不要把所有对齐接口都假设为位运算掩码。
同一规则必须覆盖所有分配路径
如果只有主地址分配器取消旧预算,而代码生成阶段还保留一个默认容量,模型会“前面通过,最后失败”。如果只有普通张量路径检查地址位宽,而分块路径绕过检查,大图又可能生成不能编码的描述符。
阅读到的改动同时处理了主分配、分块规划和生成阶段追加存储,并统一了地址范围检查。生成阶段的额外开销可能包含描述符或指令数据,所以它不能假设“张量已经放得下,后面就一定放得下”。
它还清理旧容量摘要字段,避免下游继续把过期信息当作限制。这里有一个普遍的迁移问题:删除旧逻辑以后,残留元数据仍可能让别的阶段复活旧规则。修改数据模型时,生产者、消费者和序列化信息需要一起核对。
“无容量限制”的池,仍然有很多限制
一个没有固定物理 capacity 的规划池,仍需检查至少四件事:原始大小是否为负;对齐是否有效;地址加大小是否溢出;结果是否超出最终编码格式。若存在外部基址,还要检查基址加相对偏移。
阅读到的代码也对已有外部内存摘要中的负值与加法溢出作了防护。原因很实际:某阶段写下的峰值一旦坏掉,后续分配不能把这个错误解释为“从一个很小的位置继续”。
不过,这些编译期检查依然不回答运行时是否真的成功分配内存。正确的产品接口应让地址不可表示、规划运算溢出和部署容量不足成为不同诊断,方便用户采取不同措施。
会有性能收益吗
解除错误的编译容量门槛,直接收益是允许合法规模通过规划,而非保证运行更快。模型变大以后,访存和缓存行为甚至可能变差。
受检查的整数运算通常增加少量编译器工作,但具体开销需要看分配次数与数据结构。更值得观察的是,统一规则后是否减少重复检查、是否提前拒绝无效模型,以及是否避免最后序列化阶段才报错造成的时间浪费。
峰值指标也应更精确。全局逻辑峰值、局部同时活跃峰值、产物额外开销和运行时保留量不是同一个数。把它们全部写成“memory usage”,会让性能分析比原来的错误更难定位。
报错以后,分配器还能继续用吗
容量分类清楚以后,还要审查失败路径的状态。一个分配请求通常经过对齐、选择空闲段、推进堆顶、记录映射和更新峰值。如果地址格式检查发生在部分状态已经更新之后,返回失败不一定意味着分配器仍处于请求之前的状态。
对于“失败即终止整个编译”的流程,这可能是可以接受的协议;对于希望尝试另一种布局的搜索算法,就必须提供回滚、试分配或临时上下文。不能把同一个返回 false 的接口,在两个调用方里理解成不同的保证。
还有一类诊断需要区分原始大小和对齐后的保留大小。教学请求要 21 个字节,对齐后可能占 30 个;如果错误只打印原始数,用户会疑惑为什么看上去剩余 25 字节仍放不下。明确显示起点、请求大小、保留大小和限制来源,能把一条晦涩的错误变成可手算的结果。
同理,逻辑峰值很高不一定等于同时存活数据很多。稀疏地址布局、外部保留区和描述符追加都可能拉高最大结束地址。调试时应同时看区间集合与高水位,避免只凭一个峰值数字就断定有大量可优化的内存泄漏。
回归矩阵
| 边界 | 期望 | 原因 |
|---|---|---|
| 超过旧默认预算但仍可编码 | 规划可继续 | 不再误用固定物理容量 |
| 恰好接近地址上界 | 按明确的结束地址协议处理 | 避免差一错误 |
| 对齐后越界 | 拒绝 | 原始大小合法不代表对齐后合法 |
| 起点合法、长度巨大 | 拒绝且加法不溢出 | 防止回绕 |
| 负大小或损坏的已有摘要 | 拒绝 | 不把错误转换为合法偏移 |
| 张量通过、追加产物越界 | 在生成阶段拒绝 | 后续分配同样受约束 |
提交中的测试覆盖旧预算以上分配、范围越界与生成阶段继续增长等情况。
建议在独立教学程序中使用任意精度整数作为参考,对较小地址宽度枚举 start、size 和 alignment。这样可以穷举边界而不申请大块内存,再把受检查实现与参考结果逐项比较。测试目标是算术和协议,不是靠真的耗尽机器内存来证明正确。
当编译器下一次说“内存不足”,先追问它说的是哪一种不足。正确的限制能保护程序,混在一起的限制只会让错误消息显得很有把握。
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 !