SIMT에서 thread마다 상태가 달라도 독립 코어는 아닌 이유
CUDA 프로그램에서 thread마다 변수와 조건문을 쓰면 각각이 작은 CPU 코어처럼 보인다. 그러나 많은 thread를 선언하는 일과 각 thread에 독립된 명령 발행 장치를 배정하는 일은 다르다. SIMT를 이해하려면 프로그램이 표현하는 독립 상태와 하드웨어가 명령을 묶어 실행하는 방식을 나누어 보아야 한다.
독립 상태를 가진 thread가 명령을 함께 실행한다
NVIDIA의 CUDA 문서는 warp를 32개 thread의 묶음으로 설명한다. 각 thread는 자신의 상태를 갖지만 warp의 실행에서는 공통 명령과 그 명령에 참여하는 thread를 함께 관리한다. compute capability 7.0 이후의 independent thread scheduling도 이 묶음 실행을 없애는 기능은 아니다. CUDA Programming Guide, SIMT Execution Model
예를 들어 32개 thread가 각각 y[i]=2*x[i]+1을 계산한다고 하자. 같은 곱셈 명령이 실행되어도 각 thread의 i와 x[i], 결과를 담는 상태는 다르다. 공통 명령이므로 데이터까지 하나라는 뜻은 아니다. 반대로 서로 다른 데이터를 갖는다는 사실만으로 32개의 임의 명령을 같은 시각에 따로 발행할 수 있다는 뜻도 아니다.
이 구분은 scalar thread 코드를 읽을 때 유용하다. thread 하나의 연산 경로를 먼저 이해하고, 성능을 평가할 때에는 같은 warp의 다른 thread가 어느 명령에 함께 참여하는지 살핀다. 개별 상태의 정확성과 실행 자원의 사용률은 서로 다른 관점이다.
37개 thread를 37개의 물리 실행 슬롯으로 읽지 않는다
하나의 1차원 block에 thread가 37개라면 warp 두 개로 나뉜다. 첫 warp에는 32개, 마지막에는 5개의 유효 thread가 있다. 설명용으로 두 warp가 같은 길이의 명령 경로를 한 번씩 실행하고, 명령마다 32개의 lane 자리가 있다고 세면 유효 연산 수는 37, 자리 수는 64다. 이 관점의 비율은 37/64, 약 57.8%다.
이 값은 실제 GPU 사용률이나 실행 시간의 예측이 아니다. 명령 처리량과 지연, 메모리 접근, 스케줄러 구조를 모두 지웠기 때문이다. 다만 thread 개수와 실행 묶음의 경계가 맞지 않으면 마지막 묶음에 사용하지 않는 자리가 생길 수 있음을 보여 준다.
block 크기를 키워 마지막 warp를 채우는 선택도 무조건 이득은 아니다. 더 많은 thread가 레지스터와 공유 메모리를 요구하면 동시에 배치할 block 수가 달라질 수 있다. 작업 크기가 작거나 메모리 병목이 크면 위의 lane 비율이 전체 성능을 결정하지 않을 수도 있다. 이 계산은 설계 질문의 시작점으로 사용한다.
독립 스케줄링은 암묵적인 순서를 허락하지 않는다
independent thread scheduling에서는 같은 warp 안의 thread가 항상 같은 명령 단계에 함께 있다고 가정한 코드가 문제가 될 수 있다. 문서도 예전의 암묵적인 warp 동시 실행에 의존하던 공유·동기화 코드를 재검토하도록 설명한다. 같은 문서, Independent Thread Scheduling
가령 thread 0이 공유 위치에 값을 쓴 다음 thread 1이 읽는다면, 소스 코드에서 쓰는 문장이 먼저 보인다는 사실만으로 thread 사이의 순서가 생기지 않는다. ‘같은 warp니까 아마 같이 진행할 것’이라는 추측을 데이터 전달의 계약으로 사용해서는 안 된다. 참여 범위에 맞는 명시적인 동기화와 데이터 경합 없는 접근이 필요하다.
여러 thread가 같은 위치에 동시에 비원자적 쓰기를 하는 경우도 lane 수가 결과의 결정성을 보장하지 않는다. 합계를 구하려고 각 thread의 값을 같은 변수에 그대로 더하는 코드는 각자의 독립 상태와 공유된 결과의 갱신을 혼동할 수 있다. 정확한 협력 방법을 정한 뒤 성능을 평가해야 한다.
SIMT의 장점은 thread 수준의 코드를 써서 많은 작업을 표현하면서 묶음 실행의 처리량을 얻는 데 있다. thread를 독립 코어의 개수로 세기보다, 서로 다른 상태가 어떤 공통 명령에 참여하고 어떤 순서로 값을 교환하는지 확인하면 정확성과 성능을 같은 코드에서 더 분명하게 읽을 수 있다.