llama.cpp 開始支援 GPU 了

前陣子因為重灌桌機,所以在重建許多環境... 其中一個就是 llama.cpp,連到專案頁面上時意外發現這兩個新的 feature:

OpenBLAS support
cuBLAS and CLBlast support

這代表可以用 GPU 加速了,所以就照著說明試著編一個版本測試。

編好後就跑了 7B 的 model,看起來快不少,然後改跑 13B 的 model,也可以把完整 40 個 layer 都丟進 3060 (12GB 版本) 的 GPU 上:

./main -m models/13B/ggml-model-q4_0.bin -p "Building a website can be done in 10 simple steps:" -n 512 -ngl 40

從 log 可以看到 40 layers 到都 GPU 上面,吃了 7.5GB 左右:

llama.cpp: loading model from models/13B/ggml-model-q4_0.bin
llama_model_load_internal: format     = ggjt v2 (latest)
llama_model_load_internal: n_vocab    = 32000
llama_model_load_internal: n_ctx      = 512
llama_model_load_internal: n_embd     = 5120
llama_model_load_internal: n_mult     = 256
llama_model_load_internal: n_head     = 40
llama_model_load_internal: n_layer    = 40
llama_model_load_internal: n_rot      = 128
llama_model_load_internal: ftype      = 2 (mostly Q4_0)
llama_model_load_internal: n_ff       = 13824
llama_model_load_internal: n_parts    = 1
llama_model_load_internal: model size = 13B
llama_model_load_internal: ggml ctx size =  90.75 KB
llama_model_load_internal: mem required  = 9807.48 MB (+ 1608.00 MB per state)
llama_model_load_internal: [cublas] offloading 40 layers to GPU
llama_model_load_internal: [cublas] total VRAM used: 7562 MB
llama_init_from_file: kv self size  =  400.00 MB

30B 的 model 我也試著丟上去跑,但只能丟 28 layers 上去 (全部是 60 layers),再多 GPU 的記憶體就撐不住了。

但能用 GPU 算是一個很大的進展,現在這版只快了一半的時間,不知道後面還有沒有 tune 的空間...

Ruff:用 Rust 寫的 Python Linter

Hacker News Daily 上看到「Astral (astral.sh)」這個,網站在「Astral: Next-gen Python tooling」。

裡面提到的 Ruff 專案是一套用 Rust 寫的 Python Linter,主打就是速度,從官網提供的 benchmark 就可以看出來差距:

因為是 Python ecosystem 的東西,安裝可以直接用 pip 裝預設編好的套件,而不需要透過 cargo 自己編 (當然你想要還是可以用 cagro 編)。

feedgen 測了一下,速度是真的快,這樣就比較不會嫌棄了... 要注意會冒出 .ruff_cache/ 目錄,.gitignore 要加一下。

然後用預設值先掃出 unused import 修掉,其他的有機會再看要怎麼改。

llama.cpp 的載入速度加速

Hacker News 上看到「Llama.cpp 30B runs with only 6GB of RAM now (github.com/ggerganov)」這個消息,原 pull request 在「Make loading weights 10-100x faster #613」這邊。

這個 PR 的作者 Justine Tunney 在 PR 上有提到他改變 model 檔案格式,以便改用 mmap(),大幅降低了需要預先讀取的時間 (因為變成 lazy-loading style),而且這也讓系統可以利用 cache page,避免了 double buffering 的問題:

This was accomplished by changing the file format so we can mmap() weights directly into memory without having to read() or copy them thereby ensuring the kernel can make its file cache pages directly accessible to our inference processes; and secondly, that the file cache pages are much less likely to get evicted (which would force loads to hit disk) because they're no longer competing with memory pages that were needlessly created by gigabytes of standard i/o.

這讓我想到在資料庫領域中,PostgreSQL 也會用 mmap() 操作,有點類似的概念。

另外 Justine Tunney 在這邊的 comment 有提到一個意外觀察到的現象,他發現實際在計算的時候用到的 model 內容意外的少:他用一個簡單的 prompt 測試,發現 20GB 的 30B model 檔案在他的 Intel 機器上實際只用到了 1.6GB 左右:

If I run 30B on my Intel machine:

[...]

As we can see, 400k page faults happen, which means only 1.6 gigabytes ((411522 * 4096) / (1024 * 1024)) of the 20 gigabyte weights file actually needed to be used.

這點他還在懷疑是不是他的修改有 bug,但目前他覺得不太像,也看不太出來:

Now, since my change is so new, it's possible my theory is wrong and this is just a bug. I don't actually understand the inner workings of LLaMA 30B well enough to know why it's sparse. Maybe we made some kind of rare mistake where llama.cpp is somehow evaluating 30B as though it were the 7B model. Anything's possible, however I don't think it's likely. I was pretty careful in writing this change, to compare the deterministic output of the LLaMA model, before and after the Git commit occurred. I haven't however actually found the time to reconcile the output of LLaMA C++ with something like PyTorch. It'd be great if someone could help with that, and possibly help us know why, from more a data science (rather than systems engineering perspective) why 30B is sparse.

如果不是 bug 的話,這其實冒出了一個很有趣的訊號,表示這些 model 是有可能再瘦身的?

Ruby 再引入另外一套 JIT 實做:RJIT

Hacker News Daily 上看到「RJIT #7448」這個,Ruby 上一套新的 JIT 實做。

這次的 RJIT 取代掉先前的 MJIT:

This PR replaces the current implementation of MJIT with a new JIT called "RJIT"

有些特點,其中一個是 RJIT 在 buildtime 與 runtime 都不需要 compiler,這是因為 RJIT 直接用 Ruby 實做:

RJIT uses a pure-Ruby assembler to generate native code

  • MJIT requires a C compiler at runtime. YJIT requires a Rust compiler at build time. RJIT doesn't require them.
  • This means that RJIT's warmup could be slower than YJIT, but it's still much faster than MJIT's.

另外值得注意的是,RJIT 的作者 k0kubunYJIT 的作者 Maxime Chevalier-Boisvert 都是 Shopify 的員工,可以看出 Shopify 對於 Ruby 效能的痛?決定直接自己養人改善效能。

回到 RJIT 這邊跑的測試,可以看到他是用 YJIT 的測試套件測,這也就不會太奇怪了。

跟這次取代掉的 MJIT 相比,RJIT 在 Headline 這包測試都 OK,在 Other 這包則是有來有回,而在 Micro 這包則是有不少項目輸掉 (相比於前兩者):

這樣整體看起來算是有進步,下一版 Ruby 更新應該就會有了。

由 pnpm 做出來的效能比較:npm、yarn 以及 pnpm

找資料的時候看到 pnpm 有在做幾個常見的 javascript package manager 的比較:「Benchmarks of JavaScript Package Managers」。

裡面測了九個項目,其中前八個剛好就是 cache 的有無、lockfile 的有無以及 node_modules 的有無,這三者的組合剛好八個,最後一個是測 update 的速度。

pnpm 因為用了 hard link,我先跳過去,只先關注 npmyarn 的差異。

其中幾個比較重要的項目是 (由最重要開始列):

  • with cache, with lockfile, with node_modules:這個會是一般開發的情境。
  • update:套件有更新時的時間。
  • with lockfile:這個會是第一次 clone 下來後的環境,以及 CI 的環境。

在一般開發環境下,npm 比 yarn 快一些,這點讓我比較意外,我知道 npm 這幾年一直有在改善效率,但沒想到會在 benchmark 下反超,這個是以前 yarn 很大的宣傳點,現在已經不見了。

第二個是 update (upgrade) 的部份,這邊 yarn 比 npm 快一些。

第三個因為是 CI 環境,或是第一次 clone 時的環境,慢通常可以被接受,不要差太多就好,而這邊 npm 比 yarn 快一些。

但不管是哪個,差距都不像以前那麼大了,就效率上來說,官方的 npm 已經沒有太明顯的缺點,因為效率的差異而去選擇 yarn 的理由已經不存在了。

Content Defined Chunking (CDC)

前幾個禮拜在 Hacker News Daily 上看到「CDC File Transfer (github.com/google)」這則,連結是指到 GoogleGitHub 專案上,裡面實做了 FastCDC 演算法,另外說明他們為什麼要解這個問題以及對應的成果:「google/cdc-file-transfer」。

Google 的人看起來像是是在 CI/CD 階段遇到頻寬上的問題 (從「The builds are 40-45 GB large.」這邊猜),用 scprsync 看起來都不能解,所以他們自己刻了 FastCDC 演算法來解。

但我對 Content Defined Chunking (CDC) 不熟,所以先查一下 CDC 是什麼東西,就查到 restic 這篇講得很清楚:「Foundation - Introducing Content Defined Chunking (CDC)」。

要計算 delta 很直覺的作法就是要切 chunk,而接著的直覺就是固定大小的 chunk 切開,像是這樣每 16 bytes 切一個 chunk:

0123456789abcdef 0123456789abcdef 0123456789abcdef 0123456789abcdef

如果其中一個地方有變化,但其他沒變化的話就可以透過 cryptographic hash function (像是 SHA-256) 確認 chunk 內容一樣,進而省下很多傳輸的頻寬:

0123456789abcdef 0123456789ABCDEF 0123456789abcdef 0123456789abcdef

但可以馬上看出來這個方法的大缺點是只能處理 replacement,很難處理 insert & delete 的部份,舉例來說,如果變更是在開頭的地方加上 ABC,就會造成 chunk 會完全不一樣,而導致全部都要再傳一次:

ABC0123456789abc def0123456789abc def0123456789abc def0123456789abc def

這邊其實是個經典的演算法問題:想要找出兩個 string 的差異 (把舊的內容當作一個 string,新的內容也當作一個 string)。

這個問題算是 Edit distance 類型的題目,但你會發現解 Edit distance 的演算法會需要先傳輸完整個 string 才能開始跑演算法,這就本末倒置了。

而另外一個想法是,放棄固定的 chunk 大小,改用其他方式決定 chunk 的邊界要切在哪裡。而 CDC 就是利用一段 sliding window + hash 來找出切割的點。

文章裡面提到的 sliding window 是 64 bytes,這邊就可以算出對應的 HASH(b0, b1, ..., b63),然後往右滑動變成 HASH(b1, b2, ..., b64),再來是 HASH(b2, b3, ..., b65),一直往右滑動計算。

接下來 restic 會看 hash 值,如果最低的 21 bits 都是 0 就切開,所以 chunk 大小的期望值應該是 2MB?(這邊不確定,好像不能直接用 2^21 算,應該用積分之類的方法...)

For each fingerprint, restic then tests if the lowest 21 bits are zero. If this is the case, restic found a new chunk boundary.

而這個演算法可以適應新增與刪除的操作,不會造成從新增或刪除後的資料都要重傳,只有自己這個 chunk 需要重傳 (可能前或後的 chunk 也會要)。

然後挑一下 hash function 的特性,就可以讓計算的速度也很快。這邊提到了 hash function 可以用 Rolling hash,可以很快的從 HASH(b0, b1, ..., b63) 算出 HASH(b1, b2, ..., b64),而不需要全部重算。

有了 chunk 後,再用 cryptographic hash function 比較 chunk 的內容是否一樣,這樣就可以大幅降低傳輸所需要的頻寬了。

AWS 推出加速 Lambda 啟動速度的 Lambda SnapStart

今年 AWSre:Invent 又開始了,這一個禮拜會冒出蠻多新功能的,挑自己覺得比較有興趣得來寫。

AWS 針對 Lambda 推出 Lambda SnapStart,改善冷啟動的速度:「New – Accelerate Your Lambda Functions with Lambda SnapStart」。

他拿了一個比較明顯的例子,JavaSpring Boot,範例在「Serverless Spring Boot 2 example」這邊,冷啟動的速度可以從 6 秒降到 200ms:

SnapStart has reduced the cold start duration from over 6 seconds to less than 200 ms.

方法就是把 initialization 的程式完成後的記憶體打一份 snapshot 存起來,之後的冷啟動第一動變成是 restore 而非再 initialize:

With SnapStart, the initialization phase (represented by the Init duration that I showed you earlier) happens when I publish a new version of the function. When I invoke a function that has SnapStart enabled, Lambda restores the snapshot (represented by the Restore duration) before invoking the function handler. As a result, the total cold invoke with SnapStart is now Restore duration + Duration.

不過不是所有的應用程式都可以直接套用,有些要注意的地方,比較好理解的是連線 (像是對後端資料庫的預連線) 以及暫存檔的部份 (像是預先算好某些資料後寫到暫存檔) 都需要重新建立。

比較特別的是亂數產生器需要重新 initialize,不然會有機率產生出一樣的 random data,這個是一般開發者會忽略掉的:

When using SnapStart, any unique content that used to be generated during the initialization must now be generated after initialization in order to maintain uniqueness.

所以 AWS 有針對 SnapStart 下的 OpenSSL 修正,另外外他們也確認過 Java 的 java.security.SecureRandom 本身就沒問題:

We have updated OpenSSL’s RAND_Bytes to ensure randomness when used in conjunction with SnapStart, and we have verified that java.security.SecureRandom is already snap-resilient.

另外 AWS 也推薦可以直接讀系統的 /dev/random 或是 /dev/urandom,這樣就很自然的不會因為 snapshot 而固定,當然也就沒問題:

Amazon Linux’s /dev/random and /dev/urandom are also snap-resilient.

這個功能說不用另外收費,看起來對 Java 族群還不錯?

Perl 在這十年的效能改善

看到「Perl performance evolution over the last decade」這篇,作者看到大家在講 Python 3.11 的效能改善後,也想看一下 Perl 這邊的效能改善。

作者從 2012 年的 Perl 5.12 開始測,到 2022 年出的 Perl 5.36,最後面的表可以看到大概是 30%:

以十年的跨度來看,30% 成長不怎麼多,但畢竟還是有效能提昇,用很舊的版本的人還是可以考慮昇個級,沒撞到相容性問題的話算是免費的效能提昇。

當年比較驚人的應該還是 PHP 的 5.6 到 7.0,效能差不多是翻倍,讓整個 PHP community 有更多的資源可以用,framework 可以寫的更揮霍一些 XD

AOM-AV1 v3.5.0 的效能改善

在翻資料的時候翻到「Google Releases AOM-AV1 3.5 With More Speedups & Memory Optimizations」這篇,講 AOM-AV1 v3.5.0 的效能改善,看了 v3.4.0 的 changelog 可以看到最主要的大幫助是有 multithreading 的支援,但不確定跟其他的 AV1 library 相比可以有多快...

印象中 (好幾個月前的印象) 目前最快的應該是 SVT-AV1?記得 dav1d 有宣稱很快但不是 apple-to-apple 比較,比較像是 marketing 上的宣稱。

先前測試 AV1 在 Android 上的支援度與效能,記得在老一點的手機上軟解還是很辛苦啊,最後還是先選了 VP9 用,硬體解碼支援度比較廣,加上軟解需要的 CPU resource 也低不少。

Pyston 改變方向,將主推模組載入的方式使用

Pyston 專案是一個想要提供更快速的 Python,而前陣子決定改變開發的方向:「Announcing 3.7-3.10 support and a new direction」。

本來的 Pyston-full 是直接修改 CPython 的 codebase 加速:

Our original product, which we’re retroactively calling Pyston-full, is a fork of the entire CPython codebase. Having users install a fully-custom version of Python lets us make changes across the Python implementation, leading to the most optimizations and largest speedups.

但這種方式的安裝與維護都需要另外搞,而且因為 ABI 不相容的問題,遇到一些套件可能會需要自己編 (甚至自己改?),不能直接用編好的 binary:

The flip side is that it is fairly intensive to set up. While we believe Pyston-full is one of the most highly-compatible alternative Python implementations available, it can be difficult to switch Python implementations regardless of the ease of use of either implementation. Compounded on this, we decided to break the ABI which requires users to recompile extension modules. In theory this is not a big deal, but in practice the lack of available binary packages is a significant disincentive to use an alternative implementation.

這樣雖然有 30% 的效能提昇,但對使用者的吸引力不高,所以打算要轉變方向,讓使用者更容易使用,這也是決定發展可以用 pip 安裝的 Pyston-lite 版本:

The sum of all of this was that while we were very happy to achieve a 30% speedup with Pyston-full, it was very difficult to get people to start using it. We decided to try a different form factor: a pip-installable extension module called Pyston-lite.

但效能的提昇就不像 Pyston-full 這麼高,Pyston-lite 只剩下 10% 了:

So while it’s a bit difficult to accept that we are now providing a 10% speedup instead of 30%, we’ve decided that it’s much more important to provide something that people are willing to use.

另外在文末有列出各版本的效能提昇 (與 CPython 3.8 比較),可以看到 CPython 3.11rc2 的提昇其實跟 Pyston-lite 差不多,除非 Pyston-lite 可以把效能疊加上去,不然就有點尷尬了:

但 Pyston 要支援 3.11 看起來會花不少功夫:

In the longer-term future we are planning to submit our JIT upstream as well, but we expect retargeting it to 3.11 to be significantly more work than the other versions due to the extensive amount of changes that were made to the interpreter in that version.

不過手上一些既有的東西好像可以掛上去測看看...