2024年7月

Apache基于IP

步骤1:添加并配置虚拟网卡

  1. 添加虚拟网卡
    :通常在虚拟机环境中,可以通过虚拟机软件(如VMware或VirtualBox)的网络设置来添加额外的网络适配器。
  2. 配置IP地址
    :编辑
    /etc/sysconfig/network-scripts/ifcfg-ethX
    文件,,并将它们设置为静态IP地址。
vi /etc/sysconfig/network-scripts/ifcfg-ens33
# 内容如下:
DEVICE=ens33
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.10.100
NETMASK=255.255.255.0

vi /etc/sysconfig/network-scripts/ifcfg-ens36
# 内容如下:
DEVICE=ens36
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.10.200
NETMASK=255.255.255.0

image.png

  1. 重启网络服务
systemctl restart network

步骤2:修改Apache配置文件

编辑
/etc/httpd/conf/httpd.conf
,确保以下行未被注释(即删除
#
):

IncludeOptional conf.d/*.conf

然后,在
/etc/httpd/conf.d/
目录下创建两个新的配置文件,比如
vhost1.conf

vhost2.conf
,分别用于配置基于IP的虚拟主机:

vim /etc/httpd/conf.d/vhost1.conf
# 内容如下:
<VirtualHost 192.168.10.100:80>
    DocumentRoot "/var/www/html/web1"
</VirtualHost>

vim /etc/httpd/conf.d/vhost2.conf
# 内容如下:
<VirtualHost 192.168.10.200:80>
    DocumentRoot "/var/www/html/web2"
</VirtualHost>

或者写到一个配置文件中

[root@localhost ~]# cat /etc/httpd/conf.d/vhosts-ip.conf
<VirtualHost 192.168.10.100:80>
    DocumentRoot "/var/www/html/web1"
</VirtualHost>
<VirtualHost 192.168.10.200:80>
    DocumentRoot "/var/www/html/web2"
</VirtualHost>

image.png

步骤3:创建目录和主页文件

mkdir -p /var/www/html/web1 /var/www/html/web2
echo "This is my virtual website1." > /var/www/html/web1/index.html
echo "This is my virtual website2 ." > /var/www/html/web2/index.html

重启Apache服务

systemctl restart httpd

步骤4:测试基于IP的虚拟主机

image.png
image.png

Apache基于端口

步骤5:配置基于端口的虚拟主机

编辑
/etc/httpd/conf.d/
下的新配置文件,例如
vhost3.conf

vhost4.conf
,配置不同的端口:

vim /etc/httpd/conf.d/vhost3.conf
# 内容如下:
Listen 8000
<VirtualHost *:8000>
    DocumentRoot "/var/www/html/web3"
</VirtualHost>

vim /etc/httpd/conf.d/vhost4.conf
# 内容如下:
Listen 8888
<VirtualHost *:8888>
    DocumentRoot "/var/www/html/web4"
</VirtualHost>

或者全部写到一个配置文件中

[root@localhost ~]# cat /etc/httpd/conf.d/vhosts-port.conf 
Listen 8000
<VirtualHost 192.168.10.100:8000>
    DocumentRoot "/var/www/html/web3"
</VirtualHost>
Listen 8888
<VirtualHost 192.168.10.100:8888>
    DocumentRoot "/var/www/html/web4"
</VirtualHost>

步骤6:创建目录和主页文件

mkdir -p /var/www/html/web3 /var/www/html/web4
echo "This is my virtual website3." > /var/www/html/web3/index.html
echo "This is my virtual website4." > /var/www/html/web4/index.html

步骤7:重启Apache服务

systemctl restart httpd

测试基于端口的虚拟主机
再次在Windows系统下使用浏览器访问这两个端口,确保能够看到正确的网页。
image.png
image.png

解码 xsync 的 map 实现

最近在寻找 Go 的并发 map 库的时候,翻到一个 github 宝藏库,xsync (
https://github.com/puzpuzpuz/xsync
) 。这个库提供了一些支持并发的数据结构,计数器Counter,哈希 Map,队列Queue。我着重看了下它的 Map 的实现,遇到一个新的知识点:Cache-Line Hash Table (CLHT) 。问了半天 GPT,大致了解了其中的内容,这里总结下。

利用 Cache-Line 实现无锁编程

CacheLine 是 CPU 一次性读取内存的最小单元。它在不同的硬件设备上有不同的大小。在x86-64的机器上是 64 字节。就代表着 CPU 一次性能从内存中获取64 字节的大小。CPU处理器在处理一个变量数据的时候,会依次从寄存器,CPU 缓存,内存,磁盘中进行获取,当然他们的处理速度也是依次递减。

当计算机中多个 CPU 核读写同一个数据结构的时候,他们每次都会读取CacheLine 结构的数据进入自己的 CPU 缓存中。这里不同的数据结构设计,就会有不同的性能。

假设有一个大的数据结构 Object,a1 核负责读Object 的 field1 字段,而 a2 核负责写 Object 的 field2 字段,而 field1 和 field2 都在同一个 Cache Line 中,这就意味着 a1 和 a2 在并行计算的时候,都会把包含有 field1 和 field2 字段的 Cache line 读取到自己的 CPU 缓存中。那么问题来了,当 a2 核变更 field2 字段的时候,就要想办法通知a1 核,更新 CPU 缓存,否则a1 核计算可能是有问题的。a2核变更通知 a1 核更新 CPU 缓存,这种交互机制叫做 MESI。

image-20240715003658244

当然这种交互机制是非常低效的。我们应该想办法尽量避免!

其中一种避免的方法之一就是使用锁,在修改数据的时候上锁,读取数据的时候读锁。但这种方式并不高效。我们在想,是否有一种无锁的编程方式呢?

控制 Object 结构的设计是个好办法,我们设计结构将其中的 field1 字段放在一个 CacheLine 中,另外一个 field2 在第二个 CacheLine 中,那么如果我们使用 a1 线程读 field1,a2 线程写 field2,那么我们就能做到无锁读写。

![image-20240715004513922](../../../Library/Application Support/typora-user-images/image-20240715004513922.png)

这就是利用 Cache-line 实现的无锁编程。

基于 CacheLine 的哈希表

节点设计

现在回到 hash 表,我们使用 hash 表的时候最头疼的就是 hash 表是非并发安全的,一般我们使用 hash 表的时候,都会带一个全局锁,我们读写hash 表的时候会读或者写一下这个全局锁。

但是这明显效率就比较低了。

要想效率高,Cache-Line Hash Table (CLHT) 就提出了使用 CacheLine 的逻辑来优化 hash 表。一个 hash 表一般就是一个 hash 函数+节点链表,我们如果让每个节点都保持一个 CacheLine 的大小(64 byte)。那么每次 cpu 读写的时候,就只会读取一个完整的节点进入到 cpu 缓存中,这样不是就能无锁使用 hash 表了吗?

是的,这种方案确实可行,但是最重要的就是设计这个 由多个hash节点组成的 hash 表结构。

我们先需要回答:一个 CacheLine(64 bytes = 64 * 8 bit = 512 bit) 能保存多少个hash key-value 对呢?

解:

一个指针是 uint64 类型,hash 的每个 key-value 对中 key 和 value 都是指针,key 指向一个 string,value 指向一个 interface{}, 即任何的数据结构, 那么一个 key-value 对占2*64bit = 128bit。

由于 hash 表中相同 hash的节点是通过指针链链接起来的,所以至少节点中要保存一个指向 next 节点的指针,uint64 = 64bit。

所以一个 cacheline 最多可以有3 个key-value 对 + 1 个 next 指针 = 128 * 3 + 64 = 448 bit。

解答完毕。

但是如此设计,cacheline 的空间还有盈余,还多了一个 512 - 448 = 64 bit 的大小,我们利用这个空间设计了一个 topHashMutex 结构(uint64),具体它是做什么用的,后面详聊。

我们的 bucke 节点在代码中如上设计,实现如下:

type bucket struct {
	next   unsafe.Pointer // *bucketPadded
	keys   [3]unsafe.Pointer
	values [3]unsafe.Pointer
	
	...
	topHashMutex uint64
}

而我们的 hash 表结构就有如下展示:

image-20240716171705057

不同场景分析

这样根据 cacheline 设计 hash 表,是否能实现真正的无锁化呢?我们需要分析不同场景:

1 两个 cpu 核,读取两个不同的 bucket 节点

这是我们最希望见到的情况,由于我们事先设计了每个 bucket 节点正好是一个 hash 大小。

所以两个 cpu 读取自己的cpu 缓存即可,里面的节点互相不干扰,这个时候效率非常高。

2 两个cpu核,读取相同的 bucket 节点

这个 bucket 节点会从内存中被复制两份到两个 cpu 缓存中,但是这种场景,由于没有任何更新操作,我们也用不到任何锁。

3 两个 cpu 核,对相同 bucket 节点,一个在读取,另一个在更新

这种情况,我们要保证的是读取的操作一定是原子的,我们可以读取更新前的值,也可以读取更新后的值,但是不能读取一个中间无效的值。

所以读取的 cpu 核在读取自己 cpu 缓存内容的时候,必须小心 cpu 缓存被修改,而导致了无效值。那么我们能怎么做呢?

指针快照的方法(snapshot)

首先,我们先从 bucket 节点中找到目标 key-value 对(这里如何快速找到后面会说),我们先读取一次key1 和 value1 ,但是注意,由于之前设计,我们bucket 里面存储的是key1 指针,value1 指针,所以我们实际读取的是指针。这个时候并不直接使用这个指针指向的内容,而是相当于我们为 key1 和 value1 做了一个快照。

这里要注意的是,读取 key1 和 value1 的指针快照是2 个原子操作。但是这两个原子操作,由于另外一个核在更新这个 key-value 对,就是在通过 MESI 机制同步修改我们的 cpu 缓存,我们是有可能读取到一个无效指针 value1 的(我们是否不会读到无效指针 key1,因为更新操作不会修改 key1 的指针)。

那么我们如何确定 value1 是可用的呢?办法就是我们再取一次cpu 缓存中的 key1 和 value1 指针,判断他们是否有变化。

如果快照 key1和快照 value1 等于第二次查询的 key1 和 key2,那么就证明快照的 key1 和 value1 是可用的,不是正被修改中的内存。

如果快照 key1和快照 value1 不等于第二次查询的 key1 和 key2,那么就证明快照的 key1 和 value1 是不可用的,当前正在有其他cpu 在修改我的 cpu 缓存,这时候要做的就是重新进行快照过程。

这就是 atomic snapshot 的方法。

代码实现如下:

func (m *Map) Load(key string) (value interface{}, ok bool) {
	...
	for {
		...
		atomic_snapshot:
			// Start atomic snapshot.
			vp := atomic.LoadPointer(&b.values[i])
			kp := atomic.LoadPointer(&b.keys[i])
			if kp != nil && vp != nil {
				if key == derefKey(kp) {
					if uintptr(vp) == uintptr(atomic.LoadPointer(&b.values[i])) {
						// Atomic snapshot succeeded.
						return derefValue(vp), true
					}
					// Concurrent update/remove. Go for another spin.
					goto atomic_snapshot
				}
			}
		}
		
		bptr := atomic.LoadPointer(&b.next)
		if bptr == nil {
			return
		}
		b = (*bucketPadded)(bptr)
	}
}

引申一下,这个更新操作,实际换成删除操作也是生效的,因为删除操作相当于试一次特殊的更新(将 value1 的指针替换为 nil)。

4 两个CPU核,对相同的 bucket 节点,两个都在写

在这种场景下,我们需要保证只有一个 cpu 核在写,另外一个需要等待,我们不得不使用锁了,但是这个锁是非常小的,它只保证锁住 cacheline 就行了。

锁放在哪里呢?前面设计的 uint64 topHashMutex , 我们只需要使用1bit 的大小(最后一个 bit),标记 0/1就行了,0 代表没有锁,1 代表锁。

更新操作的时候,我们需要用 atomic.CompareAndSwapUint64 来抢到这个 topHashMutex 的最后一个 bit 的锁。

如果抢到的话,当前 cpu 核就可以心安理得的处理自己的 cpu 缓存区的内容,并且通知其他的 cpu 缓存区内容进行更新。

如果没有抢到的话,当前 cpu 核使用自旋锁,进入锁等待阶段,runtime.Gosched(), 让渡这个 goroutine 的执行权。等着go 调度机制再次调度到到这个goroutine,再次抢锁。

加锁代码逻辑如下:

func lockBucket(mu *uint64) {
	for {
		var v uint64
		for {
			v = atomic.LoadUint64(mu)
			if v&1 != 1 {
				break
			}
			runtime.Gosched()
		}
		if atomic.CompareAndSwapUint64(mu, v, v|1) {
			return
		}
		runtime.Gosched()
	}
}

同样引申一下,这个情景也适用于两个 CPU的删除操作,或者更新删除操作并行的情况。cacheline 小锁的机制,保证了同一时间只有一个 cpu 核能对这个节点进行操作。

如何加速节点中 key-value 对的查找

好了,以上四种情况基本把并发读写同一个 map 节点的情景都列出来了。

但是还差一点,bucket 中的 topHashMutex 结构还有 63bit 的剩余空间,我们是否可以利用它来加速key-value 对的查找?答案是可以的,我们可以通过建立索引机制来加速。

我们将这个 63bit 分为 3 x 20 + 3 。前面的 3 个 20 是3 个 key-value 对的 key 值的索引。至于索引方式嘛,我们可以简单将 key 的 hash 值的前 20 位作为索引。这样我们在查找一个 key 的时候,先判断下其 hash 值的前 20 位在不在这个索引中,就能大概率判断出是否在这个 bucket 节点中了。

但是建立索引还是不够,前面说过了,删除某个 key-value,我们是直接将 value 的指针置为 nil,那么这个时候,它的 key 还存在,我们需要标记位来标记这个 key 是否能用。

topHashMutex 后面的 3 个 bit 就启动用处了, 0/1表示3 个 key-value 是否可用。

topHashMutex 的结构如下:

image-20240717014513117

我们再举例说明:

假设我有一个 key 为 "foo", value 为 struct Bar。存储时,我们算好要存入 bucket 的第二个 key-value 位置。

"foo" 的 hash 值为 uint64: 1721009463561,转为二进制:1100010000110110110110110110110111101001001,取前 20 位,11000100001101101101。

我们把 topHashMutex 的第二个 20bit 设置为11000100001101101101。再把 topHashMutex 的第 62(3 x 20 + 2)设置为 1.表示可用。

在查询操作,我们在拿着 key = "foo" 来查找 value 的时候,先去判断 key 的 hash是否在前 60bit 中,然后再确认下对应的 bitmap 是否是可用的,我们就能判断目标 key 大概率是在这个 bucket 的第二个位置,我们这时候再走快照逻辑,判断快照的 key 的值是否是 “foo”,并且快照原子获取其value 值。

结论

这就是这个开源 go 的 xsync 库中的 Map 结构的核心原理了。确实是非常巧妙的设计思路。核心思想就是利用cpu 一次读取 cacheline 大小的内容进 cpu 缓存区,就设计一个符合这个特性的 hash 表,尽量保证每个 cpu 的读取互不干扰,对于可能出现的并发干扰的情况,使用快照机制能保证读取的原子性,这样能有效避免全局锁的使用,提高性能。

至于可以看这个 benchmark 测评,
https://github.com/puzpuzpuz/xsync/blob/main/BENCHMARKS.md
比较了xsync 的 map 和标准库 sync.Map。基本上真是秒杀,特别是在读写混杂的情况下,xsync 能比 sync.Map 节省2/3 的时间消耗。

image-20240717015313392

参考

cacheline 对 Go 程序的影响

CPU高速缓存与极性代码设计

最新技术资源(建议收藏)
https://www.grapecity.com.cn/resources/

集算表 (Table Sheet)是一个具备高性能渲染、数据绑定功能、公式计算能力的数据表格,通过全新构建的关系型数据管理器结合结构化公式,在高性能表格的基础上提供排序、筛选、样式、行列冻结、自动更新、单元格更新等功能。

什么是集算表(Table Sheet)?

集算表是一个具有网络状行为和电子表格用户界面的快速数据绑定表的视图。

众所周知Excel的工作表(Work Sheet)是一个自由式布局,基于单元格(Cell Base)的表格,适用于一些松散式的数据布局展示,布局上来说非常灵活,但对于固定格式的大批量数据展示,不具备优势。

集算表不同于Excel的工作表,它是一个基于列(Column Base)的网状表格(Grid),适用于展示规则数据。同时它还具备了Excel工作表(Work Sheet)的用户界面和部分常见操作。并且支持Excel的部分计算功能。同时结合数据绑定的功能,对于大量固定格式的数据(例如数据库的表格)可以快速在前端进行展示。

集算表的特点正如它的名字的三个字:集,算,表:

  • 集(Data Manager):

集的意思就是数据集记和管理。集算表在前端构建了一个叫做Data Manager的数据管理模块。该模块可以简单理解为一个前端的数据库,Data Manager负责与远端的数据中心进行通信,拉取远端的数据。在前端处理数据,例如数据表的定义,表间关系等。同时Data Manager还负责处理数据的变形,例如分组,切片,排序,过滤等。

  • 算(Calculation Engine):

集算表本身基于网络结构化数据的计算引擎Calculation Engine。Calculation Engine定义不同的上下文计算层级,不同与SpreadJS中工作表(Work Sheet)基于单元格或者区域(Range)的计算层级,集算表(Table Sheet)的上下文层级是基于行级,组级,数据级。

同时通过Calculation Engine的计算串联,使得集算表(Table Sheet)与工作表(Work Sheet)之间可以进行数据串联。这使得计算表不是一个独立存在,它可以与工作表结合使用,相互配合以适应更多的需求。

  • 表(Table Sheet):

整个Table Sheet分为三层:渲染层,数据层,功能层。

渲染层复用了工作表(Work Sheet)的渲染引擎,具备双缓冲画布等高性能的优势。

数据层直连Data Manger,无需建立数据模型,相交SpreadJS更加快速。

功能层不同于传统表格(Grid),将底层结构化数据进行改造,在支持增删改查等基本功能的基础上,还额外支持了大部分工作表(Work Sheet)的对应功能,如样式,条件格式,数据验证,计算列等。

在数据底层,保证上述功能支持的基础上,还能保证数据的结构化,而非松散的数据结构。

集算表的架构:

Data Manager负责拉取远端数据,远端数据源可以是Rest API、OData、GraphQL、Local。Data Manager在拉取数据源之后会根据其中的定义构建数据表(Data Source),该表结构与数据库中的表结构类似。之后通过这些表来定义对应的数据视图(View),视图中定义了展示的结构以及计算列,关系列的添加。最终将不同的视图(View)绑定在不同的Table Sheet上。Table Sheet负责对所有的视图进行展示和操作。Calc Engine在Data Manager上工作,而非直接工作在Table Sheet上,这是为了更方便的去支持集算表与普通工作表之间的公式引用。这使得集算表与普通工作表之间产生“化学效应“,例如下面的示例:

在创建了集算表之后可以在普通的工作表中直接通过公式引入集算表的表格中的数据。这样可以做到通过集算表对数据进行展示,同时通过工作表的功能,对展示的结果进行数据分析。

甚至可以直接引用集算表中的数据当做数据数据源,创建数据透视表。

集算表的性能:

集算表是基于Column进行数据存储,相较于基于Row的存储结构,在筛选和计算方面有很大的优势。

通过性能测试,我们可以了解到,对于100W行级别的数据,集算表从发送请求加载数据到将表格绘制完毕总共的耗时是大约5秒钟。

筛选数据花费时间在50ms左右(Filter country == "UK")。

100W行数据排序花费时间在5S左右(Sort birthday == "Ascending")。

对100W行数据添加计算列,对每行数据进行计算,花费时间不明显(总计时间4807ms,但该事件包含了数据加载,绘制的总时间,对比之前的测试结果基本在4800ms左右。故添加计算列计算花费的时间不明显,可忽略不计)。

点击链接访问
性能测试示例

大家如果感兴趣自行按照上述地址体验即可。

拓展阅读

React + Springboot + Quartz,从0实现Excel报表自动化

电子表格也能做购物车?简单三步就能实现

使用纯前端类Excel表格控件SpreadJS构建企业现金流量表

论文提出了用于快速图像分类推理的混合神经网络
LeVIT
,在不同的硬件平台上进行不同的效率衡量标准的测试。总体而言,
LeViT
在速度/准确性权衡方面明显优于现有的卷积神经网络和
ViT
,比如在80%的
ImageNet top-1
精度下,
LeViT

CPU
上比
EfficientNet
快5倍

来源:晓飞的算法工程笔记 公众号

论文: LeViT: a Vision Transformer in ConvNet's Clothing for Faster Inference

Introduction


虽然许多研究旨在减少分类器和特征提取器的内存占用,但推理速度同样重要,高吞吐量对应于更好的能源效率。论文的目标是开发一个基于
Vision Transformer
的模型系列,在
GPU

CPU

ARM
等高度并行的架构上具有更快的推理速度。

在相同的计算复杂度下,
Transformer
的速度会比卷积架构更快。这是因为大多数硬件加速器(
GPU

TPU
)都经过优化以执行大型矩阵乘法,而在
Transformer
中的注意力块和
MLP
块主要依赖于矩阵乘法。相反,卷积需要复杂的数据访问模式,通常跟
IO
绑定的。

论文引入轻量的卷积组件来代替产生类似卷积特征的
Transformer
组件,同时将统一的
Transformer
结构替换为带池化的金字塔结构。由于整体结构类似于
LeNet
,论文称新网络为
LeViT

除提出
LeViT
外,论文还提供了以下缩小
ViT
模型体量的方法:

  • 使用注意力作为下采样机制的多阶段
    Transformer
    架构。
  • 计算高效的图像块提取器,可减少第一层中特征数量。
  • 可学习且平移不变的注意力偏置,取代位置编码。
  • 重新设计的
    Attention-MLP
    块,计算量更低。

Motivation


Convolutions in the ViT architecture

ViT
的图像块提取器一般为步幅
16

16x16
卷积,然后将输出乘以可学习的权重来得到第一个自注意力层的
\(q\)
,
\(k\)

\(v\)
特征。论文认为,这些操作也可以认为是对输入进行卷积函数处理。

如图2所示,论文可视化了
DeiT
第一层的注意力权值,发现权值空间都有与
Gabor
滤波器类似的模式。

卷积中权值空间的平滑度主要来自卷积区域的重叠,相邻的像素接收大致相同的梯度。对于没有区域重叠
ViT
,权值空间的平滑可能是由数据增强引起的。当图像训练多次且有平移时,相同的梯度也会平移通过下一个滤波器,因此学习到了这种平滑的权值空间。

因此,尽管
Transformer
架构中不存在归纳偏置,但训练确实会产生类似于传统卷积层的权值空间。

Preliminary experiment: grafting

ViT
的作者尝试将
Transformer
层堆叠在传统的
ResNet-50
之上,将
ResNet-50
充当
Transformer
层的特征提取器,梯度可以在两个网络传播中。然而,在他们的实验中,
Transformer
层的数量是固定的。


ViT
的实验不同的是,论文主要在相近算力的情况下对比不同数量的卷积阶段和
Transformer
层数时之间的性能,进行
Transformer
与卷积网络的混合潜力的研究。

论文对具有相似的运行时间
ResNet-50

DeiT-Small
进行实验,由于裁剪后的
ResNet
产生的激活图比
DeiT
使用的
\(14\times14\)
激活图更大,需要在它们之间引入了一个平均池化层。同时,在卷积层和
Transformer
层的转换处引入了位置编码和分类标记。对于
ResNet-50
,论文使用
ReLU
激活层和BN层。

结果如表1所示,混合架构均比单独的
DeiT

ResNet-50
的性能要好,两个阶段的
ResNet-50
的参数数量最少且准确度最高。

论文在图 3 中展示了一个有趣的观察结果:混合模型在训练期间的早期收敛类似于卷积网络,随后切换到类似于
DeiT-S
的收敛速度。由于卷积层具有很强的归纳偏差能力(尤其是平移不变性),能够更有效地学习早期层中的低级特征,而高质量的图像块编码使得训练初期能更快地收敛。

Model


Design principles of LeViT

LeViT
建立在
ViT
架构和
DeiT
训练方法之上,引入一些对卷积架构有用的组件。忽略分类标记的作用,
ViT
就是处理特征图的层堆叠,中间的特征编码可以看作是
FCN
架构中的传统
\(C\times H\times W\)
特征图(
\(B C H W\)
格式)。因此,适用于特征图的操作(池化、卷积)也可以应用于
DeiT
的中间特征。

需要注意的是,优化计算架构不一定要最小化参数数量。
ResNet
系列比
VGG
网络更高效的设计之一是在两个阶段以相对较小的额外计算消耗进行有效的分辨率降低,使得第三阶段的激活图的分辨率缩小到足够小(
14x14
),从而降低了计算成本。

LeViT components

  • Patch embedding

先前的分析表明,将小型卷积网络应用于
Transformer
的输入时可以提高准确性。在
LeViT
中,论文选择 4 层
\(3\times3\)
卷积(步幅为 2)来对输入进行处理,通道数分别为
\(C\ =\ 32,64,128,256\)
,最终输出大小为
\((256,14,14)\)
的特征。这里的特征提取仅用了184 MFLOPs,而
ResNet-18
用了 1042 MFLOPs来执行相同的降维。

  • No classification token

为了使用
\(B C H W\)
的张量格式,论文去掉了分类标记,改为在最后一个特征图上用平均池化来产生用于分类器的编码。对于训练期间的蒸馏,论文为分类和蒸馏任务训练了不同的分类器。在测试时,将两个分类器输出进行平均。在实践中,
LeViT
可以使用
\(B N C\)

\(BCHW\)
张量格式来实现,以实际效率为准。

  • Normalization layers and activations

ViT
架构中的
FC
层相当于
\(1 \times 1\)
卷积,每个注意力层和
MLP
层前都使用层归一化。对于
LeViT
,每个卷积之后都进行
BN
归一化,与残差连接相连的
BN
归一化层的权值都被初始化为零。
BN
归一化可以与前面的卷积合并推理,这是优于层归一化的运行时优势(在
EfficientNet B0
上,这种融合将
GPU
上的推理速度提高了 2 倍)。
DeiT
使用
GELU
激活函数,而
LeViT
使用
Hardswish
激活函数。

  • Multi-resolution pyramid

卷积架构一般构建为金字塔,特征分辨率随着处理过程中通道数量的增加而降低。
LeViT

ResNet
的阶段集成到
Transformer
架构中,阶段内部则是类似于
ViT
的残差结构。

  • Downsampling


LeViT
的阶段之间,使用
shrinking
注意力块来减小激活图的大小。在
Q
映射前,先应用下采样再软激活得到输出。这可以将大小为
\((C,H,W)\)
的输入张量映射到大小为
\((G^{\prime},H/2,W/2)\)
的输出张量,其中
\({C}^{\prime}\,>\,{C}\,\)
。由于尺寸的变化,使用此注意力块时没有残差连接。另外,为了防止信息丢失,论文将注意力头的数量增加为
\(C/D\)

  • Attention bias instead of a positional embedding

Transformer
架构中的位置编码是一个位置相关的可训练参数向量,将其与输入序列合并后再输入
Transformer
块进行后续计算。如果不使用位置编码,
Transformer
输出将独立于输入序列的排列关系,导致分类精度的急剧下降。

由于位置编码对于更高层特征提取也很重要,因此它很可能保留在中间特征中,导致不必要地占用特征容量。因此,论文尝试在每个注意力块内提供位置信息,并在注意力机制中显式注入相对位置信息(在注意力图计算中添加一个注意力偏差)。

对于头
\(h\in[N]\)
,两个像素
\((x,y)\ \in [H] × [W]\)

\((x^{\prime},y^{\prime})\in[H]\times[W]\)
之间的注意力值计算为

\[A_{(x,y),(x^{\prime},y^{\prime})}^{h}=Q_{(x,y),:}\bullet K_{(x^{\prime},y^{\prime}),:}+B^{h}_{|x-x^{\prime}|,|y-y^{\prime}|}.
\]

第一项是经典注意力,第二个是平移不变的注意力偏置。每个头都有
\(H\times W\)
个参数,对应不同的像素偏移情况,距离取绝对值能鼓励模型训练翻转不变性。

  • Smaller keys

注意力偏置项减少了键映射矩阵编码位置信息的压力,因此论文减小了
\({\cal{Q}}\)

\({\cal{K}}\)
通道数,缩短相关矩阵的运算时间。假设
\({\cal{Q}}\)

\({\cal{K}}\)
的通道数为
\(D\;\in\;\{16,32\}\)
,则
\({\cal{V}}\)
的通道数为
\(2D\)

对于没有残差连接的下采样层,论文将
\({\cal{V}}\)
的维度设置为
\({\mathrm{4}}D\)
以防止信息丢失。

  • Attention activation

在使用线性映射组合不同头的输出之前,论文对
\(A^{h}V\)
乘积应用
Hardswish
激活。

  • Reducing the MLP blocks

ViT
中的
MLP
残差块是一个线性层,先将输入通道数增加 4 倍,应用非线性映射后再通过另一个非线性映射将其降低回原始输入的通道数。由于
MLP
在运行时间和参数方面通常比注意力块更高,
LeViT

MLP
层替换为
\(1\times 1\)
卷积,然后通过
BN
层进行归一化。为了降低计算成本,论文将卷积的扩展因子从4减少到2,这使得注意块和
MLP
块的计算量大致相同。

The LeViT family of models

LeViT
模型可以通过改变各阶段的大小来权衡速度和精度,表2展示了论文涉及的
LeViT
系列模型。

Experiments


Experimental context

论文在
PyTorch
中运行所有实验,因此论文依赖于该
API
中可用的优化。为了获得更客观的时序,论文在三个不同的硬件平台上对推理进行计时,每个平台对应一个用例:

  • 一个
    16GB NVIDIA Volta GPU
    (峰值性能为
    12 TFLOP/s
    ),这是一个典型的训练加速器。
  • 2.0 GHz

    Intel Xeon 6138 CPU
    ,这是数据中心中的典型服务器。
    PyTorch
    针对此配置进行了很好的优化,使用
    MKL

    AVX2
    指令(16 个向量寄存器,每个寄存器 256 位)。
  • ARM Graviton2 CPU

    Amazon C6g
    实例),这是移动电话和其他边缘设备运行的处理器类型。
    Graviton2
    有 32 个内核,支持带有 32 个 128 位向量寄存器 (
    NEON
    ) 的
    NEON
    矢量指令集。

Training LeViT

论文使用 32 个
GPU
在 3 到 5 天内训练 1000 个周期,这比卷积网络的通常时间表要多,但是
ViT
本身就需要长时间的训练才能有更好的性能。论文使用类似于
DeiT
的蒸馏训练,这意味着
LeViT
使用两个具有交叉熵损失的分类头进行训练:第一个头接受来自真实类的监督,第二个来自在
ImageNet
上训练的
RegNetY-16GF
模型。实际上,
LeViT
的训练时间主要由教师网络的推理时间决定。

Speed-accuracy tradeoffs

表 3 显示了论文使用
LeViT
在速度和精度之间的权衡,图 1 则绘制了一些显着数字。在同级别的速度下,
LeViT
的速度都要优异一些。

在表 3 中,测试集还有
Imagenet Real

Imagenet V2
(
matched-frequency
),两个数据集使用与
ImageNet
相同的一组类和训练集。
Imagenet-Real
对图片重新标签,每个图像可能有多个类别,
Imagenet-V2
则使用新的测试图。通过模型在这两个数据集上的性能,可以验证超参数调整有没有对
ImageNet
验证集过度拟合。

Comparison with the state of the art

表 4 展示其他基于
Transformer
架构的结果进行了对比,可以与
LeViT
(表 3)进行比较,实验不包括速度慢的大模型。为了方便,论文直接以
FLOPs
作为速度标准,不实测时间。

Ablations

为了评估对
LeViT
性能有贡献的因素,论文使用默认设置进行实验,每次实验替换一个因素。实验仅运行 100 个训练周期以放大差异并减少训练时间,表 5 的结果展示了所有替换都会导致准确性的降低。

Conclusion


论文介绍了受卷积网络启发的
Transformer
架构
LeViT
,使用
DeiT
中的训练技术。
LeViT
的核心是在精度和速度之间进行权衡,在相当的精度下能够快 1.5 到 5 倍。



如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

1、预览地址:http://139.155.137.144:9012

2、qq群:801913255

一、前言


随着网络的发展,企业对于信息系统数据的保密工作愈发重视,不同身份、角色对于数据的访问权限都应该大相径庭。

列如

1、不同登录人员对一个数据列表的可见度是不一样的,如数据列、数据行、数据按钮等都可能不太一样。

2、表单的灵活设计及呈现。

3、流程的灵活设计及呈现。

4、接口的调用信息者及性能监控。

5、等等。

对于以上等功能,如果要做出一套完整且灵活的权限管理系统,实属不易。

而接下来作者要介绍的“
OverallAuth
”权限管理系统,可以说是最全面和最好用的权限管理系统。

说明:这篇文章作者会把完成“
OverallAuth
”系统的技术选型和大致设计思路一一分享给大家。文章较长请大家耐心看完,在文章的最后有项目的预览地址。

二、目录

三、主要技术选择

4、功能介绍及设计简述


4.1 菜单设计

菜单设计非常简单,就是普通的树形结构,但作者依赖于layui vue 强大的功能结构,在此基础上对每个菜单设计了一个重定向和菜单是否启用的开关。

说明:菜单是否启用的存在,可以精准的控制,每个菜单在什么时候开启,什么关闭(配合任务调度)。这样可以针对性调整用户在什么时候拥有什么样的菜单(比如说用于活动页面,在活动结束后,可以立马关闭),大大减低了维护成本。

如图:

4.2 角色管理

角色对于任何一个系统来说都是重中之重,本系统当然也不例外。

“OverallAuth”系统的角色几乎覆盖了所有功能,按钮、菜单、用户、数据行、数据列、流程、表单等。所以在设计方面,我分别建立了以下关系,角色和按钮、角色和菜单、角色和用户、角色和数列等。以便灵活且高效的控制系统中的任何一个功能。

当然角色也不是万能的,对于更加细致化的控制,角色也有过于“拥挤”的时候,所以在“OverallAuth”系统中,作者还加入了一些规则以便更加细致的控制每个模块。

4.3 用户管理

之前也说到,角色和用户存在关系,以便控制用户的菜单权限等,这和一般的权限管理系统基本一致,但本“OverallAuth”系统作为一个灵活且好用的权限管理系统,作者在这上面下了一定功夫,引入了“用户组”等概念。简单来说就是一个用户可以拥有多种角色,角色之间是互补状态。

4.4 按钮样式

在一个系统中,操作按钮是必不可少的,它们的样式也大相径庭,所以作者设计了自定义按钮样式功能,如图:

该功能可以控制系统中任何一个按钮的样式,从而做到实时改变每个按钮样式。

4.5 按钮列表

在拥有自定义按钮样式后,当然要把样式运用到每个按钮之上,所以作者做了一个系统列表,来控制每个功能的按钮样式和按钮事件及按钮名称等,如图:

为了高效的开发,作者对按钮控制写了一套模版,开发者只需要在有使用按钮的地方,加入模版便能够得到相应的按钮。

4.5 菜单权限、按钮权限

菜单权限和按钮权限,又称“功能级权限”,这是大多数系统都拥有的功能,它的实现方式也很简单,就是每个菜单、按钮都可以通过角色来设置用户的可操作“单元”。如图:

“功能级权限”对于一般系统来说已经足够,但对于要求较高的系统来说还远远不够。所以作者编写出了一套能高效控制“数据列”、“数据行”的功能级权限管理。

4.6 显示列设置

在说如何控制“数据列”权限前,我想先说说数据列的动态获取,只有掌握了需要显示的数据列,才能有效的控制它。

在webapi中,作者写了一个接口(通过反射),以便获取所有需要显示的数据列。其中包括字段、字段类型、字段描述等信息。并且在此基础上可以编辑改变字段显示的名称、宽度、排序方式、对齐方式、插槽、是否行权限字段等信息。如图

在上述图片中,可以看到有一个“同步属性”的按钮,只要返回字段有变动,便会自动同步其变动信息,使其作用到系统每一个显示的地方,让开发更便捷。

4.7 数据列权限

什么是数据列权限,举一个简单的列子。

人员A和人员B都有一个“员工信息”的列表。其中“员工信息”中有一列为“工资”的数据。为了工资保密,员工A有查看人员工资的权限,员工B没有查看权限,那么就需要数据列权限。

而作者在“OverallAuth”系统中,结合4.6 显示列设置 这一功能,完成了这一功能,且能很灵活的控制每个模块列的显示情况。如图所示:

下面是admin和张三两个用户数据列的对比图

4.8 数据行权限

对于数据行权限,作者使用动态解析的方式,写了一套数据行权限的解析规则(把界面上选择要建立规则的字段,动态解析成程序能识别的代码)。可以灵活的设置数据的显示规则。如图所示

通过上述图片可以看到,设置规则后,同一个用户列表,同一个登录人,只能看到符合规则的数据。