<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://www.languisher.top/</id>
    <title>Languisher</title>
    <updated>2026-04-11T09:03:37.860Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>languisher</name>
        <uri>https://www.languisher.top/</uri>
    </author>
    <link rel="alternate" href="https://www.languisher.top/"/>
    <link rel="self" href="https://www.languisher.top/atom.xml"/>
    <subtitle>没有想好要写什么</subtitle>
    <rights>Copyright © 2026 languisher</rights>
    <entry>
        <title type="html"><![CDATA[Git 基础（一）：Git 对象和引用]]></title>
        <id>https://www.languisher.top/posts/git-basics-1/</id>
        <link href="https://www.languisher.top/posts/git-basics-1/"/>
        <updated>2026-04-10T11:11:30.472Z</updated>
        <summary type="html"><![CDATA[在软件开发中，一个项目从最初的几行代码，到逐渐演化成复杂系统，整个开发过程本质上是一系列变化的积累。但问题在于，变化本身是很难管理的。我们如...]]></summary>
        <content type="html"><![CDATA[<p>在软件开发中，一个项目从最初的几行代码，到逐渐演化成复杂系统，整个开发过程本质上是一系列变化的积累。</p>
<p>但问题在于，<strong>变化本身是很难管理的。</strong> 我们如何描述项目状态的变化？如果没有工具，我们只能：</p>
<ul>
<li>手动复制文件做备份（例如 <code>project_final_v3_real_final</code>）</li>
<li>靠记忆或零散笔记记录修改原因</li>
<li>在多人协作时反复覆盖彼此的工作</li>
</ul>
<p>这在本质上是混乱的，因为我们没有一个结构化的方式来记录历史。</p>
<p><strong>版本控制系统（Version Control System, VCS）</strong> 正是为了解决这个问题而出现的。它将一个项目的演化过程抽象为一系列“快照”（snapshot）及其之间的关系。</p>
<p>从这个角度看，一个项目的开发过程可以类比为一个<strong>状态机</strong>：</p>
<ul>
<li>每一个快照是一个“状态”，而开发过程就是在这些状态之间不断演进。</li>
<li>每一个快照记录某一时刻项目的完整状态，同时附带元信息（作者、时间、修改说明等），从而让这些变化变得可追踪、可理解、可回溯。</li>
</ul>
<p>本文简单介绍 <strong>Git</strong>，作为现代版本控制系统的事实标准，是如何解决这些问题的。</p>
<h2>Git Repository and Data Types</h2>
<p><strong>Git</strong> 通常和一个文件夹关联，记录这个文件夹在不同时间点的状态。</p>
<p><strong>Git 对象</strong> 是对项目数据的分层建模：</p>
<ul>
<li>blob 表示文件内容</li>
<li>tree 表示目录结构</li>
<li>commit 表示某一时刻整个项目的状态。这里“状态”不仅是指项目的当前文件快照（内容），还包括基于哪一个上一个状态（时间关系），以及做出这些变化的作者、时间、message 等信息。</li>
</ul>
<pre><code>type object = blob | tree | commit
</code></pre>
<h3>文件系统相关：Blob, Tree 和 Snapshot</h3>
<p>文件树中的文件和目录对应 Git 的两种对象：</p>
<ul>
<li><strong>Blob</strong> - File：文件内容（纯数据）</li>
<li><strong>Tree</strong> - Directory：目录结构（名字 + 指针）</li>
</ul>
<p><img src="Attachments/TreeBlob3.png" alt="" /></p>
<p>因此：</p>
<pre><code>// a file is a bunch of bytes
type blob = array&lt;byte&gt;

// a directory contains named files and directories
type tree = map&lt;string, tree | blob&gt;
</code></pre>
<p>项目的文件<strong>快照（Snapshot）</strong>. 即某一时刻整个项目的完整结构 + 内容。Snapshot 可以用项目根目录的 tree 来表示，例如下图就展示了一个可能的文件快照。</p>
<p><img src="Attachments/TreeBlob.png" alt="" /></p>
<h3>历史追踪相关：History 和 Commit</h3>
<p><strong>History</strong>. 在 Git 中，历史可以表示为一个有向无环图（DAG），其中每个节点是项目的一个 snapshot.</p>
<ul>
<li>这意味着，除了根节点之外（即初始状态），每个 snapshot 都会指向一个或多个父节点</li>
<li>这意味着这个 snapshot 由之前项目的每个状态经过若干改变修正而来</li>
</ul>
<p><strong>Commit</strong> 记录了一次项目状态. Commit 是 Git 历史中的一个节点，它包含</p>
<ul>
<li>一个文件快照，即当前时刻项目文件的状态，也就是从项目根目录为根结点的 tree，描述该时刻文件结构和内容</li>
<li>指向其父 commit 的引用，记录是从哪一个快照而来的，从而将多个 snapshot 连接成一个有向无环图。</li>
<li>一些元信息，例如作者、提交时间和 message</li>
</ul>
<p><img src="Attachments/GitCommit.png" alt="" /></p>
<pre><code>// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parents: array&lt;commit&gt;
    author: string
    message: string
    snapshot: tree
}
</code></pre>
<p>Git 中的 commit 是不可变的。这不意味着 commit 中的错误无法被修正，只是说，对提交历史的“修改”，实际上是创建新的 commit，然后更新引用（reference）去指向这些新的 commit。</p>
<h3>总结：Commit, Tree 和 Blob</h3>
<p>Commit, tree 和 blob 的概念可以从下图体现。Commit 通过存储项目根目录 tree 指针的方式记录项目的 snapshot.</p>
<p><img src="Attachments/TreeBlob2.png" alt="" /></p>
<h2>Git 中的引用</h2>
<p>因为 Git 对象是根据项目内容 hash 标识的，基于项目历史不可篡改的项目，所有 Git 对象都是不可变的。假如我们想回到项目之前历史的某一时刻，我们需要改变项目的状态。</p>
<p>这可以通过指针来实现，用以指向某个 commit，从而表示当前所在的位置。例如，</p>
<ul>
<li>HEAD 表示当前项目的位置，通过指向某个 commit（通常经由分支）来确定当前状态</li>
<li>Branch 表示一条开发分支，指向某个 commit，并随着新的提交不断向前移动，从而记录一条状态的演化路径。与此同时，不同的 branch 表示项目历史中不同的开发路径，例如并行进行修复代码逻辑 A 的 bug 和增加新功能代码逻辑 B. 一条 Branch 可以被命名为 <code>main</code>, <code>master</code>, <code>dev-XXX</code> 等.</li>
</ul>
<p><img src="Attachments/GitHEAD.png" alt="" /></p>
<p><strong>Git Reference</strong>. 即指向某一个 commit 的指针，并且可以被灵活移动。其中，</p>
<ul>
<li>一部分 reference（如 branch）是可移动的，会随着新的提交不断向前推进；</li>
<li>而另一部分（如 tag）则用于固定标记历史中的特定位置，通常不会改变。</li>
</ul>
<h2>Git Repository：可回溯的状态空间</h2>
<p>从上面的角度来看，Git 实际上构建了一个完整的“项目状态空间”：</p>
<ul>
<li><strong>objects（blob / tree / commit）</strong> 描述了“状态是什么”</li>
<li><strong>references（HEAD / branch / tag）</strong> 描述了“当前在哪个状态，以及如何移动”</li>
</ul>
<p>因此可以理解为 <strong>Git = 一个不可变的状态集合 + 一组可移动的指针</strong>. 这使得 Git 具备了几个关键能力：</p>
<ul>
<li>可以精确记录每一个历史状态（snapshot）</li>
<li>可以在不同状态之间自由切换（checkout）</li>
<li>可以并行演化多个开发路径（branch）</li>
<li>可以在不破坏历史的前提下进行修改（通过创建新的 commit）</li>
</ul>
<p>因此，Git 构建了一个可导航、可回溯、可分叉的状态空间，而开发过程本质上就是在这个状态空间中的不停移动。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://missing.csail.mit.edu/2026/version-control/">Version Control and Git</a></li>
<li><a href="https://alexwlchan.net/a-plumbers-guide-to-git/2-blobs-and-trees/">Part 2: Blobs and trees</a></li>
<li><a href="https://medium.com/swlh/git-from-the-bits-and-pieces-beyond-the-basics-part-1-aca2d02d360b">Git from the bits and pieces, beyond the basics — Part 1</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-24T19:37:40.472Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[强化学习基础（1）：马尔可夫决策过程]]></title>
        <id>https://www.languisher.top/posts/markov-chain/</id>
        <link href="https://www.languisher.top/posts/markov-chain/"/>
        <updated>2026-03-21T17:51:58.499Z</updated>
        <summary type="html"><![CDATA[马尔可夫决策过程（Markov decision process，MDP）是强化学习的重要概念。本文在动手学强化学习著作的基础上，对于马尔可...]]></summary>
        <content type="html"><![CDATA[<p><strong>马尔可夫决策过程</strong>（Markov decision process，MDP）是强化学习的重要概念。本文在<a href="https://hrl.boyuai.com/chapter/1/%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E5%86%B3%E7%AD%96%E8%BF%87%E7%A8%8B">动手学强化学习</a>著作的基础上，对于马尔可夫决策过程进行了总结。</p>
<h2>随机过程</h2>
<p><strong>随机过程 (stochastic process)</strong> 研究对象是随时间演变的随机现象，其中：</p>
<p><strong>状态 (state)</strong>：随机现象在某时刻 $t$ 的取值是一个向量随机变量 $S_{t} \in \mathcal{S}$，其中 $\mathcal{S} = { s_{1},\dots, s_{n} }$ 表示所有可能的状态集合，且 $|\mathcal{S}|=n$ 表示可能的状态数量.</p>
<ul>
<li>举例：$S_t=$ (机器人位置，机器人速度，机器人加速度)</li>
</ul>
<p><strong>状态转移</strong>：在某个时刻 $t$ 的状态 $S_{t}$ 取决于 $t$ 时刻之前的状态 ${S_{1},\dots, S_{t-1}}$.</p>
<p><strong>状态转移概率</strong>：已知历史信息 $(S_{1},\dots,S_{t})$ 时，下一个时刻状态为 $S_{t+1}$ 的概率是：
$$
\mathbb{P}(S_{t+1}|S_{1},\dots,S_{t})
$$</p>
<h2>马尔可夫过程</h2>
<blockquote>
<p>[!note]
马尔可夫过程可以看作一个带概率的状态机：其描述了一类随机系统，其中状态随时间演化，并满足：下一时刻的状态只依赖当前状态，而与过去无关。</p>
</blockquote>
<p><strong>马尔可夫性质 (Markov property)</strong>. 在某个时刻 $t+1$ 的状态只取决于上一时刻，即在时刻 $t$ 的状态。用公式表述就是：
$$
\mathbb{P}(S_{t+1}= s_{t+1}|S_{t}=s_{t}) = \mathbb{P}(S_{t+1} = s_{t+1}| S_{1} = s_{1},\dots,S_{t}=s_{t})
$$</p>
<ul>
<li>虽然马尔可夫性质使得下一时刻的状态只依赖当前状态，看似忽略了历史信息，但实际上可以通过将历史信息编码到当前状态中，从而不丢失对未来预测所需的信息。</li>
</ul>
<p><strong>状态转移矩阵 (state transition matrix)</strong>. 基于马尔可夫性质的状态转移概率可以用矩阵表述：
$$
\mathcal{P} = \begin{bmatrix}
\mathbb{P}(S_{t+1}|S_{t})
\end{bmatrix}<em>{|\mathcal{S}| \times |\mathcal{S}|} = \begin{bmatrix}
\mathbb{P}(s</em>{1}|s_{1})  &amp;  \dots &amp;  \mathbb{P}(s_{n}|s_{1}) \
\vdots  &amp; \ddots  &amp; \vdots \
\mathbb{P}(s_{1}|s_{n})  &amp; \dots  &amp;  \mathbb{P}(s_{n}|s_{n})
\end{bmatrix}
$$</p>
<ul>
<li>每一行表示一种初始状态</li>
<li>每一列表示一种转移后的状态</li>
<li>对于第 $i$ 行第 $j$ 列矩阵表示 $\mathbb{P}(S_{t+1}=s_{j}| S_{t} = s_{i})$，即 $s_{i}\to s_{j}$ 的状态转移</li>
</ul>
<p><img src="Attachments/MarkovProcess.png" alt="" /></p>
<p><strong>马尔可夫过程 (Markov process)</strong>. 指具有马尔可夫性质的随机过程，可以表示为
$$
\text{Markov process} = \langle \mathcal{S}, \mathcal{P}\rangle
$$</p>
<p><strong>状态序列 (episode)</strong>. 一串按时间顺序排列的状态</p>
<ul>
<li>例如 $s_{1}\to s_{1} \to s_{2}\to s_{3}\to s_{6}$</li>
</ul>
<p><strong>采样 (sampling)</strong>. 从某一个状态开始，根据它的状态转移矩阵，一步一步随机生成一个状态序列的过程。</p>
<h2>马尔可夫奖励过程</h2>
<blockquote>
<p>[!note]
马尔可夫奖励过程是在马尔可夫过程的基础上，引入奖励函数，用于评估从某个状态出发的长期回报，从而衡量状态的优劣。</p>
<p>重要概念：</p>
<ul>
<li>回报描述了从当前时刻开始直到终止状态时，<strong>所有</strong>奖励的衰减之和</li>
<li>价值函数描述了从状态出发的<strong>期望回报</strong>。</li>
</ul>
</blockquote>
<h3>问题建模</h3>
<p><strong>奖励 (reward)</strong>. 对于一个状态 $s$，其奖励 $r(s) \in \mathbb{R}$ 表示处于（或者理解为从任意状态转移到）该状态时所获得的期望奖励。</p>
<ul>
<li>这是单步的</li>
<li>其向量化表示： $\mathcal{R} = \begin{bmatrix} R(s_{1})  \ \vdots  \  R(s_{n})\end{bmatrix}$</li>
</ul>
<p><strong>回报 (return)</strong>. 对于一个状态序列，从第 $t$ 时刻开始直到终止状态时，所有奖励的衰减之和
$$
G_{t}= R_{t}+ \gamma R_{t+1} + \gamma^2 R_{t+2}+\dots = \sum_{k=0}^\infty \gamma^k R_{t+k}
$$</p>
<ul>
<li>这是多步的</li>
<li>$\gamma\in [0,1)$ 是折扣因子，表示我们倾向于尽快获得奖励、对远期利益打折扣的程度</li>
<li>$R_{t}$ 表示在时刻 $t$ 获得的奖励</li>
</ul>
<p><img src="Attachments/MRP.png" alt="" /></p>
<p><strong>马尔可夫奖励过程 (Markov reward process)</strong>. 在马尔可夫过程的基础上，加上每个状态的奖励函数和回报的折扣因子，可以表示为
$$
\text{Markov reward process} =\langle \mathcal{S}, \mathcal{P}, \mathcal{R}, \gamma \rangle
$$</p>
<ul>
<li>通过定义奖励和状态转移，使我们可以用“长期期望回报”来量化一个状态的好坏</li>
</ul>
<h3>状态价值函数</h3>
<p><strong>价值 (value)</strong> 和 <strong>价值函数 (value function)</strong>. 对于一个状态 $s$，其期望回报称之为该状态的价值。所有状态的价值组成了价值函数
$$
V(s) = \mathbb{E} [G_{t}|S_{t}=s]
$$</p>
<ul>
<li>期望回报是指在给定状态转移概率（以及策略）下，对所有可能的未来状态序列的回报取期望。</li>
<li>价值函数是指对于所有状态，建立一个从状态到其价值的映射</li>
</ul>
<p><strong>贝尔曼方程 (Bellman equation)</strong>. 求解每个状态的价值. 推导：</p>
<p>$$
\begin{align}
V(s) &amp;= \mathbb{E}[G_{t}|S_{t}= s]  \
&amp;= \mathbb{E}[R_{t}+ \gamma (R_{t+1}+ \gamma R_{t+2}+\gamma^2R_{t+3}+\dots)|S_{t}=s] \
&amp;= \mathbb{E}[R_{t}+\gamma G_{t+1}|S_{t}=s] \
&amp;= \mathbb{E}[R_{t}|S_{t}=s] + \gamma \sum_{s_{t+1}\in \mathcal{S}}\mathbb{E}[G_{t+1}|S_{t+1}=s_{t+1}] \mathbb{P}(S_{t+1}=s_{t+1}| S_{t}=s_{t}) \
&amp;= r(s) + \gamma \sum_{s' \in \mathcal{S}} V(s') p(s'|s)
\end{align}
$$
可以看到，任意状态 $s$ 的价值可以理解为以下几项之和：</p>
<ul>
<li>当前拿到的及时奖励 $r(s)$</li>
<li>未来价值的期望：所有在下一个时刻可能的状态 $s'$
<ul>
<li>计算各自的价值 $V(s')$</li>
<li>计算各自的概率，即转移函数 $p(s'|s)$</li>
<li>两者相乘，再乘以折扣因子 $\gamma$</li>
</ul>
</li>
</ul>
<p><img src="Attachments/BellmanEquation.png" alt="" /></p>
<p>将价值、转移函数和奖励向量化，则得到：
$$
\mathcal{V} = \mathcal{R} + \gamma \mathcal{P} \mathcal{V} \implies \mathcal{V} = (1 - \gamma \mathcal{P})^{-1} \mathcal{R}
$$</p>
<p>其中 $\mathcal{V} = \begin{bmatrix} V(s_{1})  \ \vdots  \  V(s_{n})\end{bmatrix}$</p>
<h2>马尔可夫决策过程</h2>
<blockquote>
<p>[!note]
现在考虑有智能体参与的环境，智能体通过做出动作改变了状态之间的转移概率以及获得奖励的方式。</p>
<p>重要概念：在本章节中，虽然 {状态/动作} 价值函数同样也是表达 {特定状态/基于现在状态做出特定动作} 的远期期望收益，但是现在其和智能体采取的策略 $\pi$，也就是智能体具体会做什么动作有关。</p>
</blockquote>
<h3>问题建模</h3>
<p><strong>智能体 (agent)</strong>. 之前介绍的过程是自发改变的随机过程，现在我们引入外界的“刺激”来改变这个随机过程，我们将这个刺激称为智能体。具体而言，在基础的马尔可夫过程中，环境会从 $s_{t} \to s_{t+1} \to s_{t+2} \to \dots$ 从状态到状态的变化。智能体在中间引入了动作的概念，即：</p>
<ul>
<li>智能体有一些可选的<strong>动作(action)</strong>，其集合为 $\mathcal{A}$</li>
<li>智能体的 <strong>策略 (policy)</strong>. 这些动作是基于输入状态 $s$ 采取的。基于状态 $s$ 的情况下，智能体采取动作 $a$ 的概率为：$\pi(a|s) = \mathbb{P}(A_{t}=a|S_{t}=s)$.</li>
<li>状态转移函数：现在下一个时刻的状态不仅取决于上一个时刻的状态，还取决于上一个时刻智能体所做出的动作：$\mathbb{P}(s'|s,a)$</li>
<li>奖励函数：现在奖励函数不只是由状态，还和智能体决策（动作）一起共同决定：$r(s,a)$
<ul>
<li>这意味着智能体不再是进入状态 $s$ 就获得奖励，而是基于状态 $s$ 做出动作 $a$ 再获得奖励</li>
</ul>
</li>
</ul>
<p><img src="Attachments/MRPvsMDP.png" alt="" /></p>
<p><strong>马尔可夫决策过程 (Markov decision process)</strong>. 在马尔可夫奖励过程的基础上，引入智能体的动作概念，并且修改状态转移函数和奖励函数，使其现在和智能体的动作有关：
$$
\text{Markov decision process}= \langle \mathcal{S}, \mathcal{A}, \mathbb{P}(s'|s,a), r(s,a), \gamma \rangle
$$</p>
<h3>状态价值函数和动作价值函数</h3>
<p><img src="Attachments/StateValueandActionValueFunction.png" alt="" /></p>
<p><strong>状态价值函数 (state-value function)</strong>. 和在马尔可夫奖励过程的状态的价值函数相似，描述从状态 $s$ 出发，遵守策略 $\pi$ 所能获得期望回报：
$$
V^\pi (s) = \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s]
$$</p>
<p><strong>贝尔曼期望方程 (Bellman Expectation Equation)——状态价值函数</strong>：</p>
<ul>
<li>智能体会基于该状态 $s$ 做出动作 $a$，且概率为 $\pi(s,a)$</li>
<li>对于每个动作 $a$，会带来奖励 $r(s,a)$</li>
<li>这会导致智能体可能进入状态 $s'$，概率为 $p(s'|s,a)$</li>
<li>对应的状态 $s'$ 具有价值 $V^\pi(s')$</li>
<li>因为是远期收益，需要加上折扣因子 $\gamma$</li>
</ul>
<p>因此，可以写为：
$$
\begin{align}
V^\pi (s) &amp;= \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s]  \
&amp;= \mathbb{E}<em>{\pi}[R</em>{t}+\gamma R_{t+1}+ \gamma^2R_{t+2}+ \dots |S_{t}=s] \
&amp;= \mathbb{E}<em>{\pi}[R</em>{t}|S_{t}=s] + \gamma \mathbb{E}<em>{\pi} [G</em>{t+1}|S_{t}=s] \
&amp;= \sum_{a \in \mathcal{A}}\pi(a|s)r(s,a) + \gamma\sum_{a\in \mathcal{A}} \sum_{s' \in \mathcal{S}} \underbrace{\pi(a|s),\mathbb{P}(s'|s,a)}<em>{\text{probability of transitioning from } s \text{ to } s' \text{ via } a}\mathbb{E}</em>{\pi}[G_{t+1}|S_{t+1}=s'] \
&amp;= \sum_{a\in\mathcal{A}} \pi(a|s) \left( r(s,a) + \gamma \sum_{s' \in \mathcal{S}}  \mathbb{P}(s'|s, a) V^\pi (s') \right)
\end{align}
$$</p>
<p><strong>动作价值函数 (action-value function)</strong>. 与此同时，我们需要衡量在当前策略下，对当前状态 $s$ 做出动作 $a$ 所带来的收益：
$$
Q^\pi (s, a) = \mathbb{E}<em>{\pi}[G</em>{t}|S_{t}=s, A_{t}=a]
$$</p>
<p>动作价值函数和状态价值函数的关系：</p>
<ul>
<li>状态 $s$ 的价值函数等于
<ul>
<li>策略 $\pi$ 基于状态 $s$ 采取动作 $a$ 的概率 $\pi(a|s)$</li>
<li>乘以这个动作的价值 $Q^\pi(s,a)$</li>
</ul>
</li>
<li>动作 $(s,a)$ 的价值函数等于
<ul>
<li>及时奖励</li>
<li>加上经过衰减后的未来奖励，即
<ul>
<li>下一个时刻进入状态 $s'$ 的概率 $\mathbb{P}(s'|s,a)$</li>
<li>对应状态 $s'$ 的价值 $V^\pi (s')$</li>
<li>的加权求和</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>用公式表达即：
$$
\begin{cases}
V^\pi(s) = \sum_{a \in \mathcal{A}} \pi(a|s) Q^\pi (s, a) \
Q^\pi(s,a) = r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^\pi (s')
\end{cases}
$$</p>
<p><strong>贝尔曼期望方程 (Bellman Expectation Equation)——动作价值函数</strong>：基于以上两个关系，可以消除 $V^\pi(s)$，得到 $Q^\pi(s,a)$ 的表达式：
$$
Q^\pi(s,a) = r(s, a) + \gamma \sum_{s' \in\mathcal{S}}  \mathbb{P}(s'|s,a)  \sum_{a' \in A}\pi(a'|s') Q^\pi (s',a')
$$</p>
<blockquote>
<p>这些公式可以直接从图中直观观察到，本质上是概率乘以对应奖励加权求和。</p>
</blockquote>
<h3>马尔可夫决策过程求解</h3>
<p>通过贝尔曼方程，我们可以得到马尔可夫奖励过程的解析解。基于此，我们通过将马尔可夫决策过程退化成马尔可夫奖励过程得到其解析解：</p>
<p>状态奖励：从 $r(s,a)$ 到 $r(s)$ 做动作奖励的加权求和：
$$
r(s) = \sum_{a\in \mathcal{A}} \pi(a|s) r(s, a)
$$</p>
<p>同理，从 $p(s'|s,a)$ 到 $p(s'|s)$ 转移概率也对所有动作做加权求和：
$$
\mathbb{P}(s'|s)= \sum_{a\in \mathcal{A}} \pi(a|s) \mathbb{P}(s'|s,a)
$$
这样就可以做 MDP 到 MRP 的转换：
$$
\langle \mathcal{S}, \mathcal{A}, \mathbb{P}(s'|s,a), r(s,a), \gamma \rangle \to \langle \mathcal{S}, \mathbb{P}'(s'|s), r'(s), \gamma \rangle
$$</p>
<h2>最优策略和贝尔曼最优方程</h2>
<blockquote>
<p>[!note]
<strong>最优策略</strong>: 最优策略的目标是找到一个策略，使得其对应的价值函数在所有策略中最大。</p>
</blockquote>
<p><strong>最优策略</strong>. 如何定义一个策略 $\pi(a|s)$ 是最优的？</p>
<ul>
<li>这意味着从任意状态 $s \in \mathcal{S}$ 出发，智能体所获得的预期远期收益都是最高的</li>
<li>这个期望收益可以用状态价值函数来描述</li>
</ul>
<p>因此，如果 $\pi^* \in \Pi$ 是最优策略，它满足：
$$
\forall s\in \mathcal{S}, \quad V^{<em>} (s) = \max_{\pi}V^\pi(s)
$$
其中 $V^</em>(s) = V^{\pi^*}(s)$，</p>
<p>换言之，当系统处于状态 $s$ 的时候，最优策略是所有策略中使其价值函数最大的那一个：</p>
<p>$$
\boxed{\pi^* = \arg \max_{\pi} V^\pi(s)}
$$</p>
<p>同样的，对于任意状态-动作对 $(s,a)\in \mathcal{S}\times\mathcal{A}$（意思是处于状态 $s$ 且智能体采取动作 $a$ 的情况下），策略$\pi^<em>$ 最优意味着：在之后继续采取策略 $\pi^</em>$ 为这个状态-动作对所带来的远期价值最高。
$$
\forall (s, a) \in \mathcal{S} \times \mathcal{A}, \quad Q^*(s,a) = \max_{\pi}Q^\pi (s, a)
$$</p>
<p><strong>最优策略在任意状态 $s$ 下，一定会在所有可能的状态-动作对 $(s,.)$ 中，采取动作 $a$ 使得状态-动作对 $(s,a)$ 使得其动作价值最高。</strong> 换言之：
$$
\pi^<em>(s) \in \arg \max_{a \in \mathcal{A}}Q^</em>(s, a), \quad V^<em>(s) = \max_{a \in \mathcal{A}} Q^</em>(s, a)
$$
如果 $\pi^<em>$ 是确定性策略，则 $V^</em>(s)= Q^<em>(s, \pi^</em>(s))$</p>
<p><strong>贝尔曼最优方程 (Bellman optimality equation)</strong>. 对于最优状态价值函数和最优动作价值函数进行推导。</p>
<p>首先最优策略意味着在状态-动作对 $(s,a)$ 下，无论在下一个时刻进入哪个状态 $s'$，都会带来该状态的最优状态价值。这意味着：
$$
\begin{align}
Q^<em>(s, a)
&amp;=   r(s,a) + \gamma \sum_{s\in \mathcal{S}} p(s'|s,a)  V^</em> (s')  \
&amp;=  r(s,a) + \gamma \sum_{s\in \mathcal{S}} p(s'|s,a) \max_{a\in \mathcal{A}} Q^*(s,a)
\end{align}
$$</p>
<p>将这个表达式代回最优状态价值函数，我们得到：
$$
V^<em>(s) = \max_{a\in \mathcal{A}} \left{ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s'|s,a) V^</em>(s') \right}
$$</p>
<h2>最优策略的解</h2>
<p>在本章节我们将会介绍两种算法，在已知转移函数 $p(s'|s,a)$ 和奖励函数 $r(s,a)$ 的马尔可夫决策过程下，如何解得最优策略。</p>
<h3>策略迭代算法</h3>
<p>现在我们希望得到最优策略。现在我们知道：</p>
<ul>
<li>对于任意策略 $\pi$，其对应一种状态价值函数 $V^\pi(s)$ 和动作价值函数 $Q^{\pi}(s, a)$</li>
<li>因此当策略发生变化的时候，需要重新评估这两个值</li>
<li>可不可以通过迭代的方式，持续优化策略使得其逐渐靠近最优策略？</li>
</ul>
<p>这等价于：
$$
\dots \to \pi_{i} \to^\text{Evaluation} V^{\pi_{i}} \to^\text{Optimization} \to \pi_{i+1} \to \dots \to \pi^*
$$</p>
<p>基于以上思想，<strong>策略迭代算法</strong></p>
<ul>
<li>从初始策略 $\pi_{0}$ 开始</li>
<li>反复进行：对于当前策略 $\pi_{i}$
<ul>
<li>对该策略进行评估，即计算其状态价值函数</li>
<li>对该策略进行提升，即找到一个不差于该策略的策略 $\pi_{i+1}$</li>
<li>直到状态价值函数优化程度提升幅度小于一定程度停止</li>
</ul>
</li>
</ul>
<p><strong>策略评估</strong>：得到当前策略 $\pi_{i}$ 的状态价值函数。这可以通过<em>贝尔曼期望方程</em>中的最优状态价值函数推导。这可以通过迭代+不动点的思想计算，即对于策略 $\pi_{i}$</p>
<ul>
<li>将状态价值函数随机初始化，得到 $V^0(s)$</li>
<li>当前轮的状态价值函数用上一轮的状态价值计算</li>
<li>最终状态价值函数一定会收敛到该策略的实际状态价值函数 $V^k \to_{k \to + \infty} V^{\pi_{i}}$</li>
</ul>
<p>$$
\forall s\in\mathcal{S}, \quad V^{k+1}(s) = \sum_{a\in \mathcal{A}} \pi(a|s) \left( r(s,a) + \sum_{s' \in \mathcal{S}} p(s'|s, a)V^k(s)\right)
$$</p>
<p><strong>策略优化</strong>：找到一个新策略 $\pi_{i+1}$ 使其不差于当前策略 $\pi_{i}$. 这意味着对于任意状态
$$
\forall s \in \mathcal{S}, \quad Q^{\pi_{i}}(s, \pi_{i+1}(s)) \geq V^{\pi_{i}}(s)
$$</p>
<p><strong>我们可以对于任意状态 $s$，贪心选取在目前策略 $\pi_{i}$ 下，对每一个状态 $s$ 选取动作价值最大的动作</strong>，因此</p>
<p>$$
\forall s \in\mathcal{S}, \quad \pi_{i+1}(s) = \arg\max_{a\in \mathcal{A}} Q^{\pi_{i} } (s,a) = \arg \max_{a\in\mathcal{A}} \left{r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^\pi (s')\right}
$$
当优化程度小于一定阀值 $\Delta$ 即停止。</p>
<p>具体算法表述：
<img src="Attachments/PolicyIteration.png" alt="" /></p>
<h2>价值迭代算法</h2>
<p>贝尔曼最优方程告诉我们在最优情况下价值是怎么样的。这意味着我们可以利用这个公式，在目前时刻做出决策之后，假设未来时刻都一直是最优的，反推现在应该如何做。</p>
<p>通过贝尔曼最优方程我们得到价值最优函数的表达式：</p>
<p>$$
V^<em>(s) = \max_{a\in \mathcal{A}} \left{ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s'|s,a) V^</em>(s') \right}
$$
这个公式可以通过动态规划做求解：</p>
<ul>
<li>迭代求解，等式左边的 $V^*(s)$ 为 $V_{k+1}$，等式右边的为 $V_{k}$</li>
<li>$V^<em>$ 是这个方程的不动点，因此当 $V_{k+1}=V_{k}$ 时我们就找到了想求的 $V^</em>$</li>
</ul>
<p>再求得价值最优函数后，我们直接定义策略 $\pi$ 对每一个状态 $s$ 采取动作价值最大的动作 $a$
$$
\forall s \in\mathcal{S}, \quad \pi(s) = \arg\max_{a\in \mathcal{A}} Q^{ <em>} (s,a) = \arg \max_{a\in\mathcal{A}} \left{r(s,a) + \gamma\sum_{s' \in \mathcal{S}} \mathbb{P}(s'|s,a) V^</em> (s')\right}
$$
容易得到 $\pi$ 就是我们想求的最优策略。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://hrl.boyuai.com/chapter/1/%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E5%86%B3%E7%AD%96%E8%BF%87%E7%A8%8B">第 3 章 马尔可夫决策过程（动手学强化学习）</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-21T17:51:58.499Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[并行计算基础通信原语]]></title>
        <id>https://www.languisher.top/posts/collective-communication/</id>
        <link href="https://www.languisher.top/posts/collective-communication/"/>
        <updated>2026-03-10T08:51:38.942Z</updated>
        <summary type="html"><![CDATA[本文会介绍在并行计算中常见的通信原语。我们定义每个设备为一个 rank. 在并行集合通信中， All 表示通信的 dst 是所有设备 Red...]]></summary>
        <content type="html"><![CDATA[<p>本文会介绍在并行计算中常见的通信原语。</p>
<p><img src="Attachments/collective_communication.png" alt="" /></p>
<h2>基础概念定义</h2>
<p>我们定义每个设备为一个 rank.</p>
<p>在并行集合通信中，</p>
<ul>
<li><strong>All</strong> 表示通信的 dst 是所有设备</li>
<li><strong>Reduce</strong> 表示对于数据执行 associative/commutative 计算（例如求和/求平均）</li>
<li><strong>Gather</strong> 表示将“分散”（在各个设备）的数据 shard 合并起来</li>
<li><strong>Scatter</strong> 则是 Gather 的反面，将完整的数据分块分发给多个设备</li>
</ul>
<h2>集合通信操作定义</h2>
<h3>局部和全局</h3>
<p>假设我们希望将一个设备上的数据复制给所有其他设备，让所有设备都拥有该数据的一份<em>完整</em>备份，这就是 <strong>广播（Broadcast）</strong>。</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
|      |      | data |      |
--copy--&gt;
| data | data | data | data |
</code></pre>
<p>与广播相反的情形：假设我们所有设备都存储了相同形状的数据，<em>现在我们希望将这些数据进行 Reduce 操作（例如求和/求平均）之后</em>将计算结果只保留到一个设备上，这个操作是 <strong>归约（Reduce）</strong>.</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
| data | data | data | data |
--reduce (e.g. sum/avg)--&gt;
|      |      | data |      | (Reduced)
</code></pre>
<h3>全局和全局</h3>
<p>前面介绍了 Reduce 操作，如果我们希望把 Reduce 操作之后的结果保留在所有设备上，这个操作是 <strong>All Reduce</strong>.</p>
<pre><code>|  D0  |  D1  |  D2  |  D3  |
| data | data | data | data |
--reduce (e.g. sum/avg)--&gt;
| data | data | data | data | (Reduced)
</code></pre>
<p>假设我们现在每个设备都持有数据的一部分，例如：</p>
<ul>
<li>完整的数据是一个 array：$x = [x_{0}, x_{1}, \dots, x_{1023}] \in \mathbb{R}^{1024}$</li>
<li>现在一共有 4 个设备，rank 0 持有 $x^{(0)}= [x_{0}, \dots, x_{255}] \in \mathbb{R}^{1024/4}$, rank 1 持有 $x^{(1)} = [x_{256}, \dots, x_{511}] \in \mathbb{R}^{1024/4}$, ...</li>
</ul>
<p>现在我们希望每个设备都有该数据的完整备份，这意味着：</p>
<ul>
<li>每个 rank 需要将自己的数据复制到所有其它 rank 上</li>
<li>每个 rank 需要接收除了自己所持有的数据部分之外的数据 shard</li>
</ul>
<p>这被称之为 <strong>All Gather</strong>.</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| data[0]   | data[1]   | data[2]   | data[3]   |
--concatenate--&gt;
| data[0:3] | data[0:3] | data[0:3] | data[0:3] |
</code></pre>
<p>与 All Gather 相反的情形：</p>
<ul>
<li>假设我们所有设备都存储了相同形状的数据，</li>
<li><em>现在我们希望将这些数据进行 Reduce 操作（例如求和/求平均）之后</em></li>
<li>将数据的每一部分分发给不同的设备</li>
<li>这意味着每个设备只用负责计算 其被分配到部分的 Reduce 操作计算，并且不同设备的 Reduce 操作计算可以同时进行</li>
</ul>
<p>这被称之为 <strong>Reduce Scatter</strong>.</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| data[0:3] | data[0:3] | data[0:3] | data[0:3] |
--reduce[0]-|-reduce[1]-|-reduce[2]-|-reduce[3]-|
| data[0]   | data[1]   | data[2]   | data[3]   | (Reduced)
</code></pre>
<h2>Ring 通信</h2>
<p>在上一个章节介绍了通信的逻辑语义，实际实现算法最常见的是 ring 通信，其核心思想是：</p>
<ul>
<li>将所有 device 连成一个环。假设我们有 4 个设备，这意味着 <code>0 -&gt; 1 -&gt; 2 -&gt; 3 -&gt; 0</code></li>
<li>系统形成一个 pipeline，在每一步中：</li>
<li>每个 GPU 发送给其 <em>右</em> 邻居，并且接受来自其 <em>左</em> 邻居的数据</li>
</ul>
<p>例子：</p>
<pre><code>|    D0     |    D1     |    D2     |    D3     |
| A[0]      | A[1]      | A[2]      | A[3]      |
    -           -           -           -
| ---A[0]--&gt;| ---A[1]--&gt;| ---A[2]--&gt;| ---A[3]--&gt;|
| A[0,3]    | A[1,0]    | A[2,1]    | A[3,2]    |
      -           -           -           -
| ---A[3]--&gt;| ---A[0]--&gt;| ---A[1]--&gt;| ---A[2]--&gt;|
| A[0,3,2]  | A[1,0,3]  | A[2,1,0]  | A[3,2,1]  |
        -           -           -           -
| ---A[1]--&gt;| ---A[2]--&gt;| ---A[0]--&gt;| ---A[3]--&gt;|
| A[0,3,2,1]| A[1,0,3,2]| A[2,1,0,3]| A[3,2,1,0]|
</code></pre>
<h2>All-Reduce 计算优化</h2>
<p>假设我们有 $p$ 个设备。回顾一下，All-Reduce 操作</p>
<ul>
<li>首先，所有设备上都有形状相同的数据 $x\in \mathbb{R}^N$</li>
<li>我们希望对于所有 $x_{i}, i\in {1, \dots, N}$，跨设备进行 Reduce 操作</li>
</ul>
<p>我们可以</p>
<ul>
<li>利用 Reduce-Scatter 的思想，让 device i 负责 $[x_{i/p \times N}, \dots, x_{i/p \times N+N}]$ 的 Reduce 计算，</li>
<li>再通过一次 All-Gather 操作将每个 device 得到的 reduced data shard 合并成完整的数据，完成同步</li>
</ul>
<p>因此，一个重要的结论是：
$$
\text{All-Reduce} = \text{Reduce-Scatter} + \text{All-Gather}
$$</p>
<p><img src="Attachments/all_reduce.png" alt="" /></p>
<p>通信计算：</p>
<ul>
<li>假设不同 device 之间传递的 tensor size = $N$，$N$ 是整个 tensor 的大小且一共有 $p$ 个 device，每个设备持有 $N/p$ 的数据</li>
<li>一共要进行 $p-1$ 个 step</li>
<li>因此每个设备在整个流程中要传输 $(p-1)N/p$ 的数据</li>
</ul>
<p>在 All-Reduce 的情景下：</p>
<ul>
<li>ReduceScatter 需要发送 $(p-1) \times N/p$ 数据（这是因为进行 Reduce 操作时，需要接收所有其他设备上的数）</li>
<li>AllGather 拼接，同理也需要发送 $(p-1) \times N/p$ 数据</li>
</ul>
<p>因此，一共是
$$
2 \frac{p-1}{p} N
$$
数据，当 $p$ 数量很大的时候近似于 $2N$.</p>
<h2>总结</h2>
<p>我们介绍了 Reduce, Broadcast, All {Gather/Reduce} 和 Reduce Scatter 操作以及如何优化 All-Reduce 操作。</p>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-10T08:51:38.942Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Flash Attention v1]]></title>
        <id>https://www.languisher.top/posts/flash-attention/</id>
        <link href="https://www.languisher.top/posts/flash-attention/"/>
        <updated>2026-03-04T18:11:44.306Z</updated>
        <summary type="html"><![CDATA[本文介绍 FlashAttention 的核心思想，并推导其关键公式，说明其如何通过分块计算与在线 softmax 更新，在不显式构造 $N\times N$ attention 矩阵的情况下减少显存访问与内存开销，从而提高 GPU 计算效率。]]></summary>
        <content type="html"><![CDATA[<p>本文介绍 FlashAttention 的核心思想，并推导其关键公式，说明其如何通过分块计算与在线 softmax 更新，在不显式构造 $N\times N$ attention 矩阵的情况下减少显存访问与内存开销，从而提高 GPU 计算效率。</p>
<p><img src="Attachments/FlashAttentionvsStandardAttention.png" alt="" /></p>
<h2>Naive Attention</h2>
<p>Standard Attention 可以建模为：
<img src="Attachments/StandardAttention.png" alt="" /></p>
<p>其中 <strong>(Safe) softmax 函数</strong>定义如下. 对于一个 vector $x \in \mathbb{R}^d$，为了避免指数爆炸，定义：
$$
m(x) := \max_{i}x_{i}, \quad \text{softmax}(x) := \frac{\begin{bmatrix}
\dots  &amp; e^{x_{i}-m(x) } &amp; \dots
\end{bmatrix}}{\sum_{j=1}^d e^{x_{j}-m(x)}} \in \mathbb{R}^d
$$</p>
<p>上图左侧表示 Algorithm 0 的流程。可以看到这会导致中间结果的 <strong>materialization</strong>：需要显式构建大小为 $N \times N$ 的矩阵 $S$ 和 $P$，并将其写入和再次从内存中读取以完成后续计算。这会产生大量的 HBM 访问开销。</p>
<p>一个自然的优化思路是将上述三个步骤 <strong>融合为一个 kernel（Fused Kernel）</strong>，使得每个元素在加载后立即参与后续计算，从而避免中间矩阵的物化。然而，这种融合会面临两个主要挑战：</p>
<ul>
<li><strong>Softmax 的归一化依赖</strong>：如下图所示，softmax 的计算需要知道该行（或列）所有元素的归一化因子（即分母部分）以及向量中所有元素的最大值 $m(x)$，因此在未遍历完整个向量之前无法得到最终结果。这使得 GEMM 操作和 softmax 操作没有办法 fuse.</li>
<li><strong>训练阶段的反向传播需求</strong>：在训练过程中，需要保存 softmax 的中间结果（例如概率矩阵  $P$），以便在 backward pass 中计算梯度。</li>
</ul>
<p><img src="Attachments/SoftmaxIllu.png" alt="" /></p>
<h2>FlashAttention v1 Overview</h2>
<p><img src="https://miro.medium.com/v2/resize:fit:700/1*L1EnFbS2jq6rFTA9_cXrbg.gif" alt="" /></p>
<h2>Tiling</h2>
<p>对于矩阵乘法（GEMM）的优化，一个核心思想是 <strong>Tiling（分块计算）</strong>。其基本动机是减少对慢速内存（如 HBM/DRAM）的访问次数，使数据在片上高速存储（如 cache / shared memory）中被尽可能多次复用。</p>
<p><img src="Attachments/Tiling.png" alt="" /></p>
<p>考虑计算</p>
<p>$$
C = A B^\top, \quad A, B \in \mathbb{R}^{N \times d}, \qquad C \in \mathbb{R}^{N \times N}.
$$</p>
<p>为了提高数据复用率，我们将矩阵 $A$ 和 $B$ 按 <strong>行维度</strong>切分成多个小块（tiles）。设 tile 的大小分别为 $d_A$ 和 $d_B$，则：</p>
<p>$$
A =
\begin{bmatrix}
A_1 \
\vdots \
A_{T_A}
\end{bmatrix},
\qquad
A_i \in \mathbb{R}^{d_A \times d},
\qquad
T_A = \left\lceil \frac{N}{d_A} \right\rceil
$$</p>
<p>$$
B =
\begin{bmatrix}
B_1 \
\vdots \
B_{T_B}
\end{bmatrix},
\qquad
B_j \in \mathbb{R}^{d_B \times d},
\qquad
T_B = \left\lceil \frac{N}{d_B} \right\rceil
$$</p>
<p>对应地，输出矩阵 $C$ 也被划分为若干子块：</p>
<p>$$
C_{ij} \in \mathbb{R}^{d_A \times d_B}, \qquad
C_{ij} = A_i B_j^\top .
$$</p>
<p>因此整个矩阵乘法可以通过如下分块计算完成：</p>
<ul>
<li>For $1 \le i \le T_A$
<ul>
<li>Load $A_i$ 到片上高速内存</li>
<li>For $1 \le j \le T_B$
<ul>
<li>Load $B_j$</li>
<li>计算
$$
C_{ij} = A_i B_j^\top
$$</li>
<li>将结果写回对应的 $C_{ij}$</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>最终返回矩阵 $C$。</p>
<p>这种 <strong>分块计算（tiling）</strong> 的关键优势在于：</p>
<ul>
<li>每个 tile（如 $A_i$ 或 $B_j$）只需从全局内存读取一次</li>
<li>在片上内存中可以被多次复用</li>
<li>显著减少 HBM 访问带宽压力</li>
</ul>
<p>因此，现代 GPU 的高性能 GEMM 实现（如 CUDA kernel、TensorCore kernel）都会采用类似的 <strong>tile-based 计算策略</strong> 来提高计算效率。</p>
<h3>Tiling in FlashAttention</h3>
<p>在 FlashAttention 中，因为需要对 $Q.K$ 以及 $f(Q.K).V$ 做 GEMM 运算，因此将 $Q$, $K$ 和 $V$ 矩阵都进行分块，切分维度在 sequence dimension 上，即分别将 $Q$ 和 {$K$,$V$} 切分成 $\mathbb{R}^{B_{r}\times d}$ 和 $\mathbb{R}^{B_{c} \times d}$ 的块。</p>
<p>在计算中，outer loop 是 $K$ 和 $V$ 矩阵（对应上一小节的 $B$ 矩阵，按列切分）；inner loop 是 $Q$ 以及形状相同的 $O$ 矩阵。这可以使得 $K$ 和 $V$ 的复用最大化。在每个 tile 内都进行了大小为</p>
<p>$$
\mathbb{R}^{B_{r}\times d} \times \mathbb{R}^{d \times B_{c}} \to \mathbb{R}^{B_{r}\times B_{c}}
$$</p>
<p>的计算，后续第 9-13 行是每个 tile 的计算内容。</p>
<p><img src="Attachments/TilingInFlashAttention.png" alt="" /></p>
<h2>Online Softmax</h2>
<h3>增量 softmax</h3>
<p>首先，我们希望解决 <strong>Softmax 的数据依赖问题</strong>。理想情况下，当逐步访问向量 $x \in \mathbb{R}^d$ 的元素时，我们能够 <strong>在线（online）更新 softmax 的统计量</strong>，从而在一次扫描中完成 softmax 的计算，而不需要多次遍历整个向量。</p>
<p>在最朴素的实现中，softmax 通常需要 <strong>三次遍历向量</strong> $x$，每次都需要从内存读取 $d$ 个元素，总的内存访问量约为 $3d$：</p>
<ul>
<li>遍历 $x$，以计算出 $x$ 的最大值 $m(x)$</li>
<li>遍历 $x$，以得到  softmax 运算中的分母部分（归一化因子）</li>
<li>遍历 $x$，逐元素得到其分子部分</li>
</ul>
<p>而实际上 softmax 可以采用增量的方式来减少一次遍历，这是因为 softmax 运算中的分母部分可以跟随 $x$ 最大值的更新而更新。</p>
<p>假设遍历到第 $j$ 个元素，</p>
<ul>
<li><strong>更新所有元素的最大值</strong>：得到 $m_{j-1}(x) = \max(x_{1},\dots,x_{j-1})$，因此 $m_{j}(x) := \max{(m_{j-1}(x), x_{j})}$，其满足 $m_{j}(x) \geq m_{j-1}(x)$</li>
<li><strong>放缩：更新归一化因子，即 softmax 运算中的分母部分</strong>：得到 $d_{j-1}(x) = \sum_{t=1}^{j-1}e^{t-m_{j-1}(x)}$. 因此 $d_{j}(x) := d_{j-1} \times e^{m_{j-1}(x) - m_{j}(x)} + e^{x_{j }-m_{j}(x)}$</li>
</ul>
<p>中间推导过程：</p>
<p>$$
\begin{aligned}
d_j(x)
&amp;= \sum_{t=1}^{j} e^{x_t - m_j(x)} \
&amp;=
\underbrace{\sum_{t=1}^{j-1} e^{x_t - m_{j-1}(x)}}<em>{d</em>{j-1}(x)}
\cdot e^{m_{j-1}(x)-m_j(x)}
+
e^{x_j-m_j(x)}
\end{aligned}
$$</p>
<p>最后再遍历一次向量 $x$，逐元素计算最终的 softmax 输出：
$$
\text{softmax}(x) := \frac{\begin{bmatrix}
\dots  &amp; e^{x_{i}-m_{d}(x) } &amp; \dots
\end{bmatrix}}{d_{n}(x)} \in \mathbb{R}^d
$$</p>
<p>这样 softmax 的计算只需要 <strong>两次遍历向量</strong> $x$，总的内存访问量约为 $2d$。</p>
<p>更重要的是，通过维护运行中的最大值 $m_j(x)$ 和归一化因子 $d_j(x)$，softmax 的统计量可以在遍历过程中 <strong>逐步更新（online update）</strong>，从而避免了必须先访问所有元素才能开始计算 softmax 的问题。</p>
<p><img src="Attachments/OnlineSoftmaxAlgo.png" alt="" /></p>
<h3>合并两个向量的 Softmax 统计量</h3>
<p>在上一小节中，我们介绍了如何在遍历向量 $x' = \begin{bmatrix}x_1 &amp; \dots &amp; x_{j-1}\end{bmatrix}$ 时，通过新元素 $x_j$ 对 softmax 的统计量进行增量更新，从而得到新的归一化因子。</p>
<p>在这一个小节，我们运用同样的思想 merge 两个向量 $x^{(1)} \in \mathbb{R}^B$ 和 $x^{(2)} \in \mathbb{R}^B$ 的 softmax 结果，从而得到拼接向量 $x = \begin{bmatrix} x^{(1)}  \ x^{(2)} \end{bmatrix} \in \mathbb{R}^{2B}$ 的 softmax 拼接结果。
对于每个向量，我们保存其最大值 $m^{(1)} = \max_{i}{x_{i}^{(1)}}$ 和 $m^{(2)} = \max_{i}{x_{i}^{(2)}}$ 和分母（即归一化因子）的结果：$l^{(1)}= \sum_{i} e^{x_{i}^{(1)} - m^{(1)}}$ 和 $l^{(2)} = \sum_{i} e^{x_{i}^{(2)}-m^{(2)}}$，接下来的操作基于这 4 个 state $(m^{(1)}, m^{(2)}, l^{(1)}, l^{(2)})$ 进行。</p>
<p>定义原来两个向量 softmax 计算中的分子向量 $f^{(1)} = \begin{bmatrix} \dots &amp;  e^{x_{i}^{(1)}- m^{(1)}} &amp; \dots\end{bmatrix}$ 和 $f^{(2)} = \begin{bmatrix} \dots &amp;  e^{x_{i}^{(2)}- m^{(2)}} &amp; \dots\end{bmatrix}$ ，这意味着：
$$
\text{softmax}(x^{(i)}) = \frac{f^{(i)}}{l^{(i)}} \in \mathbb{R}^B, \quad i \in {1, 2}
$$
我们希望得到的结果：（其中 $f = f(f^{(1)}, f^{(2)})$ 和 $l= l(l^{(1)}, l^{(2)})$ 需要求解）
$$
\text{softmax}(x) = \frac{f(x)}{l(x)} \in \mathbb{R}^{2B}, \quad f(x) \in \mathbb{R}^{2B}, \quad l(x) \in \mathbb{R}
$$</p>
<p>首先<strong>更新 $x$ 的最大值</strong>：$m(x) = \max(m^{(1)}, m^{(2)})$.</p>
<p>接下来是分别对 Softmax 的分母和分子部分<strong>进行放缩</strong>（借助于我们所存储的指数计算求和结果的 $l$ 值）：</p>
<p>Softmax 的分母部分：
$$
l(x) = l^{(1)} \times e^{m^{(1)}- m(x)} + l^{(2)} \times e^{m^{(2)}- m(x)}
$$
Softmax 的分子部分因为 $x$ 的最大值发生了变化同样也需要进行放缩（逐元素操作）：
$$
f(x) = \begin{bmatrix}
f^{(1)} \times e^{m^{(1)} - m(x)} \ f^{(2)}\times e^{m^{(2)}-m(x)}
\end{bmatrix} \in \mathbb{R}^{2B}
$$</p>
<p>这使得我们能够先对矩阵的一个 <strong>chunk（或 tile）</strong> 进行局部 softmax 计算。更准确地说，我们计算的是 softmax 的 <strong>统计量（最大值和归一化因子）</strong>。随后通过重新缩放这些统计量，可以将不同 chunk 的结果合并，从而得到整个向量的正确 softmax 归一化结果。</p>
<p>这种性质使得 softmax 可以 <strong>按块（tile-wise）计算并逐步合并</strong>，从而与 attention 的分块计算方式相结合，使得 $QK^T$、softmax 和 $PV$ 可以在同一个 kernel 中完成，而无需显式物化中间的 $N\times N$ attention 矩阵。每个 tile 在加载到共享内存后，可以完成该 tile 对输出 $O$ 的 <strong>部分贡献的计算</strong>，并将结果累积到当前的输出中。</p>
<h3>Online Softmax in FlashAttention</h3>
<p>现在我们放在 Attention 情况下，思想也是完全相同：更新最大值，再对分子分母进行放缩。</p>
<p><img src="Attachments/FlashAttentionAlgo1.png" alt="" /></p>
<p>在进行矩阵分块计算的背景下，假设 outer loop index = $j$，inner loop index = $i$，且 ${Q_{i}, O_{i}} \in B_{r} \times d$，${K_{j}, V_{j}} \in d \times B_{c}$，我们希望对于：</p>
<ul>
<li>之前计算结果 $O_{i}\in B_{r} \times d$ 的 softmax 计算，可以表示为 $$O_{i} =  \frac{\text{numerator}}{l_{i}}, \quad \text{where} ; \text{numerator}=O_{i}l_{i}$$ 且 numerator 和 $m_{i}$ 相关（当 $m_{i}$ 改变时分子也要进行缩放），以及是 softmax 运算和 $V$ 矩阵计算的结果</li>
<li>以及当前 tile 的局部 softmax 计算，可以表示为 $$\tilde{\text{softmax}}<em>{ij} (\tilde{m}</em>{ij}) = \frac{\tilde{P}<em>{ij}}{\tilde{l}</em>{ij}} \in \mathbb{R}^{B_{r} \times B_{c}}$$
进行合并。</li>
</ul>
<p><strong>首先是 tile 内部的 softmax 计算</strong>。第 10 行是 tile 内部的 state 计算：$\tilde{m}<em>{ij}$ 表示每一行的最大值，$\tilde{P}</em>{ij}$ 表示每一行的 softmax 分子，$\tilde{l}<em>{ij}$ 表示每一行的 softmax 分母。
$$
\tilde{m}</em>{ij}= \text{rowmax}(S_{ij}) \in \mathbb{R}^{B_{r}}, \quad \tilde{P}<em>{ij} = \exp(S</em>{ij}-\tilde{m}<em>{ij}) \in \mathbb{R}^{B</em>{r} \times B_{c}}, \quad \tilde{l}<em>{ij}= \text{rowsum}(\tilde{P}</em>{ij}) \in \mathbb{R}^{B_{r} }
$$</p>
<p>局部的 softmax 由以下计算得到，且只与 $\tilde{m}<em>{ij}$ state 有关（因此当它被更新的时候需要对局部 softmax 也进行更新）：
$$
\tilde{\text{softmax}}</em>{ij} (\tilde{m}<em>{ij}) = \frac{\tilde{P}</em>{ij}}{\tilde{l}<em>{ij}} \in \mathbb{R}^{B</em>{r} \times B_{c}}
$$</p>
<p><strong>然后将之前的计算结果与本次计算结果进行合并</strong>：第 11 行是局部 state 与 HBM 中缓存的全局 state 的更新：
$$
m_{i}^\text{new} = \max(m_{i}, \tilde{m}<em>{ij}) \in \mathbb{R}^{B</em>{r}}, \quad l_{i}^{\text{new}} = l_{i} \times e^{m_{i}- m_{i}^{\text{new}}} + \tilde{l}<em>{ij} \times e^{\tilde{m}</em>{ij}- m_{i}^\text{new}} \in \mathbb{R}^{B_{r}}
$$</p>
<p>第 12 行最为关键：它将“旧的累计结果”与“当前 tile 的新贡献”在统一数值尺度下合并，并完成归一化更新。</p>
<p><strong>(1) 分子部分 1：当前 tile 的新贡献（未归一化的 numerator 增量）</strong></p>
<p>先计算
$$
\tilde{O}<em>{ij} = \tilde{P}</em>{ij} V_j \in \mathbb{R}^{B_r\times d}.
$$
由于本轮更新后的 running max 变为 $m_i^{new}$，需要对每一行做指数重缩放：
$$
\Delta_i
= \operatorname{diag}!\left(e^{\tilde{m}<em>{ij}-m_i^{new}}\right)\tilde{O}</em>{ij}
\in \mathbb{R}^{B_r\times d}.
$$</p>
<p><strong>(2) 分子部分 2：旧的累计结果对应的贡献（在新尺度下重缩放）</strong><br />
将先前累计的输出 $O_i$（它对应旧尺度 $m_i,l_i$）恢复为未归一化的 <strong>numerator</strong>，并将指数尺度从 $m_i$ 重缩放到新的 $m_i^{new}$：
$$
\Phi_i
= \operatorname{diag}!\left(l_i,e^{m_i-m_i^{new}}\right) O_i
\in \mathbb{R}^{B_r\times d}.
$$</p>
<p><strong>(3)</strong> 归一化因子即为 $l_{i}^\text{new}$.</p>
<p><strong>(4) 合并并归一化，得到新的输出</strong><br />
$$
O_i^{new}
= \frac{\text{old numerator} + \text{new numerator}} {\text{new denominator}} =
\operatorname{diag}(l_i^{new})^{-1}\bigl(\Phi_i + \Delta_i\bigr)
\in \mathbb{R}^{B_r\times d}.
$$</p>
<h1>这与论文中的写法等价：
$$
O_i^{new}</h1>
<p>\operatorname{diag}(l_i^{new})^{-1}
\left(
\operatorname{diag}(l_i)e^{m_i-m_i^{new}}O_i
+
e^{\tilde{m}<em>{ij}-m_i^{new}}\tilde{P}</em>{ij}V_j
\right),
$$
其中 $e^{\tilde{m}_{ij}-m_i^{new}}$ 表示对矩阵按行 broadcast 的逐行缩放。</p>
<p><strong>参数初始化</strong>：$O$ 和 $l$ 采用零初始化而 $m$ 初始化为 $-\infty$. 这意味这对于第一个 iteration，即为标准的 softmax.</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://arxiv.org/abs/2205.14135">FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness</a></li>
<li><a href="https://medium.com/ai-advances/flashattention-visually-and-exhaustively-explained-d6124670f7fb">FlashAttention — Visually and Exhaustively Explained</a></li>
<li><a href="https://huggingface.co/docs/text-generation-inference/en/conceptual/flash_attention">Flash Attention</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-04T18:11:44.306Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[CUDA 寻址]]></title>
        <id>https://www.languisher.top/posts/cuda-indexing/</id>
        <link href="https://www.languisher.top/posts/cuda-indexing/"/>
        <updated>2026-03-04T14:45:48.564Z</updated>
        <summary type="html"><![CDATA[本文简单介绍了在 CUDA 环境下 Grid, Block 和 Thread 的概念，以及如何寻找它们的地址。CUDA Kernel 是一个...]]></summary>
        <content type="html"><![CDATA[<p>本文简单介绍了在 CUDA 环境下 Grid, Block 和 Thread 的概念，以及如何寻找它们的地址。</p>
<h2>关键概念</h2>
<p><img src="Attachments/CUDAIndexing.png" alt="" /></p>
<p><strong>CUDA Kernel</strong> 是一个用 <code>__global__</code> 声明的函数，它在 GPU (device) 上执行，由 CPU (host) 发起调用。</p>
<pre><code>__global__ void myKernel() {
	...
}

// invoke
dim3 gridDim(2, 2);
dim3 blockDim(4, 3);
mykernel&lt;&lt;&lt;gridDim, blockDim&gt;&gt;&gt;();
</code></pre>
<p><strong>Grid</strong>. 当 Host 启动一个 kernel 时，CUDA runtime 会在 Device 上创建一个 <strong>grid</strong>。</p>
<ul>
<li>一个 grid 是由多个 thread blocks 组成的集合。</li>
<li>每一次 kernel launch 会生成一个 grid，因此 kernel launch 与 grid 之间是一一对应的关系。</li>
<li>不同的 kernel launch 可以使用不同的 grid 维度和 block 维度。</li>
<li><code>gridDim.{x,y,z}</code> 表示 Grid 的 shape</li>
</ul>
<p><strong>Block</strong>. 多个 thread 组成的集合。</p>
<ul>
<li>在同一个 grid 中，所有的 block 的 shape 都是一样的</li>
<li>在不同 kernel launch 对应的 grid 中，block 的 shape 可能不同</li>
<li><code>blockDim.{x,y,z}</code> 表示 Block 的 shape</li>
<li>使用 <code>blockIdx.{x,y,z}</code> 表示当前 block 在 grid 中的索引</li>
</ul>
<p><strong>Thread</strong>. 实际执行的最小 (software) execution unit.</p>
<ul>
<li>使用 <code>threadIdx.{x,y,z}</code> 表示当前 thread 在 block 中的索引</li>
</ul>
<p>Grid 和 Block 都可以是三维的，如下图所示：
<img src="Attachments/CUDAIndexing3D.png" alt="" /></p>
<h2>Thread indexing</h2>
<p>并行计算中，不同线程需要处理不同的数据。为了确定每个线程所负责的数据位置，我们需要计算每个线程的 <strong>global thread index</strong>。在这个章节我们会研究如何通过线程索引来确定每个线程处理的数据位置。</p>
<p><img src="Attachments/FlattenedIndexingIllustration.png" alt="定位 thread index 的例子" /></p>
<p>总体思路：目的是将多维的 thread 组织（grid/block/thread）映射为一维的 global thread index</p>
<ul>
<li>首先定位 block 位置，把 blockIdx flatten 成 1D，确认每个 block 有多少个 thread</li>
<li>再确定 thread 在 block 第几行，把 threadIdx flatten 成 1D，确认 block 中的每一行有多少个 thread</li>
<li>最后加上 threadIdx.x</li>
</ul>
<p>$$
\text{threadId} = \underbrace{\text{blockId}}<em>{\text{grid 内 block 的线性编号}} \times \underbrace{\text{threadsPerBlock}}</em>{\text{每个 block 的线程总数}} + \underbrace{\text{localThreadId}}_{\text{block 内 thread 的线性编号}}
$$</p>
<p><img src="Attachments/1DGrid1DBlock.png" alt="" /></p>
<p>1D Grid + 1D Block 情况：总共有 $\text{blockDim}.x \times \text{gridDim}.x$ 个 thread，
$$
\text{threadId} = (\text{blockIdx}.x \times \text{blockDim}.x) + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/1DGrid2DBlock.png" alt="" /></p>
<p>1D Grid + 2D Block 情况：总共有 $\text{gridDim}.x \times (\text{blockDim}.x \times \text{blockDim}.y)$ 个 thread，
$$
\text{threadId} = (\text{blockIdx}.x \times \text{blockDim}.x \times \text{blockDim}.y) + (\text{threadIdx}.y \times \text{blockDim}.x) + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/2DGrid1DBlock.png" alt="" /></p>
<p>2D Grid + 1D Block 情况：总共有 $(\text{gridDim}.x \times \text{gridDim}.y) \times \text{blockDim}.x$ 个 thread，
$$
\text{blockId} := (\text{blockIdx}.y \times \text{gridDim}.x) + \text{blockIdx}.x
$$
因此
$$
\text{threadId} = \text{blockId} \times \text{blockDim}.x + \text{threadIdx}.x
$$</p>
<p><img src="Attachments/2DGrid2DBlock.png" alt="" /></p>
<p>2D Grid + 2D Block 情况：总共有 $(\text{gridDim}.x \times \text{gridDim}.y) \times (\text{blockDim}.x \times \text{blockDim}.y)$ 个 thread,
$$
\text{blockId} = \text{gridDim}.x \times \text{blockIdx}.y + \text{blockIdx}.x
$$
因此
$$
\text{threadId} = \text{blockId} \times \text{blockDim}.x \times \text{blockDim}.y + \text{blockDim}.x \times \text{threadIdx}.y + \text{threadIdx}.x
$$</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://anuradha-15.medium.com/cuda-thread-indexing-fb9910cba084">CUDA Thread Indexing</a></li>
<li><a href="https://medium.com/@omkarpast/mastering-cuda-kernel-development-a-comprehensive-guide-1f3032666b94">Mastering CUDA Kernel Development: A Comprehensive Guide</a></li>
<li><a href="https://siboehm.com/articles/22/CUDA-MMM">How to Optimize a CUDA Matmul Kernel for cuBLAS-like Performance: a Worklog</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-04T14:45:48.564Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[LLM 推理基础术语解释]]></title>
        <id>https://www.languisher.top/posts/inference-terms-basic/</id>
        <link href="https://www.languisher.top/posts/inference-terms-basic/"/>
        <updated>2026-03-03T15:19:05.177Z</updated>
        <summary type="html"><![CDATA[在大模型推理系统中，涉及 batching、KV Cache、Prefill/Decode、并行策略等大量概念。本文将对这些关键机制进行系统梳理。]]></summary>
        <content type="html"><![CDATA[<p>在大模型推理系统中，涉及 batching、KV Cache、Prefill/Decode、并行策略等大量概念。本文将对这些关键机制进行系统梳理。</p>
<h2>请求处理</h2>
<h3>Chunked Prefill</h3>
<blockquote>
<p>[!note]
<strong>Chunked Prefill</strong> 通过将原本规模为 $O(S^2)$ 的 Causal Attention 计算拆分为若干较小的分块计算，从而降低 Prefill 阶段的峰值显存占用。</p>
</blockquote>
<p><img src="Attachments/ChunkedPrefill.png" alt="" /></p>
<p>Chunked Prefill 由 <a href="https://arxiv.org/abs/2308.16369">Sarathi</a>[^3] 提出。上图展示了 Chunked Prefill 的主要流程。Chunked Prefill 将原本一次性处理完整序列的 Prefill 过程拆分为多个按顺序执行的分块计算。设 Chunk size 为 $c$，则对于长度为 $S$ 的输入序列，会被划分为 $\lceil S / c \rceil$ 个 chunk 依次处理。</p>
<p>在每一次分块计算中，系统会：</p>
<ul>
<li>选取当前 chunk 中的至多 $c$ 个 token；</li>
<li>计算这些 token 的 $Q/K/V$ 表示，并将新生成的 $K/V$ 写入 KV Cache；</li>
<li>将当前 chunk 的 $Q$与已累计的历史 $K$（包括此前所有 chunk 的 token）进行因果 Attention 计算；</li>
<li>若该 chunk 为最后一段，则完成整个 Prefill 阶段，并进入后续 Decode 阶段。</li>
</ul>
<p>[^3]:Agrawal, Amey, et al. "Sarathi: Efficient llm inference by piggybacking decodes with chunked prefills." <em>arXiv preprint arXiv:2308.16369</em> (2023).</p>
<h3>Continuous Batching</h3>
<blockquote>
<p>[!note]
<strong>Continuous batching</strong> 是一种面向在线推理的调度机制，能够大幅提高系统吞吐、减少推理 Latency 以及提高 GPU 利用率.</p>
</blockquote>
<p>直接将多个请求简单拼成一个固定 batch 进行推理，会带来两个结构性问题：</p>
<ol>
<li><strong>长度不一致导致的 padding 浪费</strong>：不同请求的输入长度不同。为了将它们组织成规则 tensor 输入模型，通常需要对较短序列进行 padding。模型在计算时也会对这些“无效 token”执行 attention 和 FFN 计算，造成算力浪费。</li>
<li><strong>Decode 阶段的生命周期不同步（Tail Latency）</strong>：在生成阶段，每个请求需要生成的 token 数量不同，而且生成长度在开始时是不可预测的。在固定 batch 机制下，只有当 batch 中<strong>最后一个请求完成生成</strong>后，整个 batch 才能结束并返回结果。因此，已经提前完成生成的请求会被迫等待最长的那个请求完成，导致尾延迟（tail latency）显著上升。</li>
</ol>
<p><img src="Attachments/NaiveBatching.png" alt="" /></p>
<p>核心思想是当一个请求结束（例如生成 <code>&lt;EoS&gt;</code> Token）立刻就被替换为新的请求：
<img src="Attachments/ContinuousBatchingGoal.png" alt="" /></p>
<p>Continuous Batching 通常依赖于 <strong>ragged batching</strong> 技术来实现，即在同一个批次中拼接来自不同请求、长度不一的 token 序列。为了保证不同请求之间互不干扰，系统会通过构造块 block-diagonal 的 Causal Attention Mask（或等价的序列偏移管理机制），确保每个请求的 token 仅与自身历史 token 进行 Attention 计算，而不会与其他请求的 token 发生交互。</p>
<p><img src="Attachments/RaggedBatching.png" alt="" /></p>
<p>Continuous Batching 通常与 Chunked Prefill 结合使用。下图展示了在三个请求同时存在时的推理示例。在每一个调度 step 中，系统的执行流程如下：</p>
<ul>
<li>对正在进行 Decode 的请求执行一次 forward 计算，各生成一个新的 token；</li>
<li>在剩余的计算预算内，调度处于 Prefill 阶段的请求，按 chunk 大小处理相应数量的 token；</li>
<li>当某个请求完成（Decode 结束）时，将其从当前 batch 中移除；</li>
<li>将新到达的请求或尚未完成 Chunked Prefill 的请求加入 batch，以维持持续的计算负载。</li>
</ul>
<p><img src="Attachments/ContinuousBatching.png" alt="" />
Continuous Batching 在工程实现上通常结合 Paged Attention，以实现对 KV Cache 的更细粒度管理。若采用“一个 token 对应一个 block”的设计，会导致显存碎片化严重，并带来较高的元数据与调度开销。更高效的做法是使用固定大小的 Cache Block，每个 block 存储多个连续 token 的 KV 表示，从而在保持灵活分配能力的同时减少碎片化并提高内存利用率。</p>
<h2>KVCache 管理</h2>
<h3>KVCache 机制</h3>
<blockquote>
<p>[!note]
<strong>KVCache</strong>：在 causal self-attention 的 decode 阶段，缓存此前所有 token 的 $K$ 和 $V$ 向量，使得生成新 token 时只需计算当前 token 的 $Q$, $K$, $V$ 向量，避免计算整个序列的 attention.</p>
</blockquote>
<p>在 causal self-attention 的 decode 阶段，每次只会生成一个新的 token。对于该 token，模型只需要计算其对应的 $Q,K,V$ 向量，而此前所有 token 的 $K,V$ 向量在生成后保持不变，因此可以缓存为 KV Cache。</p>
<p>在计算 attention 时，新 token 的 $Q$ 会与缓存中所有 token 的 $K$ 拼接当前 token 的 $K$ 进行 $QK^T$ 计算，再与缓存中所有 token 的 $V$ 拼接当前 token 的 $V$ 进行加权求和，从而得到当前 token 的 attention 输出。</p>
<p><img src="Attachments/KVCacheMech.png" alt="" /></p>
<p>具体的计算过程可以如下图表示：
<img src="Attachments/KVCacheMechanism.png" alt="" /></p>
<h3>Paged Attention</h3>
<blockquote>
<p>[!note]
<strong>Paged Attention</strong> 使用类似虚拟内存分页的管理方式，使得 KV Cache 可以按需分配与回收，从而有效降低显存碎片化并提高内存利用率。</p>
</blockquote>
<p>PagedAttention 是由 <a href="https://arxiv.org/abs/2309.06180">vLLM</a>[^1]提出的，将 KV Cache 按固定大小的 Block 进行划分，每个 Block 存储若干连续 token 的 KV 表示。对于单个请求，其 KV Cache 由多个 Block 组成，并通过逻辑到物理 Block 的映射表进行管理。</p>
<p><img src="Attachments/KVCacheBlock.png" alt="" /></p>
<p>在 Paged Attention 中，每个请求维护一组 Logical KV Blocks，可类比为该请求的“虚拟页表”。这些 Logical Blocks 并不直接对应连续的物理显存，而是映射到全局内存池中的 Physical KV Blocks（固定大小的物理块）。</p>
<p>系统通过维护一张 Block Table，记录每个请求的 Logical KV Block 与 Physical KV Block 之间的映射关系。在访问 KV Cache 时，请求首先根据自身的逻辑块编号查找 Block Table，再定位到对应的物理块，从而完成对实际显存的访问。</p>
<p><img src="Attachments/PagedAttention.png" alt="" /></p>
<p>[^1]: Kwon, Woosuk, et al. "Efficient memory management for large language model serving with pagedattention." <em>Proceedings of the 29th symposium on operating systems principles</em>. 2023.</p>
<h3>Copy-on-write</h3>
<blockquote>
<p>[!note]
<strong>Copy-on-write</strong> 的核心思想是当多个请求拥有相同前缀 token 时，共享其对应的 KV cache blocks。由于 KV cache 是 append-only 结构，当请求在生成阶段产生分歧时，只为分歧后的 token 分配新的 block，而共享的前缀 block 继续复用，从而显著减少显存开销。</p>
</blockquote>
<p>如下图，可以观察到 Sample A1 和 Sample A2 都共享利用了 Physical KV blocks 中的 Block 7.</p>
<p><img src="Attachments/CopyOnWrite.png" alt="" /></p>
<h3>Radix Attention</h3>
<blockquote>
<p>[!note]
<strong>Radix Attention 通过构建前缀树</strong>（radix tree）来组织请求的 token 序列，使共享前缀在结构上合并为同一路径，从而避免了显式的重复检测过程。</p>
</blockquote>
<p>Radix Attention 由主流推理平台 SGLang 在 <a href="https://arxiv.org/pdf/2312.07104">这篇文章</a>[^2] 提出的。</p>
<p><img src="Attachments/RadixAttention.png" alt="" /></p>
<p>[^2]: Zheng, Lianmin, et al. "Sglang: Efficient execution of structured language model programs." <em>Advances in neural information processing systems</em> 37 (2024): 62557-62583.</p>
<h2>Disaggregation</h2>
<h3>(E)PD Disaggregation</h3>
<blockquote>
<p>[!note]
Encode、Prefill 与 Decode 阶段在计算形态、资源瓶颈与扩展规律上存在显著差异。<strong>EPD Disaggregation</strong> 通过将阶段解耦并分别部署，使系统能够针对阶段特征进行差异化的并行与容量配置，从而提升整体吞吐、降低尾延迟，并增强弹性扩缩容能力。</p>
</blockquote>
<p>Prefill 和 Decode phase 对于 Batchsize 的 scalability 如下图所示 [^4]。Prefill 阶段由于有大量的 GEMM 运算，因此是 compute-bound；而 Decode 阶段在每个 step 生成新的 token 时都要访问大量的 KVCache，因此是 memory-bound.</p>
<p><img src="Attachments/PDPhase.png" alt="" /></p>
<p>PD 分离是一个在 LLM 推理优化中较大的一个方向，后续会专门有一篇文章介绍常见的优化方法。</p>
<p>[^4]: Zhong, Yinmin, et al. "{DistServe}: Disaggregating prefill and decoding for goodput-optimized large language model serving." <em>18th USENIX Symposium on Operating Systems Design and Implementation (OSDI 24)</em>. 2024.</p>
<h3>AF Disaggregation</h3>
<blockquote>
<p>[!note]
<strong>AF Disaggregation</strong> 指的是 Attention 与 FFN 分离。将 Attention 与 FFN 子模块分别部署到不同设备上，通过调整不同的 A 与 F 配比可实现较高推理性能</p>
</blockquote>
<p>在主流 Decoder-only 模型中，Attention 与 FFN 层（或者 MoE 层）对 compute 和 memory 的需求存在显著差异。
在 Decode 阶段，Attention 通常是 memory-bound，而 FFN 更多是 compute-bound.
这意味着增大 batch size 时，Attention 的计算资源需求基本不变，而 FFN 则能够随着 batch size 增加获得性能收益，直到完全利用所有的计算资源。</p>
<p><img src="Attachments/AFDisaggregation.png" alt="" /></p>
<p>FFN 的最佳 batch size 满足 $$b \geq \alpha \times \gamma \times \text{Flops} / \text{Bandwidth}$$，其中 $\alpha$ 表示权重的存储大小（16-bit 为 1，例如 8-bit 就是 0.5），$\gamma$ 表示激活的参数数量（即与专家数量相关），在 dense model 情况下就是 1，比如在 256 个 Expert 中激活 8 个 Expert，$\gamma = 256/8$.</p>
<p>AF 分离意味着在每一层中，A 和 F 之间都要传输 Activation，不可避免会产生更多通信开销。更多参考资料：<a href="https://zhuanlan.zhihu.com/p/1952393747112367273"># LLM 推理提速：Attention与FFN分离(AFD)方案解析</a></p>
<h2>推理模式</h2>
<h3>Speculative Decoding</h3>
<blockquote>
<p>[!note]
<strong>Speculative Decoding</strong> 试图解决在 Auto Regressive 生成 Token 的情况下，尽可能实现一个模型同时生成多个 Token 的效果。</p>
</blockquote>
<p><img src="Attachments/SpeculativeDecoding.png" alt="" /></p>
<h2>量化程度</h2>
<p>W8A16：是一种量化模式；W8 表示权重使用 8 位整数（INT8）进行量化而 A16表示 Activation 保留 16 位浮点数（FP16/BF16）精度。
为了保持推理精度，一般来说 Activation 会保留更高的精度。其他常见的格式如 W4A16, W8A8 等。</p>
<h2>Attention 层优化</h2>
<h3>Flash Attention</h3>
<blockquote>
<p>[!note]
<strong>Flash Attention</strong> 是一种加速 Attention 运算的算法。</p>
</blockquote>
<p>Standard Attention 可以建模为：
<img src="Attachments/StandardAttention.png" alt="" /></p>
<p>可以发现需要重复将结果写入读入 HBM，对此进行优化。思路是在 tile/block 计算过程中，在线（streaming）计算 softmax，从而避免把中间的 attention matrix 写入 HBM。为了适配 block 矩阵乘法，对 softmax 计算进行优化，使得在 tile/block 计算中可以在线更新，而不是先遍历数据找到最大值再进行计算。</p>
<p>优化之后的算法：</p>
<p><img src="Attachments/FlashAttentionAlgo.png" alt="" />
具体可以参考我的文章：<a href="Flash%20Attention%20v1.md">Flash Attention v1</a>.</p>
<h2>Load Balancer</h2>
<h3>DPLB</h3>
<blockquote>
<p>[!note]
<strong>Data parallel load balancing (DPLB)</strong> 是数据并行负载均衡</p>
</blockquote>
<h3>EPLB</h3>
<blockquote>
<p>[!note]
<strong>Expert parallel load balancing (EPLB)</strong> 是负责在 MoE 模型推理情景下，使在不同设备上的 Experts 负载均衡。</p>
</blockquote>
<h2>Misc.</h2>
<ul>
<li><strong><a href="https://zhida.zhihu.com/search?content_id=267603145&amp;content_type=Article&amp;match_order=1&amp;q=Multi-Lora&amp;zhida_source=entity">Multi-Lora</a></strong>(Low-Rank Adaptation)：多LoRA适配器共用基础模型，在拥有一个基础预训练模型与针对不同任务分别微调的多个特定LoRA适配器的情况下，多LoRA服务机制能根据传入请求动态选择所需的LoRA模块。参考Lora<a href="https://zhuanlan.zhihu.com/p/1983137653336585901#ref_11">[11]</a>，Multi-LoRA<a href="https://zhuanlan.zhihu.com/p/1983137653336585901#ref_12">[12]</a>。</li>
<li><strong><a href="https://zhida.zhihu.com/search?content_id=267603145&amp;content_type=Article&amp;match_order=1&amp;q=Guided+Decoder&amp;zhida_source=entity">Guided Decoder</a></strong>：引导解码器。约束模型输出，使其严格遵循预定义的格式或语法规则（如 JSON、SQL、正则表达式等），从而生成结构化、可控的文本输出。</li>
<li><strong>Function Call/Tool Call</strong>：工具调用。利用LLM的引导解码输出支持函数调用所需参数格式，使得LLM能够调用工具，参考1<a href="https://zhuanlan.zhihu.com/p/1983137653336585901#ref_13">[13]</a>,2<a href="https://zhuanlan.zhihu.com/p/1983137653336585901#ref_14">[14]</a>。</li>
</ul>
<h2>Benchmark</h2>
<h3>Latency</h3>
<ul>
<li><strong>Time to First Token (TTFT)</strong>: 首个 token 生成的时间，衡量 prefill 性能</li>
<li><strong>End-to-End Latency (E2EL)</strong>：端到端请求时延，从输入到输出结果</li>
<li><strong>Inter Token Latency (ITL)</strong>：Decode 阶段每个 token 的生成时间</li>
</ul>
<p>以上三者之间的关系：
$$
\text{ITL} = \frac{\text{E2EL} - \text{TTFT}}{n-1}
$$</p>
<p><img src="Attachments/Benchmark.png" alt="" /></p>
<p>除此之外，还有</p>
<ul>
<li><strong>Time per output token (TPOT)</strong>：所有 tokens 生成的平均时间。计算方式是：$\text{E2EL}/n$</li>
<li><strong>Time Between Tokens (TBT)</strong>：生成 token 之间的时间差。分别记录第 $i-1$ 和第 $i$ 个 token 的生成时间，$$\text{TBT}<em>{i} = \text{Latency}</em>{i} - \text{Latency}_{i-1}.$$</li>
</ul>
<h3>Throughput</h3>
<ul>
<li>
<p><strong>Queries Per Second (QPS)</strong>：每秒处理的请求数量。假设在一段时间内处理了 $T$ 个请求，对于每个请求 $i$，其 latency 是 $\text{latency}<em>{i}$，则：$$\text{QPS} = \frac{T}{\sum</em>{\text{i}=0}^T \text{latency}_{i}}$$</p>
</li>
<li>
<p>在类似 continuous batching 的情景下，也可以使用 $t_{\text{end}}- t_{\text{start}}$ 作为所有请求处理时间的估计。</p>
</li>
<li>
<p><strong>Tokens Per Second (TPS)</strong>：每秒吞吐的 token 数目（注意区别：一个请求可能需要生成多个 token）。通常是选取一个时间段，计算这个时间段内生成的 token 数量 $n_{\text{tok}}$，则：$$ \text{TPS}= \frac{n_{\text{tokens}}}{t_{\text{start}}- t_{\text{end}}} $$</p>
</li>
<li>
<p><strong>Top Percentile 90/99 (P90/P99)</strong>：至少有90%或者99%的请求满足该条件。例如至少 90% 的请求 latency 小于等于 1s，则 P90Latency = 1s.</p>
</li>
</ul>
<h3>Misc.</h3>
<ul>
<li><strong>Request Per Second (RPS)</strong>：每秒接受到的请求数量，吞吐测试的参考指标</li>
<li><strong>Service Level Objective (SLO)</strong>：服务质量目标。例如，希望始终满足请求 TPS P99=20 tokens/s.</li>
<li><strong>Model Flops Utilization (MFU)</strong>：衡量 GPU 算力资源使用效率</li>
</ul>
<h2>采样参数</h2>
<ul>
<li><strong>Temperature</strong>：温度，操作用于调整logits的概率分布整体情况，能让概率分布变得<strong>尖锐</strong>或者<strong>平坦</strong>。当温度较大时意味着除了模型倾向的答案，同时更多考虑其他词作为备选。（后续会在备选 token 中进行随机采样，当其他备选词概率变高意味着它们更有可能被选到）</li>
<li><strong>Top-K</strong>：概率排序后只选取 Top-K 个概率最大的值.</li>
<li><strong>Top-P</strong>：概率排序后按概率最高到最低顺序选，直到累计概率达到 $P$. 剩余的丢弃</li>
<li><strong>Min-P</strong>：保留所有概率至少为最高概率的 $P$ 倍的候选词</li>
<li><strong>Frequency Penalty</strong>：频率惩罚，对出现过的词，根据其出现频率降低logits值，频率越高衰减越严重</li>
<li><strong>Presence Penalty</strong>：存在惩罚，对出现过的词，在logits中减去一个相应惩罚值，每个词至多惩罚一次</li>
<li><strong>Repetition Penalty</strong>：重复惩罚，对重复出现的词进行衰减，类似频率处理</li>
<li><strong>Beam Search</strong>：束搜索，是一种结合topK和剪枝的搜索算法，每次保留束宽（beam width）k个结果</li>
</ul>
<h2>参考资料</h2>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/1983137653336585901">大模型推理核心概念与术语总结</a></li>
<li><a href="https://bentoml.com/llm/inference-optimization/llm-inference-metrics">Key metrics for LLM inference</a></li>
</ul>
]]></content>
        <author>
            <name>languisher</name>
            <uri>https://www.languisher.top/</uri>
        </author>
        <published>2026-03-03T15:19:05.177Z</published>
    </entry>
</feed>