总所周知 HHLAB 是立党开创的跨时代的在线数据处理及数据运算框架#
HHLAB 诞生的背景#
我们都知道,安装环境,配置环境,建立服务器,打驱动是非常痛苦的一件事,而且我们确实想要某一种 SaaS 来提供开箱即用的数据处理体验,尤其是这些数据处理原员在大多数情况下的计算机水平并不比普通人强多少,他们主要的优点在于对数据处理算法的了解和对数学的了解.
所以一批又一批的数据处理框架或编程语言被开发了出来.
目前市面上主流的有:
- python + tensorflow/torch + pandas + numpy
- julia
- MATLAB/OCATAVE
- Wolfram Mathematica
- FORTRAN
- R + 万物
HHLAB 诞生的初衷就是想在网页上搭建一套数据处理平台,我们只需要打开网页就可以进行全链路的数据处理 (包括开发到出结果), 这个想法是非常好的.
通过一定的配置能达到 HHLAB 功能的软件#
首先不得不承认 HHLAB 的竞争对手有很大一部分可以通过一点配置就达到 HHLAB 现在的效果:
- 首先开发人员可以租赁一台云服务器,包含 GPU
- 然后在云服务器上搭建 Jupyter Server
- 然后安装软件
- 然后使用浏览器进行登录
上面的所有软件均可以达成这个效果,但是我们 HHLAB 并不需要上面繁琐的配置过程,及我们只管用就行了.
HHLAB 真正的竞争对手#
我们只需要掏钱,甚至连掏钱都不用就可以享有的服务:
- Google Codelab (python)
- MMA cloud
所以第一个是 Python 生态系的,第二个是 MMA 官方的,那么我就就对比一下这些产品和 HHLAB 所鼓吹的和所实现的功能有什么区别.
对于用户的易用性#
编程语言#
Python 和 JS 都是简单易学的语言,但是 Python 的灵活性比 JS 要强,Python 可以通过元编程和装饰器还有各种对对象的操作进行 DSL 级别的开发,使用体验也堪比原生编程语言的味道.
import numpy as np
A = np.array([1.,2.,3.,4.,5.])
B = np.ones(5)
print(A + B)
// import math.js
const A = [1,2,3,4,5]
const B = math.ones(1,5)
console.log(math.add(A,B))
从这个例子可以看出 JS 在语言层面的灵活度并不可以和 Python 相抗衡
但是我们的 HHLAB 却可以做到类似 python 的行为
立党老师通过写了一个 Babel 插件的形式去添加了一个仅仅针对数值运算的运算符重载 (我更愿意称之为内置运算符)
// https://github.com/Hedgehog-Computing/hedgehog-lab/blob/dev/packages/hedgehog-core/src/transpiler/operator-overload.ts
import template from 'babel-template';
import * as types from '@babel/types';
function invokedTemplate(op: any) {
return template(`
(function (LEFT_ARG, RIGHT_ARG) {
if (LEFT_ARG !== null && LEFT_ARG !== undefined
&& LEFT_ARG[Symbol.for("${op}")])
return LEFT_ARG[Symbol.for("${op}")](RIGHT_ARG);
else if (RIGHT_ARG instanceof Sym)
return (sym(LEFT_ARG)[Symbol.for("${op}")](RIGHT_ARG));
else if (Array.isArray(LEFT_ARG) && (RIGHT_ARG instanceof Mat))
return (mat(LEFT_ARG)[Symbol.for("${op}")](RIGHT_ARG));
else if (Array.isArray(LEFT_ARG) && (Array.isArray(RIGHT_ARG)))
return (mat(LEFT_ARG)[Symbol.for("${op}")](mat(RIGHT_ARG)));
else if ( (!isNaN(LEFT_ARG)) && (RIGHT_ARG instanceof Mat))
return (scalar(LEFT_ARG)[Symbol.for("${op}")](RIGHT_ARG));
else if ( (!isNaN(LEFT_ARG)) && (Array.isArray(RIGHT_ARG)))
return (scalar(LEFT_ARG)[Symbol.for("${op}")](mat(RIGHT_ARG)));
else if ( Array.isArray(LEFT_ARG) && (!isNaN(RIGHT_ARG)) )
return (mat(LEFT_ARG)[Symbol.for("${op}")]((RIGHT_ARG)));
else
return LEFT_ARG ${op} RIGHT_ARG;
})
`);
}
Python 对于符号重载的实现是 OOP 那一套所以我们也不再赘述,先说说这么做可能会带来什么问题:
- 当运算符左右的变量是
null
和undefined
的时候需要做特殊处理 - 当运算符发生了错误编译报错不友好
- 需要立党老师对左右两边所有的类型进行手动枚举,因为他没有借助派发机制
是人总会犯错,所以他对 JS 的运算符补上的这一个补丁的牢靠程度还是值得怀疑的.
我们在说回 MMA, MMA 这边内置了一个符号计算引擎,所以可以很轻而易举的处理中缀表达式并写语法糖:
g /: f[g[x_]] := fg[x]
(*当你输入*)
{f[g[2]], f[h[2]]}
(*你会得到*)
{fg[2], f[h[2]]}
这显然比 Python 和 JS 都要高明不少,符号计算引擎能够处理很多非数值的东西,当然我们 HHLAB 应该面向的不是这部分用户.
然后我们可以提一嘴编程语言的性能部分:
这一点上 Julia 毋庸置疑夺得王座,因为他不俗的 JIT 设计还有非常还用的 CUDA 互通性,
但是 Julia 并不是 HHLAB 的竞争对手所以我们并不讨论他.
我们可以看到就算是 Scipy 这种 Python 里优化了多年的工具性能还是不能和商业的 MATLAB 和 MMA 去进行抗衡的,
所以我不相信并不使用 CUDA 进行异构加速的 HHLAB 有上榜的必要性.
调试#
Python 加上 Jupyter 可以做到行级别的运行,遇到实在无法解决的错误可以通过打断点的方式解决.
MMA 提供了一下几种方式来调试,当然因为 MMA 并不是过程语言所以并不好进行直接对比:
Vocabulary | Meaning |
---|---|
With[{x=value},expr] | compute expr with x replaced by value |
Echo[expr] | display and return the value of expr |
Monitor[expr,obj] | continually display obj during a computation |
Sow[expr] | sow the value of expr for subsequent reaping |
Reap[expr] | collect values sowed while expr is being computed |
直到目前为止我还没有看到 HHLAB 做出调试工具,甚至不能分行运行.
对于用户的功能#
CPU 向量化#
numpy 使用了大量的 SIMD 代码使得用户的数据在 CPU 可以充分的向量化,
MMA 我就不说了... 第一是闭源的,第二人家是高阶语言,根据 CPU feature 做的编译,也是大量的向量化.
立党的态度是:
GPU 计算#
HHLAB 使用 GPU 作为首要的开发后端之一,这也是他最引以为傲的点.
首先,GPU 能够加速的东西是非常稠密的,大量的数值运算且包含少量跳转的,而 CPU 擅长的是偏逻辑的运算.
他是通过 GPU.js 去做的这个功能,这个库代码很多就不能列举,但我可以把描述放在下面
https://github.com/gpujs/gpu.js/wiki/Quick-Concepts
1. transpiling javascript for use on the GPU
1. read javascript to a common format, in this case a mozilla abstract syntax tree
2. type inference from any value or derivation of any value from the parsed javascript
3. translate from the mozilla abstract syntax tree to a string value of a language understood by the GPU, generally GLSL (a C++ subset), but likely more to come
4. adding required utility functions and environment corrections to said translated string
5. compiling the entire translated string, now likely in a subset of C++
2. uploading values (arguments or constants) needed to calculate the result of a kernel
3. calculating said kernel output
4. downloading value from kernel output (this step can be skipped by using the kernel setting pipeline: true)
- this is generally regarded as the most time consuming part of calculating values from a GPU
- If you find yourself here, please ask yourself:
1. "Are the values I need, really needed?"
2. "Can I offset the values I need, to the GPU, and or return less often the values I think I need from the GPU?
所以 GPU.js 是通过 WebGL 的 compute shader 的 compute shader 去执行这个过程的:
- When WebGL2 is available, it is used
- When WebGL2 is not available WebGL1 will be used
- When WebGL1 is not available CPU will be used
我对图形学的 API 有过研究,WebGL 的 compute shader 是基于 OpenGLES1.0-3.0 进行实作的,所以相较于主流的 OpenCL 和 CUDA 有大量的功能缺失 (毕竟这是拿来画图的),
其次就是浏览器对底层 API 的封装以及对象传递到 JS 后进行转码等操作带来了更大的性能损耗.
下面是一些 benchmark 数据,希望能够帮助到你们了解 WebGL 的计算性能.
这组 benchmark 是 TVM 团队进行的,其数据和我的经验相符.
所以我们看出 WebGL 的性能其实是远远低于 OpenGL 的.
目前还有一个实验性的 API 叫 WebGPU, 它是更加底层的一个接口,长得非常像 Vulkan, 同时能够提供更好的多核性能,下面是 WebGPU 的 benchmark:
所以我们可以看出 WebGL 其实仅仅是从零到有的.
另外这套技术栈有一个严重的缺点,就是当 JS 代码太复杂的时候有可能并不能翻译为 GLSL, 所以复杂的 kernel 函数 GPU.js 并不能扔给 GPU 做运算,
而 GLSL 的代码手写起来可比 CUDA 痛苦多了.
所以这里我很好奇立党为什么没有选用已经搭载了 WebGPU 支持的 TVM 框架.
另外的问题还包括 Nvida 花了那么大力气放进去的 tensorcore 在这里完全成为了摆设
既然立党的受众群体可能不在乎精度,那么这个将是绝佳的方向帮助他优化框架.
但是使用 WebGL 的技术肯定不能获得这方面的提升.
那么我们说会到其他两家平台是怎么处理问题的?
Python: 集思广益,多后端#
Python 可以使用 Tensorflow 和 Torch 进行 AI, numpy 进行数值运算.
但是 numpy 还是 CPU 的,所以现在有个新项目叫cupy, 这样也就支持 GPU 运算了.
MMA: 官方支持#
MMA 使用了一个非常高阶的语言,他直接运行在 CUDA 或 OpenCL 上,所以... 这就不用说了.
TargetDevice->"GPU"
好了,你上显卡了
不过显卡真的有你想象的那么有用吗?#
当遇到高精度需求的时候显卡甚至可能比你的 CPU 算的慢
还有就是真实世界里很多数值计算要么就是带宽需求小,逻辑复杂如各种状态机,
要么就是计算的精度比较复杂,甚至可能会遇到定义新的数字格式的问题,所以这种情况下显卡不一定是有用还是添乱.
矩阵操作#
我们先观摩一下 HHLAB 的矩阵算法的代码
这是下面这些源码的地址#
叉乘
function multiply(leftMat: Mat, rightMat: Mat): Mat {
if (leftMat.cols !== rightMat.rows)
throw new Error('Dimension does not match for operation:muitiply');
if (leftMat.mode === 'gpu' || rightMat.mode === 'gpu') return multiply_gpu(leftMat, rightMat);
const m = leftMat.rows,
n = leftMat.cols,
p = rightMat.cols;
const returnMatrix = new Mat().zeros(m, p);
for (let i = 0; i < m; i++) {
for (let j = 0; j < p; j++) {
let val = 0;
for (let it = 0; it < n; it++) val += leftMat.val[i][it] * rightMat.val[it][j];
returnMatrix.val[i][j] = val;
}
}
return returnMatrix;
}
点乘
function dotMultiplyInPlace(leftMat: Mat, rightMat: Mat): Mat {
if (leftMat.rows !== rightMat.rows || leftMat.cols !== rightMat.cols)
throw new Error('Dimension does not match for operation:dot muitiply');
for (let i = 0; i < leftMat.rows; i++) {
for (let j = 0; j < leftMat.cols; j++) {
leftMat.val[i][j] *= rightMat.val[i][j];
}
}
return leftMat;
}
逆矩阵
function (rightOperand: number): Mat {
if (this.rows !== this.cols) throw new Error('This matrix does not support ^ operator');
//if right operand is -1, return the inverse matrix
if (rightOperand === -1) {
// matrix inverse with mathjs
return new Mat(mathjs.inv(this.val));
}
if (!Number.isInteger(rightOperand) || rightOperand < 1)
throw new Error('This right operand does not support ^ operator');
const returnMatrix = this.clone();
for (let i = 2; i <= rightOperand; i++) {
multiplyInPlace(returnMatrix, this);
}
return returnMatrix;
}
emmm 甚至没有 eigenvalue... 算了,也就这样吧,这些代码非常的 leetcode 风味.
至少你可以把它 / 2/4 之类的啊,哦对不起,JS 没有多核,我的问题.
我们知道实际的矩阵运算有两种不同的情况,
稠密运算的加速就是核心越多带宽越高计算越快,就这么暴力,
但是问题来到了稀疏矩阵 (也就是立党没有预料到的场景,也是数值计算在进行正则化和过滤噪声后最常遇到的场景)
光 layout 就可能会存在以下几种形式:
- COO: Coordinate (这个就是记录位置,当然用的已经不多了,太老了)
- CSR: Compressed Sparse Row (这个就是行比较分散的时候把行进行压缩)
- CSC: Compressed Sparse Column (同,不过是列)
- BCSR: Blocked Compressed Sparse Row (对块进行压缩,相信学过线代的都应该立马能反映过来这是个什么算法)
然后现在的主流的科学计算库都会提供 Auto-tuning 机制来先进性 profile 再进行运算.
所以这个可能是立党的知识盲区吧.
对于库开发者的易用性#
如果一个工具再先进不能解释给开发者,开发者没有办法进行产出构造生态也是不行的.
包管理机制#
立党想要通过复制链接,开发者建 branch 的方式完成包管理这有什么问题呢?
假设我们有个包 A, 有个包 B, 有个包 C
B 依赖 A,C 依赖 B
B 发现 A 炸了,B 需要 frok 一份 A 然后写 patch 然后加进自己所有用到 A 的地方,
C 发现 B 发现 A 炸了,但是 C 不知道 B 已经修了,所以 Cfork 了一份 B 又 fork 了一份 A...
所以我们现在知道包管理的重要性了,相信立党也能认识到这一点.
Python 的包管理有两套:
- pip
- anaconda
这两套都很好用,pip 包更多一点,conda 更能处理科学计算相关的依赖,也能够配置环境隔离,这个我就不多赘述了,大家心里都有数.
平台服务#
不知道立党这里该怎么卷...
HHLAB 该何去何从?#
bboczeng/why-you-do-not-need-hedgehog-lab
可能写来装饰主页和给大学生留作业等需求 HHLAB 能够完美的解决.
所以 HHLAB 是一个非常优秀的框架,我们期待在未来他能改变我们的科学计算方式,使得随时随地都可以免费的运行科学计算代码.