Puzzle 12: 내적

κ°œμš”

1D TileTensor a와 1D TileTensor b의 내적을 κ³„μ‚°ν•˜μ—¬ 1D TileTensor output(단일 κ°’)에 μ €μž₯ν•˜λŠ” 컀널을 κ΅¬ν˜„ν•˜μ„Έμš”. 내적은 크기가 같은 두 λ²‘ν„°μ—μ„œ λŒ€μ‘ν•˜λŠ” μ›μ†ŒλΌλ¦¬ κ³±ν•œ λ’€, κ·Έ κ²°κ³Όλ₯Ό λͺ¨λ‘ 더해 ν•˜λ‚˜μ˜ 숫자(슀칼라)λ₯Ό κ΅¬ν•˜λŠ” μ—°μ‚°μž…λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, 두 벑터가 λ‹€μŒκ³Ό 같을 λ•Œ:

\[a = [a_{1}, a_{2}, …, a_{n}] \] \[b = [b_{1}, b_{2}, …, b_{n}] \]

내적은 μ΄λ ‡κ²Œ κ΅¬ν•©λ‹ˆλ‹€: \[a \cdot b = a_{1}b_{1} + a_{2}b_{2} + … + a_{n}b_{n}\]

μ°Έκ³ : 각 μœ„μΉ˜λ§ˆλ‹€ μŠ€λ ˆλ“œ 1κ°œκ°€ μžˆμŠ΅λ‹ˆλ‹€. μŠ€λ ˆλ“œλ‹Ή μ „μ—­ 읽기 2회, 블둝당 μ „μ—­ μ“°κΈ° 1회만 ν•„μš”ν•©λ‹ˆλ‹€.

내적 μ‹œκ°ν™” 내적 μ‹œκ°ν™”

핡심 κ°œλ…

이 νΌμ¦μ—μ„œ 배울 λ‚΄μš©:

  • Puzzle 8, Puzzle 11μ—μ„œ μ΄μ–΄μ§€λŠ” TileTensor 기반 병렬 λ¦¬λ•μ…˜
  • address_spaceλ₯Ό ν™œμš©ν•œ 곡유 λ©”λͺ¨λ¦¬ 관리
  • μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ ν˜‘λ ₯ν•΄ ν•˜λ‚˜μ˜ κ²°κ³Όλ₯Ό λ§Œλ“€μ–΄κ°€λŠ” κ³Όμ •
  • λ ˆμ΄μ•„μ›ƒμ„ μΈμ‹ν•˜λŠ” ν…μ„œ μ—°μ‚°

핡심은 TileTensorκ°€ λ©”λͺ¨λ¦¬ 관리λ₯Ό κ°„μ†Œν™”ν•˜λ©΄μ„œλ„, 병렬 λ¦¬λ•μ…˜μ˜ νš¨μœ¨μ€ κ·ΈλŒ€λ‘œ μ‚΄λ¦¬λŠ” 방식을 μ΄ν•΄ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

ꡬ성

  • 벑터 크기: SIZE = 8
  • 블둝당 μŠ€λ ˆλ“œ 수: TPB = 8
  • 블둝 수: 1
  • 좜λ ₯ 크기: 1
  • 곡유 λ©”λͺ¨λ¦¬: TPB개

μ°Έκ³ :

  • TileTensor ν• λ‹Ή: stack_allocation[dtype=dtype, address_space=AddressSpace.SHARED](row_major[TPB]()) μ‚¬μš©
  • μš”μ†Œ μ ‘κ·Ό: 경계 검사가 μžλ™μœΌλ‘œ λ”°λΌμ˜€λŠ” μžμ—°μŠ€λŸ¬μš΄ 인덱싱
  • λ ˆμ΄μ•„μ›ƒ 처리: μž…λ ₯용과 좜λ ₯용 λ ˆμ΄μ•„μ›ƒμ„ λ”°λ‘œ ꡬ성
  • μŠ€λ ˆλ“œ 쑰율: λ™μΌν•œ 동기화 νŒ¨ν„΄μœΌλ‘œ barrier() μ‚¬μš©

μ™„μ„±ν•  μ½”λ“œ

from std.gpu import thread_idx, block_idx, block_dim, barrier
from std.gpu.memory import AddressSpace
from layout import TileTensor
from layout.tile_layout import row_major
from layout.tile_tensor import stack_allocation


comptime TPB = 8
comptime SIZE = 8
comptime BLOCKS_PER_GRID = (1, 1)
comptime THREADS_PER_BLOCK = (TPB, 1)
comptime dtype = DType.float32
comptime layout = row_major[SIZE]()
comptime out_layout = row_major[1]()
comptime LayoutType = type_of(layout)
comptime OutLayout = type_of(out_layout)


def dot_product(
    output: TileTensor[mut=True, dtype, OutLayout, MutAnyOrigin],
    a: TileTensor[mut=False, dtype, LayoutType, ImmutAnyOrigin],
    b: TileTensor[mut=False, dtype, LayoutType, ImmutAnyOrigin],
    size: Int,
):
    # FILL ME IN (roughly 13 lines)
    ...


전체 파일 보기: problems/p12/p12.mojo

팁
  1. TileTensor와 address_space둜 곡유 λ©”λͺ¨λ¦¬ 생성
  2. shared[local_i]에 a[global_i] * b[global_i]λ₯Ό μ €μž₯
  3. barrier()와 ν•¨κ»˜ 병렬 λ¦¬λ•μ…˜ νŒ¨ν„΄ 적용
  4. μŠ€λ ˆλ“œ 0이 μ΅œμ’… κ²°κ³Όλ₯Ό output[0]에 기둝

μ½”λ“œ μ‹€ν–‰

μ†”λ£¨μ…˜μ„ ν…ŒμŠ€νŠΈν•˜λ €λ©΄ ν„°λ―Έλ„μ—μ„œ λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•˜μ„Έμš”:

pixi run p12
pixi run -e amd p12
pixi run -e apple p12
uv run poe p12

퍼즐을 아직 ν’€μ§€ μ•Šμ•˜λ‹€λ©΄ 좜λ ₯은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€:

out: HostBuffer([0.0])
expected: HostBuffer([140.0])

μ†”λ£¨μ…˜

def dot_product(
    output: TileTensor[mut=True, dtype, OutLayout, MutAnyOrigin],
    a: TileTensor[mut=False, dtype, LayoutType, ImmutAnyOrigin],
    b: TileTensor[mut=False, dtype, LayoutType, ImmutAnyOrigin],
    size: Int,
):
    var shared = stack_allocation[
        dtype=dtype, address_space=AddressSpace.SHARED
    ](row_major[TPB]())
    var global_i = block_dim.x * block_idx.x + thread_idx.x
    var local_i = thread_idx.x

    # Compute element-wise multiplication into shared memory
    if global_i < size:
        shared[local_i] = a[global_i] * b[global_i]

    # Synchronize threads within block
    barrier()

    # Parallel reduction in shared memory
    var stride = TPB // 2
    while stride > 0:
        if local_i < stride:
            shared[local_i] += shared[local_i + stride]

        barrier()
        stride //= 2

    # Only thread 0 writes the final result
    if local_i == 0:
        output[0] = shared[0]


TileTensorλ₯Ό ν™œμš©ν•œ 병렬 λ¦¬λ•μ…˜μœΌλ‘œ 내적을 κ³„μ‚°ν•˜λŠ” μ†”λ£¨μ…˜μž…λ‹ˆλ‹€. λ‹¨κ³„λ³„λ‘œ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€:

1단계: μš”μ†Œλ³„ κ³±μ…ˆ

각 μŠ€λ ˆλ“œκ°€ 직관적인 μΈλ±μ‹±μœΌλ‘œ κ³±μ…ˆ 연산을 ν•˜λ‚˜μ”© μ²˜λ¦¬ν•©λ‹ˆλ‹€:

shared[local_i] = a[global_i] * b[global_i]

2단계: 병렬 λ¦¬λ•μ…˜

λ ˆμ΄μ•„μ›ƒμ„ μΈμ‹ν•˜λŠ” 트리 기반 λ¦¬λ•μ…˜μž…λ‹ˆλ‹€:

μ΄ˆκΈ°κ°’:    [0*0  1*1  2*2  3*3  4*4  5*5  6*6  7*7]
        = [0    1    4    9    16   25   36   49]

Step 1:   [0+16 1+25 4+36 9+49  16   25   36   49]
        = [16   26   40   58   16   25   36   49]

Step 2:   [16+40 26+58 40   58   16   25   36   49]
        = [56   84   40   58   16   25   36   49]

Step 3:   [56+84  84   40   58   16   25   36   49]
        = [140   84   40   58   16   25   36   49]

κ΅¬ν˜„μ˜ 핡심 νŠΉμ§•

  1. λ©”λͺ¨λ¦¬ 관리:

    • address_space νŒŒλΌλ―Έν„° ν•˜λ‚˜λ‘œ 곡유 λ©”λͺ¨λ¦¬λ₯Ό κΉ”λ”ν•˜κ²Œ ν• λ‹Ή
    • νƒ€μž… μ•ˆμ „ν•œ 연산이 보μž₯되고
    • 경계 검사가 μžλ™μœΌλ‘œ λ”°λΌμ˜€λ©°
    • 인덱싱도 λ ˆμ΄μ•„μ›ƒμ„ 인식
  2. μŠ€λ ˆλ“œ 동기화:

    • 초기 κ³±μ…ˆμ΄ λλ‚˜λ©΄ barrier()
    • λ¦¬λ•μ…˜ 단계 μ‚¬μ΄λ§ˆλ‹€ barrier()
    • μŠ€λ ˆλ“œ κ°„ μ•ˆμ „ν•œ 쑰율 보μž₯
  3. λ¦¬λ•μ…˜ 둜직:

    stride = TPB // 2
    while stride > 0:
        if local_i < stride:
            shared[local_i] += shared[local_i + stride]
        barrier()
        stride //= 2
    
  4. μ„±λŠ₯상 이점:

    • \(O(\log n)\) μ‹œκ°„ λ³΅μž‘λ„
    • 병합 λ©”λͺ¨λ¦¬ μ ‘κ·Ό
    • μ΅œμ†Œν•œμ˜ μŠ€λ ˆλ“œ λΆ„κΈ°
    • 곡유 λ©”λͺ¨λ¦¬μ˜ 효율적 ν™œμš©

TileTensor 버전은 병렬 λ¦¬λ•μ…˜μ˜ νš¨μœ¨μ€ κ·ΈλŒ€λ‘œ μœ μ§€ν•˜λ©΄μ„œ, 여기에 더해:

  • νƒ€μž… μ•ˆμ „μ„±μ΄ ν•œμΈ΅ κ°•ν™”λ˜κ³ 
  • λ©”λͺ¨λ¦¬ 관리가 더 깔끔해지며
  • λ ˆμ΄μ•„μ›ƒμ„ μžλ™μœΌλ‘œ μΈμ‹ν•˜κ³ 
  • 인덱싱 문법도 μžμ—°μŠ€λŸ¬μ›Œμ§‘λ‹ˆλ‹€

배리어 λ™κΈ°ν™”μ˜ μ€‘μš”μ„±

λ¦¬λ•μ…˜ 단계 μ‚¬μ΄μ˜ barrier()λŠ” μ •ν™•ν•œ κ²°κ³Όλ₯Ό μœ„ν•΄ λ°˜λ“œμ‹œ ν•„μš”ν•©λ‹ˆλ‹€. κ·Έ 이유λ₯Ό μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€:

barrier()κ°€ μ—†μœΌλ©΄ 경쟁 μƒνƒœκ°€ λ°œμƒν•©λ‹ˆλ‹€:

초기 곡유 λ©”λͺ¨λ¦¬: [0 1 4 9 16 25 36 49]

Step 1 (stride = 4):
Thread 0 읽기: shared[0] = 0, shared[4] = 16
Thread 1 읽기: shared[1] = 1, shared[5] = 25
Thread 2 읽기: shared[2] = 4, shared[6] = 36
Thread 3 읽기: shared[3] = 9, shared[7] = 49

barrier 없이:
- Thread 0 μ“°κΈ°: shared[0] = 0 + 16 = 16
- Thread 1이 Thread 0보닀 λ¨Όμ € λ‹€μŒ 단계(stride = 2)둜 λ„˜μ–΄κ°€μ„œ
  16이 μ•„λ‹Œ 이전 κ°’ shared[0] = 0을 μ½μ–΄λ²„λ¦½λ‹ˆλ‹€!

barrier()κ°€ 있으면:

Step 1 (stride = 4):
λͺ¨λ“  μŠ€λ ˆλ“œκ°€ 합을 기둝:
[16 26 40 58 16 25 36 49]
barrier()κ°€ λͺ¨λ“  μŠ€λ ˆλ“œμ—κ²Œ 이 값듀이 보이도둝 보μž₯

Step 2 (stride = 2):
이제 μ—…λ°μ΄νŠΈλœ 값을 μ•ˆμ „ν•˜κ²Œ 읽을 수 있음:
Thread 0: shared[0] = 16 + 40 = 56
Thread 1: shared[1] = 26 + 58 = 84

barrier()λŠ” λ‹€μŒμ„ 보μž₯ν•©λ‹ˆλ‹€:

  1. ν˜„μž¬ λ‹¨κ³„μ˜ λͺ¨λ“  μ“°κΈ°κ°€ λλ‚œ 뒀에야 λ‹€μŒμœΌλ‘œ λ„˜μ–΄κ°
  2. λͺ¨λ“  μŠ€λ ˆλ“œκ°€ μ΅œμ‹  값을 λ³Ό 수 있음
  3. μ–΄λ–€ μŠ€λ ˆλ“œλ„ μ•žμ„œ λ‚˜κ°€μ§€ μ•ŠμŒ
  4. 곡유 λ©”λͺ¨λ¦¬κ°€ 항상 μΌκ΄€λœ μƒνƒœλ₯Ό μœ μ§€

이런 동기화 지점이 μ—†μœΌλ©΄:

  • 경쟁 μƒνƒœκ°€ λ°œμƒν•˜κ³ 
  • μŠ€λ ˆλ“œκ°€ 이미 μ§€λ‚œ 값을 읽게 되며
  • μ‹€ν–‰ν•  λ•Œλ§ˆλ‹€ κ²°κ³Όκ°€ 달라지고
  • μ΅œμ’… 합계가 ν‹€μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€