Sök…


Introduktion

Detta ämne introducerar några av de underliggande kärnmekanikerna för parallell datoranvändning som behövs för att fullt ut förstå och använda OpenCL.

Trådar och körning

Parallellismens nyckel är att använda flera trådar för att lösa ett problem (duh.) Men det finns vissa skillnader i klassisk multitrådad programmering i hur trådar är organiserade.

Låt oss först prata om din typiska GPU, för enkelhets skull ska jag fokusera på

En GPU har många processorkärnor, vilket gör det idealiskt att köra många trådar parallellt. Dessa kärnor är organiserade i Streaming-processorer (SM, NVidia-term), varav en GPU har ett visst antal.

Alla trådar som körs inom en SM kallas ett "trådblock". Det kan finnas fler trådar på en SM än det har kärnor. Antalet kärnor definierar den så kallade 'varpstorlek' (NVidia-term). Trådar inuti ett trådblock kastas i så kallade "varp".

Ett snabbt exempel att följa upp: En typisk NVidia SM har 32 bearbetningskärnor, varför dess varpstorlek är 32. Om mitt trådblock nu har 128 trådar att köra kommer de att kastas i 4 varp (4 varp * 32 varpstorlek = 128 trådar).

Varpstorleken är ganska viktig när du väljer antalet trådar senare.

Alla trådar i en enda varp delar en enda instruktionsräknare. Det betyder att de 32 trådarna verkligen är synkroniserade genom att varje tråd utför varje kommando samtidigt. Här ligger en prestationsfallgrop: Detta gäller också förgreningsuttalanden i din kärna!

Exempel: Jag har en kärna som har ett if-uttalande och två grenar. 16 av mina trådar inuti ett varp kommer att utföra gren en, den andra 16 gren två. Fram till if-uttalandet är alla trådar inuti varpen synkroniserade. Nu väljer hälften av dem en annan gren. Vad som händer är att den andra hälften kommer att ligga vilande tills det felaktiga uttalandet har slutförts på de första 16 trådarna. Sedan kommer dessa trådar att vara vilande tills de andra 16 trådarna har slutat sin gren.

Som ni kan se kan dåliga grenvanor bromsa din parallella kod allvarligt, eftersom båda uttalandena verkställs i värsta fall. Om alla trådar inuti ett varp bestämmer sig för att de bara behöver en av uttalandena, hoppas den andra helt över och ingen fördröjning inträffar.

Synkronisering av trådar är inte heller en enkel sak. Du kan bara synkronisera trådar med en enda SM. Allt utanför SM kan inte synkroniseras inifrån kärnan. Du måste skriva separata kärnor och starta dem en efter en.

GPU-minne

GPU erbjuder sex olika minnesregioner. De skiljer sig i deras latens, storlek och tillgänglighet från olika trådar.

  • Globalt minne: Det största tillgängliga minnet och ett av få som kan utbyta data med värden. Det här minnet har den högsta latensen och är tillgängligt för alla trådar.
  • Konstant minne: En läsbar del av det globala minnet, som bara kan läsas av andra trådar. Dess fördel är den lägre latensen jämfört med det globala minnet
  • Texturminne: Även en del av konstant minne, speciellt designad för texturer
  • Delat minne: Det här minnesområdet är placerat nära SM och kan endast nås via ett enda trådblock. Det erbjuder mycket lägre latens än det globala minnet och lite mindre latens än det konstanta minnet.
  • Register: Endast tillgängligt med en enda tråd och det snabbaste minnet av dem alla. Men om kompilatorn upptäcker att det inte finns tillräckligt med register för kärnbehovet kommer det att lägga ut variabler till lokalt minne.
  • Lokalt minne: En tråd-endast tillgänglig del av minnet i det globala minnesområdet. Används som säkerhetskopia för register, för att undvikas om möjligt.

Minneåtkomst

Det typiska scenariot för din minnesanvändning är att lagra källdata och bearbetade data i det globala minnet. När en trådblock startar, kopierar den först alla relevanta delar till det delade minnet innan de får sina delar i registret.

Minneåtkomst latens beror också på din minnesstrategi. Om du öppet får åtkomst till data får du den sämsta prestanda som möjligt.

De olika minnen är organiserade i så kallade "banker". Varje minnesförfrågan för en bank kan hanteras i en enda klockcykel. Antalet banker i det delade minnet är lika med varpstorleken. Minnets hastighet kan ökas genom att undvika motstridande bankåtkomst i ett enda varp.

För att kopiera delat minne från eller till globalt minne är det snabbaste sättet att "anpassa" dina minnessamtal. Detta innebär att den första tråden i ett varp ska komma åt det första elementet i banken i både det delade och globala minnet. Den andra tråden det andra elementet och så vidare. Detta samtal kommer att optimeras till en enda minnesöverföringsinstruktion som kopierar hela banken till målminnet på en gång.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow