HedgeHogLab 该何去何从: HHLAB技术及源码分析

总所周知HHLAB是立党开创的跨时代的在线数据处理及数据运算框架

HHLAB诞生的背景

我们都知道, 安装环境, 配置环境, 建立服务器, 打驱动是非常痛苦的一件事, 而且我们确实想要某一种 SaaS 来提供开箱即用的数据处理体验, 尤其是这些数据处理原员在大多数情况下的计算机水平并不比普通人强多少, 他们主要的优点在于对数据处理算法的了解和对数学的了解.

所以一批又一批的数据处理框架或编程语言被开发了出来.

目前市面上主流的有:

  • python + tensorflow/torch + pandas + numpy
  • julia
  • MATLAB/OCATAVE
  • Wolfram Mathematica
  • FORTRAN
  • R + 万物

HHLAB诞生的初衷就是想在网页上搭建一套数据处理平台, 我们只需要打开网页就可以进行全链路的数据处理(包括开发到出结果), 这个想法是非常好的.

通过一定的配置能达到HHLAB功能的软件

首先不得不承认HHLAB的竞争对手有很大一部分可以通过一点配置就达到HHLAB现在的效果:

  • 首先开发人员可以租赁一台云服务器, 包含GPU
  • 然后在云服务器上搭建Jupyter Server
  • 然后安装软件
  • 然后使用浏览器进行登录

上面的所有软件均可以达成这个效果, 但是我们HHLAB并不需要上面繁琐的配置过程, 及我们只管用就行了.

HHLAB真正的竞争对手

我们只需要掏钱, 甚至连掏钱都不用就可以享有的服务:

所以第一个是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那一套所以我们也不再赘述, 先说说这么做可能会带来什么问题:

  1. 当运算符左右的变量是nullundefined的时候需要做特殊处理
  2. 当运算符发生了错误编译报错不友好
  3. 需要立党老师对左右两边所有的类型进行手动枚举, 因为他没有借助派发机制

是人总会犯错, 所以他对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并不是过程语言所以并不好进行直接对比:

VocabularyMeaning
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(&#039;Dimension does not match for operation:dot muitiply&#039;);
  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;
}

逆矩阵

[Symbol.for(&#039;^&#039;)](rightOperand: number): Mat {
    if (this.rows !== this.cols) throw new Error(&#039;This matrix does not support ^ operator&#039;);
    //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(&#039;This right operand does not support ^ operator&#039;);

    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该何去何从?

可能写来装饰主页和给大学生留作业等需求HHLAB能够完美的解决.

所以HHLAB是一个非常优秀的框架, 我们期待在未来他能改变我们的科学计算方式, 使得随时随地都可以免费的运行科学计算代码.

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇