<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>性能建模 on 理塘丁真</title><link>https://www.ccyun.cloud/tags/%E6%80%A7%E8%83%BD%E5%BB%BA%E6%A8%A1/</link><description>Recent content in 性能建模 on 理塘丁真</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 22 May 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://www.ccyun.cloud/tags/%E6%80%A7%E8%83%BD%E5%BB%BA%E6%A8%A1/index.xml" rel="self" type="application/rss+xml"/><item><title>深度学习编译器中的算子调度与性能建模</title><link>https://www.ccyun.cloud/post/deep-learning-compiler-operator-schedule-performance-model/</link><pubDate>Fri, 22 May 2026 00:00:00 +0800</pubDate><guid>https://www.ccyun.cloud/post/deep-learning-compiler-operator-schedule-performance-model/</guid><description>&lt;p&gt;前面看 MLC 的时候，我有个误区：看到 &lt;code&gt;MatMul&lt;/code&gt; 就觉得它是矩阵乘法，看到 &lt;code&gt;Conv2D&lt;/code&gt; 就觉得它是卷积，好像知道算子名字就差不多知道它怎么跑了。&lt;/p&gt;
&lt;p&gt;后来我发现这只是知道“它要算什么”。离“它在某块硬件上怎么跑得快”，中间还隔着 shape、layout、schedule、内存层级和硬件指令。&lt;/p&gt;
&lt;p&gt;这篇不是深度学习编译器教程，只是我给自己补的一层理解：&lt;strong&gt;算子定义语义，调度改执行方式，性能模型帮编译器判断哪个实现更值得用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个理解对我比较有用，是因为后面做端侧部署时，问题经常不是“模型能不能转格式”，而是更底下的东西：这个 shape 有没有合适 kernel，layout 转换会不会抵消收益，中间张量会不会把内存顶满，目标硬件到底擅长哪类计算。&lt;/p&gt;
&lt;h2 id="算子像合同不像施工图"&gt;算子像合同，不像施工图
&lt;/h2&gt;&lt;p&gt;深度学习模型最后会落到一堆算子上：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Conv2D
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MatMul / GEMM
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BatchNorm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ReLU
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Softmax
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LayerNorm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Add
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Transpose
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这些名字更像语义合同。它们说清楚输入输出的数学关系，但没有告诉你循环怎么排、数据怎么搬、线程怎么分。&lt;/p&gt;
&lt;p&gt;拿矩阵乘法来说，最朴素的三层循环当然能算对：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码的问题不是“数学不对”，而是它没有表达任何硬件意识：cache 怎么复用，SIMD 怎么用，多线程怎么分块，GPU 上 block 和 shared memory 怎么配，端侧 NPU 又有哪些 kernel 和 buffer 限制。&lt;/p&gt;
&lt;p&gt;所以我现在更愿意把算子看成一个上层接口。它告诉编译器要做哪类计算；至于怎么把这个计算铺到硬件上，是调度要解决的事情。&lt;/p&gt;
&lt;h2 id="调度改的是执行形态"&gt;调度改的是执行形态
&lt;/h2&gt;&lt;p&gt;调度源语不会改变计算结果，但会改变循环结构、内存访问和并行方式。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;源语&lt;/th&gt;
 &lt;th&gt;作用&lt;/th&gt;
 &lt;th&gt;我自己的理解&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;split&lt;/code&gt; / &lt;code&gt;tile&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;拆循环、分块&lt;/td&gt;
 &lt;td&gt;不要一口吃完整个矩阵，先切成能复用的小块&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;reorder&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;调整循环顺序&lt;/td&gt;
 &lt;td&gt;让连续访问更连续，少一点 cache miss&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;vectorize&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;向量化&lt;/td&gt;
 &lt;td&gt;让一条 SIMD 指令处理多个元素&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;unroll&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;展开循环&lt;/td&gt;
 &lt;td&gt;少一点循环控制，多一点指令级并行&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;parallel&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;并行化&lt;/td&gt;
 &lt;td&gt;把不同块分给不同线程或计算单元&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;cache_read&lt;/code&gt; / &lt;code&gt;cache_write&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;缓存读写&lt;/td&gt;
 &lt;td&gt;把热点数据放到更近的存储里&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;compute_at&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;控制中间计算位置&lt;/td&gt;
 &lt;td&gt;决定中间结果在哪层循环里生成&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;inline&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;内联简单计算&lt;/td&gt;
 &lt;td&gt;减少不必要的中间结果&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些词看起来很“编译器”，但背后的目标很朴素：&lt;strong&gt;让数据少搬一点，让计算单元少等一点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如对 &lt;code&gt;MatMul&lt;/code&gt; 做 tile，代码形态可能变成这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;jo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;jo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;jo&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;jo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个例子还很粗糙。真实系统里还会继续调循环顺序、向量宽度、展开因子、线程绑定、shared memory 使用方式。重点不是 &lt;code&gt;32&lt;/code&gt; 这个数字，而是它把一个大矩阵乘法改成了一组更适合缓存和并行的小问题。&lt;/p&gt;
&lt;h2 id="调度空间很快会失控"&gt;调度空间很快会失控
&lt;/h2&gt;&lt;p&gt;一个矩阵乘法看起来简单，但只要开始调度，可选项马上变多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tile size 取多少；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i/j/k&lt;/code&gt; 三层循环怎么排序；&lt;/li&gt;
&lt;li&gt;SIMD 宽度怎么用；&lt;/li&gt;
&lt;li&gt;哪一层循环展开；&lt;/li&gt;
&lt;li&gt;哪一层并行；&lt;/li&gt;
&lt;li&gt;A、B、C 哪些数据要进 cache；&lt;/li&gt;
&lt;li&gt;中间结果什么时候写回；&lt;/li&gt;
&lt;li&gt;数据布局要不要变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些选择不是互相独立的。tile size 变了，cache 复用会变；循环顺序变了，访问模式会变；layout 变了，前后算子的连接成本也会变。&lt;/p&gt;
&lt;p&gt;这就是 AutoTVM、Ansor、Triton autotune 这类系统有意义的地方。它们本质上都在回答一个问题：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;同一个计算，在这块硬件上，哪种执行方式更划算？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这里的“划算”也不只是快。编译时间、内存峰值、部署体积、动态 shape 支持，都会影响最后选择。&lt;/p&gt;
&lt;h2 id="性能模型不能只看-flops"&gt;性能模型不能只看 FLOPs
&lt;/h2&gt;&lt;p&gt;以前我看算子性能，很容易先看计算量。比如一个大 &lt;code&gt;MatMul&lt;/code&gt; 有多少 MACs，一个卷积有多少 FLOPs。&lt;/p&gt;
&lt;p&gt;这个指标有用，但只看它会误判。&lt;/p&gt;
&lt;p&gt;一个很粗的估计是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;执行时间 ≈ 计算量 / 硬件峰值算力
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;真实执行经常卡在别的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存带宽；&lt;/li&gt;
&lt;li&gt;cache 命中率；&lt;/li&gt;
&lt;li&gt;数据复用；&lt;/li&gt;
&lt;li&gt;SIMD 利用率；&lt;/li&gt;
&lt;li&gt;kernel launch overhead；&lt;/li&gt;
&lt;li&gt;layout conversion；&lt;/li&gt;
&lt;li&gt;中间结果读写；&lt;/li&gt;
&lt;li&gt;算子是否能融合；&lt;/li&gt;
&lt;li&gt;硬件有没有对应特殊指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我现在会把性能问题写成这一串：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;operator + shape + dtype + layout + schedule + hardware
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;少一个维度，结论都可能变。&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;Conv2D&lt;/code&gt; 本身不是一个性能答案。&lt;code&gt;1x1 Conv&lt;/code&gt;、&lt;code&gt;3x3 Conv&lt;/code&gt;、depthwise conv、不同 batch、不同 layout，优化方向都可能不一样。一个 kernel 在桌面 GPU 上很快，不代表放到端侧芯片上也合适。&lt;/p&gt;
&lt;h2 id="roofline-给了一个判断边界"&gt;Roofline 给了一个判断边界
&lt;/h2&gt;&lt;p&gt;Roofline Model 里有个概念叫算术强度：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Arithmetic Intensity = 计算量 / 数据搬运量
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我觉得它最有用的地方，是帮我先判断瓶颈大概在哪。&lt;/p&gt;
&lt;p&gt;大规模 &lt;code&gt;MatMul&lt;/code&gt; 往往更接近 &lt;code&gt;compute-bound&lt;/code&gt;。它的计算量约是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;M × N × K 次 MAC
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果按 FLOPs 统计，通常约是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2 × M × N × K
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A、B 的元素会被反复复用，所以只要调度做得好，它比较容易把计算单元喂饱。这也是 GPU、NPU、TensorCore 都很重视 GEMM 的原因。&lt;/p&gt;
&lt;p&gt;反过来，&lt;code&gt;ReLU&lt;/code&gt;、&lt;code&gt;Add&lt;/code&gt; 这类算子计算很轻。以 ReLU 为例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;y = max(x, 0)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;每个元素只做一次比较，但输入要读，输出要写。它的瓶颈很容易变成访存。&lt;/p&gt;
&lt;p&gt;这时候单独优化 ReLU 的计算指令意义有限。更实际的做法是融合。&lt;/p&gt;
&lt;h2 id="融合不是为了看起来高级"&gt;融合不是为了看起来高级
&lt;/h2&gt;&lt;p&gt;比如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Conv -&amp;gt; BatchNorm -&amp;gt; ReLU
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果三个算子分开跑，中间结果会反复写回内存、再读出来。&lt;/p&gt;
&lt;p&gt;推理阶段可以先做 BN folding：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BN(Conv(x, W, b)) -&amp;gt; Conv(x, W&amp;#39;, b&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后再把 &lt;code&gt;Conv + ReLU&lt;/code&gt; 融成一个 kernel。这样 Conv 算完以后直接做 ReLU，最终结果写回一次。&lt;/p&gt;
&lt;p&gt;这里的重点不是“融合”这个词，而是它减少了中间张量的读写。对于 memory-bound 的部分，这往往比抠几条计算指令更有用。&lt;/p&gt;
&lt;h2 id="性能模型有几种粗细"&gt;性能模型有几种粗细
&lt;/h2&gt;&lt;p&gt;工程里不可能所有东西都靠完整搜索。不同系统会混着用几类方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;手工规则&lt;/strong&gt;最容易落地。&lt;/p&gt;
&lt;p&gt;比如大规模 MatMul 走某个加速库，小算子回退 CPU，&lt;code&gt;ReLU/Add&lt;/code&gt; 尽量融合，&lt;code&gt;Reshape&lt;/code&gt; 尽量消除。规则可控，但维护成本高，对新 shape 和新硬件不一定泛化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解析代价模型&lt;/strong&gt;会估计计算时间和访存时间：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Compute_Time = Ops / Effective_Compute_Throughput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Memory_Time = Bytes / Effective_Memory_Bandwidth
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Time ≈ max(Compute_Time, Memory_Time) + Overhead
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它不一定准，但能告诉你方向。比如某个卷积算出来访存时间明显大于计算时间，那就应该优先想 layout、融合、复用，而不是只盯着峰值算力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实测加搜索&lt;/strong&gt;更贴近真实硬件。&lt;/p&gt;
&lt;p&gt;系统生成多个候选 schedule，编译后在目标硬件上跑，记录时间，再继续搜索。它的好处是能适应实际环境，坏处也明显：慢、复杂、需要目标硬件反馈。&lt;/p&gt;
&lt;h2 id="编译器最后是在做选择题"&gt;编译器最后是在做选择题
&lt;/h2&gt;&lt;p&gt;把这些东西放一起看，深度学习编译器一直在做选择：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个算子放 CPU、GPU 还是 NPU？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个 shape 用哪个 kernel？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个 layout 要不要转换？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个中间结果要不要写回？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个算子能不能融合？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;这个 schedule 值不值得编译？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以它不是简单地把 ONNX、TensorFlow 或 PyTorch 模型换个格式。格式转换只是入口，后面还有图优化、算子选择、调度搜索、性能建模、内存规划和代码生成。&lt;/p&gt;
&lt;p&gt;这也接上了 MLC 第一章里讲的“抽象与实现”。算子、计算图、循环嵌套、kernel、运行库，都是同一个计算在不同层级上的表示。编译器要做的，就是在这些表示之间转换，并尽量找到适合目标环境的实现。&lt;/p&gt;
&lt;h2 id="我现在的理解"&gt;我现在的理解
&lt;/h2&gt;&lt;p&gt;这篇写完以后，我对“机器学习编译”的理解比之前具体了一点。&lt;/p&gt;
&lt;p&gt;算子是语义层，它说清楚要算什么。&lt;/p&gt;
&lt;p&gt;调度是实现层，它决定循环、并行、缓存和数据移动怎么组织。&lt;/p&gt;
&lt;p&gt;性能模型是决策层，它判断某个实现放在某块硬件上到底值不值得。&lt;/p&gt;
&lt;p&gt;以前我会把“优化模型部署”理解成换框架、转格式、开量化。现在看，真正难的地方在更下面：同一个计算，换一种执行组织方式，性能可能完全不一样。&lt;/p&gt;
&lt;p&gt;参考资料：&lt;a class="link" href="https://book-zh.mlc.ai/chapter_introduction/index.html" target="_blank" rel="noopener"
 &gt;《机器学习编译》中文课程导论&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>