CUDA PTX ISA閱讀筆記(一)

不知道這是個啥的看這裏:Parallel Thread Execution ISA Version 5.0.
簡要來說,PTX就是.cu代碼編譯出來的一種東西,然後再由PTX編譯生成執行代碼。如果不想看網頁版,cuda的安裝目錄下的doc文件夾裏有pdf版本,看起來也很舒服。
ps:因爲文檔是英文的(而且有二百多頁= =),鑑於博主英語水平有限並且時間也有限(主要是懶),因此只意譯了一些自以爲重點的內容,如想要深入學習,還是乖乖看文檔去吧

第一章 介紹

1.1. 使用GPU進行可擴展數據並行計算

介紹了一波並行計算的知識。

1.2. PTX的目標

PTX爲提供了一個穩定的編程模型和指令集,這個ISA能夠跨越多種GPU,並且能夠優化代碼的編譯等等。

1.3. PTX ISA 5.0版本

就是PTX ISA5.0的一些新特性

1.4. 文檔結構

  • 編程模型:編程模型的概要
  • PTX 機器模型:大致介紹PTX虛擬機
  • 語法:描述PTX語言的基礎語法
  • 狀態空間、類型和變量:就是描述這些玩意
  • 指令操作數
  • 應用二進制接口:描述了函數定義和調用的語法,以及PTX支持的應用二進制接口
  • 指令集
  • 特殊的寄存器
  • 版本更新介紹

第二章 編程模型

2.1. 一個高並行度的協處理器

繼續科普GPU。

2.2. 線程層級

2.2.1 合作線程陣列

2.2.2 線程陣列網格

上邊這兩節主要就是講一些基本的GPU的block啊grid啊之類的東西,想了解的可以看我的另一篇文章:《GPU高性能編程 CUDA實戰》(CUDA By Example)讀書筆記-第五章。這裏的圖就用了這個手冊裏的。

2.3. 內存層級

這個圖實在是太好了:
不同等級對應不同的內存

第三章 PTX機器模型

3.1. 一組帶有片上共享內存的SIMT多處理器

主要講一下硬件層級結構,果然圖還是最好的:
硬件結構

第四章 語法

PTX語言是由操作指令和操作數組成。

4.1. 代碼格式

使用\n換行,空格木有意義,#這個符號和C差不多,就是預編譯指令,而且大小寫敏感,每個PTX代碼都是由.version打頭,表示PTX的版本。

4.2. 註釋

和C一樣

4.3. 語句

以一個可選的標記開始,以分號結束,就像這樣:
start: mov.b32 r1, %tid.x;

4.3.1. 指示

提供了PTX的指示
PTX指示

4.3.2. 指令

提供了PTX的指令:
PTX指令
ps:關於directive和 instruction這兩個詞的區別涉及一些彙編上的知識,前者這裏翻譯爲指示,後者這裏翻譯成指令,因爲一般directive並不會產生代碼而是指示編譯器的一些行爲,而instruction則會產生實際的代碼,想了解的可以看這裏:What-is-the-difference-between-an-instruction-and-a-directive-in-assembly-language

4.4. 標識符

這個大概就是變量名的命名規則吧,基本就和C一樣啦,然後系統預定義的變量都是以%開頭的大佬變量。

4.5. 常量

這個,我猜,大概是是標號標錯了,應該是包含下面各種常量的大標題纔對。

4.6. 整型常量

每個整型常量都是64噠,分爲有符號和無符號,由.s64和.u64定義,其中各個進制的數是如下定義的:

X進制 表示方式
十六進制 0[xX]{十六進制數}+U?
十進制 0{octal 十進制數}+U?
二進制 0[bB]{0/1}+U?
小數 {非零數}{十進制數}*U?

4.6.1. 浮點常量

浮點數都是64位的,除了用一個32位十六進制去精確表達一個單精度浮點數(黑人問號臉???),具體表達方式如下:

精度 表達方式
單精度 0[fF]{十六進制數}{8}
雙精度 0[dD]{十六進制數}{16}

4.6.2. 判斷值常量

0就是false,非零就是true

4.6.3. 常量表達式

這個大概是可以對常量能夠使用的表達式,也和C基本一致啦:
表達式

4.6.4 整型常量表達式求值

和C語言一樣一樣的

4.6.5 表達式求值規則總結

C語言+1
求值

第五章 狀態空間、類型和變量

5.1. 狀態空間

這個狀態空間就我理解吧,就是在哪塊內存上操作。

5.1.1. 寄存器狀態空間

利用.reg來聲明寄存器狀態空間,該空間可以使用幾乎形式的數據,但是不同於其他狀態空間的是寄存器是沒有地址的。

5.1.2. 特殊寄存器狀態空間

用.sreg來聲明,存的主要是系統預定義的一些變量,比如grid的維數之類的數據。

5.1.3. 常量狀態空間

常量狀態空間使用.const來表示,被限制在64KB之內。並且被組織成10個區域,驅動要在這十個區域中申請空間,然後可以將這些申請到的空間用指針傳遞給核函數。

5.1.3.1. 存儲體常量寄存器(棄用)

以前這種是需要指定確定的區域號纔可以的,就像這樣:
.extern .const[2] .b32 const_buffer[];

5.1.4. 全局狀態空間

使用ld.global,st.globle和atom.global來訪問全局狀態空間。而且,訪問全局變量空間是沒有順序的,是需要使用bar.sync來同步的。

5.1.5. 本地狀態空間

.local聲明本地狀態空間,而且只能在線程內部使用。

5.1.6. 參數狀態空間

參數狀態空間被用於1.將輸入的參數從主機傳遞給核函數。2.爲在覈函數內調用的設備函數聲明形式化輸入和返回參數。3.聲明作爲函數調用參數的本地數組,特別是用來傳遞大的結構體給函數。

5.1.6.1. 核函數參數

.entry foo ( .param .b32 N, .param .align 8 .b8 buffer[64]) 
{ 
    .reg .u32 %n; 
    .reg .f64 %d; 
    ld.param.u32 %n, [N]; 
    ld.param.f64 %d, [buffer]; 
    ...

5.1.6.2. 核函數參數屬性

5.1.6.3. 核函數參數屬性: .ptr

使用這個相當於一個指針,還可以指定內存對齊的大小。

 .entry foo ( 
    .param .u32 param1, 
    .param .u32 .ptr.const.align 8 param3, 
    .param .u32 .ptr.align 16 param4 
    ) { .. }

5.1.6.4. 設備函數參數

這個最常用於傳遞大小和寄存器大小不一樣的變量,比如結構體。

 .func foo ( .reg .b32 N, .param .align 8 .b8 buffer[12] ) {
    .reg .s32 %y; 
    ld.param.f64 %d, [buffer]; 
    ld.param.s32 %y, [buffer+8]; 
    ... 
}

5.1.7. 共享狀態空間

用.shared定義,共享內存有一個特點是可以廣播,並且能夠順序訪問(有某種一致性機制?)

5.1.8. 紋理狀態空間(棄用)

紋理內存也是全局內存的一部分,被上下文的所有線程共享並且是隻讀的。使用.tex應該被.global裏的.texref來代替。就像:

  .tex .u32 tex_a;
  //轉換成下面這樣
  .global .texref tex_a;

5.2. 類型

5.2.1. 基本類型

這些基本類型就好像C語言中的int,float之類的,用來定義變量的:
ptx基本類型

5.2.2. 使用子字段的尺寸限制

像.u8, .s8,和.b8這種類型僅限於在ld,st和cvt中使用。.f16只能被轉換成並且只能從.f32,.f64類型。.f16×2這種浮點類型只允許被用在浮點數算法指令和紋理獲取指令上。

5.3. 紋理採集器和表面類型

下面這段話是從專家手冊裏摘錄的關於表面引用的解釋:

讀寫紋理和表面的指令相對於其他指令涉及了更多隱祕狀態。參數,例如基地址、維度、格式和紋理內容的解釋方式,都包含在一個header頭結構中。header是一箇中間數據結構,它的軟件抽象被稱爲紋理引用(texture reference)或表面引用(surface reference)。
這裏有個表用來講專門爲紋理狀態空間提供的不透明類型:
不透明類型

5.3.1. 紋理和表面設置

像上表中所提到的width, height, 和 depth都用來說明紋理內存的大小之類的特性。

5.3.2. 採集器設置

它有各種模式,看CUDA C Programming Guide獲取更多細節。

5.3.3. 頻道數據類型和頻道指令字段

以前之後OpenCL能用,現在都能用了。
講真,由於對紋理內存瞭解太少,這節看得很勉強。

5.4. 變量

5.4.1. 變量聲明

變量聲明需要同時聲明狀態空間和數據類型比如:

.global .u32 loc; 
.reg .s32 i; 
.const .f32 bias[] = {-1.0, 1.0}; 
.global .u8 bg[4] = {0, 0, 0, 0}; 
.reg .v4 .f32 accel; 
.reg .pred p, q, r;

5.4.2. 向量

這裏的向量的長度是被ptx固定的,只能是2或者4,也不能是判斷值(true of false),定義同普通變量:global .v4 .f32 V;

5.4.3. 數組聲明

數組的定義和C差不多,可以指定長度也可以不指定然後初始化:

      .local  .u16 kernel[19][19];
      .shared .u8  mailbox[128];
      .global .u32 index[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
      .global .s32 offset[][2] = { {-1, 0}, {0, -1}, {1, 0}, {0, 1} };

5.4.4. 初始化器

對於初始化,是這樣的:

.const .f32 vals[8] = { 0.33, 0.25, 0.125 };
.global .s32 x[3][2] = { {1,2}, {3} };
//相當於
.const .f32 vals[4] = { 0.33, 0.25, 0.125, 0.0, 0.0 }; 
.global .s32 x[3][2] = { {1,2}, {3,0}, {0,0} };

當前,變量的初始化只對常量和global狀態空間支持,默認的初始化值是0。對於數組,還可以採用以下神奇的方法來初始化:

.const .u32 foo = 42; 
.global .u32 p1 = foo; // offset of foo in .const space .global .u32 p2 = generic(foo); // generic address of foo 

// array of generic-address pointers to elements of bar .global .u32 parr[] = { generic(bar), generic(bar)+4, generic(bar)+8 };

5.4.5. 內存對齊

就是可以在定義數組什麼的時候指定內存對齊的大小:

// allocate array at 4-byte aligned address. Elements are bytes. .const .align 4 .b8 bar[8] = {0,0,0,0,2,0,0,0};

5.4.6. 參數化的變量名稱

這裏提供了一種快捷聲明變量的方法:.reg .b32 %r<100>; //就相當於聲明瞭 %r0, %r1, ..., %r99

5.4.7. 變量屬性

參見下一節

5.4.8. 變量屬性指示: .attribute

變量有個.manage屬性,這個屬性只能在.global狀態空間上使用,使用了這個屬性之後能召喚神龍可以將變量放置在一個虛擬空間上,這個空間主機和設備都能夠訪問。具體是這樣使用的:.global .attribute(.managed) .s32 g;

第六章 指令操作數

6.1. 操作數類型信息

每個指令裏的操作數都要聲明其類型,而且類型必須符合指令的模板,並沒有自動的類型轉換。

6.2. 源操作數

PTX描述的是一個存儲讀取機,因此對於ALU指令的操作數必須在.reg寄存器狀態空間。cvt指令可以的參數有多種類型和大小,可以轉換一種類型(或者大小)到另一種類型(或大小)。ld,st,mov和cvt指令從一個地址拷貝數據到另一個地址。ld,st將內容拷貝到寄存器或者從寄存器中拷貝出來,mov指令把數據從一個寄存器換到另一個寄存器。大多數指令有個可選的判斷操作,一些指令有附加的判斷類型的源操作數,這些經常被定義名爲p,q,r,s.

6.3. 目的操作數

用來得到一個結果,一般都在寄存器中。

6.4. 使用地址,數組和向量

6.4.1. 地址作爲操作數

就類似各種類型的定義:

.shared .u16 x; 
.reg .u16 r0; 
.global .v4 .f32 V; 
.reg .v4 .f32 W; 
.const .s32 tbl[256];
.reg .b32 p; .reg .s32 q; 

ld.shared.u16 r0,[x]; 
ld.global.v4.f32 W, [V]; 
ld.const.s32 q, [tbl+12]; 
mov.u32 p, tbl;

6.4.2. 數組作爲操作數

數組的使用也基本上和C語言是一樣的:

ld.global.u32 s, a[0]; 
ld.global.u32 s, a[N-1]; 
mov.u32 s, a[1]; // move address of a[1] into s

6.4.3. 向量作爲操作數

向量的感覺更像是一個結構體或者數組,使用向量可以快速地給多個數複製,很強:

.reg .v4 .f32 V; 
.reg .f32 a, b, c, d; 
mov.v4.f32 {a,b,c,d}, V;
//也可以反過來
ld.global.v4.f32  {a,b,c,d}, [addr+offset];
ld.global.v2.u32  V2, [addr+offset2];

6.4.4. 標記和函數名作爲操作數

這個主要是用來獲得標記或者函數名,在分支語句中做跳轉使用。

6.5. 類型轉換

6.5.1. 標量轉換

sext:符號擴展。zext:零擴展。chop:只保留低位。s是有符號整數,f是浮點數,u是無符號整數。2就是轉換成
類型轉換

6.5.2. 取整修改器

這裏是表示取整的標誌,有什麼向下取證向上取整之類的。(最低有效位(英語:Least Significant Bit,lsb)是指一個二進制數字中的第0位(即最低位),權值爲2^0,可以用它來檢測數的奇偶性。)
取整

6.6. 操作數耗時

不同狀態空空間的操作數會影響一個操作的速度。寄存器最快,全局變量最慢,而多線程可以掩蓋這種延遲,或者讓取值指令越簡單越好。下面是從這些地方取值的延遲:
取值延遲

第七章 抽象ABI

ABI是Application Binary Interface的縮寫,翻譯過來是二進制程序接口。直白點講就是系統提供的一系列函數。

7.1. 函數的聲明和定義

話不多說就看代碼好了

//定義了一個結構體
struct {
    double dbl;
    char   c[4];
};
//有返回值和傳入參數
.func (.reg .s32 out) bar (.reg .s32 x, .param .align 8 .b8 y[12]) 
{ 
    .reg .f64 f1; 
    .reg .b32 c1, c2, c3, c4; 
    ... 
    ld.param.f64 f1, [y+0]; 
    ld.param.b8 c1, [y+8];
    ld.param.b8 c2, [y+9];
    ld.param.b8 c3, [y+10]; 
    ld.param.b8 c4, [y+11]; 
    ... ... // computation using x,f1,c1,c2,c3,c4; 
} 
{
    .param .b8 .align 8 py[12]; 
    ...
    //通過位移來使用參數
    st.param.b64 [py+ 0], %rd; 
    st.param.b8 [py+ 8], %rc1; 
    st.param.b8 [py+ 9], %rc2; 
    st.param.b8 [py+10], %rc1; 
    st.param.b8 [py+11], %rc2; 
    // scalar args in .reg space, byte array in .param space 
    call (%out), bar, (%x, py); 
    ...

要注意,對於參數的st.param和對返回值的ld.out都必須緊跟着函數調用call。這樣才能讓編譯器優化是的.param不佔用多餘的空間。並且這個.param允許簡單的映射將有多個地址的結構映射到能夠傳給函數的變量上。

7.1.1. PTX ISA Version 1.x的改變

1.x只支持.reg,後來開始支持.param。

7.2. 列表函數

現在的ptx並不支持列表函數。(不支持說個毛,下一位!)

7.3. Alloca

同上同上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章