Tensor Program Abstraction
TensorIR에 대해 알아보기 전에 primitive tensor function
에 대해 먼저 알아보자
primitive tensor function
은 computational operation의 single “unit”에 대응하는 function이다.
- 예를 들어 convolution 또는 convolution+relu(fused) operation
primitive tensor function의 추상화는 tensor computation을 위한 multi-dimensional buffers, loop nests을 포함한다.
|
|
Key Elements of Tensor Programs
위의 코드는 primitive tensor function
의 예제로 다음의 Key Elements를 가진다.
- multi-dimensional buffer (input 2개, output 1개)
- 계산을 용이하게 하는 단일 loop nest
i
- 단일 compute statement (element-wise sum)
Extra Structure in TensorIR
(TVM의 TensorIR에서 annotation(추가적인 구조)가 필요한지를 설명하는 내용)
어떤 프로그램들은 loop’s sequence에 의존하기 때문에 마음대로 루프를 재배열하거나 병렬화할 수는 없지만 우리가 주로 다루는 대부분의 primitive tensor function
은 루프 반복 간의 독립성이라는 좋은 성질을 가지고 있다. 이전 예제에서는 block / iteration annotations을 들 수 있다.
- block annotation :
with T.block("C")
은 블록이 스케줄링을 위해 지정된 기본 계산 단위임을 의미, block()은 single computation statement, multiple computation statements, opaque intrinsics(Tensor Core instructions) 을 포함할 수 있다. - iteration annotation :
T.axis.spatial
은 vi가 에 매핑되고 모든 iterations이 독립적임을 의미한다. 블록과 축의 annotation는 프로그램 실행 자체에는 필수는 아니지만, 프로그램을 최적화하거나 변환할 때 매우 중요하다. (vi와 관련된 루프를 parallelize, reorder 할 수 있음을 나타낸다.)
Understand TensorIR Abstraction
tensor program abstraction
의 주요 목적은 hardware acceleration options 및 loop를 묘사하는 것(threading, 특수 hardware instructions 적, memory access)
예를 들어, 128*128 Matrix_Multiplication + Relu
를 python numpy code와 TensorIR로 나타내면 아래와 같다 (TVMScript 이용). 다음 각 sub-chapter에서 아래 코드에 대한 element에 대하여 설명한다.
|
|
Function Parameters and Buffers
TensorIR에서 A,B,C는 float32, (128,128) 형태의 T.Buffer
를 취하며 이러한 추가 정보는 MLC(Machine Learning Compile)가 shape 와 data type에 특화된 코드를 생성하는 데 도움이 됩니다.
|
|
상기 내용과 비슷하게 중간 할당에서도 버퍼를 사용함
|
|
Loop Iterations
Loop Iterations은 python 구문과 바로 대응 되기에 for i in range(num)
를 바로 사용하거나, T.grid
를 사용하여 코드를 축약할 수 있다.
|
|
Computational Block
computational statements에서 중요 차이점은 T.block
이라는 추가적인 요소를 포함한다. T.block
은 TensorIR내부에서 기본적인 computation unit을 나타며 numpy코드보다 더 많은 정보(block axes 세트인 (vi, vj, vk)와 이를 둘러 싼 computations)를 포함한다.
|
|
T.axis
코드(vi = T.axis.spatial(128, i) 3줄) block axes의 key properties를 나타낸다. 문법은 다음과 같다.
- [block_axis] = T.axis.[axis_type]([axis_range], [mapped_value]) 이 코드는 하기의 세부 정보를 전달한다.
- vi, vj, vk의 i, j, k와의 binding
- vi, vj, vk의 range 선언( = 128)
- iterators 의 속성 : spatial(독립적인 계산), reduce(축소/누적 계산)
또한 위의 코드는
T.axis.remap
을 이용하여 하기와 같이 표현 가능하다. - vi, vj, vk = T.axis.remap(“SSR”, [i, j, k])
역자 주
- spatial axis : 이 블록이 출력으로 쓰는 인덱스로 병렬화 가능
- reduce axis : 이 블록이 누적에 사용하는 인덱스로 병렬화가 가능할 수도 있다.(race condition 조심)
- Reduction 연산 : 다차원 배열(tensor)에서 하나의 축을 따라 값을 누적(sum(x, axis=1), matmul(A, B), mean(x), softmax(x), conv2d) 하는 연산
Block Axis Properties
T.axis
는 해당 axis가 현재 수행 중인 computation과 어떤 관계가 있는지를 나타낸다.
이 블록은 세 개의 축(vi, vj, vk)을 가지고 있으며 A[vi, vk], B[vk, vj]를 읽어 Y[vi, vj]를 업데이트한다.
vi, vj의 값이 고정되면 (Y[vi, vj]라는 한 점을 정하면) 이 점의 계산은 오직 vk를 반복하면서 이루어지며 다른 위치인 Y[vi’, vj’]와는 독립적이다.(안전하게 병렬 처리도 가능함)
그래서 vi, vj는 spatial axis (공간 좌표) vk는 reduce axis (누적을 위한 축) 라고 부른다.
(챗GPT의 해석 : TVM은 블록 내에서 어떤 축이 “누적 대상"이고, 어떤 축이 “출력 대상"인지를 명시적으로 구분하며 구분을 통해 병렬화 가능성 판단, 메모리 스케줄링, 코드 자동 최적화 등을 가능하게 함.)
Why Extra Information in Block
블록 내부의 축 정보(축의 범위와 속성 등)가 포함되기 때문에, 블록은 외부 루프와 독립적인 “자기 완결(self-contained)” 구조가 된다 즉 vi = T.axis.spatial(128, i) 처럼 축의 역할과 범위를 명시적으로 선언함으로써 루프에 의존하지 않고, 자신만으로도 무슨 계산을 어디서 수행해야 하는지 알 수 있는 완전한 구조가 되는 것. 또한 블록의 축 정보는 외부 루프가 올바른지 검사하는 기준도 제공해준다.
TensorIR Creation
이번 챕터에서는 TensorIR function을 코딩하는 방법을 기술한다.(이 챕터에서의 기술내용은 Relax모델을 컴파일하는 유저에게 필 수적인 것은 아니다.)
Create TensorIR using TVMScript
앞에서 사용한 mm_relu TVM Script를 다시 설명함(본 포스트에서는 스킵함, 필요하면 TVM DOC를 볼 것)
TensorIR Function with Dynamic Shapes
TVMScript가 Python 인터프리터에 의해 실행되지 않지만 Python과의 상호 작용은 제한적으로 가능하다. Python 변수를 사용하여 TensorIR의 shape과 Data type 변수 지정 가능하다. 또한 이를 통해 runtime dynamic shape inference가 가능하다.
|
|
Create TensorIR using Tensor Expression
TensorIR을 더 간결하게 표현할 수 있는 Tensor Expression 표현 방식이 있다. 레거시 인듯 해서 우선은 넘어가기로 하겠다.
Transformation
primitive tensor functions의 transformation은 컴파일 flow의 주요 과정이다. 이제까지 사용한 mm_relu를 평가하는 함수를 만들어 TensorIR transformation 과정을 진행한다.
|
|
Initialization Schedule
Schedule helper class를 사용하여 transformation 프로세스를 초기화 할 수 있다.
|
|
Loop Tiling
Loop Split
다음 명령을 수행하여 block Y
와 이와 관련된 루프의 reference를 얻는다.
|
|
그 다음 j
를 두개의 loop로 분리(inner loop의 길이가 8, 즉 128회를 16*8회로 나눔)하는 transformation을 수행할 수 있다.
(이 프로세스는 절차?적 이기 때문에 위의 내용을 두번 수행하면 j가 존재하지 않는다는 에러가 출력됨, 역자주, 아마 첫번째 수행에서 j가 j_0, j_1로 분리되므로 j는 사라진다는 뜻일 듯)
|
|
sch.mod.show()
로 transformation의 결과를 확인 할 수 있다.
원문을 보면 j에 관련된 루프가 j_0, j_1로 분리된 결과를 확인할 수 있다.
Loop Reorder
루프 j_0, j_1를 reordering 할 수 있다.
|
|
원문을 보면 j에 관련된 루프가 j_0, k, j_1로 reorder 된 것을 확인할 수 있다.
역자주
TensorIR에서 Transformation은 고성능 코드 생성을 위한 스케줄 최적화 작업이다.
Loop Tiling
은 큰 Loop를 작은 Loop(블록) 단위로 나누어 중첩 루프 구조로 바꾸는 최적화이다. 큰 배열이 L1/L2 캐시에 모드 들어가지 않을 때 이를 나누어 타일 단위로 계산하면 작은 덩어리의 데이터만 반복 사용하게 되어 캐시 히트율이 올라간다. 또한 벡터화나 병렬화가 가능해지며 loop fusion / compute_at 등의 고급 스케줄링을 하기 전 준비 단계로 쓰인다.Loop Reordering
도 메모리 접근의 지역성(Locality) 향상시켜 CPU캐시 히트율 향상시킨다. 또한 벡터화의 가능성을 증진 시킨다.
Leverage Localities
block C
를 loop Y 내부로 재배치 하는 reverse_compute_at
이라는 primitive를 사용할 수 있다.
|
|
본문에 T.block("C")
블록이 j_1 하위로 이동한 결과를 확인 할 수 있다.
역자주
- compute_at, reverse_compute_at은 블록의 계산 위치를 다른 루프 또는 블록 내부로 이동시키는 스케줄링
- compute_at : 앞선 블록 안에서 같이 계산, reverse_compute_at : 나중 블록을 앞 루프 안으로 옮김
- 연산을 필요한 시점에만 수행하여 메모리 접근 줄이고 연산 지역성(locality) 향상
- 버퍼 공유 또는 중간 결과를 바로 사용하는 구조에서 타일링 이후, 해당 블록을 타일 단위로 옮길 때 효과적
Rewrite Reduction
이제까지의 reduction initialization / update 는 single block body에서 수행되었으며 이러한 형태는 outer loops i, j의 initialization / update가 동기화를 필요. loop transformation 후 Y elements의 초기화를 reduction update로 부터 분리할 수 있다.
|
|
역자 주
decompose_reduction은 TVM TensorIR에서 reduction 연산(누적 연산)을 초기화 단계와 갱신 단계로 분리해 주는 스케줄링 기법 T.init()이 loop body에 있으면 병렬화/재배열 어려워 진다. 또한 루프 제어를 단순화 시킬 수 있으며 메모리 접근이 최적화 된다. reorder, compute_at, unroll 등을 적용하기도 쉬워진다.
Trace the Transformation
TensorIR schedule은 절차적 언어로 단계별로 실행된다. TVM은 schedule 또는 schedule 이력을 추적할 수 있다.
- sch.trace.show() : schedule 이력 출력
- sch.mod.show() : schedule 출력