38
GPU programming: Warp-synchronous programming Sylvain Collange Inria Rennes – Bretagne Atlantique [email protected]

GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

  • Upload
    ngophuc

  • View
    237

  • Download
    0

Embed Size (px)

Citation preview

Page 1: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

GPU programming:Warp-synchronous programming

Sylvain CollangeInria Rennes – Bretagne Atlantique

[email protected]

Page 2: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

2

Bypassing SIMT limitationsMulti-thread + SIMD

Across SIMD lanes of same thread

Direct communicationwithout synchronization

Between threads

All synchonization primitives,wait-based and lock-basedalgorithms

SIMT model

Common denominator ofoperations allowedin SIMD and multi-thread

No direct communication

No blocking algorithm

If we know the warp/thread organization,we can pretend we program a multi-thread + SIMD machine

Drawback: give up automatic divergence management

Page 3: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

3

Outline

Inter-thread communication

Warp-based execution and divergence

Intra-warp communication:warp-synchronous programming

Page 4: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

4

Inter-thread/inter-warp communication

Barrier: simplest form of synchronization

Easy to use

Coarse-grained

Atomics

Fine-grained

Can implement wait-free algorithms

May be used for blocking algorithms (locks)among warps of the same block, but not recommended

Communication through memory

Beware of consistency

Page 5: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

5

Atomics

Read, modify, write in one operation

Cannot be mixed with accesses from other thread

Available operators

Arithmetic: atomic{Add,Sub,Inc,Dec}

Min-max: atomic{Min,Max}

Synchronization primitives: atomic{Exch,CAS}

Bitwise: atomic{And,Or,Xor}

On global memory, and shared memory

Performance impact in case of contention

Atomic operations to the same address are serialized

Page 6: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

6

Example: reduction

After local reduction inside each block,use atomics to accumulate the result in global memory

Complexity?

Time including kernel launch overhead?

Block 0Block 1

Block 2

Global memory

accumulator

atomicAddatomicAdd

atomicAdd

Block n

atomicAdd

Result

Page 7: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

7

Example: compare-and-swap

Use case: perform an arbitrary associative and commutative operation atomically on a single variable

atomicCAS(p, old, new) does atomically

if *p == old then assign *p←new, return old

else return *p

__shared__ unsigned int data;unsigned int old = data; // Read onceunsigned int assumed;do {

assumed = old;newval = f(old, x); // Computeold = atomicCAS(&data, old, newval); // Try to replace

} while(assumed != old); // If failed, retry

Page 8: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

8

Memory consistency model

Nvidia GPUs (and compiler) implement a relaxed consistency model

No global ordering between memory accesses

Threads may not see the writes/atomics in the same order

Need to enforce explicit ordering

T1 T2

write A(valid data)

write B(readyflag)

read A

read B New value of B(ready=true)

Old value of A(invalid data)

Page 9: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

9

Enforcing memory ordering: fences

__threadfence_block__threadfence__threadfence_system

Make writes preceding the fence appear before writes following the fencefor the other threads at the block / device / system level

Make reads preceding the fence happen after reads following the fence

Declare shared variables as volatileto make writes visible to other threads(prevents compiler from removing “redundant” read/writes)

__syncthreads implies __threadfence_block

T1 T2

write A

write B read A

read B Old/New value of B

New value of A

__threadfence() Or, old values of A, B__threadfence()

Page 10: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

10

Outline

Inter-thread communication

Warp-based execution and divergence

Intra-warp communication:warp-synchronous programming

Page 11: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

11

Warp-based execution

Reminder

Threads in a warp run in lockstep

On current NVIDIA architectures, warp is 32 threads

A block is made of warps

Warps do not cross block boundaries

Block size multiple of 32 for best performance

Page 12: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

12

Branch divergence

Conditional block

When all threads of a warp take the same path:

if(c) {// A

}else {

// B}

Warp 0 Warp 1

A B

T0T

1T

2T

3T

4T

5T

6T

7 With imaginary 4-thread warps

Page 13: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

13

Branch divergence

Conditional block

When threads in a warp take different paths:

Warps have to go through both A and B: lower performance

if(c) {// A

}else {

// B}

Warp 0 Warp 1

A

B

A

B

Page 14: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

14

Avoiding branch divergence

Hoist identical computations and memory accessesoutside conditional blocks

if(tid % 2) {s += 1.0f/tid;

}else {

s -= 1.0f/tid;}

float t = 1.0f/tid;if(tid % 2) {

s += t;}else {

s -= t;}

When possible, re-schedule work to make non-divergent warps

// Compute 2 values per threadint i = 2 * tid;s += 1.0f/i – 1.0f/(i+1);

What if I use C's ternary operator (?:) instead of if?(or tricks like ANDing with a mask, multiplying by a boolean...)

Page 15: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

15

Ternary operator ? good : bad

Run both branches and select:No more divergence?

All threads have to take both pathsNo matter whether the condition is divergent or not

Does not solve divergence: we lose in all cases!

Only benefit: fewer instructionsMay be faster for short, often-divergent branches

Compiler will choose automatically when to use predicationAdvice: keep the code readable, let the compiler optimize

R = c ? A : B;

Warp 0 Warp 1

A

B

A

B

Page 16: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

16

Barriers and divergence

Remember barriers cannot be called in divergent blocks

All threads or none need to call __syncthreads()

If: need to split

Loops: what if trip count depends on data?

if(p) { ... // Sync here? ...}

if(p) { ...}__syncthreads();if(p) { ...}

while(p) { ... // Sync here? ...}

?

Page 17: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

17

Barriers instructions

Barriers with boolean reduction

__syncthreads_or(p) / __syncthreads_and(p)Synchronize, then returns the boolean OR / AND of all thread predicates p

__syncthreads_count(p)Synchronize, then returns the number of non-zero predicates

Loops: what if trip count depends on data?

Loop while at least one thread is still in the loop

while(p) { ... // Sync here? ...}

while(__syncthreads_or(p)) { if(p) { ... } __syncthreads(); if(p) { ... }}

Page 18: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

18

Outline

Inter-thread communication

Warp-based execution and divergence

Intra-warp communication:warp-synchronous programming

Page 19: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

20

Warp-synchronous programming

We know threads in a warp run synchronously

No need to synchronize them explicitly

Can use SIMD (PRAM-style) algorithms inside warps

Example: last steps of a reduction

t0 t1 t2

warp 0

__syncthreads()

__syncthreads()

×64

x32

×128...

...

No syncthreads needed,but still needs fences(and volatile)

warp 1

t31

__threadfence_block()

__threadfence_block()

__threadfence_block()

__threadfence_block()

Page 20: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

21

Warp-synchronous programming: tools

We need warp size, and thread ID inside a warp: lane ID

Predefined variable: warpSize

Lane ID exists in PTX, not in C for CUDACan be recovered using inline PTX

__device__ unsigned int laneID() { unsigned int lane; asm volatile("mov.u32 %0, %%laneid;" : "=r"(lane)); return lane;}

We can emit PTX using the asm keyword,but remember PTX is not assembly.

Page 21: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

22

Warp vote instructions

p2 = __all(p1)horizontal AND between predicates p1of all threads in the warp

p2 = __any(p1)OR between all p1

n = __ballot(p)Set bit i of integer nto value of p for thread ii.e. get bit mask as an integer

10 10 10 11 00 10 10 00

0

00 00 00 00 00 00 00 00

t0 t1 t2 t31

AND

10 10 10 11 00 10 10 00

1

11 11 11 11 11 11 11 11

t0 t1 t2 t31

OR

10 10 10 11 00 10 10 00

0101011100010100

t0 t1 t2 t31

0x28EA=0010100011101010

Like __syncthreads_{and,or} for a warpUse: take control decisions for the whole warp warp

Page 22: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

23

Manual divergence management

How to write an if-then-elsein warp-synchronous style?i.e. without breaking synchronization

Using predication:execute both sides always

Using vote instructions:only execute taken pathsSkip block if no thread takes it

How to write a while loop?

if(p) { A(); }// Threads synchronizedif(p) { B(); }// Threads synchronizedif(!p) { C(); }

if(p) { A(); B(); }else { C(); }

if(__any(p)) {if(p) { A(); }// Threads synchronizedif(p) { B(); }

}if(__any(!p)) {

// Threads synchronizedif(!p) { C(); }

}

Page 23: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

24

Ensuring reconvergenceWhen and where do threads reconverge?

No hard guarantees today

e.g. Can the compiler alter the control flow?

Will hopefully get standardized [1]

In practice, we can assume reconvergence:

After structured if-then-else

After structured loops

At function return

Example: can we assume reconvergence here?

[1] B. Gaster. An Execution Model for OpenCL 2.0. 2014

if(threadIdx.x != 0) { Divergent condition←if(blockIdx.x == 0) return; Uniform condition←

}// Threads synchronized at this point ?

Page 24: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

25

Ensuring reconvergenceWhen and where do threads reconverge?

No hard guarantees today

e.g. Can the compiler alter the control flow?

Will hopefully get standardized [1]

In practice, we can assume reconvergence:

After structured if-then-else

After structured loops

At function return

Example: can we assume reconvergence here?

No. Return creates unstructured control flow.Will or won't reconverge depending on the architecture.

[1] B. Gaster. An Execution Model for OpenCL 2.0. 2014

if(threadIdx.x != 0) { Divergent condition←if(blockIdx.x == 0) return; Uniform condition←

}// Threads synchronized at this point ?

Page 25: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

26

Shuffle

Exchange data between lanes

__shfl(v, i)Get value of thread i in the warp

Use: 32 concurrent lookupsin a 32-entry table

Use: arbitrary permutation...

__shfl_up(v, i) ≈ __shfl(v, tid-i), __shfl_down(v, i) ≈ __shfl(v, tid+i)Same, indexing relative to current lane

Use: neighbor communication, shift

__shfl_xor(v, i) ≈ __shfl(v, tid ^ i)

Use: exchange data pairwise: “butterfly”

10 32 54 76 98 1110 1312 31

211 132 90 74 3112 14 129 710

t0 t1 t2 t31

warp

Page 26: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

27

Example: reduction + broadcast

Naive algorithm

a0 a1 a2 a3 a4 a5 a6 a7

Σ0-1 Σ2-3 Σ4-5 Σ6-7⊕ ⊕ ⊕

Σ0-3 Σ4-7⊕

Σ0-7

Step 0

Step 1

Step 2

P0 P1 P2 P3 P4 P5 P6 P7

a[2*i] a[2*i] + a[2*i+1]←

a[4*i] a[4*i] + a[4*i+2]←

a[8*i] a[8*i] + a[8*i+4]←

a[i] a[0]←Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7

Let's rewrite it using shuffle

Page 27: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

28

Example: reduction + broadcast

With shuffle

a0 a1 a2 a3 a4 a5 a6 a7

Σ0-1 Σ0-1 Σ2-3 Σ2-3 Σ4-5 Σ4-5 Σ6-7 Σ6-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕ ⊕

Σ0-3 Σ0-3 Σ0-3 Σ0-3 Σ4-7 Σ4-7 Σ4-7 Σ4-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕

Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7 Σ0-7⊕ ⊕ ⊕ ⊕

Step 0

Step 1

Step 2

P0 P1 P2 P3 P4 P5 P6 P7

⊕ ⊕

⊕ ⊕ ⊕ ⊕

ai += __shfl_xor(ai, 1);

ai += __shfl_xor(ai, 2);

ai += __shfl_xor(ai, 4);

Exercise: implement complete reduction without __syncthreads

Hint: use shared memory atomics

Page 28: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

29

Other example: parallel prefixRemember our PRAM algorithm

a0 a1 a2 a3 a4 a5 a6 a7

a0 Σ0-1 Σ1-2 Σ2-3 Σ3-4 Σ4-5 Σ5-6 Σ6-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕ ⊕

a0 Σ0-1 Σ0-2 Σ0-3 Σ1-4 Σ2-5 Σ3-6 Σ4-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕

a0 Σ0-1 Σ0-2 Σ0-3 Σ0-4 Σ0-5 Σ0-6 Σ0-7⊕ ⊕ ⊕ ⊕

Σi-j is the sum ∑k=i

j

ak

Step 0

Step 1

Step 2

s[i] a[i]←if i ≥ 1 then

s[i] s[i-1] + s[i]←

if i ≥ 2 thens[i] s[i-2] + s[i]←

if i ≥ 4 thens[i] s[i-4] + s[i]←

if i ≥ 2d thens[i] s[i-2← d] + s[i]

Step d:

P0 P1 P2 P3 P4 P5 P6 P7

Page 29: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

30

Other example: parallel prefixUsing warp-synchronous programming

a0 a1 a2 a3 a4 a5 a6 a7

a0 Σ0-1 Σ1-2 Σ2-3 Σ3-4 Σ4-5 Σ5-6 Σ6-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕ ⊕

a0 Σ0-1 Σ0-2 Σ0-3 Σ1-4 Σ2-5 Σ3-6 Σ4-7⊕ ⊕ ⊕ ⊕ ⊕ ⊕

a0 Σ0-1 Σ0-2 Σ0-3 Σ0-4 Σ0-5 Σ0-6 Σ0-7⊕ ⊕ ⊕ ⊕

Σi-j is the sum ∑k=i

j

ak

Step 0

Step 1

Step 2

s = a;n = __shfl_up(s, 1);if(laneid >= 1)

s += n;n = __shfl_up(s, 2);if(laneid >= 2)

s += n;

n = __shfl_up(s, 4);if(laneid >= 4)

s += n;

for(d = 1; d <= 5; d *= 2) { n = __shfl_up(s, d); if(laneid >= d) s += n;}

P0 P1 P2 P3 P4 P5 P6 P7

Page 30: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

31

Example: multi-precision addition

Do an addition on 1024-bit numbers

Represent numbers as vectors of 32×32-bit

A warp works on a vector

First step: add elements of the vectors in paralleland recover carries

a7 a6 a5 a4 a3 a2 a1 a0

b7 b6 b5 b4 b3 b2 b1 b0

r7 r6 r5 r4 r3 r2 r1 r0

...a31

b31

r31

...

...

t0t1t2t31

++ ++

= ===

uint32_t a = A[tid], b = B[tid], r, c;

r = a + b; // Sum

c = r < a; // Get carryc7 c6 c5 c4 c3 c2 c1 c0c31 ...++ ++

Page 31: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

32

Second step: propagate carries

This is a parallel prefix operation

We can do it in log(n) steps

But in most cases, one stepwill be enough

Loop until all carries are propagated

r7 r6 r5 r4 r3 r2 r1r31 ...

c7 c6 c5 c4 c3 c2 c1 c0c31 ...++ ++r0

uint32_t a = A[tid], b = B[tid], r, c;

r = a + b; // Sumc = r < a; // Get carrywhile(__any(c)) { // Carry left?

c = __shfl_up(c, 1); // Move leftif(laneid == 0) c = 0;r = r + c; // Sum carryc = r < c; // New carry?

}R[tid] = r;

++ ++

++ +

...

Page 32: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

33

Mixing SPMD and SIMD

Most language features work in either SPMD mode or SIMD mode

SPMD SIMD

Warp voteShuffle

Divergentbranches

Thread-private computation,memory accesses,uniform branches...

Avoid divergent branches when writing SIMD code

Consequence: SIMD code may call SPMD functions,not the other way around

Page 33: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

34

Takeaway

Two ways to program an SIMT GPU

With independent threads, grouped in warps

With warps operating on vectors

3 levels

Blocks in grid: independent tasks, no synchronization

Warps in block: concurrent “threads”, explicitly synchronizable

Threads in warp: implicitly synchronized

Warp-synchronous still somehow uncharted territory in CUDA,but widely used in libraries

If you know warp-synchronous programming in CUDA,you know SIMD programming with masking(Xeon Phi, AVX-512…)

Page 34: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

36

Device functions

Kernel can call functions

Need to be marked for GPU compilation

A function can be compiled for both host and device

Device functions can call device functions

Newer GPUs support recursion

__device__ int foo(int i) {}

__host__ __device__ int bar(int i) {}

Page 35: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

37

Local memory

Registers are fast but

Limited in size

Not addressable

Local memory used for

Local variables that do not fit in registers (register spilling)

Local arrays accessed with indirection

Warning: local is a misnomer!

Physically, local memory usually goes off-chip

About same performance as coalesced access to global memory

Or in caches if you are lucky

int a[17];b = a[i];

Page 36: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

38

Loop unrolling

Can improve performance

Amortizes loop overhead over several iterations

May allow constant propagation, common sub-expression elimination...

Unrolling is necessary to keep arrays in registers

int a[4];for(int i = 0; i < 4; i++) {

a[i] = 3 * i;}

Indirect addressing:a in local memory

Not unrolled Unrolledint a[4];a[0] = 3 * 0;a[1] = 3 * 1;a[2] = 3 * 2;a[3] = 3 * 3;

Static addressing:a in registers

Trivialcomputations:optimized away

#pragma unrollfor(int i = 0; i < 4; i++) {

a[i] = 3 * i;}

The compiler can unroll for you

Page 37: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

39

Device-side functions: C library support

Extensive math function support

Standard C99 math functions in single and double precision:e.g. sqrtf, sqrt, expf, exp, sinf, sin, erf, j0…

More exotic math functions:cospi, erfcinv, normcdf…

Complete list with error bounds in the CUDA C programming guide

Starting from CC 2.0

printf

memcpy, memset

malloc, free: allocate global memory on demand

Page 38: GPU programming: Warp-synchronous programming · GPU programming: Warp-synchronous programming ... not in C for CUDA Can be recovered using inline PTX ... but remember PTX is not

40

Device-side intrinsics

C functions that translate to one/a few machine instructions

Access features not easily expressed in C

Hardware math functions in single-precision

GPUs have dedicated hardware forreverse square root, inverse, log2, exp2, sin, cos in single precision

Intrinsics: __rsqrt, __rcp, exp2f, __expf, __exp10f, __log2f, __logf, __log10f, __sinf, __cosf, __sincosf, __tanf, __powf

Less accurate than software implementation (except exp2f),but much faster

Optimized arithmetic functions

__brev, __popc, __clz, __ffs...Bit reversal, population count, count leading zeroes, find first bit set…

Check the CUDA Toolkit Reference Manual

We will see other intrinsics in the next part