方言
- 簡介
- 操作
- 塊
- 區域
- 值范圍
- Control Flow and SSACFG Regions
- 操作與多區域(Operations with Multiple Regions)
- 閉包(Closure)
- 圖形區域(Graph Regions)
- 參數和結果(Arguments and Results)
- 屬性
- 類型系統
- 類型別名
- 方言類型
- 內建類型
- 屬性
- 屬性
- 方言屬性
- 內置方言
簡介
方言(Dialects)是參與并擴展MLIR(多級中間表示,多級中間語言)生態系統的機制。它們允許定義新的操作、屬性和類型。每個方言都有一個唯一的命名空間,這個命名空間會作為前綴添加到每個定義的屬性、操作和類型前。例如,Affine方言定義的命名空間是:affine。
MLIR允許多個方言共存,即使這些方言不在主樹結構內,也可以在一個模塊內共同存在。方言由特定的傳遞過程產生和消費。MLIR提供了一個框架,可以在不同的方言之間進行轉換,也可以在同一個方言內進行轉換。
操作
MLIR引入了操作的概念,用于描述不同級別的抽象和計算。操作可以具有特定于應用程序的語義,并且是完全可擴展的,也就是說沒有固定的操作列表。
每個操作由一個唯一的字符串標識來識別,例如"dim"、“tf.Conv2d”、“x86.repmovsb”、"ppc.eieio"等。操作可以返回零個或多個結果,接受零個或多個操作數。它還可以存儲屬性、具有屬性的字典、具有零個或多個后繼操作以及零個或多個封閉區域。
操作的內部表示相對簡單,通常以字面形式包含所有這些元素。為了指示結果和操作數的類型,它還包括一個函數類型。
塊
一個塊(Block)是一個操作列表。在SSA CFG(靜態單賦值形式控制流圖)區域中,每個塊代表一個編譯器基本塊,其中塊內的指令按順序執行,終結操作(terminator operations)實現基本塊之間的控制流分支。
塊中的最后一個操作必須是終結操作。一個只有單個塊的區域可以通過在封閉操作上附加NoTerminator來免除這一要求。頂級的ModuleOp就是一個定義這種特性且其塊體沒有終結操作的操作的例子。
MLIR中的塊接受一個塊參數列表,表示方式類似于函數。塊參數與由個別操作語義指定的值綁定。區域入口塊的塊參數也是區域的參數,這些參數的值由包含操作的語義決定。其他塊的塊參數由終結操作的語義決定,例如將該塊作為后繼者的分支。在具有控制流的區域中,MLIR利用這種結構隱式地表示控制流依賴值的傳遞,而無需傳統SSA表示中的PHI節點的復雜細節。注意,不依賴控制流的值可以直接引用,不需要通過塊參數傳遞。
以下是一個簡單的函數示例,展示了分支、返回和塊參數:
func.func @simple(i64, i1) -> i64 {
^bb0(%a: i64, %cond: i1): // 由^bb0支配的代碼可以引用%acf.cond_br %cond, ^bb1, ^bb2^bb1:cf.br ^bb3(%a: i64) // 分支傳遞%a作為參數^bb2:%b = arith.addi %a, %a : i64cf.br ^bb3(%b: i64) // 分支傳遞%b作為參數// ^bb3從前驅接收一個參數,命名為%c,并將其與%a一起傳遞給bb4。
// %a直接從其定義操作引用,不通過^bb3的參數傳遞。
^bb3(%c: i64):cf.br ^bb4(%c, %a : i64, i64)^bb4(%d : i64, %e : i64):%0 = arith.addi %d, %e : i64return %0 : i64 // 返回也是一個終結操作。
}
區域
區域是MLIR(多級中間表示)塊的有序列表。區域內部的語義并不是由IR(中間表示)強加的,而是由包含該區域的操作定義的。MLIR當前定義了兩種區域:SSACFG區域,用于描述塊之間的控制流,以及Graph區域,不需要塊之間的控制流。操作中的區域類型通過RegionKindInterface描述。
區域沒有名稱或地址,只有區域中包含的塊有。區域必須包含在操作內,并且沒有類型或屬性。區域中的第一個塊是一個特殊的塊,稱為“入口塊”。入口塊的參數也是區域本身的參數。入口塊不能被列為任何其他塊的后繼塊。
值范圍
- 區域的層次結構:
區域提供了一種層次化的封裝方式,這意味著你不能從一個區域跳轉(branch)到另一個區域。例如,如果你有一個代碼塊在區域A,那么你不能直接跳轉到區域B的代碼塊。
值的作用范圍 - 區域自然地限制了值的可見性:
在一個區域中定義的值不會逃逸到外部的區域。比如,在內部區域定義的變量,在外部區域是不可見的。 - 區域內的操作:
在一個區域內的操作可以引用外部區域定義的值,但前提是這些值在包含該區域的操作中是合法的。比如,如果外部區域允許使用某個變量,那么這個變量在內部區域也是可以使用的。 - 限制引用的特性:
可以使用一些特性(traits)來限制引用,例如 OpTrait::IsolatedFromAbove,或者使用自定義驗證器來控制這些規則。
示例解釋:
"any_op"(%a) ({ // 如果 %a 在包含的區域中是可見的…// 那么 %a 在這里也是可見的。%new_value = "another_op"(%a) : (i64) -> (i64)
}) : (i64) -> (i64)
在這個例子中,如果 %a 在外部區域中是可見的,那么它在內部區域中也是可見的。
- MLIR中的層次支配概念:
MLIR(多級中間表示)定義了一種廣義的“層次支配”概念,用來確定一個值是否“在作用范圍內”以及是否可以被某個操作使用。
在同一個區域內,值是否可以被另一個操作使用,取決于該區域的類型。
如果一個值在一個區域中定義,那么只有當一個操作的父操作在同一區域且可以使用該值時,這個值才能被使用。
一個區域的參數定義的值,可以被該區域內的任何操作使用。
一個區域中定義的值,永遠不能在該區域外部使用。
Control Flow and SSACFG Regions
在MLIR中,有一種叫做SSACFG的區域,這個區域的操作就像我們寫代碼那樣,是按順序執行的。簡單來說:
- 操作順序執行:在一個操作執行前,它需要的所有數據(操作數)已經準備好并且有明確的值。操作執行后,這些數據的值保持不變,同時生成的結果也有明確的值。
- 操作之間的控制流:操作一個接一個地執行,直到執行到塊(代碼段)末尾的“終止操作”。然后,控制流會根據終止操作的指示,跳到其他地方繼續執行。
控制流的進入和退出
- 進入區域:控制流總是從區域的第一個塊(入口塊)開始。
- 退出區域:控制流可以通過任何帶有合適終止操作的塊退出區域。比如,某個塊的終止操作指示跳回外部操作(像函數的返回)。
實例
func.func @accelerator_compute(i64, i1) -> i64 { // 一個SSACFG區域
^bb0(%a: i64, %cond: i1): // 被 ^bb0 支配的代碼可以引用 %acf.cond_br %cond, ^bb1, ^bb2^bb1:// 這里定義的 %value 不支配 ^bb2%value = "op.convert"(%a) : (i64) -> i64cf.br ^bb3(%a: i64) // 分支傳遞 %a 作為參數^bb2:accelerator.launch() { // 一個SSACFG區域^bb0:// 嵌套在 "accelerator.launch" 下的代碼區域,它可以引用 %a 但不能引用 %value。%new_value = "accelerator.do_something"(%a) : (i64) -> ()}// %new_value 不能在區域外引用^bb3:...
}
說明
- 支配關系和變量引用
支配關系(Domination)在編譯原理中指一個基本塊B1支配另一個基本塊B2,如果每次控制流進入B2之前必定會經過B1。這個概念幫助我們理解變量的可見性和生命周期。
塊^bb0
- 支配關系:bb0是函數的入口塊,所以它支配所有其他塊(bb1、bb2和bb3)。
- 變量引用:bb0中的變量%a和%cond在bb0、bb1、bb2和bb3中都可以被引用,因為這些塊都被bb0支配。
塊^bb1
- 支配關系:bb1不是其他任何塊的支配塊,因為從bb0到bb3可以通過bb2而不經過^bb1。
- 變量引用:bb1定義了變量%value,但由于bb1不支配bb2和bb3,%value不能在bb2和bb3中引用。然而,%value可以在^bb1內部引用。
塊^bb2
- 支配關系:^bb2同樣不支配其他任何塊。
- 變量引用:bb2中的加速器啟動區域(accelerator.launch)是一個新的嵌套區域,雖然它可以引用來自外部塊(bb0)的變量%a,但不能引用來自bb1的變量%value,因為bb1不支配^bb2。
加速器啟動區域(accelerator.launch 內部的 ^bb0)
- 支配關系:這個區域是獨立的SSACFG區域,有它自己的控制流和支配關系。區域內的塊^bb0支配區域內的所有操作。
- 變量引用:區域內的操作可以引用外部區域的變量%a,但不能引用%value,因為%value的定義在當前區域的控制流之外(即不在這個區域內的支配鏈上)。
塊^bb3
- 支配關系:bb3既不支配也不被bb1或^bb2支配。
- 變量引用:bb3只能引用在bb0中定義并且被傳遞下來的變量%a,但不能引用bb1中定義的%value,因為%value的作用范圍僅限于bb1
綜上所述
- 入口塊^bb0支配所有其他塊,因此它定義的變量%a和%cond在整個函數中都是可見的。
**bb1定義的變量%value**只能在bb1內部引用,不能在bb2和bb3中引用,因為bb1不支配bb2和^bb3。
**加速器啟動區域(accelerator.launch)**可以引用外部塊(bb0)的變量%a,但不能引用來自bb1的變量%value。
bb3只能引用bb0中定義并傳遞下來的變量%a,但不能引用^bb1中的%value。
操作與多區域(Operations with Multiple Regions)
-
概念解釋:
在編程中,操作(operation)可以包含多個區域(region)。區域就像是操作內部的小塊代碼或邏輯。當控制流到達一個操作時,這個操作可以選擇將控制權傳遞給它內部的任何一個區域。當控制流從一個區域返回時,操作可以繼續將控制權傳遞給其他區域。一個操作可以同時管理多個區域,甚至可以調用其他操作中的區域。 -
實際例子:
假設我們有一個主操作 mainOp,它包含兩個子區域 regionA 和 regionB。當 mainOp 被執行時,它首先將控制權傳遞給 regionA。當 regionA 完成后,mainOp 將控制權傳遞給 regionB。
"mainOp"() ({// regionA"opA"() : () -> ()// regionB"opB"() : () -> ()
}) : () -> ()
在這個例子中,mainOp 包含了 regionA 和 regionB 兩個區域,并按順序執行它們。
閉包(Closure)
-
概念解釋:
閉包是一種將代碼塊和其環境打包成一個整體的技術。區域允許我們定義創建閉包的操作,將區域的主體“打包”成一個值。閉包可以在以后執行,具體執行的方式由操作定義。如果一個操作是異步執行的,調用方需要確保等待操作完成,以保證所用的值依然有效。 -
實際例子:
假設我們有一個操作 createClosure,它將一個區域打包成一個閉包,并返回一個函數值。
"createClosure"() ({// The region to be packed as a closure%result = "opInClosure"() : () -> (i32)
}) : () -> (function<i32()>)
在這個例子中,createClosure 將 opInClosure 操作打包成一個閉包,并返回一個函數值,可以在以后調用。
圖形區域(Graph Regions)
在MLIR(多級中間表示)中,圖形區域(graph region)的概念用于表示圖狀語義,即沒有控制流但可能存在并發語義或通用有向圖數據結構的情況。圖形區域非常適合表示沒有基本順序的循環關系或耦合值之間的關系。例如,圖形區域中的操作可以代表獨立的控制線程,而值可以代表數據流。
圖形區域有以下幾個關鍵點:
-
單一基本塊:目前,圖形區域被限制為只能包含一個基本塊(entry block)。雖然這種限制沒有特定的語義原因,但它被添加的目的是為了簡化通過的基礎設施,確保處理圖形區域的各種傳遞(passes)能夠正確處理反饋循環。未來,如果有需求,可能會允許多基本塊的圖形區域。
-
操作和值的表示:在圖形區域中,MLIR操作代表圖中的節點,而每個MLIR值代表一個多邊連接,即一個源節點和多個目標節點的連接。區域內定義的所有值都在區域內的作用域內,并且可以被區域內的其他操作訪問。
-
操作的順序無關性:在圖形區域中,基本塊內操作的順序和區域內基本塊的順序在語義上沒有意義,非終止操作可以自由重排,例如通過規范化(canonicalization)進行重排。
-
循環的可能性:在圖形區域中,循環(cycles)可以發生在單個基本塊內,也可以發生在基本塊之間。
參數和結果(Arguments and Results)
-
概念解釋:
一個區域的第一個塊的參數被視為區域的參數。參數的來源由父操作的語義決定,可能對應操作本身使用的一些值。區域會生成一個(可能為空的)值列表,操作的語義定義了區域結果與操作結果之間的關系。 -
實際例子:
假設我們有一個操作 funcOp,它包含一個區域 funcRegion,區域的參數為 %arg1 和 %arg2。
module {func @main(%arg0: i32, %arg1: i32) -> i32 {%0 = "myFuncOp"(%arg0, %arg1) : (i32, i32) -> (i32)return %0 : i32}"myFuncOp"(%input1: i32, %input2: i32) -> (i32) {^entry(%arg1: i32, %arg2: i32):%result = addi %arg1, %arg2 : i32return %result : i32}
}
關系解釋
-
父操作 myFuncOp:
myFuncOp 是父操作,它包含一個區域。
myFuncOp 接收兩個輸入參數 %input1 和 %input2,類型為 i32。
區域 funcRegion: -
funcRegion 是 myFuncOp 的區域。
funcRegion 的第一個基本塊 ^entry 接收兩個參數 %arg1 和 %arg2,這些參數直接對應父操作 myFuncOp 的輸入 %input1 和 %input2。
區域參數: -
^entry 基本塊的參數 %arg1 和 %arg2 被視為整個區域 funcRegion 的參數。
這些參數的來源是父操作 myFuncOp 的輸入 %input1 和 %input2。
區域結果: -
在 ^entry 基本塊內,我們執行一個加法操作 addi,計算 %arg1 和 %arg2 的和,并將結果存儲在 %result。
最后,區域返回計算結果 %result,這個結果成為操作 myFuncOp 的輸出。
"test.graph_region"() ({ // 一個圖形區域%1 = "op1"(%1, %3) : (i32, i32) -> (i32) // 這是允許的,%1 和 %3 都在作用域內%2 = "test.ssacfg_region"() ({%5 = "op2"(%1, %2, %3, %4) : (i32, i32, i32, i32) -> (i32) // 這是允許的,%1, %2, %3, %4 都定義在包含的區域內}) : () -> (i32)%3 = "op2"(%1, %4) : (i32, i32) -> (i32) // 這是允許的,%4 在作用域內%4 = "op3"(%1) : (i32) -> (i32)
}) : () -> ()
屬性
類型系統
在編程中,每個數據都有一個類型,比如整數、浮點數、字符串等等。MLIR也是這樣,但它有一個更靈活的類型系統,允許我們定義自己的類型。
在MLIR中,類型系統是開放的,這意味著你可以定義任何你需要的類型,沒有一個預先固定的類型列表。這對于不同的應用程序來說非常有用,因為你可以創建特定的類型來滿足你的需求。
類型的基本構成
在MLIR中,類型可以分為幾種:
- 類型別名(type-alias):一個類型的替代名字。
- 方言類型(dialect-type):為特定應用定義的類型。
- 內建類型(builtin-type):系統預定義的一些基本類型。
類型列表有兩種表示方式:
- 不帶括號的類型列表:多個類型用逗號分隔,比如 int, float。
- 帶括號的類型列表:可以是空的括號(),也可以是多個類型用逗號分隔并包含在括號內,比如 (int, float)。
當我們使用一個帶有特定類型的值時,通常用這樣的形式表示:值: 類型。
函數類型
函數類型用一個箭頭->連接輸入類型和輸出類型。比如,一個函數接受一個整數并返回一個浮點數,可以表示為:int -> float。如果有多個輸入或輸出類型,可以用括號括起來,比如:(int, float) -> (string, bool)。
類型別名
類型別名就像給一個復雜類型起了一個簡短的名字。比如,!avx_m128 = vector<4 x f32> 這句話定義了一個別名!avx_m128,它相當于vector<4 x f32>。以后在代碼中你可以用!avx_m128來代替vector<4 x f32>,這樣代碼會更簡潔和易讀。
!avx_m128 = vector<4 x f32>// Using the original type.
"foo"(%x) : vector<4 x f32> -> ()// Using the type alias.
"foo"(%x) : !avx_m128 -> ()
方言類型
方言類型是一種自定義的數據類型,它可以擴展現有的類型系統。就像編程語言允許你創建自定義的類和結構體一樣,方言類型允許你在特定的命名空間內創建自定義的類型。
方言類型的兩種表示方式
-
不透明類型(Opaque Type):
用尖括號 <> 包裹的詳細內容。
例如:!tf 表示一個 TensorFlow 的字符串類型。
“不透明”指的是類型的具體內部結構或實現細節對外部系統或用戶不可見。這意味著外部系統不需要知道或理解類型的具體實現,只需要知道這個類型存在并能夠使用它。 -
簡潔類型(Pretty Type):
省略了一些冗長的符號,使其更易讀。
例如:!tf.string 也是表示一個 TensorFlow 的字符串類型,但更簡潔。
內建類型
內建類型就是MLIR(多級中間表示)提供的一些基礎數據類型。就像編程語言里我們常見的整型、浮點型和函數類型一樣,這些類型在MLIR中也是直接可以使用的,并且其他任何自定義擴展(叫做方言)都可以利用這些基礎類型。
屬性
屬性是附加在某個操作上的額外信息。就像你給一個文件夾貼上標簽一樣,這些屬性為操作提供了更多的背景信息或特性。這些信息可以是關于操作自身的特定數據,并且可以通過特定的方法進行訪問和使用。
假設你有一個“加法操作”,你可以為這個操作添加一些屬性,比如“這兩個數字相加的結果是否需要四舍五入”。這個屬性就存儲在“加法操作”上,具體值可能是 true 或 false。你可以通過特定的方法讀取這個屬性并決定是否執行四舍五入。
%result = addi %a, %b : i32 { rounding = true }
屬性
在編程中,屬性(Attributes)是一種為操作(operation)添加額外信息的方式。想象一下,你在寫一個食譜,每個步驟(操作)可能有一些特定的要求或注釋(屬性),這些要求或注釋不能被改變,只能作為參考。
如何確定屬性類型
- 文檔和規范:通常,MLIR操作的文檔和規范會明確指出哪些屬性是必需的,哪些是可選的。
- 操作定義:在MLIR操作定義文件(.td文件)中,屬性的定義通常會表明其重要性和必要性。
- 上下文理解:通過理解操作的上下文和行為,判斷屬性是否是執行該操作所必需的。
方言屬性
方言屬性可以看作是給你的MLIR代碼添加一些自定義的標簽或者注釋,這些標簽可以攜帶特定的信息。就像你給你的代碼打上“重要”、“需要優化”這樣的標簽一樣,方言屬性可以攜帶特定的信息供后續使用。
- 假設你有一個自定義方言,命名空間為foo,你想要給某個操作添加一個字符串屬性和一個復雜屬性。
// 定義一個字符串屬性
#foo<string<"example_string">>// 定義一個復雜屬性
#foo<"a123^^^" + bar>// 在MLIR代碼中使用這些屬性
func @example() {%0 = "foo.operation"() { attr = #foo<string<"example_string">> } : () -> ()%1 = "foo.operation"() { complex_attr = #foo<"a123^^^" + bar> } : () -> ()return
}
在這個例子中,foo.operation操作使用了兩個自定義的方言屬性,一個是字符串屬性,另一個是復雜屬性。這些屬性可以在后續的編譯、優化或者代碼生成過程中被利用。
內置方言
內置方言就像是MLIR系統提供的一些基礎設施,這些基礎設施包含了一些基本的工具和數據類型,所有人都可以直接使用,而不需要自己重新發明輪子。內置方言提供了一些通用的屬性值和類型,這些屬性和類型可以被任何方言直接使用,方便了不同方言之間的互操作。
- 假設你需要使用一些基本的整數和浮點數屬性,這些屬性是MLIR系統內置的。
func @example() {// 使用內置的整數屬性%0 = "builtin.operation"() { int_attr = 42 : i32 } : () -> ()// 使用內置的浮點數屬性%1 = "builtin.operation"() { float_attr = 3.14 : f32 } : () -> ()return
}
在這個例子中,builtin.operation操作使用了內置的整數屬性和浮點數屬性。因為這些屬性是內置的,所以任何方言都可以直接使用它們,而不需要自己定義。