logo
0
0
WeChat Login
编辑文件 README.md

https://www.yuque.com/ripic/zb2dv9/ggnsyl4l4o6a2syi?singleDoc# 《VLLM && CNB》

一、机器学习的前世今生

1.1、什么是机器学习

假设我们发现了一个函数 f(x):

  • f("小红有3个苹果,给了小明1个,还剩几个?") = "2个"
  • f("我真是谢谢你们的服务,烤冷面外卖送到的时候真成了冷面。这句话是正面还是负面评价?") = "负面"
  • f("你真帅。这句话是正面还是负面评价?") = "正面"

那就认为,对这两个例子来说,函数 f(x) 就是机器学习要找的函数。

那么难点来了,怎么找到这个函数呢?

我们降低一下目标,把要找的函数设定为 y = 3x,即我们要找一个函数 f(x),<font style="color:rgb(31, 35, 40);background-color:rgba(129, 139, 152, 0.12);">f(1) = 3</font><font style="color:rgb(31, 35, 40);background-color:rgba(129, 139, 152, 0.12);">f(2) = 6</font><font style="color:rgb(31, 35, 40);background-color:rgba(129, 139, 152, 0.12);">f(10) = 30</font>

有人会说这不简单吗,y = 3x。

嗯,别急,我们此时还不知道答案呢,或者当问题变得非常复杂时,靠人脑根本找不到函数的表达式时,怎么样才能让函数寻找得以继续?

<font style="color:rgb(31, 35, 40);">Define model function -> Define loss function -> Optimization</font>

第一步:Define model function

就是定义函数,这可不是一步到位定义函数,而是定义一个具有任意数量未知参数的函数骨架,我们希望通过调整参数的值来逼近最终正确函数。

假设我们定义一个简单的一元一次函数:

y=b+wxy=b+wx

其中未知参数是 w 和 b,也就是我们假设最终要找的函数可以表示为 b + wx,但具体 w 和 b 的值是多少,是需要寻找的。

我们可以这么定义:

const modelFunction = (b: number, w: number) => (x: number) => { return b + w * x; };

其中 w 表示 weights(权重),b 表示 bias(偏移),对这个简单的例子比较好理解。这样对于每一组 w 和 b,都能产生一个唯一的函数。

你也许会觉得,一元一次函数根本不可能解决通用问题。对,但为了方便说明机器学习的基本原理,我们把目标也设定为了简单的 y = 3x。

第二步:Define loss function

就是定义损失函数,这个损失可以理解为距离完美目标函数的差距,可以为负数,越小越好。

我们需要定义 loss 函数来衡量当前 w 与 b 的 loss,这样就可以判断当前参数的好坏程度,才能进入第三步的优化。因此 loss 函数的入参就是第一步 model function 的全部未知参数:w 与 b。

有很多种方法定义 loss 函数,一种最朴素的方法就是均方误差:

loss=i(yiyi)2loss = \sum_{i} (y_i - y'_i)^2

即计算当前实际值 <font style="color:rgb(31, 35, 40);background-color:rgba(129, 139, 152, 0.12);">modelFunction(b,w)(x)</font> 与目标值 <font style="color:rgb(31, 35, 40);background-color:rgba(129, 139, 152, 0.12);">3x</font> 的平方差。那么 loss 函数可以这样定义:

const lossFunction = (b: number, w: number) => (x: number, y: number) => { const cy = modelFunction(b, w)(x); return Math.pow(y - cy, 2); };

上述函数在给定 w 与 b 的下,计算在某个 training data 下的 loss,在调用处遍历所有 training data,把所有 loss 加起来,就是所有 training data 的 loss 总和。

为了让寻找的函数更准确,我们需要想办法让 loss 函数的值最小。

第三步:optimization

optimization 就是优化函数的参数,使 loss 函数值最小。

那么代码如下:

const optimization = (b: number, w: number) => (x: number, y: number) => { const gradB = -2 * (y - modelFunction(b, w)(x)); const gradW = -2 * x * (y - modelFunction(b, w)(x)); return { gradB, gradW }; };

接着我们就可以使用 training data 开始训练,不断更新参数 w 与 b 的值,直到 loss function 的值下降到极限,就可以认为训练完毕啦。

训练有三种方式使用偏导数,随机梯度下降、批量梯度下降与小批量梯度下降。它们的区别仅在于什么时候真正更新 w 与 b。

  • 随机梯度下降:对每一个 training data 项都立刻更新 w 与 b。
  • 批量梯度下降: 对所有 training data 都计算出 w 与 b,最后取平均值一次更新,之后再进入下一轮递归。
  • 小批量梯度下降:对 training data 取一个 batch size,达到 batch size 后立刻更新 w 与 b。

代码实践

const trainingData = [[1, 3],[2, 6],[3, 9],[4, 12],[5, 15]]; let b = initB; let w = initW; function train() { let gradBCount = 0;let gradWCount = 0; trainingData.forEach((trainingItem) => { const { gradB, gradW } = optimization(b, w)( trainingItem[0], trainingItem[1]); gradBCount += gradB;gradWCount += gradW;}); b += (-gradBCount / trainingData.length) * learningRate; w += (-gradWCount / trainingData.length) * learningRate; } // 训练 500 次 for (let i = 0; i < 500; i++) {train()}

实际操作:https://dp.333.zone

1.2、Transformer架构

1.2.1 数学推导

开始之前,我们先进性一轮数学推导,对于一个单节点的神经网络而言

z1=w11x1+w12x2+b1z_1 = w_{11}x_1 + w_{12}x_2 + b_1
x1,x2x_1, x_2:输入特征;w11,w12w_{11}, w_{12}对应的权重。b1b_1:偏置。z1z_1:线性输出(激活前的值)

还可以写成矩阵的形式

z1=[w11w12][x1x2]+b1z_1 = \begin{bmatrix} w_{11} & w_{12} \end{bmatrix} \cdot \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} + b_1

拓展到多神经元:

  • 神经元 1: z1=w11x1+w12x2+b1z_1 = w_{11}x_1 + w_{12}x_2 + b_1
  • 神经元 2: z2=w21x1+w22x2+b2z_2 = w_{21}x_1 + w_{22}x_2 + b_2
  • 神经元 3: z3=w31x1+w32x2+b3z_3 = w_{31}x_1 + w_{32}x_2 + b_3

显然:

[z1z2z3]=[w11w12w21w22w31w32][x1x2]+[b1b2b3]\begin{bmatrix} z_1 \\ z_2 \\ z_3 \end{bmatrix} = \begin{bmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \\ w_{31} & w_{32} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ b_3 \end{bmatrix}

加上激活函数之后:a=σ(z)=σ(Wx+b)\mathbf{a} = \sigma(\mathbf{z}) = \sigma(W\mathbf{x} + \mathbf{b}),这里的 σ\sigma 是对向量中的每个元素分别进行操作。

最常见的通用公式如下:

Z[l]=W[l]A[l1]+b[l]Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}

麻烦的加减乘除运算替换成了矩阵运算,可以充分利用GPU的运算能力。http://www.blackbox.ai/

1.2.2 卷积神经网络CNN

特性全连接层卷积层
基本操作矩阵乘法 ( WX+bWX+b )卷积运算 ( XK+bX * K + b )
连接方式全局连接 (每个输入连每个输出)局部连接 (只看局部区域)
权重共享无 (每个位置权重独立)有 (同一个核在图上滑动,权重共用)
适用场景分类器末端,处理抽象特征特征提取端,处理图像/语音/文本
数据维度1D 向量3D 张量 (高, 宽, 通道)

卷积核:https://setosa.io/ev/image-kernels/

数字识别与可视化(卷积->池化(降维)):https://adamharley.com/nn_vis/cnn/3d.html

1.2.3 循环神经网络RNN

1.2.3.1 Embedding

文字向量化:https://ronxin.github.io/wevi/ https://projector.tensorflow.org/

文字关联度

  • 向量 A=[a1,a2,...,an]A = [a_1, a_2, ..., a_n]
  • 向量 B=[b1,b2,...,bn]B = [b_1, b_2, ..., b_n]

点积

点积直接计算两个向量在对应维度上的乘积之和。它既考虑了方向,也考虑了向量的长度(模长)。如果两个向量的值都很大且方向一致,点积会非常大。

公式:

AB=i=1naibi=a1b1+a2b2++anbnA \cdot B = \sum_{i=1}^{n} a_i b_i = a_1b_1 + a_2b_2 + \dots + a_nb_n

或者用矩阵转置表示:

AB=ATBA \cdot B = A^T B

注意: 在深度学习的注意力机制(如 Transformer)中,经常直接使用点积来衡量关联度,但为了稳定梯度,通常会除以 dk\sqrt{d_k} 进行缩放。

余弦相似度

余弦相似度通过计算两个向量夹角的余弦值来衡量相似度。它只关注向量的方向是否一致,而忽略了向量的绝对长度(即归一化了模长的影响)。取值范围通常在 -1 到 1 之间。

公式:

Cosine Similarity(A,B)=cos(θ)=ABAB\text{Cosine Similarity}(A, B) = \cos(\theta) = \frac{A \cdot B}{\|A\| \|B\|}

展开后的详细公式为:

i=1naibii=1nai2i=1nbi2\frac{\sum_{i=1}^{n} a_i b_i}{\sqrt{\sum_{i=1}^{n} a_i^2} \sqrt{\sum_{i=1}^{n} b_i^2}}

其中:

  • 分子是两个向量的点积
  • 分母是两个向量的欧几里得范数(模长)的乘积

1.2.3.2 RNN的推导

回顾前馈神经网络的公式:

Z[l]=W[l]A[l1]+b[l]Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}

然后通常接一个非线性激活函数 g()g(\cdot),得到:

A[l]=g(Z[l])=g(W[l]A[l1]+b[l])A^{[l]} = g(Z^{[l]}) = g(W^{[l]} A^{[l-1]} + b^{[l]})

这是标准的前馈网络,信息单向流动,无记忆。

在序列建模中(如文本、语音、时间序列),我们希望网络具有“记忆”能力,即当前输出不仅依赖当前输入,还依赖过去的状态。

设:

  • x(t)x^{(t)}:时刻 tt 的输入;y(t)y^{(t)}:时刻 tt 的输出(可选)。
  • h(t)h^{(t)}:时刻 tt 的隐藏状态(hidden state),可视为网络的“记忆”;

RNN 的核心思想是:将上一时刻的隐藏状态 h(t1)h^{(t-1)} 作为当前时刻的额外输入

因此,在计算当前时刻的隐藏状态时,我们同时考虑:

当前输入 x(t)x^{(t)};上一时刻的隐藏状态 h(t1)h^{(t-1)}

类比前馈网络的公式,我们将 A[l1]A^{[l-1]} 替换为两个输入的拼接(或分别加权后相加):

Z(t)=Wxx(t)+Whh(t1)+bZ^{(t)} = W_{x} x^{(t)} + W_{h} h^{(t-1)} + b

其中:

  • WxW_x:输入到隐藏层的权重矩阵;WhW_h:隐藏层到自身的循环权重矩阵(体现“循环”);
  • bb:偏置项;Z(t)Z^{(t)}:时刻 tt 的加权输入。

然后应用非线性激活函数(常用 tanh 或 ReLU):

h(t)=g(Z(t))=g(Wxx(t)+Whh(t1)+b)h^{(t)} = g(Z^{(t)}) = g(W_{x} x^{(t)} + W_{h} h^{(t-1)} + b)

这就是基本 RNN 的隐藏状态更新公式

若需要输出 y(t)y^{(t)},可再加一个输出层:

y(t)=f(Vh(t)+c)y^{(t)} = f(V h^{(t)} + c)

其中:

  • VV:隐藏层到输出的权重;
  • cc:输出偏置;
  • f()f(\cdot):输出激活函数(如 softmax 用于分类)。

h(t)=g(Wxx(t)+Whh(t1)+b)y(t)=f(Vh(t)+c)\boxed{ \begin{aligned} h^{(t)} &= g(W_{x} x^{(t)} + W_{h} h^{(t-1)} + b) \\ y^{(t)} &= f(V h^{(t)} + c) \end{aligned} }

其中初始状态 h(0)h^{(0)} 通常设为零向量或可学习参数。

对比原始公式 A[l]=g(W[l]A[l1]+b[l])A^{[l]} = g(W^{[l]} A^{[l-1]} + b^{[l]}),RNN 相当于在每个时间步 tt 上,将“前一层”扩展为两个来源:

  • 外部输入 x(t)x^{(t)}
  • 自身历史状态 h(t1)h^{(t-1)}

这种结构使得网络在时间上展开后形成一个深层网络(称为“通过时间的反向传播”,BPTT),但参数 Wx,Wh,bW_x, W_h, b 在所有时间步共享,从而具备处理变长序列的能力。

1.2.4 Transformer

上面省略了缩放、掩码和softmax(归一化)运算

在此基础上增加残差网络和归一化处理,避免梯度消失,在连接上前馈神经网络

二、大模型推理加速框架

LLM 服务的性能瓶颈在于内存(显存)。在自回归解码autoregressive decoding过程中,LLM 的所有输入tokens都会产生其注意键和值张量attention key and value tensors,这些张量保存在 GPU 内存中以生成下一个tokens。这些缓存的键和值张量通常称为 KV 缓存(KV cache)。KV 缓存有如下特点:

  • 大:LLaMA-13B 中单个序列占用高达 1.7GB 的空间。
  • 动态:其大小取决于序列长度,而序列长度变化很大且不可预测。因此,有效管理 KV 缓存是一项重大挑战。我们发现现有系统由于碎片化和过度预留而浪费了 60% - 80% 的内存。

为了解决这个问题,vLLM引入了 PagedAttention,这是一种注意力算法,其灵感来自操作系统中虚拟内存和分页的经典思想。与传统的注意力算法不同,PagedAttention 允许在非连续的内存空间中存储连续的键和值。具体来说,PagedAttention 将每个序列的 KV 缓存划分为块,每个块包含固定数量 token 的键和值。在注意力计算过程中,PagedAttention 内核会高效地识别和获取这些块。

PagedAttention 通过其块表自然地实现了内存共享。与进程共享物理页面的方式类似,PagedAttention 中的不同序列可以通过将其逻辑块映射到同一物理块来共享块。为了确保安全共享,PagedAttention 会跟踪物理块的引用计数并实现写时复制机制。

PageAttention 的内存共享功能大大降低了复杂采样算法(例如parallel sampling和beam search)的内存开销,最多可减少 55% 的内存使用量。这可以转化为高达 2.2 倍的吞吐量提升。这使得此类采样方法在 LLM 服务中变得实用。

三、实践环节

四、参考文献

材料和参考资料稍晚更新到代码仓库