天天看點

用 TornadoVM 讓 Java 性能更上一個台階

作者 | Juan Fumero

譯者 | 明知山

策劃 | 丁曉昀

在 QCon Plus 大會上,Juan Fumero 談到了 TornadoVM,一種 Java 虛拟機(JVM)高性能計算平台。Java 開發人員可以通過它在 GPU、FPGA 或多核 CPU 上自動運作程式。

像 GPU 這樣的異構裝置幾乎出現在現今的所有計算系統中。例如,移動裝置配備了一個多核 CPU 和一個內建 GPU;筆記本電腦通常有兩個 GPU,一個與主 CPU 內建,另一個有專門用途(通常用于遊戲)。甚至連資料中心也在內建像 FPGA 這樣的裝置。是以,異構裝置将會繼續存在。

所有這些裝置都有助于提升性能和運作更有效的工作負載。目前和未來計算系統的程式員需要在各種各樣的計算裝置上處理程式執行。但是,很多并行程式設計架構都是基于 C 和 C++,使用進階程式設計語言(如 Java)開發的這類系統幾乎是不存在的。這就是為什麼我們要推出 TornadoVM。

簡單地說,TornadoVM 是一個針對 Java 和 JVM 的高性能計算程式設計平台,可以在運作時将 Java 代碼加載到異構硬體加速器上運作。

TornadoVM 提供了一個 Parallel Loop API 和一個 Parallel Kernel API。在這篇文章中,我們将分别介紹它們,并提供一些性能測試基準,還将分享 TornadoVM 如何将 Java 代碼轉譯成可在并行硬體上執行的機器碼。最後,我們将介紹 TornadoVM 在行業中的應用情況,包括一些應用場景。

1

GPU 和 FPGA 的快速通道

現如今,進階程式設計語言是如何通路異構硬體的?下圖展示了一些硬體(CPU、GPU、FPGA)和進階程式設計語言(如 Java、R 語言或 Python)的例子。

用 TornadoVM 讓 Java 性能更上一個台階

看一下 Java,我們會發現它是在虛拟機中運作的。其中,OpenJDK、GraalVM 和 Corretto 都是虛拟機(VM)實作。本質上,Java 源代碼被編譯成 Java 位元組碼,然後 VM 執行這些位元組碼。如果應用程式運作得很頻繁,虛拟機可以通過将頻繁執行的方法編譯成機器碼的方式來進行優化——但這僅針對 CPU。

如果開發人員想要通路異構裝置,比如 GPU 或 FPGA,他們通常需要通過 Java 本地接口(JNI)庫來實作。

程式員必須導入一個庫,并通過 JNI 調用這個庫。程式員可以通過使用這些庫為特定的 GPU 優化應用程式。但如果應用程式或 GPU 發生變化,可能需要重新建構應用程式,或需要重新調整優化參數。類似地,對于不同的 FPGA 甚至是其他型号的 GPU 也是如此。

是以,沒有一個完整的 JIT 編譯器和運作時能夠像 CPU 那樣處理異構裝置,檢測頻繁執行的代碼,并生成優化的機器碼。而 TornadoVM 就是為此而生的。

用 TornadoVM 讓 Java 性能更上一個台階

TornadoVM 可以與現有的 JDK 結合使用。它是 JDK 的一個插件,程式員可以借助它在異構硬體上運作應用程式。目前,TornadoVM 可以運作在多核 CPU、GPU 和 FPGA 上。

2

硬體特征和并行化

下一個問題是,為什麼要支援這麼多硬體?目前正在考慮支援三種不同的硬體架構:CPU、GPU 和 FPGA。每種架構都針對不同類型的工作負載進行了優化。

用 TornadoVM 讓 Java 性能更上一個台階

優化 CPU 是為了降低應用程式的延遲,優化 GPU 是為了提高吞吐量。FPGA 介于二者之間:由于應用程式被實體連接配接到硬體上,FPGA 通常可以實作較低的延遲和較高的吞吐量。

我們将這些架構與現有的并行化類型映射起來。在上圖中,我們可以發現并行化主要有三種類型:任務并行化、資料并行化和管道并行化。

通常,CPU 是為任務并行化而優化的,這意味着每個核心可以運作不同且獨立的任務。相比之下,GPU 是為運作并行資料而優化的,這意味着執行的函數和核心是相同的,但輸入資料不一樣。最後,FPGA 非常适用于管道并行化,即不同指令的執行在不同的内部階段之間會重疊。

理想情況下,我們需要一個進階并行程式設計架構,可以它表達不同類型的并行性,進而最大化每種裝置類型的性能。現在,讓我們看看 TornadoVM 是如何建構的,以及開發人員如何用它來表達不同類型的并行性。

3

TornadoVM 概覽

TornadoVM 是 JDK(Java 開發工具包)的插件,Java 開發人員可以用它在異構硬體上自動執行程式。TornadoVM 的主要特性如下:

它有一個優化的 JIT(Just In Time)編譯器,會針對每一種架構優化代碼。這意味着為 GPU 生成的代碼不同于為 CPU 和 FPGA 生成的代碼,進而最大化每種架構的性能。

TornadoVM 可以實作架構之間、裝置之間的動态任務遷移。例如,它可以先在 GPU 上運作應用程式一段時間,然後根據需要将其遷移到另一個 GPU、FPGA 或多核 CPU 上,無需重新啟動應用程式。

TornadoVM 是完全硬體無關的:在異構硬體上運作的應用程式源代碼與在 GPU、CPU 和 FPGA 上運作的是一樣的。

最後,它可以與多種 JDK 結合适用。它是開源的(可以在 GitHub 上獲得),Docker 鏡像也可以在 NVIDIA 和 Intel 內建 GPU 上運作。

4

TornadoVM 系統棧

讓我們來看一下 TornadoVM 的系統棧。在頂層,TornadoVM 暴露了一個 API,這是因為雖然它要利用并行化,但不檢測。是以,它需要一種方法來識别應用程式源代碼中哪些地方使用了并行化。

用 TornadoVM 讓 Java 性能更上一個台階

TornadoVM 提供了一個基于任務的程式設計 API,每個任務對應一個現有的 Java 方法。也就是說,TornadoVM 是在方法級編譯代碼,就像 JDK 或 JVM 那樣,但編譯的代碼是面向 GPU 和 FPGA 的。我們也可以在方法中使用注解來訓示并行化。另外,方法可以分成任務組,在同一個編譯單元中進行編譯。編譯單元叫作 Task-Schedule:Task-Schedule 有一個名字(用于調試和優化),并包含了一組任務。

TornadoVM 引擎讀入位元組碼級别的表達式,并自動為不同的架構生成代碼。它目前有三個生成代碼的後端,分别生成 OpenCL、CUDA 和 SPIR-V 代碼。開發人員可以選擇使用哪一個,或者讓 TornadoVM 預設選擇一個。

5

模糊濾鏡示例

我們現在來看一個 TornadoVM 如何加速 Java 應用程式的例子:模糊濾鏡。我們有一張圖檔,想要對這張圖檔應用模糊效果。

在了解如何編寫代碼之前,我們先來看看這個應用程式在異構硬體上運作的性能。下圖顯示了四種不同實作的測試基準。我們将 Java 的串行實作作為參考,y 軸是相對于參考的性能增益,越高表示性能越好。

用 TornadoVM 讓 Java 性能更上一個台階

左邊的兩清單示基于 CPU 的執行結果。第一列使用标準的 Java 并行流,第二列使用運作在多 CPU 核心上的 TornadoVM,分别獲得 11 倍和 17 倍的加速。TornadoVM 獲得更好的結果,因為它為 CPU 生成了 OpenCL 代碼,而 OpenCL 非常擅長使用向量機關對代碼進行矢量化。如果應用程式在內建顯示卡上運作,與 Java 串行實作相比,可以獲得 19 倍的性能加速。如果在 NVIDIA GPU(2060)上運作應用程式,可以獲得高達 340x 的性能加速(使用 TornadoVM 的 OpenCL 後端)。我們将性能加速與 Java 并行流相比,當在 NVIDIA GPU 上運作時,TornadoVM 可以獲得 30 倍的性能加速。

6

模糊濾鏡的實作

模糊濾鏡是一種映射操作符,将一個函數(模糊效果)應用在每一個輸入的圖像像素上。這種模式非常适合進行并行化,因為每個像素都可以獨立于其他像素進行計算。

我們要做的第一件事是在 Java 方法中給代碼添加注解,讓 TornadoVM 知道如何并行化它們。

用 TornadoVM 讓 Java 性能更上一個台階

因為每一個像素的計算可以并行進行,是以我們将 @Parallel 注解添加到最外層的兩個循環中。這将向 TornadoVM 發出信号,讓它完全并行計算這兩個循環。代碼注解定義了資料并行化模式。

第二件事情是定義任務。由于輸入的是 RGB 圖像,我們可以為每個顔色通道(紅、綠、藍)建立一個任務。是以,我們要做的是對每個通道進行模糊處理。我們使用了一個包含三個任務的 TaskSchedule 對象。

用 TornadoVM 讓 Java 性能更上一個台階

此外,我們還需要定義哪些資料将從 Java 記憶體堆傳輸到裝置(例如 GPU)上。這是因為 GPU 和 FPGA 通常不共享記憶體。是以,我們需要一種方法來告訴 TornadoVM 需要在裝置之間複制哪些記憶體區域。這是通過 streamIn() 和 streamOut() 函數來完成的。

然後是定義任務集,每個顔色通道一個任務。它們有名字辨別,并通過方法引用組合在一起。這個方法現在可以被編譯成核心代碼。

最後,我們調用 execute 函數,在裝置上并行執行這些任務。現在我們來看看 TornadoVM 是如何編譯和執行代碼的。

7

TornadoVM 如何在并行硬體上啟動 Java 核心

原始的 Java 代碼是單線程的,即使已經加了 @Parallel 注解。在 execute() 函數被調用時,TornadoVM 開始優化代碼。

首先,代碼被編譯成一種中間表示,以便對其進行優化(TornadoVM 擴充了 Graal JIT 編譯器,所有的優化都發生在這一層)。然後,TornadoVM 将優化後的代碼轉換成高效的 PTX、OpenCL 或 SPIR-V 代碼。

這個時候開始執行代碼,将會啟動數百或數千個線程。TornadoVM 會啟動多少個線程取決于應用程式。

用 TornadoVM 讓 Java 性能更上一個台階

在這個例子中,模糊濾鏡有兩個并行循環,每個循環周遊一個圖像次元。是以,在運作時編譯期間,TornadoVM 建立了一個與輸入圖像具有相同次元的線程網格。每個網格單元(也就是每個像素)映射一個線程。例如,如果圖像的像素是 2000x2000,TornadoVM 将在目标裝置(例如 GPU)上啟動 2000x2000 個線程。

TornadoVM 還可以實作管道并行化,主要是針對 FPGA。當我們或 TornadoVM 選擇了 FPGA,它會自動将生成代碼的資訊插入到管道指令中。與之前的并行代碼相比,這種政策可以将性能提高一倍。

8

Parallel Loop API 與 Parallel Kernel API

現在我們來看看如何在 TornadoVM 中表示計算核心。TornadoVM 有兩個 API:一個是我們在模糊濾鏡示例中使用的 Parallel Loop API,另一個是 Parallel Kernel API。TornadoVM 的并行循環 API 是基于注解的。在使用這個 API 時,開發人員必須提供串行實作代碼,然後考慮在哪裡并行化循環。

一方面,開發速度加快了,因為開發人員隻需要向現有的 Java 串行代碼中添加注解就可以實作并行化。Parallel Loop API 适合非專業使用者,他們不需要知道 GPU 的計算細節,也不需要知道應該使用哪種硬體。

另一方面,Parallel Loop API 可以使用的模式數量有限。在使用這個 API 時,開發人員可以使用典型的 map/reduce 模式運作應用程式。但其他的并行模式,如掃描或複雜模闆,很難用這個 API 實作。此外,這個 API 不允許開發人員控制硬體,因為它是硬體無關的,但有時候開發人員确實需要控制硬體。此外,将現有的 OpenCL 和 CUDA 代碼移植到 Java 可能會很困難。

為了應對這些限制,我們加入了 Parallel Kernel API。

9

用 Parallel Kernel API 實作模糊濾鏡

用 TornadoVM 讓 Java 性能更上一個台階

我們回到之前的例子:模糊濾鏡。我們有兩個并行循環,周遊圖像的兩個次元并應用濾鏡。這可以轉換成使用 Parallel Kernel API。

我們不使用兩個循環,而是通過核心上下文引入隐式并行化。上下文是一個 TornadoVM 對象,使用者可以通過它通路到每個次元的線程辨別符、本地 / 共享記憶體、同步原語等。

在我們的示例中,濾鏡的 X 軸和 y 軸坐标分别來自上下文的 globalIdx 和 globalIdy 屬性,并像之前一樣用于應用濾鏡。這種程式設計風格更接近 CUDA 和 OpenCL 程式設計模型。

用 TornadoVM 讓 Java 性能更上一個台階

需要注意的是,TornadoVM 無法在運作時确定需要多少個線程。使用者需要通過 worker 網格進行配置。

在這個例子中,我們用圖像次元建立了一個 2D 的 worker 網格,并與函數名相關聯。當使用者的代碼調用 execute() 函數時,将網格作為參數傳進去,進而應用相應的濾鏡。

10

TornadoVM 的優勢

但是,如果 Parallel Kernel API 更接近于底層的程式設計模型,為什麼要使用 Java 而不是 OpenCL 和 PTX 或 CUDA 和 PTX,尤其是在有現有代碼的情況下?

TornadoVM 還有其他的優勢,比如實時任務遷移、自動記憶體管理和透明的代碼優化,而且代碼優化是根據不同的架構而進行的。

它還可以運作在 FPGA 上,具有完全透明和內建的程式設計工作流。你可以使用你最喜歡的 IDE,例如 IntelliJ 或 Eclipse,編寫在 FPGA 上運作的代碼。

它也可以部署在雲端,如亞馬遜雲。你可以将代碼移植到 Java 和 TornadoVM,以便免費獲得所有這些功能。

11

性能

現在我們來談談性能。TornadoVM 可不僅僅被用于給圖像應用濾鏡,它也被用在金融科技或數學模拟(例如 Monte Carlo 或 Black-Scholes)領域。它還被用于計算機視覺應用、實體模拟、信号處理等其他領域。

用 TornadoVM 讓 Java 性能更上一個台階

上圖對不同裝置上的不同應用程式的執行情況進行了對比。同樣,我們仍然将串行執行作為參考對象,條形表示加速因子,越高越好。

正如我們所看到的,我們有可能實作非常高的性能加速。例如,信号處理或實體模拟可以比 Java 的串行執行快 4000 倍。對這些結果的詳細分析,可以參閱學術出版物。(https://github.com/beehive-lab/TornadoVM/blob/master/assembly/src/docs/14_PUBLICATIONS.md)

12

TornadoVM 在行業的應用

用 TornadoVM 讓 Java 性能更上一個台階

業界的一些公司也在嘗試使用 TornadoVM。上圖是兩個正在使用 TornadoVM 的應用場景。

其中一個應用場景來自盧森堡的 Neurocom 公司,用它運作一種自然語言處理算法。到目前為止,通過在 GPU 上運作分層聚類算法,已經實作了 30 倍的性能提升。

另一個應用場景來自 Spark Works 公司,這是一家位于愛爾蘭的公司,用它處理來自物聯網裝置的資訊。他們用強大的 GPU、GPU100 運作後處理工作負載。與 Java 相比,他們可以獲得 460 倍的性能提升,已經相當好了。

你可以通路 TornadoVM 網站檢視完整的應用場景清單。

13

總結

異構裝置現在幾乎出現在每一個計算系統中,這是不可避免的。它們就在這裡,而且會一直這樣下去。

是以,目前和未來軟體系統的程式員需要面對廣泛和多樣化的裝置,例如 GPU、FPGA 或任何其他即将出現的硬體。他們可以通過 TornadoVM 對這些裝置進行程式設計。

TornadoVM 可以被看作是 Java 和 JVM 的高性能計算平台,可以與現有的 JDK(例如 OpenJDK)結合使用。

本文介紹了 TornadoVM,并簡要解釋了它的工作原理。此外,本文還通過一個用 Java 實作的圖像處理示例示範了開發人員如何充分利用異構硬體。我們解釋了 TornadoVM 中用于異構程式設計的兩個 API:一個是 Parallel Loop API,适合普通開發人員使用;另一個是 Parallel Kernel API,适合已經了解 CUDA 和 OpenCL 并想要将現有代碼移植到 TornadoVM 的專家使用。

作者簡介:

Juan Fumero 是曼徹斯特大學的一名研究員。他的研究課題是異構進階程式設計語言虛拟機、GPGPU 和分布式計算。目前,他是 TornadoVM 項目的一員,為 Java 應用程式引入自動 GPU 和 FPGA JIT 編譯和執行特性。他還與英特爾合作,将 oneAPI 引入 TornadoVM,用于對英特爾計算架構的代碼進行優化。Juan 獲得了愛丁堡大學的博士學位,主要研究在 GPU 上加速 Java、R 語言和 Ruby。此外,他曾在 Oracle 實驗室和 CERN 實習,實作編譯器,評估多核系統的并行技術。

https://www.infoq.com/articles/java-performance-tornadovm/

繼續閱讀