블록 경계 버전
1D LayoutTensor a와 1D LayoutTensor b의 1D 합성곱을 계산하여 1D LayoutTensor output에 저장하는 커널을 구현하세요.
참고: 일반적인 경우를 처리해야 합니다. 스레드당 전역 읽기 2회, 전역 쓰기 1회만 필요합니다.
구성
- 입력 배열 크기:
SIZE_2 = 15 - 커널 크기:
CONV_2 = 4 - 블록당 스레드 수:
TPB = 8 - 블록 수: 2
- 공유 메모리: 입력용
TPB + CONV_2 - 1개
참고:
- 확장 로딩: 경계 겹침 영역을 고려
- 블록 가장자리: 블록 경계를 넘는 데이터 처리
- 메모리 레이아웃: 공유 메모리의 효율적 활용
- 동기화: 적절한 스레드 간 조율
완성할 코드
comptime SIZE_2 = 15
comptime CONV_2 = 4
comptime BLOCKS_PER_GRID_2 = (2, 1)
comptime THREADS_PER_BLOCK_2 = (TPB, 1)
comptime in_2_layout = Layout.row_major(SIZE_2)
comptime out_2_layout = Layout.row_major(SIZE_2)
comptime conv_2_layout = Layout.row_major(CONV_2)
fn conv_1d_block_boundary[
in_layout: Layout, out_layout: Layout, conv_layout: Layout, dtype: DType
](
output: LayoutTensor[dtype, out_layout, MutAnyOrigin],
a: LayoutTensor[dtype, in_layout, ImmutAnyOrigin],
b: LayoutTensor[dtype, conv_layout, ImmutAnyOrigin],
):
global_i = Int(block_dim.x * block_idx.x + thread_idx.x)
local_i = Int(thread_idx.x)
# FILL ME IN (roughly 18 lines)
전체 파일 보기: problems/p13/p13.mojo
팁
LayoutTensor[dtype, Layout.row_major(TPB + CONV_2 - 1), MutAnyOrigin, address_space = AddressSpace.SHARED].stack_allocation()으로 공유 메모리 할당- 메인 데이터 로드:
shared_a[local_i] = a[global_i] - 경계 데이터 로드:
if local_i < CONV_2 - 1일 때 다음 블록의 데이터 처리 - 커널 로드:
shared_b[local_i] = b[local_i] - 입력 범위 안에서 합산:
if global_i + j < SIZE_2
코드 실행
솔루션을 테스트하려면 터미널에서 다음 명령어를 실행하세요:
pixi run p13 --block-boundary
pixi run -e amd p13 --block-boundary
pixi run -e apple p13 --block-boundary
uv run poe p13 --block-boundary
퍼즐을 아직 풀지 않았다면 출력은 다음과 같습니다:
out: HostBuffer([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
expected: HostBuffer([14.0, 20.0, 26.0, 32.0, 38.0, 44.0, 50.0, 56.0, 62.0, 68.0, 74.0, 80.0, 41.0, 14.0, 0.0])
솔루션
fn conv_1d_block_boundary[
in_layout: Layout, out_layout: Layout, conv_layout: Layout, dtype: DType
](
output: LayoutTensor[dtype, out_layout, MutAnyOrigin],
a: LayoutTensor[dtype, in_layout, ImmutAnyOrigin],
b: LayoutTensor[dtype, conv_layout, ImmutAnyOrigin],
):
global_i = Int(block_dim.x * block_idx.x + thread_idx.x)
local_i = Int(thread_idx.x)
# first: need to account for padding
shared_a = LayoutTensor[
dtype,
Layout.row_major(TPB + CONV_2 - 1),
MutAnyOrigin,
address_space = AddressSpace.SHARED,
].stack_allocation()
shared_b = LayoutTensor[
dtype,
Layout.row_major(CONV_2),
MutAnyOrigin,
address_space = AddressSpace.SHARED,
].stack_allocation()
if global_i < SIZE_2:
shared_a[local_i] = a[global_i]
else:
shared_a[local_i] = 0
# second: load elements needed for convolution at block boundary
if local_i < CONV_2 - 1:
# indices from next block
next_idx = global_i + TPB
if next_idx < SIZE_2:
shared_a[TPB + local_i] = a[next_idx]
else:
# Initialize out-of-bounds elements to 0 to avoid reading from uninitialized memory
# which is an undefined behavior
shared_a[TPB + local_i] = 0
if local_i < CONV_2:
shared_b[local_i] = b[local_i]
barrier()
if global_i < SIZE_2:
var local_sum: output.element_type = 0
@parameter
for j in range(CONV_2):
if global_i + j < SIZE_2:
local_sum += shared_a[local_i + j] * shared_b[j]
output[global_i] = local_sum
확장된 공유 메모리를 사용해 블록 경계를 넘는 1D 합성곱을 처리하는 솔루션입니다. 자세히 분석해 보겠습니다:
메모리 레이아웃과 크기 계산
테스트 구성:
- 전체 배열 크기: SIZE_2 = 15
- 그리드: 2 블록 × 8 스레드
- 합성곱 커널: CONV_2 = 4
Block 0 공유 메모리: [0 1 2 3 4 5 6 7|8 9 10] // TPB(8) + (CONV_2-1)(3) 패딩
Block 1 공유 메모리: [8 9 10 11 12 13 14 0|0 0 0] // 두 번째 블록. 데이터(7) + 그리드 채움용 패딩(1) + (CONV_2-1)(3) 패딩
크기 계산:
- 메인 데이터: TPB개 (8)
- 겹침 영역: CONV_2 - 1개 (4 - 1 = 3)
- 합계: TPB + CONV_2 - 1 = 8 + 4 - 1 = 11개
구현 상세
-
공유 메모리 할당:
# 합성곱 윈도우에 필요한 패딩을 먼저 고려 shared_a = LayoutTensor[dtype, Layout.row_major(TPB + CONV_2 - 1), MutAnyOrigin, address_space = AddressSpace.SHARED].stack_allocation() shared_b = LayoutTensor[dtype, Layout.row_major(CONV_2), MutAnyOrigin, address_space = AddressSpace.SHARED].stack_allocation()이렇게 하면 블록 데이터와 겹침 영역을 모두 담기에 충분한 공간이 확보됩니다.
-
데이터 로딩 전략:
# 메인 블록 데이터 if global_i < SIZE_2: shared_a[local_i] = a[global_i] else: shared_a[local_i] = 0 # 다음 블록의 경계 데이터 if local_i < CONV_2 - 1: next_idx = global_i + TPB if next_idx < SIZE_2: shared_a[TPB + local_i] = a[next_idx] else: # 범위 밖 원소를 0으로 초기화하여 # 미정의 동작을 유발하는 초기화되지 않은 메모리 읽기를 방지 shared_a[TPB + local_i] = 0local_i < CONV_2 - 1인 스레드만 경계 데이터를 로드- 불필요한 스레드 분기 방지
- 메인 데이터 로드의 메모리 병합 유지
- 범위 밖 원소를 명시적으로 0으로 초기화하여 미정의 동작 방지
-
커널 로딩:
if local_i < b_size: shared_b[local_i] = b[local_i]- 스레드당 1회 로드
- 커널 크기로 범위 제한
-
합성곱 연산:
if global_i < SIZE_2: var local_sum: output.element_type = 0 @parameter for j in range(CONV_2): if global_i + j < SIZE_2: local_sum += shared_a[local_i + j] * shared_b[j]@parameter로 컴파일 타임 루프 전개output.element_type으로 적절한 타입 추론- 의미적으로 올바른 경계 검사: 유효한 입력 위치에서만 합성곱 계산
메모리 접근 패턴 분석
-
Block 0 접근 패턴:
Thread 0: [0 1 2 3] × [0 1 2 3] Thread 1: [1 2 3 4] × [0 1 2 3] Thread 2: [2 3 4 5] × [0 1 2 3] ... Thread 7: [7 8 9 10] × [0 1 2 3] // 겹침 영역 데이터 사용 -
Block 1 접근 패턴: Thread 4부터는
global_i + j < SIZE_2가False가 되어 해당 반복을 건너뛰는 점에 주목하세요.Thread 0: [8 9 10 11] × [0 1 2 3] Thread 1: [9 10 11 12] × [0 1 2 3] ... Thread 4: [12 13 14] × [0 1 2] // 끝부분 제로 패딩 Thread 5: [13 14] × [0 1] Thread 6: [14] × [0] Thread 7: 건너뜀 // 모든 j에 대해 global_i + j < SIZE_2가 false, 연산 없음
성능 최적화
-
메모리 병합:
- 메인 데이터 로드: 인접 스레드가 연속된 메모리에 접근
- 경계 데이터: 필요한 스레드만 참여
- 단일 배리어 동기화 지점
-
스레드 분기 최소화:
- 메인 로딩과 경계 로딩의 깔끔한 분리
- 워프 내 균일한 연산 패턴
- 효율적인 경계 검사
-
공유 메모리 활용:
- 블록 경계 처리에 최적화된 크기 설정
- 접근 패턴에서 뱅크 충돌 없음
- 로드한 데이터의 효율적 재사용
-
경계 처리:
- 범위 밖 원소를 명시적으로 0으로 설정하여 초기화되지 않은 공유 메모리 읽기 방지
global_i + j < SIZE_2로 공유 메모리가 아닌 실제 입력 범위 기준의 경계 검사- 불필요한 연산 없이 적절한 엣지 케이스 처리
경계 조건 개선
이 솔루션은 공유 메모리 범위를 확인하는 대신 if global_i + j < SIZE_2:를 사용합니다. 이 패턴은:
- 수학적으로 정확: 입력 데이터가 실제로 존재하는 위치에서만 합성곱 계산
- 더 효율적: 입력 배열을 넘어선 위치에 대한 불필요한 연산 회피
- 더 안전: 공유 메모리의 제로 패딩 동작에 의존하지 않음
이 구현은 블록 간 합성곱을 효율적으로 수행하면서 다음을 유지합니다:
- 적절한 경계 검사를 통한 메모리 안전성
- 최적화된 메모리 접근을 통한 높은 성능
- LayoutTensor 추상화를 활용한 깔끔한 코드 구조
- 최소한의 동기화 오버헤드
- 수학적으로 건전한 경계 처리