IREE, MLIR, 특히 MLIR Linalg 방언을 다루는 튜토리얼입니다. MLIR/Linalg 기본 문법과 linalg.generic, parallel/reduction 반복자, 정적·동적 형태, 행렬 곱(named op와 일반화), 정수 타입과 부호 확장, 그리고 IREE 도구(iree-compile, iree-run-module, iree-opt) 사용법을 예제와 함께 설명합니다.
이 튜토리얼은 IREE, MLIR, 그리고 그중에서도 MLIR Linalg 방언에 대해 동시에 다룹니다.
MLIR은 프로그래밍 언어지만, MLIR 자체만 놓고 보면 거의 빈 껍데기에 가깝습니다. 실제로 제공하는 것은 MLIR 방언을 정의할 수 있는 프레임워크이며, 기능은 방언에서 나옵니다.
MLIR 이름의 "IR"은 "중간 표현(intermediate representation)"을 의미합니다. 즉 MLIR은 주로 컴파일러 내부 코드 표현을 위해 만들어졌습니다. 하지만 MLIR은 사람이 다루기에도 꽤 편하고, 처음부터 직접 MLIR 프로그램을 작성하는 것도 어렵지 않습니다. 이 튜토리얼의 주제가 바로 그것입니다.
MLIR 이름의 "ML"은 "다중 레벨(multi-level)"을 의미합니다(기계 학습이 아님!). 같은 MLIR 프로그램 내에서 여러 방언을 자유롭게 섞을 수 있다는 뜻입니다. 각 방언은 연산, 타입, 속성을 정의할 수 있고, 단일 MLIR 문장에서도 서로 다른 방언의 연산/타입/속성을 혼합해 사용할 수 있습니다.
Linalg는 본질적으로 하나의 연산 linalg.generic으로 구성된 MLIR 방언입니다. 이 방언의 다른 대부분의 연산은 linalg.generic의 특수한 경우를 편의상 가명으로 제공하는 것입니다. 따라서 Linalg 방언을 설명한다는 것은 곧 linalg.generic을 설명하는 것과 같습니다.
이러한 설계의 요점은 단 하나의 연산인 linalg.generic이 다음과 같다는 것입니다:
이 특성들 덕분에 Linalg 방언은 기계 학습 컴파일러의 이상적인 "미들엔드" IR이 됩니다.
IREE는 MLIR 컴파일러이자 런타임으로, MLIR 프로그램을 점진적으로 더 낮은 수준의 방언들로 내리며 최종적으로 다양한 CPU, GPU 및 기타 하드웨어 타깃을 위한 머신 코드를 생성합니다. 개발자 개요 문서와 ML 프레임워크 문서를 참고하세요.
프런트엔드는 다양한 기계 학습 프레임워크에서 작성된 소스 프로그램을 MLIR Linalg 방언으로 흡수할 수 있습니다. 경계는 유동적이지만, Linalg까지를 전부 "프런트엔드"로 생각해도 무방합니다. 예를 들어 PyTorch 연동의 경우 프런트엔드는 torch-mlir이고, 최종 사용자는 IREE, torch-mlir, PyTorch를 통합한 iree-turbine 사용을 권장합니다.
이 튜토리얼은 Linalg 방언만을 다루며, Linalg 프로그램을 직접 작성하는 법을 배웁니다. 프런트엔드에 대한 위 설명은, 어떤 방식으로 프로그램을 IREE에 입력하더라도 내부에서는 Linalg 프로그램으로 재작성된다는 점을 분명히 하기 위한 것입니다. 즉 Linalg가 이 컴파일러의 실제 중간 표현이라는 뜻입니다.
IREE 빌드는 다운로드하거나 Python 패키지로 설치하거나 소스에서 빌드할 수 있습니다.
시작하기 전에: 공식 Linalg 튜토리얼도 있습니다. 본 튜토리얼과 접근 방식이 다르므로 서로 보완적입니다.
다음은 첫 번째 Linalg 함수입니다. 이 프로그램에서 사용된 스칼라 타입 f32는 32비트 부동소수점입니다.
MLIR 문법의 일부 요소를 살펴봅시다:
%는 SSA 값(여기서는 %result)을 나타냅니다.@는 함수(여기서는 @foo)를 나타냅니다.^는 블록(여기서는 ^bb0)을 나타냅니다.#는 속성 별칭(여기서는 #map_1d_identity)을 나타냅니다.x는 형태에서 차원 구분자 및 형태와 원소 타입 사이의 구분자로 쓰입니다. 예컨대 10xf32는 길이 10의 1차원 형태에 원소 타입이 f32임을 의미합니다.dialect.name 형식을 가집니다. 예를 들어 tensor.empty는 tensor 방언의 empty 연산이고, func.func는 func 방언의 func 연산입니다.// 아래에서 사용할 1차원 항등 사상.
#map_1d_identity = affine_map<(m) -> (m)>
// `%lhs`와 `%rhs` 두 개의 텐서 인수를 받고 텐서를 반환하는 @foo 함수를 정의합니다.
func.func @foo(
%lhs : tensor<10xf32>,
%rhs : tensor<10xf32>
) -> tensor<10xf32> {
// 아래에서 사용할 상수.
%c0f32 = arith.constant 0.0 : f32
// 결과의 "초기값(init value)"을 만듭니다. 추상적인 "할당"으로 생각하면 됩니다.
// 텐서를 생성하지만 그 원소들에 특정 값을 부여하지 않습니다. 이 텐서에서
// 어떤 원소라도 읽으면 정의되지 않은 동작이 됩니다.
%result_empty = tensor.empty() : tensor<10xf32>
// 연산을 수행합니다. 다음은 전부 하나의 linalg.generic 연산입니다.
%result = linalg.generic {
// 이 {...} 구간은 "속성(attributes)"입니다 - 이 연산에 대한 컴파일 타임 설정입니다.
indexing_maps=[
// `ins(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity,
#map_1d_identity,
// `outs(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity
],
// 텐서 차원이 하나 있으며, 이는 병렬 반복 차원입니다.
// 즉 결과 텐서의 차원으로도 나타납니다. 대안은 "reduction"이며,
// 결과 텐서에 나타나지 않는 차원입니다.
iterator_types=["parallel"]
} // 이 linalg.generic의 속성 끝. 다음은 매개변수입니다:
// `ins`는 일반 입력 매개변수를 전달하는 곳입니다
ins(%lhs, %rhs : tensor<10xf32>, tensor<10xf32>)
// `outs`는 "출력"을 전달하는 곳이지만, linalg에서 그 의미는 미묘합니다.
// 여기서는 tensor.empty를 전달하는데, 이는 기존 원소 값이 없는 출력 자리표시자입니다.
// 누산기가 있는 다른 예시에서는 여기에 누산기를 전달합니다.
outs(%result_empty : tensor<10xf32>)
// 매개변수 끝. 다음 {...} 부분이 "코드 블록"입니다.
{
// bb0는 각 입력 텐서에서 하나의 스칼라를 인수로 받아 해당 출력 텐서 원소를
// 계산하여 "yield"(즉 반환)하는 코드 블록입니다.
^bb0(%lhs_entry : f32, %rhs_entry : f32, %unused_result_entry : f32):
%add = arith.addf %lhs_entry, %rhs_entry : f32
linalg.yield %add : f32
} // 기본 블록 끝. 마지막으로 반환 타입을 기술합니다.
-> tensor<10xf32>
// linalg.generic 연산 끝.
// 함수의 반환 값을 반환합니다.
return %result : tensor<10xf32>
}
다음과 같이 컴파일합니다:
$ iree-compile \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu \
prog.mlir \
-o /tmp/prog.vmfb
참고
여기서는 CPU에서 동작시키기 위한 최소한의 iree-compile 플래그만 사용하며, 성능 극대화를 시도하지 않습니다.
GPU나 기타 비-CPU 타깃에서 실행하려면 --iree-hal-target-device=의 다른 값을 확인하세요. 그러면 아래의 iree-run-module에 매칭되는 --device=를 전달해야 합니다.
크로스 컴파일을 위해서는 --iree-llvmcpu-target-triple=을 확인하세요.
CPU 기능을 활성화해 더 높은 CPU 성능을 원하면:
--iree-llvmcpu-target-cpu=를 확인하세요(예: AMD Zen4 타깃인 --iree-llvmcpu-target-cpu=znver4).--iree-llvmcpu-target-cpu-features=를 확인하세요.--iree-llvmcpu-target-cpu=host를 사용하세요. CPU 아키텍처와 무관하게 동작합니다.보다 유용한 iree-compile 플래그는 이 문서를 참고하세요.
다음과 같이 실행합니다:
$ iree-run-module --module=/tmp/prog.vmfb \
--input=10xf32=[0,1,2,3,4,5,6,7,8,9] \
--input=10xf32=[90,80,70,60,50,40,30,20,10,0]
EXEC @foo
result[0]: hal.buffer_view
10xf32=90 81 72 63 54 45 36 27 18 9
여기서 각 --input 매개변수는 하나의 입력을 지정합니다. 먼저 형태와 원소 타입 10xf32를 적고, 그다음 대괄호 [...] 안에 예제 배열 원소를 나열합니다. 위 iree-run-module의 출력은 결과의 내용을 보여줍니다.
본 튜토리얼의 나머지 부분에서는 단순화를 위해 주로 정적 형태에 집중하겠지만, 동적 형태도 문제없음을 한 번은 보여주겠습니다. 아래는 이전 예제의 동적 형태 버전입니다.
#map_1d_identity = affine_map<(m) -> (m)>
func.func @foo(
%lhs : tensor<?xf32>,
%rhs : tensor<?xf32>
) -> tensor<?xf32> {
%c0f32 = arith.constant 0.0 : f32
%c0 = arith.constant 0 : index
%size = tensor.dim %lhs, %c0 : tensor<?xf32>
%result_empty = tensor.empty(%size) : tensor<?xf32>
%result = linalg.generic {
indexing_maps=[
// `ins(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity,
#map_1d_identity,
// `outs(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity
],
iterator_types=["parallel"]
} ins(%lhs, %rhs : tensor<?xf32>, tensor<?xf32>)
outs(%result_empty : tensor<?xf32>)
{
^bb0(%lhs_entry : f32, %rhs_entry : f32, %unused_result_entry : f32):
%add = arith.addf %lhs_entry, %rhs_entry : f32
linalg.yield %add : f32
}
-> tensor<?xf32>
return %result : tensor<?xf32>
}
이 프로그램은 앞선 예제와 완전히 동일한 방식으로 컴파일하고 실행할 수 있습니다. 단, 이제 iree-run-module 명령에서 임의 길이의 입력을 지정할 수 있습니다. 유일한 요구사항은 두 입력의 길이가 같아야 한다는 것입니다. 그렇지 않으면 linalg.generic은 정의되지 않은 동작을 보이게 됩니다.
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb \
--input=10xf32=[0,1,2,3,4,5,6,7,8,9] \
--input=10xf32=[90,80,70,60,50,40,30,20,10,0]
EXEC @foo
result[0]: hal.buffer_view
10xf32=90 81 72 63 54 45 36 27 18 9
outs에 입력 중 하나를 전달하기링크아래는 더 적은 코드 줄로 동일한 결과를 내는 간결한 변형이며, outs(...) 매개변수 목록이 할 수 있는 일에 대해 처음 맛보게 해줍니다. 처음부터 소개하지 않은 이유는 다소 비관용적이기 때문입니다. outs는 우리가 곧 살펴볼 reduction 반복자에서 비로소 정말 필요(그리고 관용적)해집니다. 이전 예제에서는 outs에 단지 tensor.empty 자리표시자만 전달했습니다. 이번 예제는 결과와 모양이 같은 입력이라면 실제로 그 어떤 입력이든 여기에 전달할 수 있음을 보여줍니다.
#map_1d_identity = affine_map<(m) -> (m)>
func.func @foo(
%lhs : tensor<10xf32>,
%rhs : tensor<10xf32>
) -> tensor<10xf32> {
%result = linalg.generic {
indexing_maps=[
// `ins(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity,
// `outs(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity
],
iterator_types=["parallel"]
} ins(%lhs : tensor<10xf32>)
outs(%rhs : tensor<10xf32>)
{
^bb0(%lhs_entry : f32, %rhs_entry : f32):
%add = arith.addf %lhs_entry, %rhs_entry : f32
linalg.yield %add : f32
}
-> tensor<10xf32>
return %result : tensor<10xf32>
}
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb \
--input=10xf32=[0,1,2,3,4,5,6,7,8,9] \
--input=10xf32=[90,80,70,60,50,40,30,20,10,0]
EXEC @foo
result[0]: hal.buffer_view
10xf32=90 81 72 63 54 45 36 27 18 9
reduction 예시: 1차원 배열 원소 합계 구하기링크이 함수는 1차원 실수 배열을 받아 그 합계를 반환합니다. tensor<f32>는 0차원 텐서 타입입니다. 단일 f32 원소를 추출해 그대로 반환할 수도 있지만, 예제를 가능한 한 단순하게 유지하려고 이렇게 했습니다.
여기서 미묘한 점은 linalg.generic의 bb0 블록이 이제 %result_entry를 arith.addf의 피연산자로 실제로 사용하고, 매 반복마다 이 덧셈의 결과를 yield한다는 것입니다. 이는 암묵적으로 그 덧셈 결과가 대상에 저장되고, 다음 반복에서 다시 %result_entry로 로드된다는 뜻입니다. 따라서 SSA 값 %result_entry는 반복마다 다른 값을 갖습니다.
이제 outs 매개변수에서 가져온 값이 실제로 사용되므로, 원소가 초기화되지 않은 tensor.empty를 그대로 전달할 수 없습니다. 결과 원소를 0으로 초기화해야 하며, 이는 linalg.fill로 달성합니다.
#map_1d_identity = affine_map<(m) -> (m)>
#map_1d_proj_0d = affine_map<(m) -> ()>
func.func @foo(
%input : tensor<10xf32>) -> tensor<f32> {
%result_empty = tensor.empty() : tensor<f32>
%cst_0 = arith.constant 0.0 : f32
%result_init = linalg.fill ins(%cst_0 : f32) outs(%result_empty : tensor<f32>) -> tensor<f32>
%result = linalg.generic {
indexing_maps=[
// `ins(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_identity,
// `outs(...)`에 나열된 매개변수의 인덱싱 맵
#map_1d_proj_0d
],
iterator_types=["reduction"]
} ins(%input : tensor<10xf32>)
outs(%result_init : tensor<f32>)
{
^bb0(%input_entry : f32, %result_entry : f32):
%add = arith.addf %input_entry, %result_entry : f32
linalg.yield %add : f32
}
-> tensor<f32>
return %result : tensor<f32>
}
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb --input=10xf32=[0,1,2,3,4,5,6,7,8,9]
EXEC @foo
result[0]: hal.buffer_view
f32=45
parallel과 reduction 반복자 결합: 2차원 배열 각 행의 합계링크이것이 첫 번째 2차원 예시입니다. 따라서 처음으로 iterator_types가 어떻게 열거되는지 설명해야 하고, 좀 더 흥미로운 affine_map 예시를 보게 됩니다.
#map_2d_identity = affine_map<(m, n) -> (m, n)>
#map_2d_proj_first = affine_map<(m, n) -> (m)>
func.func @foo(
%input : tensor<3x5xf32>) -> tensor<3xf32> {
%result_empty = tensor.empty() : tensor<3xf32>
%cst_0 = arith.constant 0.0 : f32
%result_init = linalg.fill ins(%cst_0 : f32) outs(%result_empty : tensor<3xf32>) -> tensor<3xf32>
%result = linalg.generic {
indexing_maps=[
// `ins(...)`에 나열된 매개변수의 인덱싱 맵
#map_2d_identity,
// `outs(...)`에 나열된 매개변수의 인덱싱 맵
#map_2d_proj_first
],
iterator_types=[
// 규칙: i번째 iterator_type은 위에서 정의한 affine_map들의 원천 좌표계 (m, n)의
// i번째 좌표에 대응합니다. 따라서:
"parallel", // 이는 affine-map의 `m` 좌표를 가리킵니다.
// 결과에서 유지되는 좌표입니다. 위의 map_2d_proj_first를 보세요.
"reduction" // 이는 affine-map의 `n` 좌표를 가리킵니다.
// 위의 map_2d_proj_first에 의해 제거되므로 1차원 결과에는 존재하지 않습니다.
]
} ins(%input : tensor<3x5xf32>)
outs(%result_init : tensor<3xf32>)
{
^bb0(%input_entry : f32, %result_entry : f32):
%add = arith.addf %input_entry, %result_entry : f32
linalg.yield %add : f32
}
-> tensor<3xf32>
return %result : tensor<3xf32>
}
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb \
--input=3x5xf32=[[0,1,2,3,4],[5,6,7,8,9],[10,11,12,13,14]]
EXEC @foo
result[0]: hal.buffer_view
3xf32=10 35 60
linalg.matmul과 linalg.generic으로서의 행렬 곱셈링크이제 linalg.generic으로 행렬 곱셈을 표현하는 방법을 볼 준비가 되었습니다. 하지만 직접 손으로 작성하기보다는, Linalg가 대신 해주게 해보겠습니다. 실제로 linalg.generic 외에도 Linalg에는 여러 "이름 있는 연산(named op)"이 있으며, 이는 본질적으로 linalg.generic의 특수한 경우를 간단히 표기한 것입니다. 그중 하나가 누산기(accumulator)에 누적하는 행렬 곱을 수행하는 linalg.matmul입니다. 아래는 linalg.matmul을 사용해 행렬-곱-누적을 수행하는 간단한 함수입니다. 이 예시에서도 동적 형태(형태에 ?, 위에서 이미 다룸)를 사용하지만, 정적 형태를 써도 무방합니다.
func.func @foo(%lhs: tensor<?x?xf32>, %rhs: tensor<?x?xf32>, %acc: tensor<?x?xf32>) -> tensor<?x?xf32> {
%result = linalg.matmul
ins(%lhs, %rhs: tensor<?x?xf32>, tensor<?x?xf32>)
outs(%acc: tensor<?x?xf32>)
-> tensor<?x?xf32>
return %result: tensor<?x?xf32>
}
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb \
--input=2x2xf32=[[1,2][3,4]] \
--input=2x2xf32=[[1,4][3,2]] \
--input=2x2xf32=[[0,0][0,0]]
EXEC @matmul_dynamic
result[0]: hal.buffer_view
2x2xf32=[7 8][15 20]
이제 또 다른 IREE 도구인 iree-opt를 만납니다. MLIR 프로그램을 타깃 장치에서 바로 실행 가능한 .vmfb까지 끝까지 컴파일하는 iree-compile과는 달리, iree-opt는 선택한 변환만 적용합니다.
다음과 같이 실행합니다:
iree-opt --linalg-generalize-named-ops prog.mlir
그러면 다음이 출력됩니다:
#map = affine_map<(d0, d1, d2) -> (d0, d2)>
#map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
#map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
module {
func.func @foo(%arg0: tensor<?x?xf32>, %arg1: tensor<?x?xf32>, %arg2: tensor<?x?xf32>) -> tensor<?x?xf32> {
%0 = linalg.generic {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0, %arg1 : tensor<?x?xf32>, tensor<?x?xf32>) outs(%arg2 : tensor<?x?xf32>) {
^bb0(%in: f32, %in_0: f32, %out: f32):
%1 = arith.mulf %in, %in_0 : f32
%2 = arith.addf %out, %1 : f32
linalg.yield %2 : f32
} -> tensor<?x?xf32>
return %0 : tensor<?x?xf32>
}
}
즉 위의 linalg.matmul 형태와 동등하게 행렬 곱셈을 구현하는 linalg.generic입니다. 위 프로그램과 동일하게 컴파일하고 실행하면 정확히 같은 결과를 얻습니다.
여기서 나열된 3개의 iterator_types인 ["parallel", "parallel", "reduction"]은 affine_map들에 적힌 3개의 좌표 (d0, d1, d2)에 각각 대응합니다. 따라서 d0와 d1은 병렬 차원이고 d2는 축소 차원입니다. 그래서 앞의 두 affine_map 결과에는 d2가 등장합니다(각각 LHS %arg0와 RHS %arg1에 해당). 반면 마지막 affine_map 결과에는 병렬 차원인 d0와 d1만 등장합니다(결과 행렬을 가리키기 때문).
참고
일부 현재 IREE 컴파일러 최적화는 linalg.generic의 동등한 형태가 아니라 linalg.matmul 같은 이름 있는 연산에만 적용됩니다. 이는 본질적인 제약이 아니라 현재의 제한일 뿐이며, 시간이 지나면서 해결될 예정입니다. 단기적으로 성능이 중요할 때는 linalg.matmul을 사용하세요.
MLIR은 2의 거듭제곱이 아닌 비트 폭을 포함해 어떤 비트 폭이든 정수 타입을 정의하며, 부호에 대해 세 가지 변형을 제공합니다:
si로 표시합니다.ui로 표시합니다.i로 표시합니다. "부호 없음"이란 타입이 부호 정보를 담지 않는다는 뜻입니다. 정수 값은 부호 있는 값으로도, 부호 없는 값으로도 사용될 수 있지만, 그것은 그 값을 피연산자로 사용하는 "연산"의 속성이지 "타입"에 인코딩되는 것이 아닙니다.예를 들어 si16은 16비트 부호 있는 정수 타입이고, ui24는 24비트 부호 없는 정수 타입이며, i8은 부호 없음 8비트 정수 타입입니다.
이제 IREE에서 우리와 관련 있는 MLIR 방언들이 작동하는 매우 중요한 원칙을 소개합니다:
참고
부호 없음(sign-less) 타입만 사용하세요. 부호 정보는 타입이 아니라 연산에 인코딩하세요.
예를 들어, LHS가 부호 있는 8비트 정수, RHS가 부호 없는 8비트 정수, 누산기가 부호 있는 32비트 정수인 행렬 곱셈을 수행하려면 다음과 같습니다. LHS가 부호 있고 RHS가 부호 없다는 사실은 오직 linalg.generic 기본 블록의 구현에서만 반영되며, LHS와 RHS 항목을 각각 부호 확장(arith.extsi)과 무부호 확장(arith.extui)하는 방식으로 표현됩니다:
#map = affine_map<(d0, d1, d2) -> (d0, d2)>
#map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
#map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
module {
func.func @foo(%lhs: tensor<?x?xi8>, %rhs: tensor<?x?xi8>, %acc: tensor<?x?xi32>) -> tensor<?x?xi32> {
%result = linalg.generic
{indexing_maps = [#map, #map1, #map2],
iterator_types = ["parallel", "parallel", "reduction"]}
ins(%lhs, %rhs : tensor<?x?xi8>, tensor<?x?xi8>)
outs(%acc : tensor<?x?xi32>) {
^bb0(%lhs_entry: i8, %rhs_entry: i8, %acc_entry: i32):
%lhs_extended = arith.extsi %lhs_entry : i8 to i32
%rhs_extended = arith.extui %rhs_entry : i8 to i32
%mul = arith.muli %lhs_extended, %rhs_extended : i32
%add = arith.addi %acc_entry, %mul : i32
linalg.yield %add : i32
} -> tensor<?x?xi32>
return %result : tensor<?x?xi32>
}
}
$ iree-compile prog.mlir -o /tmp/prog.vmfb \
--iree-hal-target-device=local \
--iree-hal-local-target-device-backends=llvm-cpu
$ iree-run-module --module=/tmp/prog.vmfb \
--input=2x2xi8=[[-1,-2][-3,-4]] \
--input=2x2xi8=[[1,4][3,2]] \
--input=2x2xi32=[[0,0][0,0]]
EXEC @foo
result[0]: hal.buffer_view
2x2xi32=[-7 -8][-15 -20]