Zhangzhe's Blog

The projection of my life.

0%

URL

机器学习编译的本质与关注点

  • MLC 的本质:张量函数之间的转换
  • MLC 的关注点:
    • 所以可能的张量函数抽象表达
    • 所有可能的张量函数转换

构造 IR_Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@tvm.script.ir_module
class MyModule:
@T.prim_func
def relu0(X: T.Buffer[(1, 128), "float32"], Y: T.Buffer[(1, 128), "float32"]):
# function attr dict
T.func_attr({"global_symbol": "relu0", "tir.noalias": True})
for i, j in T.grid(1, 128):
with T.block("Y"):
vi, vj = T.axis.remap("SS", [i, j])
Y[vi, vj] = T.max(X[vi, vj], T.float32(0))

@T.prim_func
def linear0(
X: T.Buffer[(1, 784), "float32"],
W: T.Buffer[(128, 784), "float32"],
B: T.Buffer[(128,), "float32"],
Z: T.Buffer[(1, 128), "float32"],
):
T.func_attr({"global_symbol": "linear0", "tir.noalias": True})
Y = T.alloc_buffer((1, 128), "float32")
for i, j, k in T.grid(1, 128, 784):
with T.block("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + X[vi, vk] * W[vj, vk]

for i, j in T.grid(1, 128):
with T.block("Z"):
vi, vj = T.axis.remap("SS", [i, j])
Z[vi, vj] = Y[vi, vj] + B[vj]

@T.prim_func
def linear1(
X: T.Buffer[(1, 128), "float32"],
W: T.Buffer[(10, 128), "float32"],
B: T.Buffer[(10,), "float32"],
Z: T.Buffer[(1, 10), "float32"],
):
T.func_attr({"global_symbol": "linear1", "tir.noalias": True})
Y = T.alloc_buffer((1, 10), "float32")
for i, j, k in T.grid(1, 10, 128):
with T.block("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + X[vi, vk] * W[vj, vk]

for i, j in T.grid(1, 10):
with T.block("Z"):
vi, vj = T.axis.remap("SS", [i, j])
Z[vi, vj] = Y[vi, vj] + B[vj]

@R.function
def main(
x: Tensor((1, 784), "float32"),
w0: Tensor((128, 784), "float32"),
b0: Tensor((128,), "float32"),
w1: Tensor((10, 128), "float32"),
b1: Tensor((10,), "float32"),
):
with R.dataflow():
lv0 = R.call_tir(linear0, (x, w0, b0), (1, 128), dtype="float32")
lv1 = R.call_tir(relu0, (lv0,), (1, 128), dtype="float32")
out = R.call_tir(linear1, (lv1, w1, b1), (1, 10), dtype="float32")
R.output(out)
return out
  • @tvm.script.ir_module 装饰 IR_Module
  • @T.prim_func 装饰 主张量函数
  • @R.function 装饰 高层神经网络执行的抽象,(将整个 IR_Module 中的主张量函数串起来组成一个计算图)
  • R.dataflow() 用于标记程序计算图区域
  • R.call_tir(prim_func, inputs, output_shape, dtype) 分配输出内存并和输入一起输入主张量函数

构建并运行模型

1
2
3
4
5
ex = relax.vm.build(MyModule, target="llvm")
vm = relax.VirtualMachine(ex, tvm.cpu())
nd_res = vm["main"](
data_nd, nd_params["w0"], nd_params["b0"], nd_params["w1"], nd_params["b1"]
)
  • 可执行文件 = build(IR_Module)
  • 虚拟机执行器 = 虚拟机(可执行文件)
  • 运行结果 = 虚拟机执行器(模型输入)

使用现有库避免重复造轮子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@tvm.script.ir_module
class MyModuleWithExternCall:
@R.function
def main(
x: Tensor((1, 784), "float32"),
w0: Tensor((128, 784), "float32"),
b0: Tensor((128,), "float32"),
w1: Tensor((10, 128), "float32"),
b1: Tensor((10,), "float32"),
):
# block 0
with R.dataflow():
lv0 = R.call_tir("env.linear", (x, w0, b0), (1, 128), dtype="float32")
lv1 = R.call_tir("env.relu", (lv0,), (1, 128), dtype="float32")
out = R.call_tir("env.linear", (lv1, w1, b1), (1, 10), dtype="float32")
R.output(out)
return out
  • 上图中的 env.linear 是库张量函数,同一个 IR_Module 中可使用库张量函数,也可使用自定义张量函数,也可以二者混用。
1
2
3
@tvm.register_func("env.linear", override=True)
def torch_linear(x: tvm.nd.NDArray, w: tvm.nd.NDArray, b: tvm.nd.NDArray, out: tvm.nd.NDArray):
...
  • @tvm.register_func,注册库张量函数

绑定参数到 IR_Module

  • 以上的所有 IR_Module 都是在调用时才传入 数据权重参数,但 权重参数 是不变的,所以可以将 权重参数 提前绑定到 IR_Module 中,调用时只传入输入数据。
1
MyModuleWithParams = relax.transform.BindParams("main", nd_params)(MyModuleMixture)
  • relax.transform.BindParams(计算图入口函数,模型参数)(IR_Module) 将模型参数绑定到 IR_Module 的计算图入口函数中,返回一个绑定好模型参数的 IR_Module

总结

  • 计算图抽象(一般表示 main 函数) 有助于将元张量函数拼接在一起以进行端到端执行。
  • Relax 抽象的关键要素包括
    • call_tir 构造,将目标传递规范的元函数嵌入到计算图中
    • Dataflow block
  • 计算图允许调用环境库函数和 TensorIR 函数。

三种成像相关坐标系

  • 像素坐标系:一种 2D 坐标,以图片左上角为原点,横轴(宽度方向)向右为 x 轴正方向,纵轴(高度方向)向下为 y 轴正方向,单位是像素
  • 相机坐标系:一种 3D 坐标,以相机光心为原点,垂直相机平面远离相机方向为 z 轴正方向,垂直于 z 轴且平行于相机平面,水平向右为 x 轴正方向,竖直向下为 y 轴正方向,单位是米
  • 世界坐标系:一种 3D 坐标,一种人为定义的,且x, y, z 轴两两垂直的坐标系,单位是米

相机内参

  • 相机内参可以实现像素坐标系与相机坐标系之间相互转换,通常使用一个 3 * 3 矩阵表示

camera_1.jpg

camera_2.jpg

  • 根据小孔成像和相似三角形原理,可以得出相机坐标系与成像坐标系点的对应关系:Zf=Xx=Yy\frac{Z}{f}=\frac{X}{x}=\frac{Y}{y}

    其中:(X,Y,Z)(X,Y,Z) 为相机坐标系下的点的坐标, (x,y)(x, y) 为投影到成像平面上的点的坐标, ff 表示焦距。

  • 再根据成像坐标系到像素坐标系的对应关系:
    $
    \begin{matrix}
    u=\alpha \cdot x + c_x \
    v = \beta \cdot y + c_y
    \end{matrix}
    $

    其中:

    • α,β\alpha,\beta 分别表示 x,yx, y 方向上成像宽度到像素宽度的投影
    • 由于成像坐标系原点为成像中心,像素坐标系原点为像素左上角,所以需要加上原点的偏移, cx,cyc_x,c_y 分别表示 x,yx, y 方向上原点的偏移。
  • 所以,相机坐标系 (X,Y,Z)(X,Y,Z) 与像素坐标系 (u,v)(u, v) 可通过相机内参相互转换:
    $
    Z \begin{bmatrix} u \ v \ 1 \end{bmatrix} = \begin{bmatrix} f_x & 0 & c_x \ 0 & f_y & c_y \ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}X\ Y\ Z \end{bmatrix}
    $

    其中: fx=αf, fy=βf, Zf_x=\alpha \cdot f,\ f_y=\beta \cdot f,\ Z 表示相机坐标系下的深度

  • [fx0cx0fycy001]\begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} 被称为相机内参 KK

相机外参

  • 相机外参可以实现相机坐标系与世界坐标系之间相互转换(刚体变换),通常用一个 3 * 3 的旋转矩阵 RR 和一个 3 * 1 的平移矩阵 TT 表示
    $
    \begin{bmatrix} X_c\ Y_c \ Z_c \end{bmatrix}=\begin{bmatrix} R_{11} & R_{12} & R_{13} \ R_{21} & R_{22} & R_{23} \ R_{31} & R_{32} & R_{33} \end{bmatrix} \begin{bmatrix} X_w\ Y_w \ Z_w \end{bmatrix} + \begin{bmatrix} T_{1} \ T_{2} \ T_{3} \end{bmatrix}
    $

    其中: (Xc,Yc,Zc)(X_c,Y_c,Z_c) 表示相机坐标系下的点, (Xw,Yw,Zw)(X_w,Y_w,Z_w) 表示世界坐标系下的点

  • 齐次化之后,得到一个 4 * 4 的矩阵:
    $
    \begin{bmatrix} X_c\ Y_c \ Z_c \ 1\end{bmatrix}=\begin{bmatrix} R_{11} & R_{12} & R_{13} & T_1 \ R_{21} & R_{22} & R_{23} & T_2 \ R_{31} & R_{32} & R_{33} & T_3 \ 0 & 0 & 0 & 1\end{bmatrix} \begin{bmatrix} X_w\ Y_w \ Z_w \ 1\end{bmatrix}
    $

总结

  • camera intrisic matrix×camera coordinate system=depth in camera coordinatesystemimage coordinate systemcamera\ intrisic\ matrix \times camera\ coordinate\ system = depth\ in\ camera\ coordinate system \cdot image\ coordinate\ system
  • camera extrisic matrix×world coordinate system=camera coordinate systemcamera\ extrisic\ matrix \times world\ coordinate\ system = camera\ coordinate\ system
  • camera intrisic matrix×camera extrisic matrix×world coordinate system=depth in camera coordinatesystemimage coordinate systemcamera\ intrisic\ matrix \times camera\ extrisic\ matrix \times world\ coordinate\ system = depth\ in\ camera\ coordinate system \cdot image\ coordinate\ system

URL

TL;DR

  • 本文是 BEV (bird eye view) 的开山之作,通过隐式 2D 深度估计和像素坐标到世界坐标转换,将多张(6张)车周环视图拼接得到一张鸟瞰图。
  • 具体实现请看代码,代码中有非常详细的注释。

Algorithm

eval000007_000.jpg

在 inference 过程中,下半部分的 6 张环视图为输入,上半部分的鸟瞰图为输出(地图和本算法无关)。

Dataset

LSS1.png

  • 输入的 6 张图来自上图的 6 个绿色 camera

  • 世界坐标系如图 IMU 所示原点定为 车后轴中心x 轴正方向为车辆前进方向, y 轴正方向为面向车辆前进方向的左手边,z 轴正方向为竖直向上。

相关知识

  • 相机内外参:参考 相机内参与外参

  • 体素:

    • 立体像素
    • 在本文中,一个体素表示 x 方向上 0.5m,y 方向上 0.5m,z 方向上 20m 的立方体区域
    • 体素为鸟瞰视角中可区分的最小单元

算法细节

特别细节的看代码

1. 特征提取(Lift)

  • 使用 EfficientNet 进行 2D 特征提取 + 隐式深度估计
    • 输入shape = [4, 6, 3, 128, 352],分别表示:[batch, cameras, channel, height, width]
    • 输出shape = [24, 64, 41, 8, 22],分别表示:[batch * cameras, features, depth, height, width]
      • 使用 64 维向量编码深度(不是直接预测深度,所以被称为隐式深度估计)
      • 深度从 4m ~ 45m,编码精度为 1m,所以有 41 种离散深度,相机坐标系下深度估计的目的是:从像素坐标转化为世界坐标
      • 长宽各下采样 16 倍,减小计算量

2. 像素坐标和相机坐标系下深度到世界坐标的映射(Splat)

  • 使用如下参数将像素坐标和相机坐标系下深度映射到世界坐标
    • 相机内参
    • 相机外参
      • 旋转
      • 平移
    • 像素坐标系内变换参数(缩放 + 裁剪(平移))
      • 原图(900, 1600) -> 模型输入图(128, 352) -> 模型预测图(8, 22)
  • 体素池化:将属于同一个体素的深度估计向量求和
  • 输入
    • 深度估计:shape = [24, 64, 41, 8, 22]
    • 相机内外参和缩放参数
  • 输出shape = [4, 64, 200, 200]
    • 200 * 200 个体素
      • X 方向上 [-50m, 50m) 0.5m 为一个 bin,200 个 bin
      • Z 方向上 [-50m, 50m) 0.5m 为一个 bin,200 个 bin
      • Y 方向不分 bin
    • 每个体素用 64 维向量编码
  • 本质是:
    1. 构造一个 [24 * 41 * 8 * 22, 3] 的查找表,输入为 backbone 输出特征图的每一个 pixel,输出为这个 pixel 对应的世界坐标(这个查找表可由相机内外参和图像缩放系数计算得到)
    2. 将离散的世界坐标点合并,合并规则是属于同一个体素的坐标点则合并

3. 体素编码降维(Shoot)

  • 输入shape = [4, 64, 200, 200]
  • 输出shape = [4, 1, 200, 200]BEV 图

4. 训练 loss

非常简单粗暴

  • 将 GT bbox3d 同样映射到 BEV 空间 [4, 1, 200, 200],然后做 pixel-wise loss(分割 loss)

Thought

  • 代码中 像素坐标和相机坐标系下深度到世界坐标的映射 部分比较难懂,需要较强的相机成像原理 / 图像 2D3D 背景才能看懂

URL

TL;DR

  • 本文提出一种新型 Transformer 结构使用 Window Multi-head Self-attention (W-MSA)Shifted Window Multi-head Self-attention (SW-MSA) 结构替代原始 Transformer 使用的 Multi-head Self-attention (MSA) 结构。

    • 大大节省了原始 Transformer 的计算复杂度。

    • 在视觉任务中吊打了一众 CNNTransformer

Algorithm

1.png

总体结构

  • swin-Transformer 总体按照类似 CNN 的层次化构建方式构建网络结构,分为 4 个 stage,每个 stage 都会将分辨率缩小一倍,channel 数扩大一倍 (like vgg)

  • Swin Tranformer Block 像大多数 Transformer Block 一样,不改变输入特征 shape,可以看做是一种比较高级(加入了 self-attention)的激活函数。

  • 图中 Swin Tranformer Block 都是以连续偶数次出现,因为是一个 W-MSA Transformer Block + 一个 SW-MSA Transformer Block,如右边子图 b 所示。

Patch Partition

  • 作用与 ViT 第一步很像:将图片切成若干等大小不重叠的 patchpatch_size = P,然后把每个 patch(P, P, c) 拉成 1 维

  • 本实验中 patch_size = 4,所以一张图被裁切成了 H4×W4\frac{H}{4}\times\frac{W}{4} 个长度为 4 * 4 * 3 = 48 的向量。

Linear Embedding

  • 与一个 shape = (48, C) 的矩阵乘将 48 维映射到 C 维。

与上一步结合可以变成一个 in_channel = 3, out_channel = C, kernel_size = stride = 4 的 Conv2d,官方实现中实际上也是这么做的( class PatchEmbed )。

Patch Merging

  • 由两步组成,作用是 将分辨率缩小一倍,channel 扩大一倍

    • H4×W4×C\frac{H}{4}\times\frac{W}{4}\times C 的输入使用 bayer2rggb (space2depth with block_size = 2) 变成 H8×W8×4C\frac{H}{8}\times\frac{W}{8}\times 4C

    • 再将 H8×W8×4C\frac{H}{8}\times\frac{W}{8}\times 4C 与一个 4C×2C4C\times 2C 的矩阵相乘,输出一个 H8×W8×2C\frac{H}{8}\times\frac{W}{8}\times 2C 的矩阵。

W-MSA

  • 全称为 Windows Multi-head Self-attention,目的是为了减少 Self-attention 计算量。

    • 原始的 MSA 会直接对完整输入计算其 self-attention 结果

    • W-MSA 先将输入拆成大小为 M×MM\times M 且互不重叠的 windows,然后计算每个 windowself-attention 结果。

    • 计算公式还是: Attention(Q,K,V)=softmax(QKTdk)VAttention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V

  • MSAW-MSA 计算量对比(假设输入特征图 XRH×W×CX \in \mathbb{R}^{H\times W\times C},且 Wq,Wk,WvRC×CW_q,W_k,W_v \in \mathbb{R}^{C\times C},且 Multi-headhead 数为 1):

    • MSA 计算量:

      • X -> Q / K / V 计算量:RH×W×C×RC×C\mathbb{R}^{H\times W\times C}\times \mathbb{R}^{C\times C} 计算量为 3HWC23HWC^2

      • QKTQK^T 计算量:RHW×C×RC×HW\mathbb{R}^{HW\times C}\times \mathbb{R}^{C\times HW} 计算量为 H2W2CH^2W^2C

      • 不考虑 softmax 和 ..dk\frac{..}{\sqrt{d_k}} 计算量。

      • softmax 结果 ×V\times V 计算量: RHW×HW×RHW×C\mathbb{R}^{HW \times HW}\times \mathbb{R}^{HW \times C} 计算量为 H2W2CH^2W^2C

      • 因为需要 Multi-head,所以需要将 ×V\times V 之后的矩阵再 ×Wo\times W_o,且 head 数为 1,计算量: R1HW×C×RC×C\mathbb{R}^{1*HW \times C}\times \mathbb{R}^{C \times C} 计算量为 HWC2HWC^2

      • 总计算量: 4HWC2+2H2W2C4HWC^2 + 2H^2W^2C

    • W-MSA 计算量:

      • 上图的 HM, WMH\rightarrow M,\ W\rightarrow M,一共重复 HWM2\frac{HW}{M^2} 次,所以总计算量为:

      (4M2C2+2M4C)×HWM2=4HWC2+2HWM2C(4M^2C^2 + 2M^4C)\times \frac{HW}{M^2}=4HWC^2+2HWM^2C

    • 相比之下 W-MSA 会比 WSA 计算量少: 2HWC(HWM2)2HWC(HW-M^2)

SW-MSA

  • 为了节省计算量 W-MSA 会将输入的完整特征图分 window,每个 window 独立去做 self-attention,这会导致 window 之间的关联性消失,这有悖于 self-attention 会在全图上建立长距离全局相关性依赖的特点。
  • 所以,作者引入 SW-MSA 的方案通过 滑窗 解决这一问题。

2.png

  • 滑窗会带来边角处的零碎区域(长或宽小于 window_size 的区域),由于 H%M=0, W%M=0H \% M = 0,\ W \% M = 0 (W-MSA 要求),所以零碎的区域可以通过调整位置拼成完整 window

  • 完整的滑窗区域与 W-MSA 一样独立做 Self-attention

  • 由零碎区域拼成的滑窗区域中:

    • 本属于同一零碎区域的位置可以 Self-attention

    • 本不属于同一零碎区域的位置在原图上不相邻,不能做 Self-attention

    • 具体做法是来自不同区域位置之间的 Self-attentionmask 掉 ,只保留来自相同区域的位置间的 Self-attention

W-MSA + SW-MSA

  • 如网络结构图子图 b 所示,W-MSA 连接 SW-MSA 之后,数学表示:

    z^l=WMSA(LN(zl1))+zl1\hat{z}^l=W-MSA(LN(z^{l-1})) + z^{l-1}

    zl=MLP(LN(z^l))+z^lz^l=MLP(LN(\hat z^l)) + \hat z^l

    z^l+1=SWMSA(LN(zl))+zl\hat z^{l+1} = SW-MSA(LN(z^l))+z^l

    zl+1=MLP(LN(z^l+1))+z^l+1z^{l+1} = MLP(LN(\hat z^{l+1}))+\hat z^{l+1}

Relative Position Bias

  • Self-attention 加上 相对位置偏置 bias 之后,精度提升巨大。

  • 原始 Self-attentionAttention(Q,K,V)=softmax(QKTdk)VAttention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V

  • Relative Position Bias Self-attentionAttention(Q,K,V)=softmax(QKTdk+Bias)VAttention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}} + Bias)V

3.png

Through

  • 质疑了很多人都没有质疑的点,例如给 Self-attention 加上相对位置偏置。

  • 本质是对 Tranformer 的一种很 work 的加速方法。

  • Shift window 逻辑比较复杂,没有经典模型该有的简约美,盲猜之后会被更 make sense 的结构替代。

Python 闭包 (closure)

闭包定义

  • 闭包: 在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

  • 支持将函数当成对象使用的编程语言,一般都支持闭包。比如 Python, JavaScript

闭包的示例

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def function_1(arg_1):
    def function_2(arg_2):
    return arg_1 * arg_2

    return function_2


    times_8 = function_1(8)
    out = times_8(9)
    print(f"times_8(9) = {out}")
    # 闭包中的 cell
    print(f"times_8.__closure__ = {times_8.__closure__}")
    # 闭包中的 cell 对象的内容
    print("times_8.__closure__.cell_contents:")
    for i in times_8.__closure__:
    print(i.cell_contents)
  • 输出

    1
    2
    3
    4
    times_8(9) = 72
    times_8.__closure__ = (<cell at 0x7ff39d4d2a30: int object at 0x5642a56c5e20>,)
    times_8.__closure__.cell_contents:
    8

闭包的用处

1. 可以读取函数内部的变量

  • 如上面给出的例子

2. 让这些变量的值始终保持在内存中

  • 例如一个棋盘游戏,棋子每次可以选择上下左右方向中的一个,在此方向上移动距离 step,使用闭包实现代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def create(pos=[0, 0]):

    def go(direction, step):
    new_x = pos[0] + direction[0] * step
    new_y = pos[1] + direction[1] * step

    pos[0] = new_x
    pos[1] = new_y

    return pos


    return go

    player = create()
    print(player([1, 0], 10))
    print(player([0, 1], 20))
    print(player([-1, 0], 10))
  • 输出

    1
    2
    3
    [10, 0]
    [10, 20]
    [0, 20]

棋子每次更新后的位置都会存储在闭包中。

3. 用于装饰器

  • 可以读取函数内部的变量让这些变量的值始终保持在内存中 都可以使用 Python 的类实现,但 装饰器 是闭包的一个典型用处。

装饰器 (Decorators)

装饰器的定义

  • 装饰器: 由闭包的概念引申而来,是一种 增加函数或类功能的方法,它可以快速地给不同的函数或类传入相同的功能。

函数装饰器的示例

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import time


    def count_time(some_fun):
    def wrapper():
    t1 = time.time()
    some_fun()
    print(f"运行时间为: {round(time.time() - t1, 2)} s")

    return wrapper


    # 装饰器语法糖
    @count_time
    def function_1():
    time.sleep(1)
    print("run function_1")


    function_1()

    # 不使用语法糖的方法
    def function_2():
    time.sleep(1)
    print("run function_2")


    new_function = count_time(function_2)
    new_function()

  • 输出

    1
    2
    3
    4
    run function_1
    运行时间为: 1.0 s
    run function_2
    运行时间为: 1.0 s

类别装饰器示例

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import time


    class Timer:
    def __init__(self, func) -> None:
    self.func = func

    def __call__(self, *args, **kwargs):
    start = time.time()
    ret = self.func(*args, **kwargs)
    print(f"Time: {time.time()-start}")
    return ret


    # 使用装饰器语法糖实现
    @Timer
    def add_1(a, b):
    time.sleep(1)
    print(f"{a} + {b} = {a+b}")


    add_1(2, 3)

    # 不使用装饰器语法糖实现
    def add_2(a, b):
    time.sleep(1)
    print(f"{a} + {b} = {a+b}")


    new_add_2 = Timer(add_2)
    new_add_2(2, 3)
  • 输出

    1
    2
    3
    4
    2 + 3 = 5
    Time: 1.0011768341064453
    2 + 3 = 5
    Time: 1.001098394393921

URL

TL;DR

  • 本文用比较简洁的方式给出了神经网络的通用量化方法,是量化领域的必读论文。

Algorithm

1. 量化基础知识

1.1 硬件背景

  • 一个 y=Wx+by=Wx+b 实际上是由 乘法器累加器 组合而成的,实际的计算过程如下:

quant_4.png

卷积实际上也是通过 image to column 操作变成 y=Wx+by=Wx+b 操作

  • 常见的 int8 量化会将上述过程变成如下过程:

quant_1.png

weightinput 都被量化为 int8 ,同时保留各自的量化 scale,乘法操作是整形乘法器(更快),累加器是 int32 类型,最后再量化为 int8 放到 OCM

1.2 均匀仿射量化

  • 均匀仿射量化也被称为 非对称量化,由三个量化参数定义:

    • 比例因子 scale
    • 零点 zero_point
    • 比特宽度 bits
  • 非对称量化:

    • for unsigned integers: Xint=clamp(Xs+z;0,2b1)X_{int} = clamp(\lfloor\frac{X}{s}\rceil+z;0,2^b-1)
    • for signed integers: Xint=clamp(Xs+z;2b1,2b11)X_{int} = clamp(\lfloor\frac{X}{s}\rceil+z;-2^{b-1},2^{b-1}-1)
    • 这里的 \lfloor\rceil 表示 round 运算
  • 对称量化是非对称量化的简化版本,具体是将零点 zero_point 固定为 0

  • 对称量化:

    • for unsigned integers: Xint=clamp(Xs;0,2b1)X_{int} = clamp(\lfloor\frac{X}{s}\rceil;0,2^b-1)
    • for signed integers: Xint=clamp(Xs;2b1,2b11)X_{int} = clamp(\lfloor\frac{X}{s}\rceil;-2^{b-1},2^{b-1}-1)
  • 对称量化和非对称量化的含义:

quant_3.png

quant_2.png

  • 2 的指数幂量化:

    • 限制 s=2ks=2^{-k}
    • 优势:scale 过程变成了硬件移位,对硬件更友好。
    • 劣势:会使得 round 和 clip 误差的权衡变难。
  • 量化颗粒度:

    • per-tensor: 硬件更友好,但限制了量化的自由度。
    • per-channel: 反之。

1.3 量化模拟

  • 量化模拟是指在浮点计算设备上模拟定点计算设备的过程,通常用于训练。

quant_2.png

左边是定点计算过程,右边是用浮点设备模型定点计算的过程

  • 为了减少数据搬运和不必要的量化步骤,通常会做:
    • batch norm 折叠:batch norm 在推理时是静态的,因此可以和前面的 conv 等层合并。
    • 激活函数融合:在实际的硬件解决方案中,通常会在非线性操作(如 ReLU)之后直接进行量化,而不是先将激活写入内存然后再加载回计算核心。

1.4 实践考量

  • 对称量化和非对称量化:

    • 对称量化:zero-point == 0
    • 非对称量化:zero-point != 0
  • 为了方便计算,通常情况下,会将权重设置为对称量化(zw=0z_w=0),将特征设置为非对称量化(zx0z_x\ne 0

    • 原因分析:
      • W=Sw(WintZw)W=S_w(W_{int} - Z_w)
      • X=Sx(XintZx)X=S_x(X_{int} - Z_x)
      • WX=SwSx(WintZw)(XintZx)=SwSxWintXintSwSxZwXintSwSxZxWint+SwSxZwZxWX=S_wS_x(W_{int} - Z_w)(X_{int} - Z_x)\\=S_wS_xW_{int}X_{int}-S_wS_xZ_wX_{int}-S_wS_xZ_xW_{int}+S_wS_xZ_wZ_x
      • 在推理阶段:Sw, Sx, Zw, Zx, WintS_w,\ S_x,\ Z_w,\ Z_x,\ W_{int} 已知,因此:
        • 等式的第三项和第四项可提前算出,无需推理耗时。
        • 第一项和第二项由于关联动态输入 XintX_{int},因此需要额外耗时;但是如果设置 Zw=0Z_w=0,则第二项恒等于0,可节省计算量

2. 训练后量化(PTQ,post-training quantization)

  • 训练后量化是指用 float32 精度训练的模型直接转成量化模型,无需任何数据和训练。

2.1 量化范围的设置

  • 最大最小值法(min-max):qmin=minV,  qmax=maxVq_{min}=minV,\ \ q_{max}=maxVVV 是待量化 tensor
  • 均方差法(MSE):arg minqmin,qmaxVV^(qmin,qmax)F2\argmin_{q_{min},q_{max}}||V-\hat{V}(q_{min}, q_{max})||^2_F
  • 交叉熵法(cross entropy):arg minqmin,qmax=H(softmax(V),softmax(V^(qmin,qmax)))\argmin_{q_{min},q_{max}}=H(softmax(V),softmax(\hat{V}(q_{min},q_{max}))),其中 HH 表示 cross entropy function
  • 批量归一化法(BN based):qmin=min(βαγ),  qmax=max(β+αγ)q_{min}=min(\beta-\alpha\gamma),\ \ q_{max}=max(\beta+\alpha\gamma),其中 β, γ\beta,\ \gamma 分布表示 batch norm 学到的 per channelshiftscaleα>0\alpha>0 是超参数
  • 组合法(comparsion):以上方法的自由组合

quant_5.png
quant_6.png

使用不同量化方法分别量化 weightactivation 后的精度

2.2 跨层均衡(Cross-Layer Equalization)

  • 这是一种 通过修改模型权重 来改善神经网络量化性能的技术,CLE 的目的是减少网络中不同 channel 之间由于量化引起的性能不平衡,这种问题在 depth-wise conv layer 中尤其容易出现。

quant_8.png

mobilenetv2 第一个 depth-wise conv 层的 per output channel weight range

  • 想要实现跨层均衡的模型,需要激活函数满足交换律,即:f(sx)=sf(x)f(sx)=sf(x),常见的 ReLUPReLU 都满足。

quant_7.png

  • CLE 原理:

    • y=f(W2(W1x+b1)+b2)=f(SW2(S1W1x+S1b1)+b2)=f(W2^(W1^x+b1^)+b2)y=f(W_2(W_1x+b_1)+b_2)\\=f(SW_2(S^{-1}W_1x+S^{-1}b_1)+b_2)\\=f(\hat{W_2}(\hat{W_1}x+\hat{b_1})+b_2)
    • 其中:
      • W1^=S1W1\hat{W_1}=S^{-1}W_1
      • b1^=S1b1\hat{b_1}=S^{-1}b_1
      • W2^=SW2\hat{W_2}=SW_2
      • Si=ri1ri2ri2S_i=\frac{\sqrt {r_i^1r_i^2}}{r_i^2},其中 rijr_i^j 表示 j tensori channel
  • abosrbing high bias 是一种 解决模型中过大 bias 的技术,原理是:

    • y=W2h+b2=W2(f(W1x+b1))+b2=W2(f(W1x+b1)+cc)+b2=W2(f(W1x+b1^)+c)+b2=W2(f(W1x+b1^))+b2^=W2h^+b2^y=W_2h+b_2\\=W_2(f(W_1x+b_1))+b_2\\=W_2(f(W_1x+b_1)+c-c)+b_2\\=W_2(f(W_1x+\hat{b_1})+c)+b_2\\=W_2(f(W_1x+\hat{b_1}))+\hat{b_2}\\=W_2\hat{h}+\hat{b_2}
    • 其中:
      • b2^=b2+W2c\hat{b_2}=b_2+W_2c
      • h^=hc\hat{h}=h-c
      • b1^=b1c\hat{b_1}=b_1-c
      • ci=max(0,minx(W1ix+b1i))c_i=max(0, min_x(W_{1i}x+b_{1i}))

URL

https://mlc.ai/zh/chapter_tensor_program/index.html

元张量函数

1.png

  • 元张量函数 表示机器学习模型计算中的单个单元计算。
    • 一个机器学习编译过程可以有选择地转换元张量函数的实现。

张量程序

2.png

  • 张量程序 是一个表示元张量函数的有效抽象。
    • 关键成分包括: 多维数组,循环嵌套,计算语句。
    • 程序变换可以被用于加速张量程序的执行。
    • 张量程序中额外的结构能够为程序变换提供更多的信息。

TensorIR: 张量程序抽象案例研究

  • TensorIR 是标准机器学习编译框架 Apache TVM 中使用的张量程序抽象。

目标

  • 使用 TensorIR 张量程序抽象 ReLU(A @ B) 张量函数。

  • 数学表示:

    • Yi,j=kAi,k×Bk,jY_{i,j}=\sum_k A_{i,k}\times B_{k,j}
    • Ci,j=ReLU(Yi,j)=max(Yi,j,0)C_{i,j}=ReLU(Y_{i,j})=max(Y_{i,j}, 0)

不同实现方法

使用 Numpy 实现

1
2
3
4
5
dtype = "float32"
a_np = np.random.rand(128, 128).astype(dtype)
b_np = np.random.rand(128, 128).astype(dtype)
# a @ b is equivalent to np.matmul(a, b)
c_mm_relu = np.maximum(a_np @ b_np, 0)

使用 Low Level Numpy 实现

Low Level Numpy 是指只使用 Numpy 的数据结构而不调用 Numpy 的 API

1
2
3
4
5
6
7
8
9
10
11
12
# Use low level numpy to implement matmal ReLU
def lnumpy_mm_relu(A: np.ndarray, B: np.ndarray, C: np.ndarray):
Y = np.empty((128, 128), dtype="float32")
for i in range(128):
for j in range(128):
for k in range(128):
if k == 0:
Y[i, j] = 0
Y[i, j] = Y[i, j] + A[i, k] * B[k, j]
for i in range(128):
for j in range(128):
C[i, j] = max(Y[i, j], 0)

使用 TensorIR 实现

TensorIRTVMScript 中的一种 Python 方言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import tvm
from tvm.ir.module import IRModule
from tvm.script import tir as T


@tvm.script.ir_module
class MyModule:
@T.prim_func
def mm_relu(A: T.Buffer[(128, 128), "float32"],
B: T.Buffer[(128, 128), "float32"],
C: T.Buffer[(128, 128), "float32"]):
T.func_attr({"global_symbol": "mm_relu", "tir.noalias": True})
Y = T.alloc_buffer((128, 128), dtype="float32")
for i, j, k in T.grid(128, 128, 128):
with T.block("Y"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(128, 128):
with T.block("C"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))

TensorIR 代码与 Low Level Numpy 代码对比

函数参数

1
2
3
4
5
6
7
8
# TensorIR
def mm_relu(A: T.Buffer[(128, 128), "float32"],
B: T.Buffer[(128, 128), "float32"],
C: T.Buffer[(128, 128), "float32"]):
...
# numpy
def lnumpy_mm_relu(A: np.ndarray, B: np.ndarray, C: np.ndarray):
...

buffer

1
2
3
4
# TensorIR
Y = T.alloc_buffer((128, 128), dtype="float32")
# numpy
Y = np.empty((128, 128), dtype="float32")

循环

1
2
3
4
5
6
# TensorIR
for i, j, k in T.grid(128, 128, 128):
# numpy
for i in range(128):
for j in range(128):
for k in range(128):

计算块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# TensorIR
with T.block("Y"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]

# coressponding numpy code
vi, vj, vk = i, j, k
if vk == 0:
Y[vi, vj] = 0
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]

块(Block)TensorIR 中的基本计算单位。

  • 值得注意的是,对于一组固定的 vi 和 vj,计算块在 Y 的空间位置 (Y[vi, vj]) 处生成一个点值,该点值独立于 Y 中的其他位置(具有不同的vi, vj 值的位置)。我们可以称 vi、vj 为 空间轴,因为它们直接对应于块写入的缓冲区空间区域的开始。 涉及归约的轴(vk)被命名为 归约轴

  • 空间轴上的每个点都独立于其他点。

1
2
3
4
5
6
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
# 使用语法糖可等价写成如下形式
# SSR means the properties of each axes are "spatial", "spatial", "reduce"
vi, vj, vk = T.axis.remap("SSR", [i, j, k])

函数属性

1
T.func_attr({"global_symbol": "mm_relu", "tir.noalias": True})

其中:

  • global_symbol 对应函数名。
  • tir.noalias 是一个属性,表示所有的缓冲存储器不重叠。

装饰器

  • @tvm.script.ir_module 表示被装饰的类是一个 IRModule。
  • @T.prim_func 表示被装饰的函数是一个张量函数。

URL

https://mlc.ai/zh/chapter_introduction/index.html

什么是机器学习编译

  • 机器学习编译 (machine learning compilation, MLC) 是指,将机器学习算法从开发阶段,通过变换和优化算法,使其变成部署状态。

1.png

机器学习的痛点之一是:训练框架繁多/部署终端种类繁多,开发与部署存在 gap 。

  • 开发形式 是指我们在开发机器学习模型时使用的形式。典型的开发形式包括用 PyTorch、TensorFlow 或 JAX 等通用框架编写的模型描述,以及与之相关的权重。

  • 部署形式 是指执行机器学习应用程序所需的形式。它通常涉及机器学习模型的每个步骤的支撑代码、管理资源(例如内存)的控制器,以及与应用程序开发环境的接口(例如用于 android 应用程序的 java API)。

  • 机器学习编译的目标

    • 集成与最小化依赖
    • 利用硬件加速
    • 通用优化

机器学习编译的关键要素

  • 张量
  • 张量函数
  • 抽象:做什么
  • 实现:怎么做

2.png

绿色节点表示张量,白色节点表示张量函数

3.png

机器学习编译过程中的张量函数变换过程

4.png

抽象和实现

总结

  • 机器学习编译的目标
    • 集成与最小化依赖
    • 利用硬件加速
    • 通用优化
  • 为什么学习机器学习编译
    • 构建机器学习部署解决方案
    • 深入了解现有机器学习框架
    • 为新兴硬件建立软件栈
  • 机器学习编译的关键要素
    • 张量和张量函数
    • 抽象和实现是值得思考的工具

需求弹性

  • 弹性:衡量需求量或供给量对其某种决定因素的变动的反应程度的指标。

  • 需求价格弹性(Price elasticity of demand):衡量一种物品需求量对其价格变动反应程度的指标,用需求量变动百分比除以价格变动百分比来计算。

需求价格弹性的决定因素

  • 相似替代品的可获得性
  • 必需品和奢侈品:必需品往往缺乏弹性,奢侈品往往富有弹性。
  • 市场的定义:任何一个市场的需求弹性都取决于我们如何划定市场的边界,狭窄定义的市场比宽泛定义的市场的弹性更大,例如:香草冰淇淋的弹性很大,但冰淇淋的弹性较小。
  • 时间范围:物品的需求往往在长期内更富有弹性。

需求价格弹性的计算

Price elasticity of demand=Demand change PercentagePrice change percentage   ,(change percentage>=0)Price\ elasticity\ of\ demand=\frac{Demand\ change\ Percentage}{Price\ change\ percentage}\ \ \ ,(change\ percentage >= 0)

假设某种物品存在如下两个需求价格统计点:

  • A:价格 4 / 需求量 120
  • B:价格 6 / 需求量 80

则需求价格弹性计算可知:

  • A>B Price elasticity of demand=(12080)/120(64)/4=23A -> B\ Price\ elasticity\ of\ demand = \frac{(120 - 80)/120}{(6 - 4)/4}=\frac{2}{3}
  • B>A Price elasticity of demand=(12080)/80(64)/6=1.5B -> A\ Price\ elasticity\ of\ demand = \frac{(120 - 80)/80}{(6 - 4)/6}=1.5

可以看出 A -> B 的需求价格弹性和 B -> A 的需求价格弹性不一致,因此提出中点法。

中点法:一个计算变动百分比和需求价格弹性更好的方法

Price elasticity of demand=(Q2Q1)/[(Q2+Q1)/2](P2P1)/[(P2+P1)/2]Price\ elasticity\ of\ demand = \frac{(Q_2-Q_1)/[(Q_2+Q_1)/2]}{(P_2-P_1)/[(P_2+P_1)/2]}

  • A -> B 的中点 C:价格 5 / 需求量 100
  • 因此 AB and BA Price elasticity of demand=(12080)/100(64)/5=1A \rightarrow B\ and\ B \rightarrow A\ Price\ elasticity\ of\ demand = \frac{(120-80)/100}{(6-4)/5} = 1

各种需求价格曲线

1.png

总收益

  • 总收益:一种物品的买者支付从而卖者得到的量,用该物品的价格乘以销售量来计算。

1.png

可以由上图中阴影部分表示

  • 当需求富有弹性(价格弹性大于 1 )时,价格和总收益反方向变动;价格上升,总收益减少。
  • 当需求缺乏弹性(价格弹性小于 1 )时,价格和总收益同方向变动;价格上升,总收益增加。
  • 如果需求是单位弹性的(价格弹性等于 1 )时,价格变动,总收益保持不变。

其他需求弹性

  • 需求收入弹性:衡量一种物品需求量对消费者收入变化反应程度的指标;用需求量变动百分比除以收入变动百分比来计算。

  • 需求交叉价格弹性:衡量一种物品需求量对另一种物品价格变动的反应程度的指标;用第一种物品的需求量变动百分比除以第二种物品的价格变动百分比来计算。

供给弹性

市场与竞争

  • 市场:由某种物品或服务的买者与卖者组成的一个群体。

  • 竞争市场:由许多买者与卖者,以至于每个人对市场价格的影响微乎其微的市场。

需求

  • 需求量:买者愿意并且能够购买的一种商品的数量。

  • 需求定理:认为在其他条件不变时,一种物品的价格上升,对该物品的需求量减少的观点。

  • 需求表:表示一种物品的价格和需求量之间关系的表格(离散)。

  • 需求曲线:表示一种物品的价格和需求量之间关系的曲线(连续)。

市场需求是个人需求之和,一个市场的需求量是所有买者在每一价格水平下需求量的总和。

6.png

  • 需求增加:使每一种价格水平下的需求量都增加的任何变动,都会使 需求曲线向右移动,我们称之为需求增加。

  • 需求减少:使每一种价格水平下的需求量都减少的任何变动,都会使 需求曲线向左移动,我们称之为需求减少。

影响需求的因素

  • 价格
  • 收入
    • 正常物品:在其他条件相同时,收入增加引起需求量增加 的物品。
    • 低档物品:在其他条件相同时,收入增加引起需求量减少 的物品。
  • 相关物品的价格
    • 替代品:一种物品需求量 上升 引起另一种物品需求量 上升 的两种物品。
    • 互补品:一种物品需求量 上升 引起另一种物品需求量 下降 的两种物品。
  • 爱好
  • 预期
  • 买者的数量

其中,除了 “价格” 因素是将需求沿着需求曲线移动之外,其余的因素都会使得需求曲线移动。

供给

  • 供给量:卖者愿意并且能够出售的一种商品的数量。

  • 供给定理:认为在其他条件不变时,一种物品的价格上升,对该物品的供给量增加的观点。

  • 供给表:表示一种物品的价格和供给量之间关系的表格(离散)。

  • 供给曲线:表示一种物品的价格和供给量之间关系的曲线(连续)。

市场供给是个人供给之和,一个市场的供给量是所有卖者在每一价格水平下供给量的总和。

5.png

  • 供给增加:使每一种价格水平下的供给量都增加的任何变动,都会使 需求曲线向右移动,我们称之为供给增加。

  • 供给减少:使每一种价格水平下的供给量都减少的任何变动,都会使 供给曲线向左移动,我们称之为供给减少。

影响供给的因素

  • 价格
  • 投入品价格
  • 技术
  • 预期
  • 卖者的数量

其中,除了 “价格” 因素是将需求沿着供给曲线移动之外,其余的因素都会使得供给曲线移动。

供给与需求的组合

4.png

  • 均衡:市场价格达到使供给量与需求量相等的水平时的状态。

  • 均衡价格:使供给与需求达到均衡状态的价格。

  • 均衡数量:均衡价格下的供给量与需求量。

1.png

  • 过剩:供给量大于需求量的状态。

  • 短缺:需求量大于供给量的状态。

  • 供求定理:认为任何一种物品的价格都会自发调整,使该物品的供给与需求达到平衡的观点。

分析均衡变动的三个步骤

  1. 确定该事件是使供给曲线移动还是需求曲线移动(或者使两者都移动)
  2. 确定曲线的移动方向
  3. 用供求图说明这种移动如何改变均衡价格和均衡数量

2.png

  • 需求供给变化笛卡尔积表

2.png