CHAPTER 51 / Compiler 与 runtime
Tensor lowering 与内存规划:值什么时候可以共用一块空间
为什么一次看似原地的更新,编译器有时必须插入复制?
这一章要弄清楚
- 区分tensor值语义与memref存储语义
- 用活跃区间判断安全复用
- 解释layout、stride与地址计算
先备知识:拥有一组数据:array、vector与string / Tensor shape、stride 与 GEMM 分块 / AST、IR 与 SSA:同一个表达式的三种表示
C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。
从数学值走向实际存储
高层tensor操作可以把结果看成一个新值。例如输入x为1、2、3,产生y=x加十,那么y为11、12、13,而旧x仍有自己的数学含义。底层机器却要把这些数放进真实buffer。把tensor表示转成实际buffer操作的过程通常称为bufferization,它必须决定结果放哪里、何时分配、能否复用,以及何时需要复制。
MLIR的公开材料把高层变换和更低层的memref操作区分开。保留tensor结构时,一些tile、fusion和shape相关变换更容易表达;降低到具体存储后,地址、步长和生命周期变得更显式。这不是简单把所有类型名字从tensor替换成memref,而是在保持语义的前提下引入存储决策。
原地更新需要证明旧值不再被需要
如果y产生后没有任何操作再读取旧x,可以考虑复用x的存储。如果程序随后还要计算旧x的总和六,直接在原buffer上加十会把这次读取变成三十六,改变语义。例一分别构造安全新buffer和错误别名更新,展示这个具体反例。两种路径都在普通C++中运行,不依赖MLIR安装。
两行 MLIR:产生新值,再读取旧值(补充,另估25分钟)
先复用 IR-45 的 SSA 名字与使用关系:%updated、%old 等名字指向值,等号左边接收操作结果。这里换成了 MLIR 的操作,不能把 LLVM 指令表直接套用。
| 写法 | 本例的最小读法 |
|---|---|
tensor.insert、tensor.extract |
点号前是 dialect(操作所属的一组规则),点号后是该组中的操作名 |
tensor<3xf32> |
一个维度、长度为3、元素为32位浮点数的 tensor;x 分隔尺寸与元素类型,不是这行代码中的乘法 |
%index |
一个类型为 index 的位置值;这里 rank 为1,所以只提供一个索引,合法范围为0–2 |
tensor.insert %value into %old[%index] |
%value 必须是匹配的 f32 标量;返回把该位置替换后的 tensor 值,原来的 %old 仍有原义 |
tensor.extract %old[%index] |
从指定的旧 tensor 读取一个元素,返回类型是 f32 |
**纸上走一次:**给定 %old 表示 [1,2,3],%index=1,%value=9。第一行产生 %updated=[1,9,3];第二行仍从 %old 的位置1取数,因此 %still_old=2。只把第二行的 %old 改成 %updated,结果才是9。索引3超出这个例子的合法范围,不能给它指定正常输出,也不能假定任意动态越界都会被编译器提前拒绝。
这描述的是值语义,不保证机器一定分配新 buffer、实际复制三个元素。后续降低若能证明保留了所有旧值使用的结果,可以消除复制;本章 CPU 例子的6/36对照提供反例思路,并非 MLIR 执行证据。读法来源为官方 tensor.insert 和 tensor.extract,滚动文档核对于 2026-09-08;下面片段仍未用 MLIR 工具链验证。
// MLIR tensor语义示意,未用MLIR工具链验证。
%updated = tensor.insert %value into %old[%index] : tensor<3xf32>
%still_old = tensor.extract %old[%index] : tensor<3xf32>
// %still_old 使用旧tensor值,限制直接覆盖其buffer。
编译器需要分析use-def链与可能冲突的读写。Destination-passing一类设计提供候选目标,但不是无条件原地写保证。读取旧内容的路径、别名关系和控制流会影响最终是否需要新buffer或copy。
复用还需要时间不重叠
例二给三个教学buffer定义半开活跃区间:A在0到2之前使用,B在2到4之前使用,C在1到3之前使用。A与B没有重叠,可以候选共用一个槽;C与二者都重叠,需要另一个槽。因此本例两个槽足够。这里的时间是程序阶段编号,不是CPU时间或设备周期。
半开区间要有精确约定:结束点二表示从阶段二开始不再使用A。若真实异步消费者直到阶段三才完成,A的生命必须延长,原来的复用判断就失效。静态计划不能忽略runtime依赖,编译器和runtime需要共同保证buffer不会被提前覆盖。
Shape一样,地址公式也可能不同
二维矩阵的地址通常可理解为基址加行号乘行步长,再加列号乘列步长。连续行主序只是其中一种布局;转置view可能交换步长而不立刻复制数据。后续kernel能否接受这种view,取决于其地址合同。某些优化需要连续或特定tile排列,可能不得不插入转换。
因此降低过程还要跟踪shape、stride、元素类型、alignment和memory space。一个指向合法内存的地址,如果按错误步长访问,也会得到错误矩阵;这类问题可能完全不触发内存越界工具。应使用坐标编码输入并逐元素验证。
成本目标不只有少分配
少一次分配不一定总体更快。如果复用导致同步等待,或强制不利布局,节省空间可能换来更长关键路径。编译器应在合法候选中考虑复制、重算、峰值内存和执行成本;最初学习时先证明正确,再比较一个明确因素。
本章让你能解释为什么生成代码里出现copy,以及什么时候可能安全消除它。它没有实现完整内存规划器,也不把小型区间模型称为MLIR优化结果。阅读真实bufferization诊断时,先找旧值的后续使用和冲突写入,往往比盲目删除copy更有效。
旧值约束buffer复用
旧x仍需后续读取。
新值需要独立语义。
覆盖会破坏旧值。
阅读完整推演文字
- 输入
x:1 2 3;future use:sum(x)
旧x仍需后续读取。
- 结果
x:1 2 3;y:11 12 13
新值需要独立语义。
- 检查
correct sum(x):6;overwrite sum:36
覆盖会破坏旧值。
跟着例子,走完一遍
旧tensor值仍被读取
x=[1,2,3],y=x+10,之后还要sum(x)。
- 安全路径产生独立y
- 保留x的旧总和6
- 错误原地覆盖后旧读取变36
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <vector>
#include <numeric>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const std::vector<int> x{1,2,3};
auto y=x;for(int& v:y) v+=10;
const int old_sum=std::accumulate(x.begin(),x.end(),0);
auto overwritten=x;for(int& v:overwritten) v+=10;
const int bad_old_sum=std::accumulate(overwritten.begin(),overwritten.end(),0);
require(old_sum==6 && bad_old_sum==36 && y==overwritten);
std::cout<<"old_sum="<<old_sum<<" overwritten_sum="<<bad_old_sum<<'\n';
}
old_sum=6 overwritten_sum=36。
值语义决定是否允许共用buffer。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 47-a.cpp -o example && ./example预期标准输出:
old_sum=6 overwritten_sum=36
三个活跃区间使用两个槽
A=[0,2)、B=[2,4)、C=[1,3)。
- 判断A/B不重叠
- C同时与A/B重叠
- A/B共享,C另用一槽
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
struct Life{int begin,end;};
bool overlaps(Life a,Life b){return a.begin<b.end && b.begin<a.end;}
int main() {
const Life a{0,2},b{2,4},c{1,3};
const bool reuse=!overlaps(a,b), separate=overlaps(a,c)&&overlaps(b,c);
require(reuse && separate);
std::cout<<"A_B_reuse="<<reuse<<" C_separate="<<separate<<" slots=2\n";
}
A_B_reuse=1 C_separate=1 slots=2。
区间是教学阶段,真实异步完成可能延长生命。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 47-b.cpp -o example && ./example预期标准输出:
A_B_reuse=1 C_separate=1 slots=2
删除copy却没检查旧值
看到新旧tensor shape相同就原地覆盖。
修正思路:检查所有后续旧值使用、别名及异步生命,再决定是否复用。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
把A的生命从[0,2)延长到[0,3),还能和B共享吗?
给我一点提示
- 看2到3之间
- 异步完成会改变区间
查看答案与推理
不能,A与B在[2,3)重叠。若强行共用,B写入可能覆盖A尚未完成的读取。应分开buffer或建立使生命不重叠的合法调度。
练习 2
x=[2,5],y更新第0项为9,之后读取旧x[0],应得到什么?
给我一点提示
- tensor旧值仍是旧值
- 不要把名称不同当存储不同的保证
查看答案与推理
旧x[0]应为2,y为[9,5]。可用新buffer或等价的保存策略实现;直接覆盖唯一buffer又从同址读旧x会错误得到9。
动手看真实 Clang / LLVM IR / 汇编与链接 →
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
bufferization只是把tensor改名memref吗?
参考回答 / English answer
不是,需要选择真实存储并保持值语义,分析复用、copy、别名和生命周期。
Bufferization assigns storage while preserving value semantics. It must reason about reuse, copies, aliases, and lifetimes.y=x+10后还读旧x,为什么可能必须copy?
参考回答 / English answer
原地覆盖会改变后续旧值读取;除非能证明不存在冲突。
Overwriting x would change a later read of its old value. In-place execution needs a proof that no conflicting use remains.A=[0,2)、B=[2,4)能共用空间吗?
参考回答 / English answer
本模型可以,因为半开区间不重叠;真实异步使用必须全部在2前完成。
They can share storage under these half-open lifetimes. Any outstanding asynchronous use must also be complete.转置一定复制矩阵吗?
参考回答 / English answer
不一定,可通过stride view表达;下游kernel不支持该布局时可能再转换。
A transpose may be represented by changed strides. A later consumer may still require materialization or conversion.copy很多时先查什么?
参考回答 / English answer
查旧tensor值的后续使用、别名、目标buffer合同及layout要求。
I inspect later uses of old values and aliasing constraints. Destination and layout requirements may also require copies.少分配一定更快吗?
参考回答 / English answer
不一定,复用可能增加同步或迫使不利布局;需合法性和成本一起评估。
Less allocation does not guarantee lower latency. Reuse can introduce synchronization or unfavorable layouts.继续查证
- MLIR Bufferization ↗
Destination-passing style and read-after-write conflicts
- MLIR Toy lowering tutorial ↗
Partial lowering and memref operations
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。