🌐English
目录
  1. 误差估计
  2. CPULESS: 打破异构系统交互墙
  3. MNIST 网站计数器
  4. Cambricon-Q:量化训练架构
  5. Cambricon-FR:分形可重配指令集结构计算机(通用分形计算机)
  6. Cambricon-F:一种具有分形冯·诺伊曼体系结构的机器学习计算机

误差估计

去年在做 Cambricon-Q:量化训练架构 的工作时,我发现许多同学不会做误差估计,也没有意识到随机实验中需要进行误差估计。体系结构领域似乎并不重视这一点,同行做实验时常常在真实系统上测试跑一个几十毫秒的程序,然后直接用time命令截取用时,再汇报模拟实验结果的加速比。这样的测量是有误差的,结果常常在一个范围内波动;如果加速比达到上百倍,一点误差可能会导致最终报告出完全不同的数值。 小初高通识教育中,实验课会教我们“多次测量取平均值”,但“多次”应该是多少次?5次够吗?10次呢?100次呢?还是直到工作截稿之前,一直重复做下去呢?我们需要了解科学的实验方法:重复随机实验中的误差估计。我发现并非所有本科概率论课程都教授这些,但要从事科学研究工作,这却是一项必备技能。

我不是统计学家,本科时我也是全校挂科王,肯定算不上概率论专家。但我曾经作为业余爱好钻研过蒙特卡洛模拟,我来展示我自己是如何完成这个过程的。如果有误,还请概率论专家们不吝纠正。

高斯估计

我们有一组实验测量值,现在假设它们独立同分布,服从于高斯分布

虽然分布参数中有,这是我们想得到的,但不会直接体现在测量值中。为了逼近,我们可以取测量值的算术平均值。我们首先先求和,得到,然后除以。这时算术平均值。注意一个高斯分布随机变量除以后,方差会相应缩减倍。

虽然以上推演过程首先假设了高斯分布,但是根据列维-林德伯格中心极限定理(Levy-Lindeberg CLT),任意无限个随机变量叠加后,其和都趋近于服从高斯分布,只要这些随机变量满足以下两个条件:

  • 方差是有限的;
  • 独立。

这样,无论实验测量值实际服从的分布是什么,我们都可以直接从CLT导出最终的分布,结果是一样的。 只要实验次数足够多,就趋近于

高斯分布告诉我们,我们有99.7%的把握说,与我们想要获取的真实值之间的差距不超过三倍的标准差,即。我们说,的99.7%置信度的置信区间是。 随着实验次数增多,的标准差还会持续缩小。原来我们只知道多次测量取平均值会奏效,现在我们还知道收敛速度了:每增加4倍样本,误差将缩小2倍。

我们用到的常数3和99.7%是查表得出的高斯分布累积分布函数(CDF)上的一个点,类似常用值还有1.96和95%。从确定的置信区间推算置信度,我们使用CDF:,但CDF只留了右边尾分布不计,但我们估计的区间是双边的,因此置信度还要扣除左边尾分布,即。如果要用目标置信度来推算置信区间,则需要使用CDF的逆函数ICDF,也叫PPF。

编程实现时,高斯分布的CDF可以比较直接地用数学库函数erf实现。它的逆函数erfinv并不常见,在C语言和C++的标准库里就不存在,不过因为CDF都是连续、单调、可导的,其导函数即概率密度函数(PDF),因此用牛顿法、割线法、二分法等数值方法都可以比较高效地求逆。

然而,以上估计完全依赖于CLT,CLT毕竟只描述了一个极限情况。实际实验时我们取5次、10次实验结果,离无限次实验差得远,难道也强行应用CLT吗?这显然不科学。

“学生” t-分布估计

描述有限多次随机实验叠加值,有一个更专用的概率分布,即“学生” t-分布。除了以上高斯估计推算的过程,t-分布还额外考虑了和真实误差之间的差异。

t-分布与自由度直接相关,在重复随机实验里自由度可以理解为。自由度低时,t-分布的两端更长,可以导出一个更保守的误差估计;当自由度趋近于无穷时,t-分布也趋近于高斯分布。例如5次重复实验,自由度为4,如果用高斯估计99.7%置信度的误差结果为3倍标准差,用“学生” t-分布则估计为6.43倍,显著地扩大了。

当试验次数不多时,我们直接用t-分布代替高斯分布来描述。特别是在依靠误差估计来判定重复实验的提前结束时,使用高斯估计更容易发生过早的结束(特别是在前两、三个样本凑巧相差不大时)。

实现

在Python中,直接调包:高斯估计使用scipy.stats.norm.cdfscipy.stats.norm.ppf;“学生” t-分布估计使用scipy.stats.t.cdfscipy.stats.t.ppf

C++里比较难做,STL库功能不全,而且引入外部库不方便。下面的代码给出了两种误差估计的C++实现。两条注释条之间的内容可以抽出组成一个头文件供直接使用,更下面的部分则展示了一个样例程序:从分布的随机变量中重复实验,直到误差估计认为有95%的把握将真实值囊括在估计值的正负10%的范围内。

代码
errest.cppview raw
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/*
error estimator for repeated randomized experiments.
every scientists should know how to do this...
requires c++17 or above.

Copyright (c) 2021,2022 zhaoyongwei<zhaoyongwei@ict.ac.cn>
IPRC, ICT CAS. Visit https://yongwei.site/error-estimator

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// ====================================================================
// contents before the DEMO rule is a header.
// ====================================================================
// #pragma once // <-- don't forget opening this in a header.
// compilers don't like this in the main file.

#define __STDCPP_WANT_MATH_SPEC_FUNCS__ 1 // for std::beta
#include <cmath>
#include <iterator>
#include <tuple>

namespace errest {

// gaussian distribution. works as an approximation in most cases
// due to CLT, but is over-optimistic, especially when samples are
// few.
template<class RealType = double>
struct gaussian {
static RealType critical_score(RealType cl, size_t df) {

// due to CLT, mean of samples approaches a normal distribution.
// `erf` is used to estimate confidence level.
// now we set a confidence level and try obtain corresponding
// confidence interval, thus we need the inverse of `erf`,
// i.e. `erfinv`.
using std::log; using std::sqrt; using std::erf; using std::exp;

// solve erfinv by newton-raphson since c++stl lacks it.
auto erfinv = [](RealType x) {
constexpr RealType k = 0.88622692545275801; // 0.5 * sqrt(pi)
RealType step, y = [](RealType x) { // a rough first approx.
RealType sign = (x < 0) ? -1.0 : 1.0;
x = (1 - x)*(1 + x);
RealType lnx = log(x);
RealType t1 = RealType(4.330746750799873)+lnx/2;
RealType t2 = 1 / RealType(0.147) * lnx;
return sign * sqrt(-t1 + sqrt(t1 * t1 - t2));
}(x);
do {
step = k * (erf(y) - x) / exp(-y * y);
y -= step;
} while (step); // converge until zero step.
// this maybe too accurate for our usecase?
// add a threshold or cache if too slow.
return y;
};
// we hope that the "truth" E[mean] lands in the interval
// [mean-x*sigma, mean+x*sigma] with a probability of
// conf_level = erf( x/sqrt(2) )
// conversely,
// x = sqrt(2) * erfinv(conf_level)
return sqrt(RealType(2)) * erfinv(cl);
// to get interval, mult this multiplier on the standard error of mean.
}
};

// student's t-distribution. yields an accurate estimation even for few
// samples, but is more computational expensive.
template<class RealType = double>
struct student {
static RealType critical_score(RealType cl, size_t df) {

// similar to gaussian, we need to evaluate ppf for critical score.
// ppf is the inverse function of cdf, and pdf is the derivative
// function of cdf. we find the inverse of cdf by newton-raphson.
using std::beta; using std::sqrt; using std::pow; using std::lgamma;
using std::exp; using std::log; using std::nextafter;

auto pdf = [](RealType x, RealType df) {
return pow(df / (df + x * x), (df + 1)/2) /
(sqrt(df) * beta(df/2, RealType(0.5)));
};

auto cdf = [](RealType x, RealType df) {
// ibeta, derived from https://codeplea.com/incomplete-beta-function-c
// original code is under zlib License,
// Copyright (c) 2016, 2017 Lewis Van Winkle
// i believe this snippet should be relicensed due to thorough rework.
auto ibeta = [](RealType a, RealType b, RealType x){
const RealType lbeta_ab = lgamma(a)+lgamma(b)-lgamma(a+b);
const RealType first = exp(log(x)*a+log(1-x)*b-lbeta_ab) / a;
RealType f = 2, c = 2, d = 1, step; int m = 0;
do {
RealType o = -((a+m)*(a+b+m)*x)/((a+2*m)*(a+2*m+1));
m++; RealType e = (m*(b-m)*x)/((a+2*m-1)*(a+2*m));
RealType dt = 1 + o/d; d = 1 + e/dt;
RealType ct = 1 + o/c; c = 1 + e/ct;
f *= (step = (ct+e)/(dt+e));
} while(step - 1.);
return first * (f - 1);
};
RealType root = sqrt(x*x+df);
return ibeta(df/2, df/2, (x + root)/(2*root));
};

cl = 1 - (1 - cl) / 2;
// solve the root of cdf(x) - cl = 0.
// first use newton-raphson, generally starting at 0 since cdf is
// monotonically increasing. disallow overshoot. when overshoot
// is detected, an inclusive interval is also determined, then
// switch to bisection for the accurate root.
RealType l = 0, r = 0, step;
while((step = (cdf(r, df) - cl) / pdf(r, df)) < 0) {
l = r; r -= step;
}
while (r > nextafter(l, r)) {
RealType m = (l + r) / 2;
(cdf(m, df) - cl < 0 ? l : r) = m;
}
return nextafter(l, r); // let cdf(return val) >= 0 always holds.
}
};

template<template<class> class Distribution = gaussian, class RealType = double>
struct estimator {

// std::tie(mean, error) = estimator::test(first, last, cl)
//
// perform error estimation over the range [`first`, `last`).
// the ground truth should be included in the interval
// [`mean-error`, `mean+error`] with a probability of `cl`.
template<class InputIt>
static std::tuple<RealType,RealType> test
(InputIt first, InputIt last, RealType cl = 0.95) {

// sampling has 1 less degrees of freedom.
auto n = distance(first, last);
auto df = n - 1;

// pairwise sum to reduce rounding error.
using std::distance; using std::next; using std::sqrt;
auto sum = [](InputIt first, InputIt last,
auto&& trans, auto&& sum)->RealType {
if (distance(first, last) == 1) {
return trans(*first);
} else {
auto mid = first + distance(first, last) / 2;
return sum(first, mid, trans, sum) + sum(mid, last, trans, sum);
}
};

auto trans_mean = [n](RealType x)->RealType {
return x/n;
};
auto mean = sum(first, last, trans_mean, sum);
auto trans_var = [mean, df](RealType x)->RealType {
return (x - mean) * (x - mean) / df;
};
auto var = sum(first, last, trans_var, sum);

// assuming samples are i.i.d., we estimate `mean` as
//
// samples[i] ~ N(mean, var)
// sum = Sum(samples)
// ~ N(n*mean, n*var)
// mean = sum/n
// ~ N(mean, var/n)
// the standard error of mean, namely `sigma`,
// sigma = sqrt(var/n).
//
// due to Levy-Lindeberg CLT, above estimation to `mean`
// approximately holds for any sequences of random variables when
// `m_iterations`->inf, as long as:
//
// * with finite variance
// * independently distributed

auto sigma = sqrt(var/n);

// finally, confidence interval is (+-) `critical_score*sigma`.
// here critical_score is dependent on the assumed distribution.
// we provide `gaussian` and `student` here.
//
// - `gaussian` works well for many (>30) samples due to CLT,
// but are over-optimistic when samples are few;
// - `student` is more conservative, depends on number of
// samples, but is much more computational expensive.

return { mean, Distribution<RealType>::critical_score(cl, df) * sigma };
}
};

using gaussian_estimator = estimator<gaussian>;
using student_estimator = estimator<student>;

};

// ====================================================================
// DEMO: experiment with RNG, try to obtain its expectation.
// ====================================================================
#include <random>
#include <vector>
#include <iostream>
#include <iomanip>

int main(int argc, char** argv) {

// the "truth" hides behind a normal distribution N(5,2).
// after this demo converged, we expect the mean very close to 5.
// we use student's t-distribution here, which works better for
// small sets.
// if you change the distribution to gaussian, you will likely
// see an increased chance to fail the challenge.
std::random_device rd;
std::mt19937 gen(rd());
std::normal_distribution<double> dist{5,2}; // <-- the "truth"

// demo configuration.
constexpr double confidence_level = 0.95; // 95% confidence to pass the challenge.
constexpr double error_tolerance = 0.10; // stop below +-10% error, ~[4.5, 5.5].

double mean, error;
size_t iteration = 5; // experiments per test.
std::vector<double> samples;

while (1) {
// draw 5 more samples then test.
std::cout << "Single Experiment Results: ";
for (size_t i = 0; i < iteration; i++) {
double sample = dist(gen); // <-- perform a single experiment.
samples.push_back(sample);
if (i < 10)
std::cout << sample << " ";
else if (i < 11)
std::cout << "...";
}
std::cout << std::endl;

// test the confidence interval with our estimator.
std::tie(mean, error) = errest::student_estimator::test(samples.begin(), samples.end(), confidence_level);
// or with structured binding declaration [c++17]:
// auto [mean, error] = errest::student_estimator::test(samples.begin(), samples.end(), confidence_level);

std::cout << " * Error Estimator Report: "
"collected " << samples.size() << " results, "
"mean: " << mean << ", error: " << error << std::endl
<< std::endl;

// stop if error is below 10%,
if (error < mean * error_tolerance) break;

}

std::cout << "demo converged with " << (confidence_level * 100) << " percent confidence "
"within " << (error_tolerance * 100) << " percent error range. "
"is the \"truth\" 5 within the interval "
"[" << mean-error << ", " << mean+error << "]?" << std::endl;
return 0;

}

CPULESS: 打破异构系统交互墙

由于现代深度学习技术的不断发展,深度学习任务具有控制逻辑复杂化、宿主-设备交互频繁化的发展趋势;而传统的以CPU为中心的加速器系统宿主-设备交互开销大,交互速度提升缓慢,最终形成“交互墙”问题:交互速度提升与设备计算速度提升之间形成剪刀差。根据阿姆达尔定律,这将严重限制加速器的应用。

针对这一问题,CPULESS加速器提出了融合流水线结构,将系统的控制中心移至深度学习处理器上来,省去独立的宿主CPU芯片;采用面向异常编程方式,能够将标量控制单元和向量运算单元之间的交互开销降至最低。

实验表明,在多种具有复杂控制逻辑的现代深度学习任务上,CPULESS系统相比传统以CPU为中心的GPU系统能够实现10.30倍性能提升,并节约92.99%的能耗。

论文发表在《IEEE Transactions on Computers》。[DOI] [PDF] [实验源码]

MNIST 网站计数器

为本站设计的点击计数器,就是页面底部的那个。 虽然我估计如今99%的人和MNIST打交道时都是使用Python的, 但本站承诺不使用Python,选择用C++实现了这个小功能。 整个做下来体验并不比Python复杂太多,料想能节约不少碳排放😁。

实现

下载MNIST测试集,用gzip解压。 因为LeCun说测试集的前5000个比较容易识别,所以程序里只使用了这5000个。 访问次数记录在名为fcounter.db的文件里, 每一位数字从测试集中随机抽选,组成PNG图片 (这里使用了Magick++来比较方便地生成PNG), 然后通过FastCGI接口返回给webserver。

代码
counter.cppview raw
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>
#include <fstream>
#include <cstring>
#include <random>
#include <map>
#include <Magick++.h>
#include <fcgio.h>

std::ifstream t10k_images("./t10k-images-idx3-ubyte", std::ios_base::binary | std::ios_base::in);
std::ifstream t10k_labels("./t10k-labels-idx1-ubyte", std::ios_base::binary | std::ios_base::in);
size_t count = 0;

enum {
IMAGE_IN = 16,
IMAGE_NUM = 5000,
IMAGE_SIZE = 28,
IMAGE_BYTE = IMAGE_SIZE * IMAGE_SIZE,
LABEL_IN = 8,
};

std::multimap<int, size_t> categories;
std::random_device rd;
std::mt19937 mt(rd());

void init() {
t10k_labels.seekg(LABEL_IN);
for (size_t i = 0; i < IMAGE_NUM; i++) {
unsigned char c;
t10k_labels.read(reinterpret_cast<char*>(&c), 1);
categories.insert({c, i * IMAGE_BYTE + IMAGE_IN});
}
std::ifstream fcounter("./fcounter.db", std::ios_base::binary | std::ios_base::in);
fcounter.read(reinterpret_cast<char*>(&count), sizeof(count));
fcounter.close();
}

void select(std::array<unsigned char, IMAGE_BYTE>& img, unsigned char c) {
auto range = categories.equal_range(c);
auto first = range.first; auto last = range.second;
auto n = std::distance(first, last);
std::uniform_int_distribution<> dist(0, n - 1);
auto sk = std::next(first, dist(mt))->second;
t10k_images.seekg(sk);
t10k_images.read(reinterpret_cast<char*>(img.data()), IMAGE_BYTE);
}

void hit(std::ostream& os) {
count++;
std::ofstream fcounter("./fcounter.db", std::ios_base::binary | std::ios_base::out);
fcounter.write(reinterpret_cast<char*>(&count), sizeof(count));
fcounter.close();
std::string str = std::to_string(count);
if (str.length() < 6)
str = std::string(6 - str.length(), '0') + str;
size_t w = IMAGE_SIZE * str.length(), h = IMAGE_SIZE;
std::vector<unsigned char> canvas(w*h, 0);
size_t i = 0;
for (auto&& c : str) {
std::array<unsigned char, IMAGE_BYTE> img;
select(img, c - '0');
for (int y = 0; y < IMAGE_SIZE; y++) {
std::memcpy(&canvas[y * w + i * IMAGE_SIZE], &img[y * IMAGE_SIZE], IMAGE_SIZE);
}
i++;
}
Magick::Image image(IMAGE_SIZE*str.length(), IMAGE_SIZE, "I", Magick::CharPixel, canvas.data());
Magick::Blob blob;
image.type(Magick::GrayscaleType);
image.magick("PNG");
image.write(&blob);
os << "Content-Type: image/png\r\n";
os << "Content-length: " << blob.length() << "\r\n\r\n";
os.write(reinterpret_cast<const char*>(blob.data()), blob.length()) << std::flush;
}

int main() {
FCGX_Request request;
init();
FCGX_Init();
FCGX_InitRequest(&request, 0, 0);
while (FCGX_Accept_r(&request) == 0) {
fcgi_streambuf osbuf(request.out);
std::ostream os(&osbuf);
hit(os);
}
return 0;
}

以上代码就贡献给Public Domain了。

编译

  • APT安装libmagick++-dev libfcgi-dev
  • 为了使用FastCGI++,需要添加编译选项-lfcgi++ -lfcgi
  • 一个简单的CMake就可以自动找到Magick++;不使用CMake的话,添加magick++-config提供的编译选项。

部署

我用spawn-fcgi来启动编译出来的二进制(在systemd里通过设置服务,自动启动)。 主流的webserver都支持FastCGI接口,设置一个FastCGI反向代理,指向spawn-fcgi启动的端口,部署配置就完成了。 我用的是Caddy:

1
2
3
reverse_proxy /counter.png localhost:21930 {
transport fastcgi
}

/counter.png加在页面底部HTML里,每次刷新页面都能观察到数字增加; 如果浏览器缓存了图片,在链接之间跳转不触发对服务器的访问,数字就不会增加。

Cambricon-Q:量化训练架构

为了追求高能效,深度学习加速器大多采用8比特乃至更低位宽的运算单元,尤其是在移动平台上。这样的低位宽加速器采用特殊技术手段可以满足推理任务的精度要求,但是在训练时就不行了,因为训练过程对数值精度的敏感性远远高于推理。怎样扩展架构才能让移动平台上的加速器支持高效的移动端训练呢?

针对这个问题,我们开展了Cambricon-Q的研究。

Cambricon-Q引入了三种新模块:

  • SQU支持数据传输过程中的沿途统计和量化
  • QBC管理片上缓存中的混合数据精度和格式;
  • NDPO在近存端完成权重更新。

该结构能够支持多种量化训练方法。实验表明,Cambricon-Q在几乎不损失训练精度的前提下,实现了高效的深度学习训练。

论文发表在ISCA 2021。[DOI] [PDF]

Cambricon-FR:分形可重配指令集结构计算机(通用分形计算机)

本文工作是 Cambricon-F:一种具有分形冯·诺伊曼体系结构的机器学习计算机 的延续。

Cambricon-F通过分形执行实现了编程与系统规模无关的重要性质,缓解了机器学习计算机的编程难题。 但是,该计算机上的分形执行是由硬件控制器操控实现的,只支持少数常用基本算子(卷积、池化等), 其他功能需要通过这些基本算子串行拼搭来实现。 我们发现,用有限、固定的指令集支持复杂多变的实际应用负载时,会产生失效现象,影响机器的计算效率。

在支持传统的卷积神经网络等规整的算法时,该机能够达到最优效率。 但在复杂多变的应用场景下,即使应用本身符合分形运算的定义,也会产生失效现象。 失效现象定义为某些应用执行在分形计算机上时,计算或通信复杂度发生了改变。 本文还以TopK和3DConv两种并不十分复杂的算子举例说明了失效现象。

举一个直观的例子: 用户希望执行应用“贝叶斯网络”,该应用是符合分形运算定义的,本能够以分形方式高效执行; 但由于Cambricon-F中并不存在专门的“贝叶斯网络”指令,该应用只能分解为一系列基本运算后串行执行。 如果能够扩展指令集,增加一条BAYES分形指令,就可以在达到叶子节点前一直保持分形执行,显著提升计算效率了。

据此,我们改进了Cambricon-F的架构,提出具有分形可重配指令集结构的Cambricon-FR。 理论地看,Cambricon-F是一台分形计算机,而Cambricon-FR则可以称为一台通用分形计算机; Cambricon-F能够在某种特定应用负载上实现高效计算,Cambricon-FR则能够在复杂多变的应用负载上实现高效计算。

论文发表于《IEEE Transactions on Computers》。[DOI] [PDF]

Cambricon-F:一种具有分形冯·诺伊曼体系结构的机器学习计算机

在寒武纪从事软件架构工作期间,我深切体会到了软件开发的痛处。2016年我刚刚接手的时候,核心软件是由我和王禹卿两人负责开发,代码规模1万5千行;在2018年我离开的时候,开发团队增加到60余人,代码规模72万行。从代码行数来看,软件的复杂度每5个月就翻一倍。无论增加多少人手,团队仍旧承担着巨大的开发压力:客户需求急迫,需要立刻应对;硬件规模在变化,冲击了现有软件设计方案,很可能需要针对新硬件重新设计;新功能需要实现,老代码需要重构,越积攒越多;文档还没建立;测试还没建立……

我可能称不上一个专业的软件架构师,但是又有谁能在一开始就保证预见了未来的变化?试想一下,底层硬件一开始是单核,一年后变为多核,又一年后变为NUMA多核。在这种变化速度和幅度之下,同一套软件不经过大规模重构,怎么可能能够保持适应?问题的关键在于硬件的规模增加了,因而需要编程控制的抽象层次也在增多,使编程变得复杂。我们将问题定义为编程-规模相关性

为了解决这个工程实践问题,我们开始了Cambricon-F的研究。

为了解决编程的规模相关性,需要引入一种规模不变量,我们找到的这种不变量是分形:分形的几何图形,在不同规模尺度上自相似。我们将应用负载以分形方式定义,将硬件结构也以分形方式定义,两个规模不变量均可自由缩放,直至找到互相适配的规模。

Cambricon-F首次提出分形冯·诺伊曼体系结构。该结构最大的特点是:

  • 串行编程,自动展开为适合硬件规模的并行执行;
  • 编程-规模无关性——程序中不体现硬件规模,可以在不同规模的Cambricon-F实例上迁移;
  • 通过分形流水线,保持了高效率。

论文发表在ISCA 2019。[DOI] [PDF]