CUDA çekirdeğini çalıştırdığınızda içeride neler olur
(fergusfinn.com)- Basit bir vektör toplama CUDA programı bile
2.000000sonucunu elde edene kadar derleme hattı, sürücü çağrıları, GPU komut kuyruğu, warp zamanlama, bellek hiyerarşisi ve tamamlama semaforlarından geçer nvcc, host kodu ile device kodunu ayırır;ciccile PTX,ptxasile SASS üretir ve cubin ile PTX'i bir fatbin içinde paketleyip Linux yürütülebilir dosyasının içine yerleştirirvadd<<<4096, 256>>>launch sözdizimi bir host launch stub'a dönüştürülür;da,db,dc,nargümanları CUDA runtime velibcuda.so.1üzerinden sürücüye iletilir- GPU yürütmesi QMD, pushbuffer, GPFIFO,
GP_PUTve doorbell MMIO yazımıyla başlar; RTX 4090'ın 128 SM'i 4096 blok ve 256 thread yapılandırmasını warp düzeyinde yürütür - Bu çekirdek, her float toplama başına 12 bayt aktarım gerektiren düşük aritmetik yoğunluk nedeniyle Nsight Compute'ta 10.78μs, DRAM tepesinin %79.65'i ve %5.17 warp issue ile bellek bant genişliğine bağımlıdır
Örnek çekirdek ve inceleme kapsamı
- Örnek program, iki float dizisini toplayıp üçüncü diziye yazan
vaddCUDA çekirdeğini kullanırn = 1 << 20ile 1.048.576 adet float işlenir- launch yapılandırması
vadd<<<4096, 256>>>(da, db, dc, n)olup4096 * 256 = nthread kullanır
- RTX 4090 için
nvcc -arch=sm_89ile derlenip çalıştırıldığındac[0]=2.000000 c[n-1]=2.000000çıktısı alınır - Bu tek satırlık sonucun arkasında bile on milyonlarca CPU komutu, device file, yaklaşık 900 adet
ioctlve belleğe eşlenmiş doorbell register'ı bulunur
nvcc yürütülebilir dosyayı nasıl oluşturur
nvcc --keepkullanıldığında derleme hattısının çıktıları doğrudan görülebilirvadd.ptx:cicctarafından üretilen device kodunun PTX'ivadd.sm_89.cubin:ptxastarafından üretilen device kodunun SASS'ıvadd.fatbin: cubin ve PTX'i bir araya getiren fatbinvadd.cudafe1.stub.c: host launch stub ve çekirdek kayıt koduvadd.o: fatbin'i içeren son host object
- Host kodu host derleyici tarafından işlenirken,
vadddevice çekirdeğiciccveptxasaşamalarından geçer - PTX, tipli sonsuz sanal register kullanan bir sanal ISA'dır ve gerçek donanımdaki register sayısını doğrudan yansıtmaz
- Örnek PTX;
blockIdx.x * blockDim.x + threadIdx.xhesabı, sınır denetimi, global load, float add ve global store içerir - CUDA pointer'ları varsayılan olarak generic pointer olduğu için,
ld.globalkullanılmadan öncecvta.to.globalile global adrese dönüştürülür mul.wide.s32, index'isizeof(float)olan 4 baytlık ofsete çevirir ve 32 bitten 64 bite genişletir
- Örnek PTX;
- SASS, mimariye özgü gerçek komutlardır ve RTX 4090 hedefli çıktıda PTX'ten daha sıkıştırılmış görünür
S2R,SR_CTAID.X,SR_TID.Xgibi özel register'ları genel amaçlı register'lara kopyalar- PTX'teki
mul.wideileaddbirleşimi SASS'taIMAD.WIDEolarak birleştirilir cvtadönüşümü adresleme sürecine yedirilir
c[0x0][...]operandı, sürücü tarafından yönetilen constant bank 0'ı işaret edera,b,cpointer'ları0x160,0x168,0x170konumlarındadırn,0x178konumundadırblockDim.xgibi launch geometrisi ve ABI değerleri de aynı bankta yer alır
- cubin, Linux yürütülebilir dosyalarıyla aynı kapsayıcı biçim olan bir ELF dosyasıdır
- fatbinary, cubin ve PTX'i birlikte paketler
- Bu RTX 4090 üzerinde gerçek yürütme SASS ile yapılır, ancak PTX başka mimarilerde sürücünün JIT derleme yapabilmesi için fallback olarak eklenir
- PTX ayrıntılı bir düz metin olduğundan
nvccbunu varsayılan olarak sıkıştırır
Host kodu launch'ı nasıl hazırlar
- Derleyici frontend'i
cudafe++,main'den önce çalışan gizli bir constructor ekler- Bu constructor, gömülü fatbinary'yi CUDA runtime'a kaydeder
- Host tarafındaki
vaddfonksiyon pointer'ını, fatbin içindeki mangled device kernel name ile eşler
vadd<<<4096, 256>>>(da, db, dc, n)sözdizimi, üretilmiş bir host launch stub ile değiştirilirda,db,dc,n; host belleğindeki argument buffer'a sırasıyla0,8,16,24ofsetleriyle hizalanarak yerleştirilir- Bu ofsetler, SASS'ın constant bank 0'dan okuduğu
0x160,0x168,0x170,0x178konumlarıyla eşleşir
- Stub,
__cudaLaunchçağrısı yaparken host tarafındaki dummyvaddfonksiyon adresini iletir- Bu adres, CPU'da çalıştırılacak bir fonksiyon adresi değil; runtime kayıt tablosunda arama yapmak için kullanılan bir anahtardır
- Runtime karşılık gelen device symbol name'i bulduktan sonra kapalı kaynak user-mode driver olan
libcuda.so.1e geçer
- İlk GPU çağrısında CUDA runtime,
libcuda.so.1dosyasını dinamik olarak açar ve bir context oluştururstraceiçinde/lib/x86_64-linux-gnu/libcuda.so.1dosyasının açıldığını görmek mümkündür- Bu context, CPU'nun GPU ile haberleştiği channel'ı içerir
- CUDA 12.2'den itibaren module loading varsayılan olarak lazy'dir
- Belirli bir çekirdek ilk kez launch edilene kadar SASS cubin yüklemesi ertelenir
- Bu davranış
CUDA_MODULE_LOADINGile kontrol edilebilir
GPU'ya iş aktaran komut kuyruğu
- GPU, CPU gibi fonksiyon çağrısı alıp bir entry point'e atlamaz
- Bunun yerine, PCIe bus üzerinden host belleğindeki sürücü komut akışını okur
cuLaunchKernel, tamamlanmış launch komutunu bu akışa yerleştirir ve GPU'ya haber verir
- İlk çalıştırmada sürücü, çekirdeğin SASS kodunu GPU belleğine kopyalar
- Bir code buffer ayırır ve SASS'ı buraya kopyalar
- Channel içinde host RAM'de duran iki temel yapı vardır
- pushbuffer: sürücünün GPU komutu olan method'ları yazdığı bellek alanı
- GPFIFO: pushbuffer span'lerini gösteren bir pointer ring buffer
- Bir GPFIFO girdisi, pushbuffer span'inin
(base, length)bilgisini taşıyan iki adet 32 bitlik word'den oluşur - GPU ve sürücü, tüketim ve üretim konumlarını iki cursor ile izler
GP_GET: GPU'nun ne kadarını tükettiğini gösterirGP_PUT: sürücünün ne kadarını ürettiğini gösterir- Her ikisi de USERD adlı channel başına yapıda bulunur
- Çekirdek launch edildiğinde sürücü, pushbuffer span'ine method'ları yazar, GPFIFO girdisini buna işaret ettirir ve ardından
GP_PUTdeğerini ilerletir - Modern GPU'larda host engine cursor'ları sürekli izlemediği için doorbell gerekir
- GPU, prosese küçük bir register window eşler
- Sürücü, channel'ın work-submit token'ını doorbell register'ına yazar
- Host engine, doorbell sinyalini aldıktan sonra
GP_PUTdeğerini okur ve GPFIFO girdisiyle pushbuffer span'ini DMA üzerinden getirir
QMD'nin taşıdığı yürütme bilgisi
- Launch,
SET_INLINE_QMD_ADDRESS_A/BveLOAD_INLINE_QMD_DATAmethod burst'ü ile başlar - QMD(Queue Meta Data), compute grid için kullanılan launch descriptor'dur
4096ve256olan grid ve block boyutlarını içerir- Thread başına register sayısı ile shared memory gereksinimini içerir
- Program başlangıç adresi ile çekirdek argümanlarını taşıyan constant bank adresini içerir
- Tamamlamanın nereye bildirileceğini de içerir
- Host stub'ın paketlediği argümanlar sürücü tarafından constant bank'e kopyalanır ve bu bankın adresi QMD'ye yazılır
- QMD, GPU'ya SASS'ın nerede olduğunu, paralel programın nasıl yapılandırılacağını ve completion signal'ın nereye yazılacağını söyler
cuLaunchKernel, doorbell çaldığı anda geri döner- Çağrı asenkron olduğu için CPU, GPU işi sürerken çalışmaya devam edebilir
SM, warp ve doluluk
- Host engine, QMD'yi compute work distributor'a iletir
- Bu bileşen tüm GPU üzerinde tektir
- Doğrusal SASS instruction stream'ini SM'lere dağıtarak paralel program olarak yürütülmesini sağlar
- Hedef GPU olan GeForce RTX 4090, 128 SM kullanır
- Launch, 4096 bloktan ve blok başına 256 thread'den oluşur
- Her SM'in yerel bir instruction cache'i vardır ve aktif warp'lar kendi program counter'larını korur
- Volta sonrasında thread başına program counter ve call stack sunan Independent Thread Scheduling modeli vardır
- Ancak issue işlemi hâlâ warp düzeyinde yapılır
- Örnek çekirdekte block residency'yi kaynak sınırları belirler
- Blok başına
256 threads = 8 warps ptxas, thread başına 16 register ayırır- Register açısından SM başına 16 blok mümkündür
- Ancak thread kapasitesi SM başına 1.536 aktif thread olduğundan
1536 / 256 = 6blokla sınırlanır - Dolayısıyla SM başına en fazla 6 blok, yani 48 warp resident durumda olabilir
- Blok başına
- SM, 4 processing block yani sub-partition'a ayrılır
- Resident 48 warp, 4 sub-partition arasında eşit dağıtılır
- Her warp scheduler tam dolu durumda 12 aktif warp yönetir
- Her cycle'da eligible olan bir warp seçilir ve sonraki komut 32 lane'e dispatch edilir
Bir warp'ın eligible duruma gelme koşulları
- GPU, CPU'nun out-of-order yürütmesine benzer biçimde tek bir thread içinden büyük ölçüde dinamik bağımlılık çıkarmaz
- Bunun yerine çok sayıda resident warp tutar ve biri stall olduğunda gecikmeyi gizlemek için diğerine geçer
- Derleyici öngörülebilir zamanlamayı planlar, donanım scoreboard ise öngörülemez kısımları yönetir
- 128 bitlik SASS instruction içinde
ptxastarafından yazılan control-code payload bulunur- Sabit gecikmeli instruction'larda statik stall count yer alır
- Yield hint, scheduler önceliğinin bırakılıp bırakılmayacağını belirtir
- Değişken gecikmeli işlemler için warp başına 6 fiziksel scoreboard barrier kullanılır
- Örnek SASS bölümünde iki
LDG.E, aynı scoreboard barrier olanB2yi set ederFADD,B2için wait-on bilgisi taşır- İki load geri dönüp barrier temizlenene kadar o warp ineligible durumda kalır
- Bu sırada scheduler aynı sub-partition'daki başka bir warp'ı seçer
FADDileSTG.Earasındaki bölüm sabit gecikme olarak ele alınırFADD,stall=5taşır veR9sonucu hazır olana kadar warp'ı birkaç cycle bekletir- Ayrı bir barrier gerekmez
- Bu control payload,
nvdisasmvarsayılan çıktısında gizlidircuobjdump -sasstarafından verilen ham 128 bit encoding'de ikinci 64 bitlik word içinde yer alır- Yerleşim belgelenmiş değildir; microbenchmarking ile yeniden oluşturulmuştur
Bellek erişimi ve performans ölçümü
- Warp,
LDG.Eyürüttüğünde 32 thread'in her biri kendi adresini hesaplar- Örnekte ardışık float dizi erişimi olduğu için, tüm warp
32 * 4 = 128 bytesboyutunda ardışık bir blok ister
- Örnekte ardışık float dizi erişimi olduğu için, tüm warp
- SM load/store unit, request coalescing yapar
- 32 adet 4 baytlık isteği 4 adet 32 baytlık sector request'e birleştirir
- Erişimler ardışık olmasaydı, gerekenden daha fazla veri okunabilirdi
- Coalesced request önce SM içindeki yerel L1 Data Cache'i kontrol eder
- Miss olursa crossbar interconnect üzerinden 72MB L2 Cache slice'a gider
- L2'de de miss olursa memory controller ve memory bus üzerinden GDDR6X VRAM'e gider
STG.Estore işlemi de ilke olarak bunun ters yönündeki aynı yolu izler- Nsight Compute ölçümleri, bu çekirdeğin memory-bound olduğunu gösterir
launch__grid_size: 4.096launch__block_size: 256launch__registers_per_thread: 16launch__waves_per_multiprocessor: 5.33sm__warps_active.avg.pct_of_peak: 82.77%smsp__issue_active.avg.pct_of_peak: 5.17%dram__throughput.avg.pct_of_peak: 79.65%gpu__time_duration.sum: 10.78μs
- Çekirdeğin aritmetik yoğunluğu çok düşüktür
- İki adet 4 baytlık load ve bir adet 4 baytlık store ile toplam 12 bayt aktarım başına yalnızca bir float toplama yapılır
- DRAM okuma açısından bakıldığında, 8.4MB veri 10.78μs'de okunarak yaklaşık 780GB/s elde edilir; bu, tepe değerin yaklaşık beşte dördüdür
- 4MB boyutundaki
cçıktısı 72MB L2'ye sığdığı için, device-to-host copy bunu okumadan önce DRAM'e flush edilmez
Sonucun CPU'ya geri dönmesi
- Kernel launch, doorbell çaldığı anda CPU'ya döndüğünden, GPU tamamlandığını ayrıca bildirmek zorundadır
- 4096 bloğun tamamı retire edildiğinde GPU, QMD'de belirtilen completion semaphore'u post eder
- QMD'nin fence alanı 23–24. word'lerde yer alır
- Varsayılan stream'de
cudaMemcpy(c, dc, ...), çekirdeğin arkasına yerleştirilir- GPU copy engine, semaphore yükselene kadar gated durumda kalır
châlâ 72MB L2 içinde dirty durumda olduğundan, copy engine read işlemi DRAM'e gidip gelmeden L2 üzerinden tamamlanır- Veri, PCIe üzerinden host belleğine taşınır
- Kopyalama bittiğinde copy engine kendi semaphore'unu post eder
- Host tarafındaki
cudaMemcpybekleyişi sona erer cyeniden sıradan host belleği olurprintf,c[0]vec[n-1]değerlerini RAM'den okuyup stdout'a yazar
- Host tarafındaki
Launch'ın içini nasıl görebilirsiniz
- Açık kernel modüllerini okumak tek başına yeterli değildir; çünkü
libcudakapalı kaynak olduğundan bazı davranışlar doğrudan doğrulanamaz - Method write işlemleri syscall üzerinden geçmez; önceden eşlenmiş write-combined buffer'a doğrudan yazılır, bu yüzden pushbuffer'ı görmek için belleğin kendisini okumak gerekir
LD_PRELOADshim ilemmapsarmalanarak/dev/nvidia*üzerinde eşlenen bölgeler kaydedilebilir- Test programı launch'tan hemen sonra shim'in dump fonksiyonunu çağırırsa, eşlenen pushbuffer çıktılanabilir
- Dump,
SET_INLINE_QMD_ADDRESS_Aile ilişkili method burst'ü arar
- Pushbuffer method header'ı, opcode, payload count, subchannel index ve register offset'i bit alanlarında taşır
0x0318,SET_INLINE_QMD_ADDRESS_A'ya karşılık gelir0x0320 + i * 4,LOAD_INLINE_QMD_DATA(i)anlamına gelir- Dump içinde count değeri 66 olan increasing-method burst görülür; 2 adres word'ü ile 64 QMD word'ü, yani toplam 256 baytlık QMD inline biçimde taşınır
- QMD içindeki 12. word
0x1000, 18. word ise0x100olup launch'taki 4096 ve 256 değerlerine karşılık gelir
- Sürücü kurulumu
ioctlile yapılır- Tek çekirdekli bir programda
strace, 948 adetioctlkaydeder - Bunların çoğu tek seferlik kurulum işlemleridir
- Başlıca file descriptor'lar
/dev/nvidiactlve/dev/nvidia-uvm'dir - NVIDIA resource manager ioctl magic byte'ı
0x46, yani'F'tir 0x2Akomut numarasıNV_ESC_RM_CONTROL,0x2BiseNV_ESC_RM_ALLOColarak yorumlanır
- Tek çekirdekli bir programda
nvcc --keepile üretilenvadd.cudafe1.stub.ciçinde başlangıç kayıt kodu da görülebilir__attribute__((__constructor__))ile işaretlenen bir fonksiyonmain'den önce çalışır__cudaRegisterBinaryve__cudaRegisterEntryaracılığıyla host function pointervaddile device entry point_Z4vaddPKfS0_Pfibirbirine bağlanır
1 yorum
Hacker News görüşleri
İlgi çekici bir yazıydı; varsayılan akışın semaforları açıklaması da güzeldi
CUDA'nın komut senkronizasyonunu örtük olarak halletmesi ve paralel komutları akışlar üzerinden isteğe bağlı kullandırması hoş
Senkronizasyon karmaşıklığının tamamını en baştan kullanıcıya yükleyen Vulkan ile tezat oluşturuyor
Donanım tarafında bazı açık belgeler var
Metot dokümantasyonunu ya da QMD biçimini bulmak için mutlaka kernel kaynağını okumak gerekmiyor
https://github.com/NVIDIA/open-gpu-doc/blob/master/classes/c... bkz.
Çok faydalıydı
Özellikle doorbell ve QMD kısmı, CUDA çalıştırma söz diziminin GPU'ya gerçekten gönderilen içerikle nasıl bağlandığını gösterdiği için en yararlı bölümdü
Çoğu açıklama kernel, blok ve warp civarında duruyor; bu yazıysa CPU→sürücü→GPU yolunu takip etmeyi çok daha kolaylaştırıyor
Kontrol kodu, yazıda anlatılandan biraz daha karmaşık
Gerçekte kontrol sözcüğü içindeki bitlerden ziyade daha çok tablo araması gibi
Artık kernel'leri optimize edip daha hızlı çalıştırmayı ana işi yapan şirketler var
Bu şirketlerin bir gün bunu çok iyi yapan bir açık kaynak kütüphaneye yenilip yenilmeyeceğini merak ediyorum
Nvidia'nın böyle bir şeyi istediği anda çıkarabilecek gibi bir hali var
Ya da büyük sağlayıcılar çıkarım hızını artıran bir
moatolarak görmek için bu şirketleri satın alırsa daha da iyi durumda olabilirlerYine de kernelbench gibi ilgili benchmark'larda modellerin gelişimine bakınca, daha genel amaçlı çözümler de sonunda ortaya çıkacaktır diye düşünüyorum
Sorun şu ki her yeni donanım neslinde mevcut modellerin daha önce görmediği kısıtlar ya da özellikler sık sık çıkıyor
Örneğin Blackwell'in tcgen05'i bir dönem dağılım dışı bir örnekti
Modeller daha iyi genelleme yapmaya başladığında bu ölümcül bir engel olmayabilir ama en azından şu anda hâlâ bir pürüz
[1] https://kernelbench.com/
Nvidia kütüphanelerine daha fazla bağımlı olmayı bekleyen pek kimse görmedim
Çünkü iş yükünün ayrıntıları; yani tam parametreler, bellekteki veri temsili, değer aralıkları vb. optimizasyon stratejilerini büyük ölçüde ayrıştırıyor
HPC yüksek lisansını yeni bitirdim; CUDA, MPI+CUDA ve OpenCL dersleri aldım, bu tür bir yazıyı derslerden önce okumuş olsam çok daha faydalı olurdu
Özellikle bir warp'ın çalıştırılabilir olmasının ne anlama geldiğini ele alan bölümün öncesi ve sonrası çok iyiydi
Öncelikle, pek çok köşeyi bucağı iyi kurcalayan güzel bir yazı
Ancak CUDA'nın
runtime APIsi kullanılmadığında kullanıcı alanındaki büyü gibi görünen birçok kısım ortadan kalkıyorSürücü API'sini kullanıp kernel kaynağını string olarak alarak NVIDIA'nın çalışma zamanı derleyicisiyle derlerseniz, neler olduğunu çok daha iyi görebilirsiniz
Hepsi değil ama epey büyük bir kısmı şeffaflaşıyor
Daha “ham” bir sürüm burada:
https://github.com/NVIDIA/cuda-samples/tree/master/cpp/0_Int...
Aynı şeyi çok daha okunabilir ama yine de tamamen şeffaf bir modern C++ API biçiminde görmek isterseniz buna bakın:
https://github.com/eyalroz/cuda-api-wrappers/blob/master/exa...
Bu, benim CUDA API wrappers başlık-dosyası-yalnız kütüphanemin örnek programı
Çalışma sırasında kodu değiştirerek geliştirme yapabilmek eğlenceli
Bare metal'de mi?