CHAPTER 51 / Compiler 与 runtime

Tensor lowering 与内存规划:值什么时候可以共用一块空间

为什么一次看似原地的更新,编译器有时必须插入复制?

阅读与推演约 55 分钟练习时间另计

这一章要弄清楚

  • 区分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.inserttensor.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.inserttensor.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复用

x1 2 3future usesum(x)01 / 03 · COMPILERx1 2 3future usesum(x)01 / 03 · COMPILER
输入

旧x仍需后续读取。

1 / 3
阅读完整推演文字
  1. 输入

    x:1 2 3;future use:sum(x)

    旧x仍需后续读取。

  2. 结果

    x:1 2 3;y:11 12 13

    新值需要独立语义。

  3. 检查

    correct sum(x):6;overwrite sum:36

    覆盖会破坏旧值。

跟着例子,走完一遍

例题 01C++20 · 本机可运行

旧tensor值仍被读取

x=[1,2,3],y=x+10,之后还要sum(x)。

  1. 安全路径产生独立y
  2. 保留x的旧总和6
  3. 错误原地覆盖后旧读取变36
47-a.cpp
下载
// 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';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

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
例题 02C++20 · 本机可运行

三个活跃区间使用两个槽

A=[0,2)、B=[2,4)、C=[1,3)。

  1. 判断A/B不重叠
  2. C同时与A/B重叠
  3. A/B共享,C另用一槽
47-b.cpp
下载
// 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共享吗?

给我一点提示
  1. 看2到3之间
  2. 异步完成会改变区间
查看答案与推理

不能,A与B在[2,3)重叠。若强行共用,B写入可能覆盖A尚未完成的读取。应分开buffer或建立使生命不重叠的合法调度。

练习 2

x=[2,5],y更新第0项为9,之后读取旧x[0],应得到什么?

给我一点提示
  1. tensor旧值仍是旧值
  2. 不要把名称不同当存储不同的保证
查看答案与推理

旧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.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。