LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

HedgeHogLab 该何去何从: HHLAB技術及源碼分析

68457373.png

總所周知 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 應該面向的不是這部分用戶.

然後我們可以提一嘴編程語言的性能部分:

photo_2022-05-07_18-40-56.jpg

這一點上 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 做的編譯,也是大量的向量化.

立黨的態度是:

圖片 - 4.png

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 團隊進行的,其數據和我的經驗相符.

opengl-benchmark.png

所以我們看出 WebGL 的性能其實是遠遠低於 OpenGL 的.

目前還有一個實驗性的 API 叫 WebGPU, 它是更加底層的一个接口,長得非常像 Vulkan, 同時能夠提供更好的多核性能,下面是 WebGPU 的 benchmark:

1-q5xCQtlrqv7TjhBU7nOrxg.png

所以我們可以看出 WebGL 其實僅僅是從零到有的.

另外這套技術棧有一個嚴重的缺點,就是當 JS 代碼太複雜的時候有可能並不能翻譯為 GLSL, 所以複雜的 kernel 函數 GPU.js 並不能扔給 GPU 做運算,
而 GLSL 的代碼手寫起來可比 CUDA 痛苦多了.
所以這裡我很好奇立黨為什麼沒有選用已經搭載了 WebGPU 支持的 TVM 框架.

另外的問題還包括 Nvida 花了那麼大力氣放進去的 tensorcore 在這裡完全成為了擺設

Screenshot-2022-05-07-at-19-10-35-Nvidias-TENSOR-CORES.png

既然立黨的受眾群體可能不在乎精度,那麼這將是絕佳的方向幫助他優化框架.
但是使用 WebGL 的技術肯定不能獲得這方面的提升.

那麼我們說會到其他兩家平台是怎麼處理問題的?

Python: 集思廣益,多後端#

Python 可以使用 Tensorflow 和 Torch 進行 AI, numpy 進行數值運算.
但是 numpy 還是 CPU 的,所以現在有個新項目叫cupy, 這樣也就支持 GPU 運算了.

MMA: 官方支持#

MMA 使用了一個非常高階的語言,他直接運行在 CUDA 或 OpenCL 上,所以... 這就不用說了.

TargetDevice->"GPU"

好了,你上顯卡了

不過顯卡真的有你想像的那麼有用嗎?#

當遇到高精度需求的時候顯卡甚至可能比你的 CPU 算的慢

Screenshot-2022-05-07-at-19-07-19-MatmulNumaccCUDA.pdf.png

還有就是真實世界裡很多數值計算要麼就是帶寬需求小,邏輯複雜如各種狀態機,
要麼就是計算的精度比較複雜,甚至可能會遇到定義新的數字格式的問題,所以這種情況下顯卡不一定是有用還是添亂.

矩陣操作#

我們先觀摩一下 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 更能處理科學計算相關的依賴,也能夠配置環境隔離,這個我就不多贅述了,大家心裡都有數.

平台服務#

Screenshot-2022-05-07-at-19-48-19-Wolfram-Data-Drop-Universal-Data-Accumulator.png

不知道立黨這裡該怎麼卷...

HHLAB 該何去何從?#

 GitHub

bboczeng/why-you-do-not-need-hedgehog-lab

可能寫來裝飾首頁和給大學生留作業等需求 HHLAB 能夠完美的解決.

所以 HHLAB 是一個非常優秀的框架,我們期待在未來他能改變我們的科學計算方式,使得隨時隨地都可以免費的運行科學計算代碼.

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。