Ripple은 LLVM에 가벼운 확장을 도입해 SIMD 하드웨어를 대상으로 SPMD와 루프 어노테이션 기반 병렬 프로그래밍을 직접 표현할 수 있는 컴파일러 해석형 API를 제공합니다. 새로운 IR 명령어나 타입, 별도 백엔드 없이 다양한 차원의 연산을 한 함수 내에서 공존시킬 수 있으며, 인트린식·인라인 어셈블리와도 자연스럽게 혼용됩니다. 설계 배경과 목표, 알고리즘(모양 전파와 if-변환), MLIR 방언, 그리고 AArch64 SME 최적화 사례를 포함합니다.
우리는 Single-Program, Multiple-Data(SPMD) 및 루프 어노테이션 기반 병렬 프로그래밍을 SIMD 하드웨어에서 지원하기 위해 LLVM에 소량의 기능을 더하는 Ripple을 개발해 왔습니다. 우리는 이 두 모델을 지원하는 병렬 프로그래밍 API를 제안합니다. 이는 GPU 스타일의 SPMD와 달리, 서로 다른 차원의 블록 계산(0차원 포함)이 같은 함수 안에 공존할 수 있다는 점에서 다릅니다. 이를 통해 스칼라, 벡터, 텐서 계산의 혼합을 명시적으로 표현하기가 쉬워집니다.
Ripple의 또 다른 핵심 측면은 새로운 종류의 LLVM 명령어나 타입을 요구하지 않는다는 것입니다. 또한 새로운 타깃으로의 포팅 시에도 새로운 백엔드가 필요하지 않습니다.
마지막으로, Ripple API는 다른 최적화된 SIMD 형태를 배제하지 않습니다.
예를 들어, SIMD 인트린식과 인라인 어셈블리는 Ripple 코드를 포함하는 함수 내에서 함께 사용할 수 있습니다.
Ripple의 개발은 현대 프로세서에서 효율적인 벡터화 코드를 작성하는 과정을 단순화하고 향상시키려는 필요성에서 비롯되었습니다. 머신러닝부터 과학 계산에 이르기까지 다양한 분야에서 계산 수요가 계속 증가함에 따라, 하드웨어 기능을 효과적으로 활용하는 능력은 점점 더 중요해지고 있습니다.
현재 LLVM이 지원하는 벡터 프로그래밍 모델은 주로 다음과 같습니다.
simd 루프 어노테이션이 이에 해당합니다. 이러한 벡터화 전략은 데이터 의존성과 성능 트레이드오프를 컴파일러가 정확히 모델링하여 벡터화 전략을 결정할 수 있다는 가정에 의존합니다. OpenMP simd의 경우, 컴파일러는 주석(힌트)이 유효한지 판단한 뒤에 어노테이션된 루프의 벡터화를 시도합니다.이러한 모델들은 보수적이어서 개발자의 생산적인 SIMD 코드 작성 능력을 제한하거나, 혹은 SIMD 아키텍처 간 이식에 과도한 노력을 요구합니다.
우리의 주요 목표는 개발자와 컴파일러 간 더 직접적인 계약을 도입하는 것입니다. 여기서 개발자는 데이터 의존성, 스칼라/벡터/텐서 SIMD 계산의 최적 혼합, 그리고 이러한 선택의 성능 영향에 대해 알고 있다는 신뢰를 전제로 합니다.
개발자는 대상 아키텍처에 존재하는 스칼라, 벡터, 텐서 실행 유닛의 조합에 맞춰 병렬 프로그램을 조정할 수 있어야 합니다.
프로그래밍 추상화는 이식 가능해야 하며, 즉 대상 SIMD 아키텍처 간에 동일해야 합니다.
프로그래밍 추상화는 타깃 특화 SIMD 빌트인과 인라인 어셈블리 사용을 배제하지 않아야 합니다. 이는 점진적 최적화 접근을 가능케 하며, 개발자는 Ripple 병렬 코드를 시작점으로 사용하고 성능 격차가 발견되는 부분만 인트린식이나 인라인 어셈블리로 치환할 수 있습니다.
목표는 성능 이식성(performance-portable) 해법을 만드는 것이 아닙니다.
핵심 API와 의미론은 SIMD 아키텍처 간 이식 가능하지만, 개발자는 SIMD 폭, 캐시 등 특정 하드웨어에 맞게 프로그램을 최적화해야 합니다.
개발자는 병렬성과 데이터 지역성을 지정하는 API를 제공받고, 컴파일러는 해당 API를 해석하여 개발자가 의도한 벡터 프로그램을 “렌더링”합니다.
우리는 병렬성을 표현하는 기반으로 Ripple SPMD 도입을 제안합니다. GPU SPMD와 마찬가지로 처리 요소는 블록, 즉 텐서로 표현됩니다. API를 사용해 블록 형태를 설정/조회하고, 블록 안에서의 처리 요소 인덱스에 접근합니다.
다음 1차원 예제는 크기 8의 두 float 벡터를 더하는 데 Ripple SPMD API를 사용합니다.
1: void vector_add_1D(unsigned pindex, float *a, float *b, float *sum) {
2: ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, /* block index 0 with size */ 8);
3: size_t ripple_index = ripple_id(BS, /* block index */ 0);
4: sum[ripple_index] = a[ripple_index] + b[ripple_index];
5: }
ripple_set_block_shape()는 VECTOR_PE라는 별칭을 가진 SIMD 엔진의 SPMD 블록 형태를 정의합니다.
여기서는 1차원 크기 8의 처리 요소 블록을 정의합니다.
VECTOR_PE의 블록은 SIMD 레인에 매핑된 처리 요소들의 집합을 나타내며, ripple_id(BS, 0)로 인덱싱되는 1차원 집합입니다.
4행의 계산은 ripple_id 호출에 의존하며, 그 결과 1차원 SIMD(일명 “벡터”) 명령어가 생성됩니다.
Ripple은 블록 요소 레이아웃, 즉 블록 요소와 하드웨어 SIMD 요소 간의 매핑을 정의합니다.
레이아웃은 0차원에서의 연속되는 값들이 연속적인 SIMD 레인에 매핑되도록 정의되며, OpenCL(R)과 CUDA(R)가 가정하는 레이아웃과 호환됩니다.
블록 차원의 부분집합에 의존하는 명령은 해당 차원 부분집합이 정의하는 서브블록에서 실행됩니다.
이 규칙은 서로 다른 형태의 명령이 공존하도록 허용합니다.
이러한 형태는 다음 코드에서 보이듯 함수의 블록 형태의 부분집합입니다.
ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 8, 8); // 블록 형태는 8x8
size_t a = x * 3; // 스칼라(x가 스칼라라고 가정)
size_t v0 = ripple_id(BS, 0); // shape(v0) = 8x1
size_t v1 = ripple_id(BS, 1); // shape(v1) = 1x8
size_t v_sum = v0 + v1 + a; // shape = 8x8
입력 형태와 다른 출력 형태를 갖는 연산(리덕션, 브로드캐스트, 슬라이싱)을 수행하는 추가 API가 있습니다(해당 API는 Ripple 매뉴얼 참고).
이들 API 함수 외에는, 프로그램의 LLVM IR Instruction의 형태는 피연산자의 형태에 의해 정의되며, 암묵적 브로드캐스트 규칙(피연산자들을 공통 형태로 브로드캐스트하고, 그 공통 형태가 Instruction의 형태가 됨)에 따릅니다.
벡터 요소의 재배열(벡터 내부 및 벡터 간)은 SIMD 코드에서 매우 흔한 연산입니다.
이는 레지스터 파일 내부의 데이터 이동을 가능하게 합니다.
우리는 이러한 이동을 위한 API를 정의했으며, LLVM IR 명명법을 따라 이를 “셔플(shuffle)”이라 부릅니다.
shufflevector 명령을 직접 사용할 수 있게 합니다.API 정의와 예시는 매뉴얼(아래 참고 자료 섹션)에서 확인할 수 있습니다.
코드 실행이 SIMD 조건 값에 의해 제어될 때, 제어되는 코드는 if-변환이 적용되어 마스크드 코드가 생성됩니다.
이를 설명하기 위해, 다음 예제에서 5행의 증가 연산은 3개의 SIMD 명령(벡터 로드, 덧셈, 벡터 스토어) 시퀀스로 바뀝니다.
하지만 5행의 증가 연산은 4행 조건식에 의해 제어되는 블록 안에 있으므로, 결과 벡터 로드/스토어는 조건부로 실행됩니다. 출력 로드/스토어에 사용되는 SIMD 조건은 짝수 SIMD 레인에서만 참입니다.
void increment_even(int16_t x[8]) {
2: ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 8);
3: size_t v = ripple_id(BS, 0);
4: if (v % 2 == 0)
5: x[v] += 1;
6: }
clang의 문법 변환은 루프 어노테이션 기반 병렬 프로그래밍 모델도 가능하게 합니다. OpenMP(R)와 유사하게, ripple_parallel() 어노테이션은 어노테이션된 루프의 반복들이 처리 요소 블록에 라운드-로빈 방식으로 분배되어야 함을 정의합니다. 아래 예시와 같습니다.
void vecadd_subarray(int N, int start, int end,
float x[N], float y[N], float xpy[N]) {
ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 32);
ripple_parallel(BS, 0);
for (int i = start; i < N; ++i) {
xpy[i] = x[i] + y[i];
}
}
Ripple 루프 병렬 어노테이션은 pragma 형태로도 사용할 수 있습니다. 예시는 다음과 같습니다.
void vecadd_subarray(int N, int start, int end,
float x[N], float y[N], float xpy[N]) {
ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 32);
#pragma ripple parallel Block(BS) Dims(0)
for (int i = start; i < N; ++i) {
xpy[i] = x[i] + y[i];
}
}
ripple_parallel()은 루프를 전체 벡터 루프와 에필로그로 분리합니다.
사용자가 N이 완전한 벡터 묶음에 대응함을 아는 경우, ripple_parallel_full()을 사용하거나 #pragma ripple parallel 어노테이션에 NoRemainder 절을 추가해서 전체 벡터 루프만 생성할 수 있습니다.
Ripple LLVM 패스는 두 개의 서브패스로 동작합니다.
형태는 먼저 각 ripple_id() 호출에 연관되고, 암묵적 브로드캐스트 규칙과 Ripple API 함수에 연관된 특수 전파 규칙을 사용하여 제어 독립적인 데이터플로 고정점 알고리즘으로 전파됩니다.
Instruction들은 분기 Instruction을 포함해 다양한 형태를 가질 수 있습니다.분기 명령이 스칼라가 아닌 값에 의존할 경우, 그 분기가 제어하는 코드는 if-변환됩니다.
현재는 스칼라가 아닌 값이 제어하는 CFG 부분 그래프를 대체로 SESE(Single-Entry Single-Exit)로 제한합니다.
사전 패스가 비-SESE 형태로 접혀 있는 SESE 서브-CFG를 복원합니다.
우리는 동일한 타깃 독립 Ripple 패스를 사용하여 Hexagon(R), X86-64(R), Arm(R) SIMD 코드를 생성할 수 있었습니다. 더 많은 SIMD 타깃도 큰 노력 없이 지원될 수 있다고 추정합니다.
Ripple은 LLVM 다른 부분에 대한 영향을 최소화하도록 설계되었습니다.
-fenable-ripple 플래그로 게이트됩니다.SPMD 프로그래밍 모델은 처리 요소를 “블록”, 즉 잠재적으로 다차원 배열로 모델링하는 데 의존합니다. GPU SPMD 모델이 블록의 모든 처리 요소가 동일한 코드를 실행한다고 보는 반면, Ripple SPMD 모델은 블록 차원의 부분집합에 의존하는 문장은 해당 차원 부분집합이 정의하는 서브블록이 실행한다고 봅니다. 이를 통해 다양한 차원의 계산이 같은 함수 안에 공존할 수 있습니다.
foo_redundant는 중복 계산을 수행하는 반면 foo는 수행하지 않습니다.1: void foo(int a, int b, int *z) {
2: ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 8);
3: size_t v = ripple_id(BS, 0);
4: int x = a * 2; // 스칼라
5: int y = x + b; // 스칼라
6: z[v] = y; // 여기서 y가 브로드캐스트됨
7:}
1: void foo_redundant(int a, int b, int *z) {
2: ripple_block_t BS = ripple_set_block_shape(VECTOR_PE, 8);
3: size_t v = ripple_id(BS, 0);
4: int x = ripple_broadcast(BS, 0b1, a) * 2; // 벡터: a를 명시적으로 브로드캐스트
5: int y = x + b; // 벡터: b와 y가 암묵적으로 브로드캐스트됨
6: z[v] = y; // 벡터(암묵 브로드캐스트 불필요)
7:}
SPMD와 루프 어노테이션 프로그래밍 모델은 주석이 달린 스칼라 코드처럼 병렬 코드를 작성할 수 있어서 편리합니다. SIMD 프로세서는 일반적으로 스칼라 연산의 원소별 벡터 등가 연산을 수행하고, 벡터 레지스터 내부/간으로 원소를 이동할 수 있습니다. 이러한 기능은 Ripple 컴파일러 패스에서 타깃 독립적으로 다뤄집니다.
그러나 SIMD 프로세서에는 다른 프로세서에는 없는 SIMD 명령이 있거나, 표준 스칼라 연산의 원소별 버전이 아닌 경우도 자주 있습니다. Ripple은 벡터 라이브러리 지원을 통해 SPMD 및 루프 어노테이션 모델 안에서 이러한 SIMD 명령에 접근할 수 있도록 합니다.
벡터 라이브러리 제작자는 헤더 파일에 해당 명령의 스칼라 버전을, 바이너리(LLVM 비트코드) 파일에 벡터 버전(들)을 제공합니다. 명명 규약에 따라, Ripple은 스칼라 라이브러리 호출의 SIMD 해석(“확장”)을 비트코드 라이브러리의 관련 벡터 함수 호출로 대체합니다.
이 함수들의 링크 및 인라인은 Ripple 이후 패스로 수행하거나 링크 타임에 수행할 수 있습니다. 전자는 라이브러리 호출 간 더 많은 최적화를 가능케 하고, 후자도 링크 타임 최적화의 이점을 누릴 수 있습니다.
Ripple API는 현재 고정 블록 형태를 지원합니다. 이는 RISC-V(R) “V” 확장과 Arm(R) SVE 같은 스케일러블 SIMD 아키텍처를 직관적으로 표현하지 못합니다.
이러한 아키텍처에서는 실행 중에 알 수 있는 “스케일” 인자가, 해당 아키텍처가 그 아키텍처 계열(Arm SVE, RISC-V V)의 기본 SIMD 폭 대비 얼마나 큰지 정의합니다.
우리는 이러한 아키텍처를 위해 몇 가지 해결책을 검토 중입니다.
개발자는 스케일 인자를 검사하고, 고정된 스케일 값 집합에 대해 함수를 특수화할 수 있습니다. 이 접근은 C++ 템플릿과 C 매크로를 사용하면 쉽게 구현할 수 있습니다. 그러나 이런 스타일로 작성된 코드의 이식성은 선택한 특수화 스케일 값들로 제한됩니다. 또한 특수화는 인스턴스 수만큼 코드 크기를 늘립니다.
우리는 SIMD 블록의 마지막 차원을 동적으로 정의할 수 있는 Ripple 확장을 고려 중입니다.
이는 개발자가 스케일 인수의 함수로 코드를 정의할 수 있게 합니다.
size_t scale; // 스케일 인자를 나타냄
// ...
auto BS = ripple_set_block_shape(VECTOR_PE, 8, scale);
마지막 차원만 동적으로 허용하면, 모든 차원을 동적으로 허용할 때 발생할 수 있는 정적 코얼레싱 감지 능력 손상을 피하면서 동적 블록 크기를 가능케 합니다.
SPMD 프로그래밍 모델은 수십 년 전부터 존재해 왔으며, 메시지 패싱 인터페이스인 MPI에 의해 대중화되었습니다.
2000년대에는 CUDA(R)가 NVIDIA(R) 프로그래머블 GPU의 명령어 수준 병렬성까지 포함한 다단계 병렬성을 표현하는 데 SPMD를 사용했습니다.
이 전통적 형태의 SPMD(본 RFC에서는 “GPU SPMD”라 부름)는 블록으로 표현되는 동종 처리 요소의 전체 집합이 하나의 프로그램을 일괄적으로 실행한다고 가정합니다. Ripple SPMD 모델은 이질적인 처리 요소를 표현하고, 서로 다른 차원의 부분집합 처리 요소만 프로그램을 실행할 수 있도록 합니다.
OpenCL(R) 규격은 CUDA(R) 모델을 계승하면서, 프로그래밍 모델 내에 GPU의 아키텍처적 제약(동기화 기능, 로컬 메모리 구조)을 유지했습니다. OpenCL 함수에 전달할 수 있는 형식 매개변수의 종류 제한(예: 상수 크기)도 GPU 특유의 제약에서 유래했습니다. 이러한 제약은 많은 비-GPU 프로세서에는 무관하며, 우리는 종종 비-GPU 머신에서 효율적인 벡터 코드 작성을 방해한다고 주장합니다.
SYCL은 OpenCL의 기본 개념을 사용하여 C++에서 데이터 병렬 계산을 표현하기 위한 클래스 집합을 정의합니다. SYCL 핸들러 메서드를 통해 “병렬 루프” 개념을 제공합니다.
HIP은 GPU SPMD 프로그래밍 모델의 또 다른 구현입니다. HIP의 특징은 동일한 프로그램을 호스트와 GPU 디바이스 양쪽에서 실행하도록 컴파일할 수 있다는 점입니다.
OpenMP(R)는 원래 공유 메모리 머신을 위한 멀티스레드 코드를 작성하기 위해 개발되었습니다. 이후 많은 개선을 거쳐 가속기로 코드 오프로딩, 루프 차원에 대한 자동 벡터화 힌트 제공 등이 가능해졌습니다. OpenMP와 Ripple의 루프 어노테이션 모델 간 큰 차이는 Ripple에서는 사용자가 벡터화를 지시하고 컴파일러는 사용자의 벡터화 선택을 렌더링한다는 점입니다.
OpenMP가 simd 어노테이션을 힌트로 취급하고 벡터화 여부를 컴파일러에 맡기는 반면, Ripple에서는 벡터 코드 생성을 컴파일러 분석이 가로막지 않습니다.
OpenACC(R)는 OpenACC 지원 컴파일러가 수행할 루프 변환을 정의하는 pragma 기반 루프 어노테이션 시스템입니다. OpenACC가 목표로 하는 루프 변환에는 타일링 형태, 계산 오프로딩, 벡터화 등이 포함됩니다.
OpenMP(R)와의 교집합이 크기 때문에, LLVM에서는 OpenACC가 주로 OpenMP 관점에서 구현됩니다. OpenACC는 SPMD 프로그래밍 모델을 제공하지 않습니다.
C++23은 (1차원) SIMD 벡터를 표현하는 실험적 simd 클래스를 제공합니다. 이 simd 인터페이스에는 (1차원, 전체) 리덕션, 표준 수학 함수, 정렬 태그 등이 포함됩니다.
C++23 simd와 Ripple의 가장 두드러진 차이는 지원 언어, C++ simd에 루프 벡터화 표기가 없다는 점, 그리고 지원되는 SIMD 차원의 수( C++ simd는 1차원으로 제한)입니다.
Ripple 프로그래머 매뉴얼: Ripple Manual
LLVM 21.1.0 기반 Ripple 구현: llvm-ripple
우리는 LLVM Ripple 인트린식으로 하향 변환할 수 있는 핵심 Ripple 인트린식 집합을 독립 MLIR 방언으로 제공합니다. 현재 SIMD 블록 모델링, 리덕션, 브로드캐스트, 슬라이싱, LLVM 스타일 셔플을 위한 연산을 제공합니다. ripple 방언의 연산은 ripple 접두사를 가집니다.
다음 예시는 MLIR에서 SIMD 블록을 정의하는 방법을 보여줍니다.
func.func @main() {
%peid = arith.constant 0 : i32
%dim = arith.constant 0 : i32
%size_1 = arith.constant 2 : i32
%size_2 = arith.constant 128 : i32
%bs = ripple.setshape %peid [%size_1, %size_2 : i32, i32] : i32 -> !ptr.ptr<#ptr.generic_space>
%nv = ripple.getsize %bs [%dim : i32 to i32] : !ptr.ptr<#ptr.generic_space>
return
}
커스텀 어셈블리 포맷에서는 Ripple 블록 정보를 담는 구조체를 나타내기 위해 불투명 포인터를 사용합니다. 이 포인터는 getsize 같은 추가 연산에 전달되어 전역 형태 정보를 유지하는 대신 SIMD 형태 정보를 명시적으로 식별합니다. 우리는 이전과 같이 %dim 인자를 사용하여 특정 SIMD 레인을 식별합니다.
다음 예시는 Ripple 블록 정보를 사용해 명시적으로 브로드캐스트된 벡터를 작성하고, 그로부터 슬라이스를 추출하는 방법을 자세히 보여줍니다.
func.func @main() {
...
%bs = ripple.setshape %peid [%size_1, %size_2 : i32] : i32 -> !ptr.ptr<#ptr.generic_space>
%v0 = ripple.index (%bs : !ptr.ptr<#ptr.generic_space>) [%dim : i32] -> i32
%nv = ripple.getsize (%bs : !ptr.ptr<#ptr.generic_space>) [%dim : i32] -> i32
%zero = arith.constant 0 : i64
%vzero = ripple.broadcast (%bs : !ptr.ptr<#ptr.generic_space>) [ %zero : i64, %dim : i32] -> i32
%slice = arith.constant -1 : i64
%vzero_half = ripple.slice [%vzero : i32, %slice : i64, %zero : i32] -> i32
return
}
또한 C API와 유사한 LLVM 스타일 셔플을 작성할 수 있는 방법도 제공합니다. 셔플 함수는 호출의 스코프 내에 정의되어야 합니다.
func.func @foo(%k : i32, %n : i32) -> i32 {
%result = %arith.subi %n, %k : i32
return %result
}
func.func @main() {
...
%foo = func.constant @foo : (i32, i32) -> i32
%vzero_shuff = ripple.ishuffle [%vzero : i32, %foo : (i32, i32) -> i32] -> i32
return
}
SME는 2차원 SIMD 명령을 제공합니다.
Ripple은 다차원 SPMD 블록 계산을 통해 SIMD 계산을 표현할 수 있게 하므로, 우리는 2차원 블록 기반 Ripple 코드에서 SME의 2차원 SIMD 명령을 생성하는 방법을 구현했습니다.
설계 제약은 다음과 같습니다.
Ripple 패스는 LLVM의 타깃 독립 최적화 단계의 일부입니다. 자체적으로 타깃 독립 패스이기도 합니다. 이는 타깃 독립 코드는 Ripple 패스에, 타깃 종속 코드는 타깃 백엔드와 컴파일러 라이브러리에 둠으로써 강한 분리를 유지하는 데 도움이 됩니다.
블록의 다차원 표현은 Ripple 패스 내부에서만 유지됩니다. Ripple 패스 외부에서는 SIMD 계산에 대해 LLVM의 벡터 표현을 사용합니다.
Ripple 벡터화 이후, 행렬-행렬 및 행렬-벡터 연산은 LLVM IR 벡터에서 수행됩니다. 64비트 Arm(R) Scalable Matrix Extension(SME) 매트릭스 처리 엔진을 최대한 활용하기 위해, 우리 패스는 벡터 형태에서 행렬 구조를 재구성하고, 512비트 벡터 및 매트릭스 처리 기능을 활용하는 효율적인 SVE/SME 명령을 선택하는 인트린식 선택 패스를 적용합니다.
그 결과, Ripple AArch64 SME 컴파일러는 사용자 공간 Ripple 어노테이션, 패턴 인식 기반의 컴파일러 IR 재작성, 그리고 SVE/SME 인트린식 선택을 결합하여 자동 C→SME 코드 생성을 가능하게 합니다.
다음 섹션은 외적(outer-product) 행렬 곱셈과 전치(transpose)에 대한 제안 접근을 제시합니다.
컴파일러가 SVE/SME 코드를 자동 생성하려면, 행렬-행렬 및 행렬-벡터 연산을 식별해 동등한 SVE/SME 인트린식 명령으로 변환해야 합니다. 이를 지원하기 위해, 후기 IR–초기 코드 생성 단계에 해당하는 SVESMEIntrinsicSelection 패스를 도입했습니다. 이 패스는 Ripple로 벡터화된 고정 길이의 와이드 벡터 패턴을 AArch64 SVE/SME 인트린식으로 변환합니다. 또는 인트린식 선택을 Ripple 이후 처리 패스로도 적용할 수 있습니다.
요컨대, 이 IR 재작성 접근은 패턴 매칭을 통해 1차원 벡터 연산에서 행렬 정보를 인식/재구성하고, 그에 맞춰 효율적인 SVE/SME 인트린식을 선택하는 데 초점을 둡니다.
이 IR 리라이터의 주요 기능은 외적 선택, 저장 확장, 루프 구성, 프레디케이트 생성입니다. 추가로, 전치와 인터리브에 사용되는 shufflevector 같은 특정 행렬/벡터 연산은 패턴 매칭되어 해당 SVE/SME 명령으로 대체될 수 있어 IR 리라이터의 역량을 확장합니다.
컴파일러 변환의 핵심 기능을 설명하기 위해, 다음 섹션에서는 일련의 IR 변환 예시를 살펴봅니다. 예시는 부동소수점 데이터 타입을 기반으로 하며, 32x32 블록 크기와 512비트 스트리밍 벡터 길이를 가정합니다.
아래 Ripple 코드처럼, 행렬-행렬 곱셈에서 C = A x B는 외적의 합으로 계산된다고 가정합니다.
#include <ripple.h>
#define SME_LANES 0
#define SME_SIZE 32
void matmul_arg(float *A, float *B, float *C, int M, int N, int K) {
ripple_block_t sme_block =
ripple_set_block_shape(SME_LANES, SME_SIZE, SME_SIZE);
ripple_parallel(sme_block, 1);
for (int i = 0; i < M; i++) {
ripple_parallel(sme_block, 0);
for (int j = 0; j < N; j++) {
__builtin_assume(K > 0);
float tile = 0;
for (int k = 0; k < K; k++) {
tile += A[k * M + i] * B[k * N + j];
}
C[i * N + j] = tile;
}
}
}
이 표현은 외적 같은 행렬 연산 가속에 최적화된 ARM Scalable Matrix Extension(SME)을 활용하기에 적합합니다.
아래 IR 예시는 외적을 수행하는 k 루프의 한 반복을 보여줍니다. 벡터 A와 B는 각각 nx32f32 벡터로 먼저 로드됩니다. A의 로드는 수평 스플랫(특정 셔플)을 거쳐 nx1024f32로 표현되는 행렬 유사 구조를 형성하고, B의 로드는 수직 스플랫(셔플)되어 유사한 nx1024f32 레이아웃을 생성합니다. 그런 다음 이 재구성된 벡터들에 융합 곱-더하기(fmuladd)가 적용됩니다.
이 벡터 연산 시퀀스는 실질적으로 SME FMOPA 명령에 매핑됩니다. IR 리라이터의 역할은 이 패턴을 인식해 해당 SME 인트린식으로 치환하는 것입니다. 이 변환에는 고정 길이 nx32f32 벡터 로드를 두 개의 스케일러블 nxv4f32 벡터로 분할하고 SME의 ZA 매트릭스 레지스터에서 사용 가능한 네 개 타일을 모두 활용하는 과정도 포함됩니다.
%load.A = load <32 x float>, ptr %arrayidx9, align 4
%.ripple.bcast = shufflevector <32 x float> %load.A, <32 x float> poison, <1024 x i32> <i32 0, i32 0, ..., i32 0, i32 1, i32 1, ..., i32 1, ... , i32 31, i32 31, ..., i32 31> ; horizontal shuffle
%load.B = load <32 x float>, ptr %arrayidx15, align 4
%.ripple.bcast129 = shufflevector <32 x float> %load.B, <32 x float> poison, <1024 x i32> <i32 0, i32 1, ..., i32 31, i32 0, i32 1, ..., i32 31, ..., i32 0, i32 1, ..., i32 31> ; vertical shuffle
%.ripple.vectorized = tail call <1024 x float> @llvm.fmuladd.v1024f32(<1024 x float> %.ripple.bcast, <1024 x float> %.ripple.bcast129, <1024 x float> %tile.041.ripple.vectorized)
변환된 IR은 아래와 같습니다.
%load.A = load <32 x float>, ptr %arrayidx9, align 4
%load.B = load <32 x float>, ptr %arrayidx15, align 4
%A0 = call <16 x float> @llvm.vector.extract.v16f32.v32f32(<32 x float> %load.A, i64 0)
%A0.vscale = call <vscale x 4 x float> @llvm.vector.insert.nxv4f32.v16f32(<vscale x 4 x float> poison, <16 x float> %A0, i64 0)
%A1 = call <16 x float> @llvm.vector.extract.v16f32.v32f32(<32 x float> %load.A, i64 16)
%A1.vscale = call <vscale x 4 x float> @llvm.vector.insert.nxv4f32.v16f32(<vscale x 4 x float> poison, <16 x float> %A1, i64 0)
%B0 = call <16 x float> @llvm.vector.extract.v16f32.v32f32(<32 x float> %load.B, i64 0)
%B0.vscale = call <vscale x 4 x float> @llvm.vector.insert.nxv4f32.v16f32(<vscale x 4 x float> poison, <16 x float> %B0, i64 0)
%B1 = call <16 x float> @llvm.vector.extract.v16f32.v32f32(<32 x float> %load.B, i64 16)
%B1.vscale = call <vscale x 4 x float> @llvm.vector.insert.nxv4f32.v16f32(<vscale x 4 x float> poison, <16 x float> %B1, i64 0)
; 첫 번째 매개변수는 타일 ID입니다.
call void @llvm.aarch64.sme.mopa.nxv4f32(i32 0, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %A0.vscale, <vscale x 4 x float> %B0.vscale)
call void @llvm.aarch64.sme.mopa.nxv4f32(i32 1, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %A0.vscale, <vscale x 4 x float> %B1.vscale)
call void @llvm.aarch64.sme.mopa.nxv4f32(i32 2, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %A1.vscale, <vscale x 4 x float> %B0.vscale)
call void @llvm.aarch64.sme.mopa.nxv4f32(i32 3, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %A1.vscale, <vscale x 4 x float> %B1.vscale)
Ripple로 벡터화된 IR에서 32x32 행렬 블록은 하나의 store <1024 x float> 명령으로 저장됩니다. 그러나 SVE/SME는 ZA 매트릭스 전체를 한 번에 메모리에 쓰는 단일 명령을 지원하지 않습니다. 따라서 컴파일러는 ZA 매트릭스의 각 벡터 슬라이스를 한 줄씩 메모리에 저장하는 루프 구조를 생성해야 합니다.
for.cond.cleanup7: ; preds = %for.body8
%arrayidx22 = getelementptr inbounds nuw [16 x [32 x [32 x float]]], ptr %C, i64 %indvars.iv51, i64 %indvars.iv46, i64 0, i64 0
store <1024 x float> %.ripple.vectorized, ptr %arrayidx22, align 4
...
br i1 %exitcond50.not, label %for.cond.cleanup3, label %for.cond5.preheader
for.body8: ; preds = %for.cond5.preheader, %for.body8
%indvars.iv = phi i64 [ 0, %for.cond5.preheader ], [ %indvars.iv.next, %for.body8 ]
%tile.041.ripple.vectorized = phi <1024 x float> [ zeroinitializer, %for.cond5.preheader ], [ %.ripple.vectorized, %for.body8 ]
...
%.ripple.vectorized = tail call <1024 x float> @llvm.fmuladd.v1024f32(<1024 x float> %.ripple.bcast, <1024 x float> %.ripple.bcast134, <1024 x float> %tile.041.ripple.vectorized)
%indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
%exitcond.not = icmp eq i64 %indvars.iv.next, %K
br i1 %exitcond.not, label %for.cond.cleanup7, label %for.body8
변환된 IR은 원래의 외적 k 루프에서 `%store.preheader` 블록으로 새로운 제어 흐름 탈출을 도입하는 방식을 보여줍니다. 거기에서 `%store.body`로 분기하여 ZA 매트릭스를 슬라이스별로 저장합니다. 저장이 완료되면 제어는 원래 루프 종료 지점인 `%for.cond.cleanup7`로 진행합니다.
이 루프 기반 구조는 기존의 `store <1024 x float>`를 대체합니다. 또한 ZA 타일에서 데이터를 8비트 정수(`i8`)로 읽어 두 개의 출력 Z 벡터 레지스터를 메모리에 연속으로 저장할 수 있게 합니다.
store.preheader: ; preds = %for.body8 ... br label %store.body
store.body: ; preds = %store.body, %store.preheader %indvar.st = phi i64 [ 0, %store.preheader ], [ %indvar.st.next, %store.body ] ... ; 세 번째 매개변수는 타일 ID입니다. %v0 = call <vscale x 16 x i8> @llvm.aarch64.sme.read.horiz.nxv16i8(<vscale x 16 x i8> undef, <vscale x 16 x i1> splat (i1 true), i32 0, i32 %slice.0) %v1 = call <vscale x 16 x i8> @llvm.aarch64.sme.read.horiz.nxv16i8(<vscale x 16 x i8> undef, <vscale x 16 x i1> splat (i1 true), i32 0, i32 %slice.1) %v2 = call <vscale x 16 x i8> @llvm.aarch64.sme.read.horiz.nxv16i8(<vscale x 16 x i8> undef, <vscale x 16 x i1> splat (i1 true), i32 0, i32 %slice.2) %v3 = call <vscale x 16 x i8> @llvm.aarch64.sme.read.horiz.nxv16i8(<vscale x 16 x i8> undef, <vscale x 16 x i1> splat (i1 true), i32 0, i32 %slice.3) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v0.cast, ptr %st.addr0, i32 4, <vscale x 4 x i1> splat (i1 true)) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v1.cast, ptr %st.addr1, i32 4, <vscale x 4 x i1> splat (i1 true)) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v2.cast, ptr %st.addr2, i32 4, <vscale x 4 x i1> splat (i1 true)) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v3.cast, ptr %st.addr3, i32 4, <vscale x 4 x i1> splat (i1 true)) %indvar.st.next = add i64 %indvar.st, 1 %exit.cond = icmp eq i64 %indvar.st.next, 16 br i1 %exit.cond, label %for.cond.cleanup7, label %store.body
for.cond.cleanup7: ; preds = %store.body ... br i1 %exitcond50.not, label %for.cond.cleanup3, label %for.cond5.preheader
for.body8: ; preds = %for.cond5.preheader, %for.body8 %indvars.iv = phi i64 [ 0, %for.cond5.preheader ], [ %indvars.iv.next, %for.body8 ] ... call void @llvm.aarch64.sme.mopa.nxv4f32(i32 0, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %27, <vscale x 4 x float> %31) call void @llvm.aarch64.sme.mopa.nxv4f32(i32 1, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %27, <vscale x 4 x float> %33) call void @llvm.aarch64.sme.mopa.nxv4f32(i32 2, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %29, <vscale x 4 x float> %31) call void @llvm.aarch64.sme.mopa.nxv4f32(i32 3, <vscale x 4 x i1> splat (i1 true), <vscale x 4 x i1> splat (i1 true), <vscale x 4 x float> %29, <vscale x 4 x float> %33) %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1 %exitcond.not = icmp eq i64 %indvars.iv.next, %K br i1 %exitcond.not, label %store.preheader, label %for.body8
### [](https://discourse.llvm.org/t/rfc-ripple-a-compiler-interpreted-api-to-support-spmd-and-loop-annotation-programming-for-simd-targets/88241#p-351616-predicate-generation-for-partial-tiles-38)**부분 타일을 위한 프레디케이트 생성**
Arm SME/SVE2.1은 프레디케이트 레지스터와 올-폴스 프레디케이트 사이에서 선택을 가능하게 하는 PSEL(Predicate Select) 명령을 도입했습니다. 즉, PSEL은 두 번째 소스 프레디케이트의 인덱스된 원소가 참이면 첫 번째 소스 프레디케이트의 내용을 대상 프레디케이트에 복사하고, 그렇지 않으면 대상 프레디케이트를 올-폴스로 설정합니다.
임의의 행렬 크기를 처리할 때, 최종적으로 벡터 스토어에 사용될 프레디케이트를 생성하는 전략이 다른 네 가지 타일 유형이 있습니다. 1) 전체 타일: 올-트루 프레디케이트 사용; 2) B에 대한 프레디케이션이 있는 타일: pred.B를 직접 적용; 3) A에 대한 프레디케이션이 있는 타일: pred.A를 사용해 올-트루와 올-폴스 중 선택; 4) A와 B 모두에 대한 프레디케이션이 있는 타일: pred.A를 사용해 pred.B를 선택.

아래 IR 조각은 이 논리를 보여주기 위해 타일 4에서 추출한 것입니다.
%cmp145 = icmp slt <32 x i32> %add144.ripple.LS.instance.ripple.branch.clone, %N.ripple.bcast.splat538 %cmp145.bcast = shufflevector <32 x i1> %cmp145, <32 x i1> poison, <1024 x i32> <i32 0, i32 1, ..., i32 31, i32 0, i32 1, ..., i32 31, ..., i32 0, i32 1, ..., i32 31> ; vertical shuffle on pred.B ... %cmp87 = icmp slt <32 x i32> %add86.ripple.vectorized, %M.ripple.bcast.splat %cmp87.bcast = shufflevector <32 x i1> %cmp87, <32 x i1> poison, <1024 x i32> <i32 0, i32 0, ..., i32 0, i32 1, i32 1, ..., i32 1, ..., i32 31, i32 31, ..., i32 31> ; horizontal shuffle on pred.A ... %.ripple.branch.mask.apply563 = and <1024 x i1> %cmp145.bcast, %cmp87.bcast tail call void @llvm.masked.scatter.v1024f32.v1024p0(<1024 x float> %.ripple.vectorized549, <1024 x ptr> %arrayidx172, i32 4, <1024 x i1> %.ripple.branch.mask.apply563)
변환된 IR에서는 원래 `masked.scatter`의 마스크를 구성하던 `icmp` 명령을 재활용해 네 개의 SVE `whilelt` 명령( pred.A용 2개, pred.B용 2개)을 생성합니다. 그 뒤 `psel` 명령이 이어지며, `pred.A`를 사용해 `pred.B` 또는 올-폴스 프레디케이트 중 하나를 선택합니다. 이렇게 생성된 프레디케이트를 `masked.store` 명령에 전달하여 데이터 쓰기를 완료합니다.
store.preheader13: ; preds = %for.body156.ripple.branch.clone.ripple.branch.clone %tilebase.jj.2 = add i64 %tilebase.jj, 16 %pred.B0 = call <vscale x 4 x i1> @llvm.aarch64.sve.whilelt.nxv4i1.i64(i64 %tilebase.jj, i64 %N) %pred.B1 = call <vscale x 4 x i1> @llvm.aarch64.sve.whilelt.nxv4i1.i64(i64 %tilebase.jj.2, i64 %N) %pred.B0.conv = call <vscale x 16 x i1> @llvm.aarch64.sve.convert.to.svbool.nxv4i1(<vscale x 4 x i1> %pred.B0) %pred.B1.conv = call <vscale x 16 x i1> @llvm.aarch64.sve.convert.to.svbool.nxv4i1(<vscale x 4 x i1> %pred.B1) %tilebase.ii.2 = add i64 %tilebase.ii, 16 %pred.A0 = call <vscale x 4 x i1> @llvm.aarch64.sve.whilelt.nxv4i1.i64(i64 %tilebase.ii, i64 %M) %pred.A1 = call <vscale x 4 x i1> @llvm.aarch64.sve.whilelt.nxv4i1.i64(i64 %tilebase.ii.2, i64 %M) ... br label %store.body12
store.body12: ; preds = %store.body12, %store.preheader13 %indvar.st = phi i64 [ 0, %store.preheader13 ], [ %191, %store.body12 ] ... %idx = trunc i64 %indvar.st to i32 %pred.st0 = call <vscale x 16 x i1> @llvm.aarch64.sve.psel.nxv4i1(<vscale x 16 x i1> %pred.B0.conv, <vscale x 4 x i1> %pred.A0, i32 %idx) %pred.st0.conv = call <vscale x 4 x i1> @llvm.aarch64.sve.convert.from.svbool.nxv4i1(<vscale x 16 x i1> %pred.st0) %pred.st1 = call <vscale x 16 x i1> @llvm.aarch64.sve.psel.nxv4i1(<vscale x 16 x i1> %pred.B1.conv, <vscale x 4 x i1> %pred.A0, i32 %idx) %pred.st1.conv = call <vscale x 4 x i1> @llvm.aarch64.sve.convert.from.svbool.nxv4i1(<vscale x 16 x i1> %pred.st1) %pred.st2 = call <vscale x 16 x i1> @llvm.aarch64.sve.psel.nxv4i1(<vscale x 16 x i1> %pred.B0.conv, <vscale x 4 x i1> %pred.A1, i32 %idx) %pred.st2.conv = call <vscale x 4 x i1> @llvm.aarch64.sve.convert.from.svbool.nxv4i1(<vscale x 16 x i1> %pred.st2) %pred.st3 = call <vscale x 16 x i1> @llvm.aarch64.sve.psel.nxv4i1(<vscale x 16 x i1> %pred.B1.conv, <vscale x 4 x i1> %pred.A1, i32 %idx) %pred.st3.conv = call <vscale x 4 x i1> @llvm.aarch64.sve.convert.from.svbool.nxv4i1(<vscale x 16 x i1> %pred.st3) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v0, ptr %addr0, i32 4, <vscale x 4 x i1> %pred.st0.conv) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v1, ptr %addr1, i32 4, <vscale x 4 x i1> %pred.st1.conv) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v2, ptr %addr2, i32 4, <vscale x 4 x i1> %pred.st2.conv) call void @llvm.masked.store.nxv4f32.p0(<vscale x 4 x float> %v3, ptr %addr3, i32 4, <vscale x 4 x i1> %pred.st3.conv) ... %indvar.st.next = add i64 %indvar.st, 1 %exit.cond = icmp eq i64 %indvar.st.next, 16 br i1 %exit.cond, label %for.cond.cleanup155.ripple.branch.clone.ripple.branch.clone, label %store.body12
### [](https://discourse.llvm.org/t/rfc-ripple-a-compiler-interpreted-api-to-support-spmd-and-loop-annotation-programming-for-simd-targets/88241#p-351616-shufflevector-pattern-matching-39)**ShuffleVector 패턴 매칭**
앞 절에서는 행렬 곱셈에 대한 자동 C→SME 코드 생성을 위해 필요한 핵심 컴파일러 변환을 보였습니다. 하지만 Ripple API는 더 큰 유연성과 폭넓은 기능을 제공하여, 컴파일러 변환이 행렬 곱셈을 넘어 확장되도록 합니다. 예를 들어 `ripple_shuffle` API는 사용자가 소스 인덱스에서 대상 인덱스로의 매핑을 사용자 정의할 수 있게 하며, 일반화된 셔플 연산을 사실상 지원합니다. 이는 행렬 전치나 인터리브 연산을 지원하는 데 특히 유용합니다. 타일 기반 행렬 전치를 예로 들어 설명하겠습니다.
#### [](https://discourse.llvm.org/t/rfc-ripple-a-compiler-interpreted-api-to-support-spmd-and-loop-annotation-programming-for-simd-targets/88241#p-351616-transpose-mapping-shufflevector-to-sme-za-tile-loads-and-stores-40)**전치: ShuffleVector를 SME ZA 타일 로드/스토어로 매핑**
다음은 Ripple 전치 코드입니다.
#include <ripple.h> #include <assert.h> #define TILE_SIZE 32
static attribute((always_inline)) float transpose_tile(float *tile_addr, size_t v) { auto transpose = [](size_t k, size_t block_size) -> size_t { unsigned offset = k / TILE_SIZE; unsigned row_idx = k % TILE_SIZE; return row_idx * TILE_SIZE + offset; }; return ripple_shuffle(tile_addr[v], transpose); }
void transpose_ripple(float *dest, float *src, unsigned m, unsigned k) { assert(m % TILE_SIZE == 0); assert(k % TILE_SIZE == 0); ripple_block_t sme_block = ripple_set_block_shape(0, TILE_SIZE, TILE_SIZE); size_t x = ripple_id(sme_block, 0); size_t y = ripple_id(sme_block, 1); for (int i = 0; i < m; i += TILE_SIZE) for (int j = 0; j < k; j += TILE_SIZE) { dest[y * TILE_SIZE + x] = transpose_tile(&src[i * k + j], y * k + x); dest += TILE_SIZE * TILE_SIZE; } }
행렬이 1차원 벡터로 평탄화되어 있으므로, 전치 연산은 인덱스 재배열—즉 열 원소를 줄 단위로 선택하는 작업이 됩니다. 이 32x32 블록을 사용하는 타일 기반 전치는 ZA 매트릭스 타일로의 수평 로드와 메모리로의 수직 스토어를 지원하는 SME 하드웨어 기능과 잘 맞습니다.
아래 타일 기반 행렬 전치 코드에서, Ripple로 벡터화된 IR은 일반적으로 세 단계로 구성됩니다.
1. 32x32 행렬 타일 로드;
2. 행렬을 행 우선에서 열 우선 순서로 재배열하는 `shufflevector` 명령;
3. 전치 결과 저장.
for.body7: ; preds = %for.body7.lr.ph, %for.body7 ... %load = tail call <1024 x float> @llvm.masked.gather.v1024f32.v1024p0(<1024 x ptr> %gep, i32 4, <1024 x i1> splat (i1 true), <1024 x float> poison) %.ripple.vectorized = shufflevector <1024 x float> %load, <1024 x float> poison, <1024 x i32> <i32 0, i32 32, ..., i32 992, i32 1, i32 33, ..., i32 993, ..., i32 31, i32 63, ..., i32 1023> ; transpose of a 32x32 block store <1024 x float> %.ripple.vectorized, ptr %dest.addr.132, align 4 ... br i1 %exit.cond, label %for.body7, label %for.cond.cleanup9
IR 리라이터는 “load + shufflevector + store” 패턴을 식별하고 이를 두 개의 명시적 루프로 변환합니다.
- 한 루프는 ZA 매트릭스 타일로 데이터를 수평으로 로드합니다.
- 다른 루프는 ZA 매트릭스로부터 데이터를 수직으로 메모리에 저장합니다.
이 변환은 `shufflevector` 명령의 필요성을 완전히 제거합니다. 반대로 수직 로드 후 수평 저장을 해도 같은 효과를 얻습니다.
for.body7: ; preds = %for.body7, %for.body7.lr.ph ... br label %load.loop
load.loop: ... ; 세 번째 매개변수는 타일 ID입니다. call void @llvm.aarch64.sme.ld1w.horiz(<vscale x 4 x i1> splat (i1 true), ptr %load.gep.tile0, i32 0, i32 %ld.iv.trunc) call void @llvm.aarch64.sme.ld1w.horiz(<vscale x 4 x i1> splat (i1 true), ptr %load.gep.tile1, i32 1, i32 %ld.iv.trunc) call void @llvm.aarch64.sme.ld1w.horiz(<vscale x 4 x i1> splat (i1 true), ptr %load.gep.tile2, i32 2, i32 %ld.iv.trunc) call void @llvm.aarch64.sme.ld1w.horiz(<vscale x 4 x i1> splat (i1 true), ptr %load.gep.tile3, i32 3, i32 %ld.iv.trunc) ... br i1 %load.exit.cond, label %store.loop, label %load.loop
store.loop: ... ; 세 번째 매개변수는 타일 ID입니다. call void @llvm.aarch64.sme.st1w.vert(<vscale x 4 x i1> splat (i1 true), ptr %store.gep.tile0, i32 0, i32 %st.iv.trunc) call void @llvm.aarch64.sme.st1w.vert(<vscale x 4 x i1> splat (i1 true), ptr %store.gep.tile1, i32 1, i32 %st.iv.trunc) call void @llvm.aarch64.sme.st1w.vert(<vscale x 4 x i1> splat (i1 true), ptr %store.gep.tile2, i32 2, i32 %st.iv.trunc) call void @llvm.aarch64.sme.st1w.vert(<vscale x 4 x i1> splat (i1 true), ptr %store.gep.tile3, i32 3, i32 %st.iv.trunc) ... br i1 %store.exit.cond, label %for.body7.split, label %store.loop
for.body7.split: ... br i1 %exit.cond, label %for.body7, label %for.cond.cleanup9
--
Arm은 Arm Limited(또는 그 자회사)의 등록상표입니다.
CUDA는 NVIDIA Corporation의 등록상표입니다.
Hexagon은 Qualcomm Incorporated의 등록상표입니다.
NVIDIA는 NIVIDA Corporation의 등록상표입니다.
OpenACC는 NVIDIA Corporation의 등록상표입니다.
OpenCL은 Apple Incorporated의 등록상표입니다.
OpenMP는 OpenMP Architecture Review Board의 등록상표입니다.
RISC-V는 RISC-V International의 등록상표입니다.
SYCL은 Khronos Group Inc.의 등록상표입니다.
X86-64는 Advanced Micro Devices, Incorporated의 등록상표입니다.