Leo's blog

建议麦当劳

论文阅读: COLT5: Faster Long-Range Transformers with Conditional Computation

这篇文章是google research的,主要聚焦在这个方法怎么来让Transformer-based LLM能支持更长的context length,并做了一个64k context lenght的LLM,达到了GPT-4的两倍。但是,我个人认为,这个方法的应用潜力不止于此,详见『总结』部分。

作者说,鉴于Transformer decoder的 $O(n^2)$ 量级的计算复杂度,对于超长context的处理一直是极为困难的。但是,尤其是对于长文本来讲,序列中并不是所有token都同样重要。所以,作者提出了COLT5这个方法,在FNN和attention部分进行选择性计算,把更多计算资源投入到更重要的token中。实验证明COLT5比LongT5用更少的计算量达到了更好的效果。作者还训练了一个64k context length 的LLM。

Introduction

处理长文本一直是自然语言处理任务的难题之一。过去的工作大多聚焦在减少attention部分的计算量。但是,对于大模型来说,大部分计算量是在FNN和投影部分。

image-20230326153829070

本文提出了一个条件性计算来减少计算量的方式:使用一个『light』的MLP(有更少的隐藏层)和一个『light』attention(有更少的attention head;只有local attention),并在需要的时候路由至『heavy』的MLP和attention来更好的分配资源。

方法

作者的观点认为,应该把有限的计算力分配给更重要的token。作者简单地把当前层的表示乘以一个投映矩阵后得到路由分数并选择一个固定比例 token来输入到heavy模块来减少计算量。特别的,作者对FNN、Attention Q和Attention K使用了3个不同的router。

image-20230326154212490

最终把flops的常数项(尤其是 $n^2$ 的常数项)优化了非常多。

总结

这篇文章提出了一个可以有效减少计算时间中 $n^2$ 常数项的方法,可以有效的帮助支持更长的context length。

但是,我想说的是另一个问题:在现有的transformer架构中,无论使用哪种模型(T5这样encoder和decoder都有的模型,还是GPT这样只有decoder的模型,抑或是COLT5这样选择性计算的模型),模型的计算量都是仅与输入+输出序列的长度有关的。但是,事实上真的是这样的吗?

仔细思考一下便会发现并不是。人在思考问题的时候,思考时间(可以近似为计算量)显然是和问题的复杂程度有关的。比如,加法的计算复杂度(以下的 $n$ 均指代输入序列长度,或者说信息量/bit)是 $O(n)$ ,乘法是 $O(n^2)$ ,而transformer的计算时间复杂度是 $O(n^2)$ ,那么我们是否可以认为transformer-based模型是可以完全『学会』加法和乘法的,即达到百分之百的正确率?而对于更复杂的问题,判断$n$是否为质数,这个时候的输入长度是 $\log n$ ,计算量却是 $\sqrt{n}$ ,而transformer的计算时间却是 $\log^2 n$ 量级的。我们是否可以认为transformer永远无法完全『学会』判断质数?(如果能的话,说明模型找到了一种新的判断质数的算法。)

计算量不够可能就是LLM无法完成『答案简短的数学运算』的原因之一(答案太短了导致计算量不够)。『Chain-of-thoughts』技术之所以有效,或许原因之一就是context长度显著变长之后计算量更大了?

这里拿数学计算任务举例,但是同样的结论显然也可以应用更复杂的如QA等任务上。LLM使用『大力出奇迹』的方式,用固定的、极大的计算量期望解决大部分问题。但是,这样不仅浪费了算力,同时也会导致LLM会对于大计算量需求的任务给出一个似是而非的答案,同时从根本上永远无法给出正确答案。如果LLM可以自主决策『对于这个任务我要使用更多算力』,可能可以更好的解决任务,同时对于简单任务使用更少的资源。

前言

我一直对操作系统的内部实现非常感兴趣。最近,我在家里的内网部署了 kanidm,在过程中也对 Linux 的身份认证系统有了更多了解。因此,在此分享一下我的学习过程。

在本文的开头,我将讲述一小段我了解 sudo 是怎么工作的流程,这些内容以CC-BY-NC-SA 4.0 协议开源。后面的内容是我对于 Firstyear 的这篇博文 的翻译。

Forewords

I’ve always been curious about the internal implementations of operating systems. I recently deployed kanidm in my home lab, and I’ve leaned a lot about how does linux authentication works during this process. These paragraph is licensed under CC-BY-NC-SA 4.0. I’m writing this post to share my thoughts and share my translation of this fantastic blog from Firstyear in Chinese.

SUDO 是怎么工作的?

在我学习操作系统课程的时候,我突然想到,『sudo是怎么工作的呢?』在当时,我用我贫瘠的知识做出了以下假设:

  • 应该有一个用于提权的系统调用
  • sudo程序在用户态收集用户的密码,并调用这个系统调用提升到root

但是,仔细一想便会发现,这个假设有很大的问题:

  • sudo接受的是当前用户的密码,而不是root的密码,而哪些用户可以执行sudo是在sudoers配置文件里的
    • 所以,这个『用于提权的系统调用』应该需要知道哪些用户可以执行sudo
    • 但是不太可能是读那个配置文件?
  • 同时,sudo还支持更细颗粒度的权限限制,比如要求某个用户只能以root执行某个特定的指令。

经过一番搜索,我找到了 setuid 这个系统调用,发现它可以设置当前进程的uid。进一步搜索,发现这个系统调用只能用于
『降低权限』:有 CAP_SETUID 能力的进程才可以调用。那么,在sudo这种场景下,权限的『提升』是什么时候完成的呢?

经过我的尝试,我发现在调用sudo输入密码之前,sudo这个进程的uid就已经是0了,说明是sudo这个二进制程序本身有
特殊的权限,让他能直接提升到root。思考到了这一步,结论就十分显然:这个特殊的权限只能是存在文件系统里的。经过一番搜索,我发现文件系统除了读写执行等权限之外,还维护了几个特殊的权限:sudo用的是一个叫做setuid的权限:

The Unix access rights flags setuid and setgid (short for set user identity and set group identity) allow users to run an executable with the file system permissions of the executable’s owner or group respectively and to change behaviour in directories.

这样,任何用户在运行 sudo 的时候,权限都会临时的提升到 root(也就是 sudo 这个文件的 owner)。 sudo 自然可以自己判断用户是否有权限保留 root 权限,或者切换到其他用户。这也是为什么如果对 /usr/bin 运行 chmod -R 755 会导致 sudo 没法用了的原因: setuid 权限被清除了。

这就是我对 Linux 身份认证系统最初的理解的来源。

How does sudo works?

While I was learning Operating Systems at my school, it suddenly occurred to me that I don’t know how sudo works. Based on my little knowledge of Linux, I made the following assumption:

  • There should be a syscall for elevating privileges
  • sudo collects user’s password, and pass it to the syscall

However, almost immediately, I found some problems of my assumption:

  • sudo asks for the password of the current user, not the root user; and who is allowed to sudo is stored in sudoers
    • So the syscall for elevating privileges should be able to know who can do that
    • But it is unlikely that the syscall reads the sudoers file
  • sudo supports controlling which command a user is allowed to run
    • This is too complicated to be integrated into the kernel.

After some digging, I found the syscall setuid. It is able to set the uid of the current process. After some further searching, I found that this syscall is only for lowering the permissions: You have to have CAP_SETUID to run it. Then, in a scenario like sudo, when does the permission “elevation” happens?

After some trial, I discovered that when sudo is asking for password, the uid of it’s process is already 0, which means that the binary file sudo have something unique which makes it can directly be run as root. At this point, the answer is quite obvious: the only reasonable answer is that the permission is stored in the file system. Again, after some searching, I found that the file system maintains some special permission other than regular rwx. sudo uses setuid:

The Unix access rights flags setuid and setgid (short for set user identity and set group identity) allow users to run an executable with the file system permissions of the executable’s owner or group respectively and to change behaviour in directories.

So, the permission is temporarily elevated to root (who is the owner of sudo). sudo itself can determin whether the user is able to maintain the root permission or switch to other users. That is also why sudo breaks if you run chmod -R 755 /usr/bin.

This my first understanding of linux authentication system.


以下内容翻译自 Firstyear 的这篇博文以下内容的版权属于原作者

The following content is translation of this fantastic blog from Firstyear. Copyright remains to original author.

Linux 身份认证系统 – 从哪儿学起?

最近有一个人问我,应该如何学习 Linux 的身份认证系统的各个组件是如何组合到一起、如何通讯的。网上并没有很多有关这个话题的资料,所以我决定来写这篇博客。

你…是谁?

Linux 的身份中的第一个模块就是 NSS 或者 nsswitch (注意不要和密码学库中的 NSS 混淆)。 nsswitch(name service switch)在 glibc 中提供了一个获取 uid/gid 以及名字和账户详情的方法。nsswitch可以有很多个『模组』叠加在一起,对于每个请求,第一个相应的模组的答案会被返回。

一个 nsswitch.conf 的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
passwd: compat sss
group: compat sss
shadow: compat sss

hosts: files mdns dns
networks: files dns

services: files usrfiles
protocols: files usrfiles
rpc: files usrfiles
ethers: files
netmasks: files
netgroup: files nis
publickey: files

bootparams: files
automount: files nis
aliases: files

这个文件使用 服务: 模组1 模组2...的格式。一个简单的例子是:当一个程序使用gethostbyname方法来进行dns查询时,它访问host服务,先通过files模组解析/etc/hosts,再通过mdns模组(也叫做 avahi/bonjour),最后调用dns模组解析。

passwd/group/shadow是有关身份的三行。最常见的情况下,你会使用files模组。它会查询/etc/passwd/etc/shadow来返回响应。compat模组和files类似,只是增加了对于NIS的兼容。另一个常见的模组是sss,它会访问 System Services Security Daemon (SSSD)。对于我自己(这里指源博文的作者)的IDM项目,我们使用kanidm这个 nsswitch 模组。

你可以用getent命令来测试 nsswitch 是如何解析身份的,比如:

1
2
3
4
5
6
7
8
9
# getent passwd william
william:x:654401105:654401105:William:/home/william:/bin/zsh
# getent passwd 654401105
william:x:654401105:654401105:William:/home/william:/bin/zsh

# getent group william
william:x:654401105:william
# getent group 654401105
william:x:654401105:william

注意到,uid 和名字都可以被用于获取身份。

这些模组都是动态链接库,你可以用以下命令找到他们:

1
# ls -al /usr/lib[64]/libnss_*

当一个进程想要通过nsswitch来解析什么东西时,它会调用glibc,glibc会在运行时加载这些动态链接库并运行他们。这就是通常你想要给某个发行版一个新的 nsswitch 模组时需要仔细审计的原因:这些模组可能会进到每个进程的地址空间!这也同时有一些安全上的影响,因为每个模组,在被每个进程加载的时候,都需要访问/etc/passwd或者访问网络来解析身份。有些模组(比如sss)改善了这一点,我们会在这个blog的后面部分讲到。

证明你自己

如果nsswitch回答了『你是谁』的问题,那么PAM(pluggable authentication modules,可插拔认证模组)就是『证明你自己』。PAM是真正做出检查你的密码等信息是合法的、检查你可以登录的模块。PAM通过有不同的服务来调用不同的模块工作。大多数的 Linux 发行版都有一个包括了所有的服务定义的 /etc/pam.d 文件夹(和Linux上不常用的/etc/pam.conf有一点点语法上的区别)。我们拿ssh举个例子:当你ssh到一台机器上的时候,ssh联系PAM并告诉它:我是ssh,你能帮我验证这个身份吗?

然后,PAM会读取/etc/pam.d/服务名称,在我们这个例子中是/etc/pam.d/ssh。以下是一个从Fedora中提取的例子(Fedora和RHEL都是非常常见的发行版;每个发行版都对这些配置文件有一些微调,这也让理解它们更困难):

1
2
3
4
5
6
7
8
# cat /etc/pam.d/ssh
#%PAM-1.0
auth include system-auth
account include system-auth
password include system-auth
session optional pam_keyinit.so revoke
session required pam_limits.so
session include system-auth

注意 “include” 分别对于 auth, account, password, session 重复了四次。它们都引用 system-auth, 那么让我们来看看:

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
# cat /etc/pam.d/system-auth

auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth [default=1 ignore=ignore success=ok] pam_localuser.so
auth sufficient pam_unix.so nullok
auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular
auth sufficient pam_sss.so forward_pass
auth required pam_deny.so

account required pam_unix.so
account sufficient pam_localuser.so
account sufficient pam_usertype.so issystem
account [default=bad success=ok user_unknown=ignore] pam_sss.so
account required pam_permit.so

session optional pam_keyinit.so revoke
session required pam_limits.so
-session optional pam_systemd.so
session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session required pam_unix.so
session optional pam_sss.so

password requisite pam_pwquality.so local_users_only
password sufficient pam_unix.so yescrypt shadow nullok use_authtok
password sufficient pam_sss.so use_authtok
password required pam_deny.so

所以,首先我们在『验证』阶段。这个阶段是PAM依次访问验证模组来检查你的用户名和密码(或者其他形式,如TOTP等)直到返回成功。我们从 pam_env.so 开始,它返回『通过但未结束』,于是我们继续访问 faildelay,以此类推。这些模组一个一个被访问,它们的结果与前面的『规则』( required/sufficient 或者自定义)来合并到一起,得到『成功,验证结束』、『成功但是继续验证』、『失败但是继续验证』、『失败但是结束』这四种结果。在这个例子中,能真正的验证用户的是 pam_unix.sopam_sss.so 。所以,如果这两个都没有返回『成功,验证结束』,pam_deny.so 就会最终返回一个 『失败但是结束』。这个阶段只检查你的等录凭据(密码等)

第二个阶段是『账户阶段』。其实它更应该被叫做『验证』阶段:这些模块被再次访问,来检查你的账户是否有权限访问这个服务。结果以类似的形式结合到一起。

第三个阶段是『会话阶段』。每个PAM模块可以影响和设置新创建的会话:一个简单的例子是 pam_limit.so,它负责设置新会话的 CPU /内存/文件描述符等限制。

第四个阶段是『密码阶段』。可能有点令人疑惑:这个阶段并不是用来验证身份的,而是在你运行passwd命令来修改这个密码的。每个模块依次被询问:你可以修改这个用户的密码吗?如果最终失败了,你会得到一个『authentication token manipulation error』,一般只是说『这个栈中的一些模块失败了,但是我不能告诉你是哪个』。

这些模块都是动态链接库,一般可以在 /usr/lib64/security 找到。就像 nsswitch 一样,使用pam的应用都链接到 libpam.so,它会在运行时加载 /usr/lib64/security 中的动态链接库。鉴于/etc/shadow只能被root用户读取,同时任何需要验证密码的东西都需要来读取这个文件,这基本意味着任何pam模块实际上在任何都运行在 root 的地址空间中。这就是发行版仔细审计和控制哪个模块可以添加一个pam模组的原因。同时,这也意味着进程很可能需要访问网络来调用远程的身份验证服务。

那么,网络验证呢?

现在,我们已经覆盖了进程和守护进程如何找到用户、验证凭据的基础。现在,我们来看看SSSD,一个解析身份的守护程序的实现。

正如之前提到的,nsswitch和pam都有让动态链接库在应用程序的上下文中运行的限制,这也通常意味着,在过去,pam_ldap.so可能在root的地址空间运行,同时需要访问网络的权限以及需要解析asn.1g格式(一个通常用于远程代码执行的库,也可以被用作编/解码二进制结构体)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
root: uid 0 │
│ │

│ ┌─────────────┐ │ ┌─────────────┐
│ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │
│ │ SSHD │──┼────────▶│ LDAP │
│ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │
│ └─────────────┘ │ └─────────────┘

│ │ Network

└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

SSSD改变了这一点:在本地运行一个可以通过unix socket访问的守护进程。这允许了pam和nsswitch模组id仅仅提供最小化的功能,仅仅负责联系一个独立的守护进程,而大部分工作都交给守护进程完成。这有非常非常多的安全性改善,包括不需要由root进程来解析网络上的不可信的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐      ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
root: uid 0 sssd: uid 123 │
│ │ │ │

│ ┌─────────────┐ │ │ ┌─────────────┐ │ ┌─────────────┐
│ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ SSHD │──┼──────┼─▶│ SSSD │──┼─────────▶│ LDAP │
│ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │
│ └─────────────┘ │ │ └─────────────┘ │ └─────────────┘

│ │ │ │ Network

└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

另一个很大的好处是SSSD现在可以安全的缓存网络服务的响应,允许用户在离线的时候继续解析身份。这甚至包括缓存密码!

这就是SSSD能在很多主流的发行版中都占据重要地位的原因。用一个很复杂的本地守护程序来完成真正的验证工作,和能使用很多不同的验证后端的能力,这使得它被广泛部署,并且会在基于网络的验证场景上替代pam_ldappam_krb5

巨兽之内

SSSD内部由很多个互相协作的模块组成。知道它们是如何工作的能很好的帮助debug:

1
2
3
4
5
6
7
8
9
10
11
12
13
# /etc/sssd/sssd.conf

//change the log level of communication between the pam module and the sssd daemon
[pam]
debug_level = ...

// change the log level of communication between the nsswitch module and the sssd daemon
[nss]
debug_level = ...

// change the log level of processing the operations that relate to this authentication provider domain ```
[domain/AD]
debug_level = ...

现在我们知道了一个新的概念:一个SSSD domain。这和Active Directory中的doamin不同。一个SSSD domain仅仅是一个『验证服务提供者』。一个SSSD的实例可以同时从多个不同的验证服务提供者解析身份。但是在主流的设置中,一般都只使用一个domain。

在大部分情况下,如果你在使用SSSD的过程中遇到问题,错误应该都在domain部分,所以这永远应该是第一个被检查的地方,

每个domain都可以不同的服务商来完成『身份』、『验证』、『访问』、『修改密码』工作。比如:

1
2
3
4
5
[domain/default]
id_provider = ldap
auth_provider = ldap
access_provider = ldap
chpass_provider = ldap

id_procider负责解析名字和uid/gid到身份。

auth_provider负责验证密码。

access_provider负责判断这个身份是否有权限访问这台系统。

chpass_provider负责更改密码。

正如你可以看到的,在这个设计中有很大的灵活性:比如,你可以使用krb5来验证身份,但是使用ldap来修改密码。

正因为这个设计,SSD可以从很多个不同的身份源来验证身份,包括samba(ad)、ldap和kerberos。这意味着在某些受限的场景下,你可能需要具体身份来源的背景知识来解决SSSD的问题。

常见问题

性能

在某些情况下,SSSD在第一次访问时会非常慢,但是在登录完成后会变快。在某些情况下,你可能会在这个时候在LDAP服务器上看到很高的负载。这是用户和用户组解析的方式产生的问题:每当你需要解析一个用户的时候,你需要解析他所在的组;当解析这些组的时候,这些组又要加载它的全部成员…我希望你能看出来这是递归的。在最差的情况下,当一个用户登录的时候,整个LDAP/AD域都被枚举,在某些情况下可能要花几分钟。

如果要避免这一点,你可以设置:

1
ignore_group_members = False

这样能避免组加载他们的成员。这样,所有用户组看上去都是没有成员的,不过所有用户都会展示他们所在的用户组。鉴于绝大部分程序都使用『是xx的成员』这个模式,这样设置没有什么负面作用。

清除缓存

SSSD在本地缓存了网络服务的响应。他附带一个缓存管理工具: sss_cache。它允许标记记录为失效,这样sssd会尽快重新从网络加载。

这样有两个问题:在某些情况下,清除缓存看起来没有作用,失效的记录被继续使用;同时,sss_cache 工具的-E选项并不总是会使全部记录失效。

在这样的情况下,一个通常的建议是关闭sssd,删除/var/lib/sss/db文件夹内的所有东西(但是不要删掉文件夹)然后重启sssd。

调试 Kerberos

Kerberos的难以调试是臭名昭著的。这是因为它并没有一个真正的详细/调试模式,至少不显然。为了获取到调试输出,你需要设置一个环境变量:

1
KRB5_TRACE=/dev/stderr kinit user@domain

这个trick在任何链接到kerberos的进程都有效,所以它在389-ds, sssd, 和很多很多其他工具上都有效。你可以使用这个方法来追踪哪里出了问题。

总结

以上就是全部内容了,我可能会持续更新这个博文!

(翻译于2023-3-26,如果原文有更新,欢迎在评论区叫我更新翻译)

一直觉得应该找一个形式把读过的论文以除了组会分享报告之外的形式记录下来。最近看到同学的论文阅读分享,终于决定也开始写blog。

今天看的这篇是 Self-Instruct , 是最近很火的 instruct tuning(给LLM增加 follow instruct 的能力)方法。之前InstructGPT等方法都是需要人工标注的instruct数据(在SFT阶段),这篇工作提出,既然LLM的能力已经足够强,只需要构造Prompt,从LLM中挖掘instruct信息,从而来tune自己。

本质上,作者证明了『可以从 few-shot 能力很强的LLM来bootstrap出一套 SFT 数据集,从而得到 zero-shot 或者说 Instruct-tuned LLM』。

作者来自华盛顿大学,感觉是要投今年的ACL。他们声称用GPT3达到了类似 text-davinci-001 (仅SFT的InstructGPT)的效果,比用 SuperNaturalInstructions 数据集训练的效果类似。这篇文章发布于22年12月,当时chatgpt刚发布不久,所以只对比了 InstructGPT?(好像跟ChatGPT也没法比)。

Stanford 也用这个方法,用 text davinci 003 造了个数据集后基于 LLaMa 训练了 Alpaca 模型,在7B这样一个参数量下达到了相当惊艳的效果。这也是证明 instruct tuning 不需要花很多钱就能做到的一个证据?Alpaca开源了他们生成的52k个instruction,这可能是 llm 的 stable diffusion moment 的开端?

Introduction

作者先讲了最近大家都在做 instruction tuning 的工作,说现在的方法都需要人工构建instruct数据集。于是,他们提出self-instruct方法,视图让LLM自我构建一个 instruct 的数据集,来做到自己训练自己:从一个很短的(175个)人手写的数据集来bootstrap,主要有一下步骤:

  1. 要求模型生成新任务的instruction
    1. 利用现有的任务描述,让模型生成新的任务和新的指令。通常,模型会定义一些新的任务出来
  2. 生成 input-output pair
  3. 过滤掉重复的和低质量的pair
  4. 然后把他们添加到 task pool 中
    方法结构图

总结一下贡献:

  1. 提出 self-instruct 方法
  2. 跑了很多实验来证实有效性
  3. 开放了52k个instructions
    1. (alpaca 开放了用同样方法构建的、基于text-davinci-003的数据)

Method

Instruction Generation

第一步是生成一些新的task和instruction。他们使用以下的prompt来生成:

1
2
3
4
5
6
7
Come up with a series of tasks:

Task1: {from pool}
Task2: {from pool}
...
Task8: {from pool}
Task9:

简单来讲就是让模型进行 in-context learning. 用175个人写的instruction来初始化 task pool,之后每次随机选6个人写的和2个机器生成的task来生成新的task。

Classification Task Identification

因为后续的步骤对于 Classification 任务和其他任务的处理方法不同(在这里,他们把 有有限的、小的输出label集合的任务定义为分类任务),他们用了原版GPT3的few shot learning 来做这个分类,用了12个分类任务的instruction和19个非分类任务的instruction作为prompt。

Instance Generation

第三步是用task来生成一些任务的 input-output pair。作者认为,传统的方法先生成input再生成output,在分类任务上会有类别不平衡的问题。所以,对于分类任务,他们用了 output-first 的方法:先生成可能的label,再根据这些label生成可能的输入。

Filtering and Post-processing

为了保证instruct的多样性,他们仅当新的instruction和旧的的 rouge-l 分数小于 0.7 时才会把它加入到 task pool 中。同时,他们排除了常见的llm无法解决的问题的关键词(image等)。最后,在生成 instance 的时候,他们去掉了完全相同的或者有相同input的。

Finetuning the LM

最后一步是用这些数据来finetune LM。 为了保证鲁棒性,他们混和多种prompt(如是否添加 Task: 等)。

实验结果

作者先在 super-naturalinstructions 数据集上跑了和 t0、gpt3的对比试验,发现加上self-instruct后效果先祖很好、能和instructGPT接近。

同时,在一套新的instruct set 上进行了人工评分:

效果和 InstructGPT_001 类似。

结论

作者证明了可以用 LLM 来生成自己的训练数据,也就是说,只需要花几百刀(作者说用了$338左右)来调用OpenAI的API就可以复现一个InstructGPT。同时,后续stanford释出的数据集和alpaca模型也都证明了这个方法的有效性。

作者自己说,缺点包括会强化LLM的bias(如性别、种族等)。我感觉,是否能改善LLM的safeness也许要更多试验。

下面是我在这学期的《编译原理》课程的作业。记录在此,请读者批评指正!

基础软件包括什么?回答这个问题,只需要看计算机科学与技术专业的本科生学的四大基础课程:《计算机组成原理》、《数据库原理》、《操作系统》、《编译原理》。在计算机相关的任何应用都无法脱离这四门基础课程的知识,同理,计算机在工程领域的软件中,EDA软件、数据库、操作系统以及编译器是四种最基础的基础软件。下面,本文以世界以及中国的编译器软件为例讨论我国基础软件现状。

国际上,知名的编译器有 GNU (Gnu is Not Unix) 组织的 『GCC (the GNU Compiler Collection)』是Linux中最知名的编译器,也是世界范围内应用最广泛的编译器,支持C、C++、Objective-C、Fortran等多种语言。由UIUC研发,并被苹果公司广泛应用的『LLVM编译基础设施(Low-Level Virtual Machine Compiler Infrastructure)』通过分离编译器前后端并通过LLVM这一中间语言桥接,实现了多种语言与ISA支持,并提供强大的优化能力。除了上述两个开源的编译器之外,也有大量闭源的编译器被广泛应用:由Intel公司开发的 『ICC (Intel C++ Compiler)』主要面向Intel平台,由于其出色的定向(面向Intel x86和amd64架构CPU)的优化能力以及向量化(vectorization)能力,被广泛应用在HPC场景。由微软公司开发的『MSVC(MicroSoft Virtual C++)』主要面向Win/x86场景,随后适配amd64以及arm32、arm64架构,作为Win 32开发的主要编译器,也得到了广泛的应用。

更现代的语言在实现能够自举的编译器时,主要有两种思路。第一种是如Go语言,从0开始实现编译器,没有任何依赖,甚至不依赖于系统汇编器,而是自己实现了一套全新的汇编器。第二种是如Rust语言,实现面向LLVM中间语言的编译器前端,使得可以直接适配LLVM所支持的所有ISA,并获得其优秀的优化能力。也正因如此,rustc的工程量十分小,但是其输出的LLVM的质量显著低,使得在编译一个同样的程序时,其相比与GCC系列编译器的速度慢5-10倍[1]。同时,一些新的语言也同时采用两种方式,如微软公司的TypeScript语言,可以通过其编译器配合巴别塔(the BabelJS)转译为JavaScript,也可以通过另一个编译器编译为运行在嵌入式设备上的二进制代码。这俩类思路各有利弊,前者工程量更大,需要自己支持各类ISA以及各类优化,但是更为自由。后者程序产物需要依赖现有工具链,但是工程量更小,开发更为便捷。

现有的『国产』编译器中,『太极(Taichi)』[2]收获了很多的关注。作为一门领域特定的编程语言(Domain Specific Programming Language),其具有优秀的GPU开发能力,相较CUDA等传统库,更容易上手、使用。另一个获得广泛关注乃至争议的语言是『木兰』。木兰作为一款面向嵌入式平台的小型编译器,由于在宣传上的失误以及广大群众对于『中芯』等项目的恐惧,木兰受到了很大的批评以及指责。笔者个人认为,单从技术上来看,在面向嵌入式平台的同时提供基于Python的『套皮』模拟器,是十分正常且合理的,并不能因其使用了Python而受到批评。

由此可见,对于基础语言(如C++、C)的编译器,我国还是显著依赖于世界范围内的开源编译器,并没有实现真正的『自主』。那么,下一个需要讨论的问题就是使用开源编译器『安全』吗?如果不经过任何代码审计就拿来使用,显然是不安全的。无论是在编译的生成结果中嵌入恶意代码,还是在标准库中故意暴露漏洞,都有可能对软件造成很大的破坏。比如,对于安全要求较高的地方,如OpenSSL等密码学库,都不依赖于标准库提供的随机数生成器,而是选择自己实现随机数生成器(密码学的大部分算法都要求随机数具有足够的熵。如果随机数不够强,攻击者就可以找到攻击向量)。

但是,对于一个平台,除非从硬件到固件到软件都是自主设计或者经过安全审计的,否则总有一些环节需要『信任』。如,在上述的例子中,OpenSSL选择不信任编译器标准库提供的随机数(定然,也一定程度上因为功能性和强度的需求),选择自己实现随机数生成器。但是,在生成随机数的算法依赖与操作系统提供的真随机数作为种子。凭什么说操作系统提供的真随机数种子是可以信任的呢?如果使用在线服务生成真随机数(如random.org声称提供基于大气噪音生成的真随机数),又凭什么说这些服务是可以信任的呢?如果依赖CPU硬件提供的、基于电气热噪声的随机数,也同样存在这个信任问题。对于开源操作系统提供的基于用户输入的真随机数,可以通过审计操作系统源码的方式来确保真实性,但是其他随机数生成方式仍然存在信任问题。因此,大公司往往采用自主设计的随机数生成硬件来采样随机数,避免暴露攻击向量给攻击者,如Cloudflare公司提供SSL服务,需要大量的、具有足够熵的随机数,因此其采用自主设计的『熔岩灯随机数(LavaRand)』[3]硬件生成随机数。

『生成可信的随机数』这个小问题都有如此复杂的信任问题,更不要说复杂问题以及完整的工程的信任问题了。如,现有的操作系统提供的对于应用程序安全性全部依赖于CPU硬件提供的内核态与应用态的隔离,但是如果CPU本身是不可信的呢?Intel x86前日爆出一个隐藏的,用于修改微码的指令[4]。该指令虽然对于现有系统的安全性没有危害,但是不禁引人思考:如果CPU中有一个隐藏的指令,可以不经授权直接从用户态提升到内核态,那么现代操作系统提供的所有安全保证都会像气球一样,一触即溃。

既然如此,那么,是否有必要投入编译器等基础软件的开发呢?经过以上的讨论,答案显然是『有』。对于已有成熟开源产品的应用场景,如编译器或操作系统,我们可以采取审计源码的方式,维护一个我们可控的分支。对于没有成熟开源产品的领域,如EDA软件,我们则要投入精力开发。面向消费领域,可能实现很高的安全性没有很大的意义,但是对于国防领域和科研领域,安全性则是重中之重。但是,实现自主的这条路任重而道远。我们通过自主实现编译器、汇编器、链接器,实现了编译软件的可控,接下来要面对的就是操作系统的安全问题;我们通过编写操作系统或者审计操作系统源码来保证操作系统的安全性,接下来要面对的就是CPU的安全问题;我们通过自主实现CPU来保证安全性,接下来还要面对外设的安全性问题:如果网卡中有后门怎么办?由此可见,在追求安全、可控的道路上没有终点。现有来看,基础软件中,开源编译器、操作系统、数据库等不存在所谓『卡脖子』问题,但是如EDA等工程领域的软件,我们仍然受制于人(如中芯和海思等,被列入实体清单后无法使用EDA软件)。但是,尽管过程艰难,最终的目标仍然是有必要的,需要坚持的走下去。

笔者一直以来关注国产软件和硬件的开发,见证了汉芯、龙芯、木兰、太极等项目的兴衰,一直想总结一下个人对于这方面的观点。这次借助作业这个机会,有感而发,陈述一下自己的观点,不知不觉已经写了这么多。请老师同学们批评指正!


  1. Why does rust compile a simple program 5-10 times slower than gcc/clang? - stack overflow[EB/OL]. https://stackoverflow.com/questions/37362640/why-does-rust-compile-a-simple-program-5-10-times -slower-than-gcc-clang/37365065. ↩︎

  2. HU Y. The taichi programming language[C/OL]//SIGGRAPH ’20: ACM SIGGRAPH 2020 Courses. Association for Computing Machinery, 2020: 1–50. https://doi.org/10.1145/3388769.3407493. ↩︎

  3. Randomness 101: Lavarand in production[EB/OL]. https://blog.cloudflare.com/randomness-101-lav arand-in-production/. ↩︎

  4. Undocumented x86 instructions in intel cpus that can modify microcode | hacker news[EB/OL]. https: //news.ycombinator.com/item?id=26519693. ↩︎

随想 有关乱码

首先给出几种常见『乱码』:

1
2
3
4
5
6
md5 378e3ce9f0c8243012cb32cedde1ad31
sha1 b4d254b6620924a05e95bf76f5dace64edcf9086
sha256 3e9818cc4bf74e65419e72f89104dca674dcb215c1487dd25fed541cd6363d72
base32 MNUGC3THNJUWC3TMOVQW43LBBI======
base64 Y2hhbmdqaWFubHVhbm1hCg==
密码哈希函数 $2y$10$P5DEiAiuSr0XT9085ioTQeIW9QrrnGEkSvdJSPmlGDnvcANlvFDwm

本文的重点不在于怎么判断常见乱码,但是还是简单介绍一下:

  • $$分割的,可能是密码哈希函数的输出或者是JWT
  • 后面可能有=,内容是字母和+/或者-_,可能是base64系列。如果只有大、小写字母,可能是base32。如果有+/,是传统base64 or base32。如果有-_,是urlsafe的base64 or base32
  • base64解码后的内容如果不是utf-8,可能是哈希函数的输出。看长度(多少bit)
  • hex看长度。常见的md5和sha系列哈希的长度。

正视『乱码』

之前一直觉得『乱码』这个词被滥用了。

遇到一串自己看不懂的东西,就把他称为『乱码』。我一直觉得这个词不太合适,因为这串东西在别人眼里很可能是能看懂的,有意义的。但是,我本人一直也找不到一个非常恰当的称呼,因此一直没有在意。

直到某次给Golang提issue的时候,Google的工作人员分析我的程序,看到一串『乱码』。他是这么描述的:

image-20211129110312524

『这个字符串在我来看是乱码,你知道是什么吗?』

确实,用『我看来是乱码』一词形容无疑更为准确。由于对密码哈希函数的不熟悉(也十分正常),看不出这串字符串是什么,但是无疑,它是有意义的,不是无意义的。

为什么今天突然想到写这个东西呢?总有一些人,认为这串东西自己看不懂就没有意义。有一次和别人一起调试一个程序的时候,他描述我的程序『输出了乱码』。在我反复多次强调『让我看看输出』之后,仍然坚持认为乱码没有意义,没有发给我看。实际上,那段程序输出的就是形如$2y$10$P5DEi...的哈希值。

所以,当别人的程序输出了一些我们看不懂的东西的时候,不要只描述为『输出乱码』,请复制乱码内容,一起询问可能能看出这是什么的人。

个人题解,和比赛官方无关,仅供参考

Changing problem statement during contest

It is well known that we should not change the problem statement during the contest. But, we also know that a company named “Hua***” liked to change problem statement during the contest .

There were some breathtaking errors in the problem statements during the 2021 ICPC Asia Regionals Online Contest (I). So, naturally, we needed to change these statements.

For example, problem A needs change. As we all know, the word “lexicographic” means "order of the dictionary(字典序), ‘a generalization of the alphabetical order of the dictionaries to sequences of ordered symbols.’ ", but somehow the person who wrote this problem misinterpreted the word as “ascending”.

Naturally, during reasonable contests, what he needs to do is to update the testcase data, changing it from ascending order to lexicographic order, matching the problem statement, then rejudge all submissions of
this problem. But, he doesn’t think it that way. “Why changing testcase data while we can change the problem statement?” Surely he didn’t know that submitting “Wrong Answer” would add penalty(罚时) to the team’s time cost. I mean, otherwise, why would he do this?

Can you help him change all “lexicographic” to “ascending”?

Input

The first line of input contains a integer $n$. $(1 \leq n \leq 100)$.

In the following $n$ lines, each line contains a string. It is guaranteed that the length of the string is less than $100$ characters, and the string only contains a to z.

Output

Output updated problem statements, replace all lexicographic by ascending.

Examples

Example 1

Input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
14
print
the
labels
of
all
the
busiest
nodes
in
lexicographic
order
separated
by
spaces

Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print
the
labels
of
all
the
busiest
nodes
in
ascending
order
separated
by
spaces

Example 2

Input

1
2
1
whoalexicographic

Output

1
whoalexicographic

In the first example, we replace “lexicographic” on the 10th line to
“ascending”.

In the second example, we do nothing because we don’t need to replace
word containing “lexicographic”.

题解

题目难度:简单。

阅读题面之后会发现是一个非常简单的字符串比较。循环 $n$ 次,每次读入一个字符串,如果是lexicographic就替换为ascending就好了。

标准答案:

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
#include <stdio.h>
#include <string.h>
int main() {
char input_string[101];
// 注意,存储长度为100的字符串,数组长度需要是101
const char *lexicographic = "lexicographic";
int n;
scanf("%d", &n);
for(int i = 0; i < n; ++ i) {
// scanf 传入的是地址
// input_string 是数组名字,本身就是地址,不需要加
// 取地址符 &
// 了解更多: https://zh.cppreference.com/w/c/language/array
scanf("%s", input_string);
if (strcmp(input_string, lexicographic) == 0) {
// 相等,输出ascending
printf("ascending\n");
} else {
// 不相等,原样输出
printf("%s\n", input_string);
}
}
// 别忘了return 0
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;
int main() {
int n;
cin >> n;
while (n--) {
// 定义字符串 a
string a;
// 读入
cin >> a;
// 如果等于lexicographic,就替换为ascending
if (a == "lexicographic") {
a = "ascending";
}
// 输出
cout << a << endl;
}
return 0;
}
0%