目次
SASS とは何か?SASSについて知って何の役に立つのかについて。
PTXのさらに後ろにあるほぼネイティブアセンブリ。
PTX を ptxas に入れると、NVIDIA GPU用の機械語を含むcubinが出るが、 この cubin をcuobjdumpを使って逆アセンブルした結果として確認できる。
asfermi という非公式のツールを使えば SASS->cubin へのアセンブルもできる
SASS が何の略かは公式にはわからないが、Shader ASSembly ではないかとの説が。
人が頑張って機械語を解析して、独自に作った decuda というツールがあった。
このdecudaを活用して書かれた "Micro-benchmarking the GT200" は全CUDAプログラマが読むべきものであると思う。
(多分)CUDA3.0になって、cuobjdumpが公式に入った。これによってdecudaは必要無くなった。
1. nvcc を使って cubinを出す
$ cat a.cu __global__ void func(int *p) { p[0]++; } $ nvcc -cubin a.cu
2. cuobjdump を使って SASS を見てニヤニヤする
$ cuobjdump -sass a.cubin code for sm_10 Function : _Z4funcPi /*0000*/ /*0x1000c8010423c780*/ MOV R0, g [0x4]; /*0008*/ /*0xd00e000580c00780*/ GLD.U32 R1, global14 [R0]; /*0010*/ /*0x2001820500000003*/ IADD32I R1, R1, 0x1; /*0018*/ /*0xd00e0005a0c00781*/ GST.U32 global14 [R0], R1; ..........................
cubin はただのELFなので、readelfでもシンボルの確認等ができる
$ nm a.cubin 00000000 T _Z4funcPi $ readelf -h a.cubin ELF Header: Magic: 7f 45 4c 46 01 01 01 33 02 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: <unknown: 33> ABI Version: 2 Type: EXEC (Executable file) Machine: NVIDIA CUDA architecture Version: 0x1 Entry point address: 0x0 Start of program headers: 668 (bytes into file) Start of section headers: 52 (bytes into file) Flags: 0xa010a Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 3 Size of section headers: 40 (bytes) Number of section headers: 7 Section header string table index: 1
-arch を変えると結構内容が変わる
$ nvcc -cubin -arch sm_20 a.cu $ cubojdump -sass a.cubin code for sm_20 Function : _Z4funcPi /*0000*/ /*0x00005de428004404*/ MOV R1, c [0x1] [0x100]; /*0008*/ /*0x80009de428004000*/ MOV R2, c [0x0] [0x20]; /*0010*/ /*0x00201c8588000000*/ LDU R0, [R2]; /*0018*/ /*0x04001c034800c000*/ IADD R0, R0, 0x1; /*0020*/ /*0x00201c8590000000*/ ST [R2], R0; /*0028*/ /*0x00001de780000000*/ EXIT; .......................... $ nvcc -cubin -arch sm_30 a.cu $ cuobjdump -sass a.cubin code for sm_30 Function : _Z4funcPi /*0008*/ /*0x10005de428004001*/ MOV R1, c [0x0] [0x44]; /*0010*/ /*0x00009de428004005*/ MOV R2, c [0x0] [0x140]; /*0018*/ /*0x00201c8580000000*/ LD R0, [R2]; /*0020*/ /*0x04001c034800c000*/ IADD R0, R0, 0x1; /*0028*/ /*0x00201c8590000000*/ ST [R2], R0; /*0030*/ /*0x00001de780000000*/ EXIT; /*0038*/ /*0xe0001de74003ffff*/ BRA 0x38; ..........................
arch は、nvcc.pdf に詳しく書いてあるが、何かの役に立つと思うのでいくらか解説しておく。
arch は、使える機能を決める。code は、cubinに入れる機械語の種類を決める。
cubin は、一個の機械語しか入れられないが、このELFとPTXをくっつけた(多分) fatbin には、 複数のアーキテクチャ用の機械語 + PTXを入れられる。
$ nvcc -fatbin -arch compute_10 -code=compute_10,sm_20,sm_30 a.cu $ cuobjdump a.fatbin $ cuobjdump.exe -ptx -sass a.fatbin Fatbin elf code: ================ arch = sm_30 code version = [1,4] producer = cuda host = windows compile_size = 32bit identifier = a.cu code for sm_30 Function : _Z4funcPi /*0008*/ /*0x10005de428004001*/ MOV R1, c [0x0] [0x44]; /*0010*/ /*0x00009de428004005*/ MOV R2, c [0x0] [0x140]; /*0018*/ /*0x00201c8580000000*/ LD R0, [R2]; /*0020*/ /*0x04001c034800c000*/ IADD R0, R0, 0x1; /*0028*/ /*0x00201c8590000000*/ ST [R2], R0; /*0030*/ /*0x00001de780000000*/ EXIT; /*0038*/ /*0xe0001de74003ffff*/ BRA 0x38; .......................... Fatbin elf code: ================ arch = sm_20 code version = [1,4] producer = cuda host = windows compile_size = 32bit identifier = a.cu code for sm_20 Function : _Z4funcPi /*0000*/ /*0x00005de428004404*/ MOV R1, c [0x1] [0x100]; /*0008*/ /*0x80009de428004000*/ MOV R2, c [0x0] [0x20]; /*0010*/ /*0x00201c8580000000*/ LD R0, [R2]; /*0018*/ /*0x04001c034800c000*/ IADD R0, R0, 0x1; /*0020*/ /*0x00201c8590000000*/ ST [R2], R0; /*0028*/ /*0x00001de780000000*/ EXIT; .......................... Fatbin ptx code: ================ arch = sm_10 code version = [1,4] producer = cuda host = windows compile_size = 32bit identifier = a.cu ptxasOptions = .version 1.4 .target sm_10, map_f64_to_f32 // 長いので略 .entry _Z4funcPi ( .param .u32 __cudaparm__Z4funcPi_p) { .reg .u32 %r<5>; .loc 15 2 0 $LDWbegin__Z4funcPi: .loc 15 3 0 ld.param.u32 %r1, [__cudaparm__Z4funcPi_p]; ld.global.s32 %r2, [%r1+0]; add.s32 %r3, %r2, 1; st.global.s32 [%r1+0], %r3; .loc 15 4 0 exit; $LDWend__Z4funcPi: }
ELFをくっつけただけ?
CUDA コンパイラは、fatbinをランタイムに渡すホストコードを出す。
CUDA ランタイムは、
という挙動をする。使いかたとしては、
という感じになる。
-arch=sm_xx は、 -arch=compute_xx -code=sm_13,compute_13 と対応している。(なので昔のGPU用にコンパイルしてあCUDAコードは今のGPUでも動く)
アトミック演算や倍精度等の新しい機能を昔のGPUで使う場合は、 -arch=compute_30 -code=sm_10 とか、将来、できるといいなぁ…と、マニュアルには書いてある(今はできない)
まとめると、実行時によろしくやってくれるので、-arch sm_10 で特に問題無い。
cuobjdumpのマニュアルに、一応命令一覧と、ひとこと説明が書いてある。
…が、詳しい説明が無くて、どうやったら使えるかわからない命令が多い。
Load and Lock とかあるが、 ll/sc できるのだろうか…
ここまで読んだ方は、「これって何かの役に立つの…?」という感が拭えないと思うので、 役に立った事例など紹介。
命令単位のパフォーマンス取りたい場合は結構あって、NVGPU には、clock() というのがあるし、それが使えるのだが、 ptx は dead code elimination (使ってない命令を消す) 的な最適化をするので、ちゃんと依存関係作ってあげないと測定できない場合が多い。
$ cat clock.cu __global__ void f(int n) { __shared__ int p; for (int i=0; i<n; i++) { int v = 0; asm("add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" :"+r"(v)); p = v; } } $ nvcc -O2 -ptx clock.cu $ cat clock.ptx // (略) $Lt_0_1794: //<loop> Loop body line 1, nesting depth: 1, estimated iterations: unknown .loc 15 6 0 mov.s32 %r6, 0; mov.u32 %r7, %r6; add.s32 %r7, %r7, %r7; add.s32 %r7, %r7, %r7; add.s32 %r7, %r7, %r7; add.s32 %r7, %r7, %r7; mov.s32 %r8, %r7; add.s32 %r4, %r4, 1; .loc 15 1 0 ld.param.s32 %r1, [__cudaparm__Z1fi_n]; .loc 15 6 0 setp.ne.s32 %p2, %r1, %r4; @%p2 bra $Lt_0_1794; // (略)
PTXでは出てるのに、実際測ってみると何もとれない
SASS で見ると消えてる…
$ cuobjdump.exe -sass clock.cubin code for sm_10 Function : _Z1fi /*0000*/ /*0x307cc9fd6c20c7c8*/ ISET.S32.C0 o [0x7f], g [0x4], R124, LE; /*0008*/ /*0x3000000300000280*/ RET C0.NE; /*0010*/ /*0x1000f8010403c780*/ MOV R0, R124; /*0018*/ /*0x2001800100000003*/ IADD32I R0, R0, 0x1; /*0020*/ /*0x3000c9fd6c2147c8*/ ISET.S32.C0 o [0x7f], g [0x4], R0, NE; /*0028*/ /*0x1000300300000280*/ BRA C0.NE, 0x18; /*0030*/ /*0x00000a01e43f0781*/ R2G.U32.U32 g [0x5], R124; ......................
入力をメモリにするとちゃんと出る
$ cat clock.cu __global__ void f(int n) { __shared__ int p; for (int i=0; i<n; i++) { int v = p; asm("add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" "add.s32 %0, %0, %0;\n\t" :"+r"(v)); p = v; } } $ cuobjdump.exe -sass clock.cubin code for sm_10 Function : _Z1fi /*0000*/ /*0x307cc9fd6c20c7c8*/ ISET.S32.C0 o [0x7f], g [0x4], R124, LE; /*0008*/ /*0x3000000300000280*/ RET C0.NE; /*0010*/ /*0x1000ca010423c780*/ MOV R0, g [0x5]; /*0018*/ /*0x1000f8050403c780*/ MOV R1, R124; /*0020*/ /*0x20008000 */ IADD32 R0, R0, R0; /*0024*/ /*0x20008000 */ IADD32 R0, R0, R0; /*0028*/ /*0x2001820500000003*/ IADD32I R1, R1, 0x1; /*0030*/ /*0x2000000104000780*/ IADD R0, R0, R0; /*0038*/ /*0x3001c9fd6c2147c8*/ ISET.S32.C0 o [0x7f], g [0x4], R1, NE; /*0040*/ /*0x2000000104000780*/ IADD R0, R0, R0; /*0048*/ /*0x1000400300000280*/ BRA C0.NE, 0x20; /*0050*/ /*0x00000a01e4200781*/ R2G.U32.U32 g [0x5], R0; ......................
こういうのを確認するときにcuobjdump必須
64bitマシンでは、アドレスの足し算が64bit加算になるが、NVGPUは 64bit加算が無くて2命令になってしまう。
__global__ void f(int *p, int *q, int off, int n) { for (int i=0; i<n; i++) { int *ptr = &p[off+i]; if (ptr < q) { *ptr += 1; } } } ↓ /*0020*/ /*0x2000000504004780*/ IADD R1, R0, R1; /*0028*/ /*0x2000c809042007c0*/ IADD.C0 R2, g [0x4], R0; /*0030*/ /*0x3040ca0d043f0780*/ IADD.CARRY0 R3, g [0x5], R124;
が、GT200 は、GPU側のアドレス空間が32bitしか無いので、実際に64bit加算が必要無い場合が多い。 ptxas は涙ぐましい努力をして、ptx の add.u64 を 32bit 加算にする努力をしているように見える。
__global__ void f2(int *p, int *q, int off, int n) { for (int i=0; i<n; i++) { int *ptr = &p[off+i]; *ptr += 1; } } /*0020*/ /*0x2000000504004780*/ IADD R1, R0, R1; /*0028*/ /*0x2000c80d04200780*/ IADD R3, g [0x4], R0; /*0030*/ /*0xd00e060980c00780*/ GLD.U32 R2, global14 [R3];
こういうのを確認するのに使える。
ただ残念ながらsm_20以降はGPUアドレス空間も64bitになって、涙ぐましい努力は行われなくなっている
/*00c8*/ /*0x04009c034800c000*/ IADD R2, R0, 0x1; /*00d0*/ /*0x20411c034801c000*/ IADD R4.CC, R4, 0x8; /*00d8*/ /*0x5021dc2318804005*/ ISETP.LT.AND P0, pt, R2, c [0x0] [0x154], P0; /*00e0*/ /*0xfc515c4348000000*/ IADD.X R5, R5, RZ; /*00e8*/ /*0x200001e74003fffe*/ @P0 BRA 0x78; /*00f0*/ /*0x5001dc23188e4005*/ ISETP.LT.AND P0, pt, R0, c [0x0] [0x154], pt; /*00f8*/ /*0x000021e780000000*/ @!P0 EXIT; /*0108*/ /*0x00401c8584000000*/ LD.E R0, [R4];
TeslaC2050(Fermi世代)がでたころ、社内でsm_20だとsumがちゃんと動かないんだけど…とかメールがあった。
原因としては、reductionするときのメモリバリアをちゃんと書いていなかったというものだった。
当時のCUDAマニュアルには、"__threadfence_block()書かないとコンパイラがメモリアクセス消しちゃうからちゃんと__threadfence_block()書いてね" とか書いてあるが、その現象では、コンパイラはちゃんとコード出してた、が、__threadfence_block()を入れると問題が解決した。
→ __threadfence_block() は、コンパイラの最適化を抑制する以上の効果があるのか?
というときも、SASSを見れば一発
// __global__ void f() { __threadfence_block(); } Fatbin elf code: ================ arch = sm_20 code version = [1,4] producer = cuda host = windows compile_size = 32bit identifier = tf.cu code for sm_20 Function : _Z2tfv /*0000*/ /*0x00005de428004404*/ MOV R1, c [0x1] [0x100]; /*0008*/ /*0x00001c05e0000000*/ MEMBAR.CTA; /*0010*/ /*0x00000007d0000000*/ BPT.DRAIN 0x0; /*0018*/ /*0x00001de780000000*/ EXIT; ....................... Fatbin elf code: ================ arch = sm_10 code version = [1,2] producer = cuda host = windows compile_size = 32bit identifier = tf.cu code for sm_10 Function : _Z2tfv /*0000*/ /*0xf0000001e0000001*/ NOP; .......................
sm_10 では、何もしていないが、sm_10 では、member.cta というのが出ている。→ sm_20では、なんかメモリモデル変だからメモリバリア必要か、と推測できる
(自分ではここまで追いつめられたことないのでよく知らない)
CUDAではレジスタ数、スレッド数のバランスが結構センシティブなのだが、 ptxas の maxreg で調整するとスケジューリング悪くなって、でもPTXだとレジスタよくわからなくて…とかになる(らしい)
SASSは実レジスタが見えるので、そういう時もどこでレジスタ足りなくなってるか、とか確認できる
2ch の トリップ検索スレによると実際に確認してる人がいるらしい
492 : ◆MERIKEN4.k :2011/09/11(日) 01:06:24.80 ID:tGzUJSSY0 >>490 SASS見てみましたけど、予想通りS-Boxのあたりでレジスタを大量に消費してました。 30以上使ってたので、実際に必要な数よりかなり多いですね。 cubinとPTXのコードが違うというのはもちろん理解してますけど、 実際の挙動はPTXのコードとプロファイリングの結果からある程度予測できるという印象です。 >>491 自分の調べた限りでは、現在のBitslice DESの最小ゲート数の実装は、John the Ripperで 使われているRoman Rusakov氏のものでした。>>323のSlashdotの記事で紹介されてますけど、 Kahn氏のオリジナルより17%ゲート数が少ないとのことです。
PTXよりも情報量あるので、PTX読むくらいならSASS読みましょう
この文書は関東GPGPU勉強会のために書かれました。