LLVMにおけるAArch64スケーラブルマトリックス拡張のサポート

1. はじめに

AArch64 SME ACLE は、PSTATE.SM および PSTATE.ZA を制御するための多数の属性をユーザーに提供します。AArch64 SME ABI は、少なくとも1つの関数が PSTATE.SM または PSTATE.ZA を使用する場合の関数間の呼び出しに関する要件を記述しています。

このドキュメントでは、SME ACLE 属性が LLVM IR 属性にどのようにマップされるか、および LLVM がこれらの属性をどのように低レベルに変換して ABI のルールと要件を実装するかについて説明します。

以下では、LLVM IR 属性と、C/C++レベルの ACLE 属性との関係について説明します。

aarch64_pstate_sm_enabled

__arm_streaming を持つ関数に使用されます。

aarch64_pstate_sm_compatible

__arm_streaming_compatible を持つ関数に使用されます。

aarch64_pstate_sm_body

__arm_locally_streaming を持つ関数に使用され、関数の定義でのみ有効です(宣言ではありません)。

aarch64_new_za

__arm_new("za") を持つ関数に使用されます。

aarch64_in_za

__arm_in("za") を持つ関数に使用されます。

aarch64_out_za

__arm_out("za") を持つ関数に使用されます。

aarch64_inout_za

__arm_inout("za") を持つ関数に使用されます。

aarch64_preserves_za

__arm_preserves("za") を持つ関数に使用されます。

aarch64_expanded_pstate_za

__arm_new_za を持つ関数に使用されます。

Clang は、上記の属性が関数の宣言/定義と呼び出しサイトの両方に追加されるようにする必要があります。これは、定義または宣言が利用できない、属性付き関数ポインターの呼び出しに重要です。

2. PSTATE.SM の処理

PSTATE.SM を変更すると、FP/ベクター演算の実行が別の処理要素に転送される可能性があります。これには3つの重要な意味があります。

  • ランタイム SVE ベクター長が変わる可能性があります。

  • FP/AdvSIMD/SVE レジスタの内容がゼロになります。

  • 許可される命令のセットが変わります。

これにより、IR および最適化に特定の制限が生じます。たとえば、PSTATE.SM の値が異なる可能性がある関数間で、ベクター長に依存する状態を共有することは未定義の動作です。フロントエンドは、LLVM IR を生成する際にこれらの制限を遵守する必要があります。

ランタイムの SVE ベクター長が変わる可能性があっても、LLVM IR および CodeGen のほとんどすべての部分では、ランタイムの vscale の値は変更されないと仮定できます。コンパイラが呼び出し境界の周りに適切な smstart および smstop 命令を挿入する場合、SVE 状態への影響を軽減できます。状態の変更を呼び出しの周りのごく短いウィンドウに限定することで、操作のスケジュール方法と、状態遷移間でライブ値がどのように保持されるかを制御できます。

このレベルの粒度で PSTATE.SM を制御するために、イントリンシックではなく、関数と呼び出しサイトの属性を使用します。

属性の制限

  • 異なる SVE ベクター長を使用する可能性がある関数との間で、スケーラブルベクターオブジェクト(へのポインター)を渡したり、返したりすることは未定義の動作です。これには、ストリーミングインターフェイスではないが、aarch64_pstate_sm_body でマークされた関数が含まれます。

  • 関数を aarch64_pstate_sm_compatibleaarch64_pstate_sm_enabled の両方で装飾することは許可されていません。

  • 関数を次の属性のうち複数で装飾することは許可されていません: aarch64_new_za, aarch64_in_za, aarch64_out_za, aarch64_inout_za, aarch64_preserves_za

これらの制限は、より高レベルの SME ACLE にも適用されます。つまり、Clang で診断をエミットして、ユーザーに誤った動作を知らせることができます。

コンパイラが挿入するストリーミングモードの変更

以下の表は、異なる属性を持つ関数間の呼び出しを行う際に、コンパイラが考慮する必要がある PSTATE.SM の遷移について説明しています。この表では、次の省略形を使用します。

N

通常のインターフェイスを持つ関数(エントリ時に PSTATE.SM=0、リターン時に PSTATE.SM=0)

S

ストリーミングインターフェイスを持つ関数(エントリ時に PSTATE.SM=1、リターン時に PSTATE.SM=1)

SC

ストリーミング互換インターフェイスを持つ関数(エントリ時に PSTATE.SM は 0 または 1 のいずれかになり、リターン時には変更されません)。

__attribute__((arm_locally_streaming)) を持つ関数は、呼び出し側にとって属性が「ストリーミング」と同義であり、呼び出し先にとっては呼び出し側に明示的に公開されていない実装の詳細にすぎないため、この表から除外されています。

表 4 異なる属性を持つ関数に対する呼び出しの組み合わせ

From

To

呼び出し前

呼び出し後

例外後

N

N

N

S

SMSTART

SMSTOP

N

SC

S

N

SMSTOP

SMSTART

SMSTART

S

S

SMSTART

S

SC

SMSTART

SC

N

呼び出し前の PSTATE.SM が 1 の場合、SMSTOP

呼び出し前の PSTATE.SM が 1 の場合、SMSTART

呼び出し前の PSTATE.SM が 1 の場合、SMSTART

SC

S

呼び出し前の PSTATE.SM が 0 の場合、SMSTART

呼び出し前の PSTATE.SM が 0 の場合、SMSTOP

呼び出し前の PSTATE.SM が 1 の場合、SMSTART

SC

SC

呼び出し前の PSTATE.SM が 1 の場合、SMSTART

PSTATE.SM の変更により FP/ベクターレジスタがゼロになるため、レジスタ割り当ての前に smstart および smstop 命令を発行して、レジスタアロケータがモード変更の周りでレジスタをスピル/リロードできるようにすることをお勧めします。

また、コンパイラは、どの操作が呼び出し/関数の引数/結果の一部であり、どの操作が関数の本体の一部であるかに関する十分な情報を持っている必要があります。これにより、モード変更を正確な位置に配置できます。これを行うのに適切な場所は SelectionDAG のようです。SelectionDAG では、呼び出しの引数/戻り値を低レベルに変換して、指定された呼び出し規約を実装します。SelectionDAG は、操作の順序を指定し、命令のスケジューリングを事前に制御するためのチェーンとグルーを提供します。

状態を保持する例

通常のインターフェイスを持つ関数からストリーミングインターフェイスを持つ関数に float 値を渡したり返したりする場合、呼び出しサイトは、引数/結果のレジスタが保持されており、smstart/smstop と呼び出しの間に他のコードがスケジュールされていないことを保証する必要があります。

define float @foo(float %f) nounwind {
  %res = call float @bar(float %f) "aarch64_pstate_sm_enabled"
  ret float %res
}

declare float @bar(float) "aarch64_pstate_sm_enabled"

プログラムは、レジスタ s0 の浮動小数点引数と戻り値の値を保持する必要があります。

foo:                                    // @foo
// %bb.0:
        stp     d15, d14, [sp, #-80]!           // 16-byte Folded Spill
        stp     d13, d12, [sp, #16]             // 16-byte Folded Spill
        stp     d11, d10, [sp, #32]             // 16-byte Folded Spill
        stp     d9, d8, [sp, #48]               // 16-byte Folded Spill
        str     x30, [sp, #64]                  // 8-byte Folded Spill
        str     s0, [sp, #76]                   // 4-byte Folded Spill
        smstart sm
        ldr     s0, [sp, #76]                   // 4-byte Folded Reload
        bl      bar
        str     s0, [sp, #76]                   // 4-byte Folded Spill
        smstop  sm
        ldp     d9, d8, [sp, #48]               // 16-byte Folded Reload
        ldp     d11, d10, [sp, #32]             // 16-byte Folded Reload
        ldp     d13, d12, [sp, #16]             // 16-byte Folded Reload
        ldr     s0, [sp, #76]                   // 4-byte Folded Reload
        ldr     x30, [sp, #64]                  // 8-byte Folded Reload
        ldp     d15, d14, [sp], #80             // 16-byte Folded Reload
        ret

ISD ノードに正しいレジスタマスクを設定し、適切な場所に smstart/smstop を挿入することで、これが正しく行われるようにする必要があります。

命令選択ノード

AArch64ISD::SMSTART Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]
AArch64ISD::SMSTOP  Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]

SMSTART/SMSTOP ノードは、条件付き SMSTART/SMSTOP の場合に CurrentState および ExpectedState オペランドを受け取ります。命令は、CurrentState != ExpectedState の場合にのみ実行されます。

CurrentStateExpectedState がコンパイル時に評価できる場合(つまり、両方とも定数である場合)、無条件の smstart/smstop 命令が発行されます。それ以外の場合、ノードは、比較/分岐と smstart/smstop に展開される疑似命令に一致します。これは、SC -> N および SC -> S からの遷移を実装するために必要です。

アンチェーンド関数呼び出し

aarch64_pstate_sm_enabled」を持つ関数がストリーミング互換ではない関数を呼び出す場合、コンパイラは呼び出しの前に SMSTOP を挿入し、呼び出しの後に SMSTOP を挿入する必要があります。

呼び出される関数が副作用がなく、関数呼び出しに低レベル変換されるイントリンシック(例: @llvm.cos())である場合、@llvm.cos() の呼び出しはどのチェーンにも属していません。自由にスケジュールできます。

コールサイトの低レベル変換では、次のノードの小さなチェーンが作成されます。

  • 呼び出しシーケンスを開始する

  • 仮想レジスタから ABI で指定された物理レジスタに値をコピーする

  • 分岐とリンクを実行する

  • コールシーケンスを停止します。

  • 出力値を物理レジスタから仮想レジスタにコピーします。

コールサイトのChainが使用されていない場合、チェーンされたシーケンスからの結果値のみが使用されますが、Chain自体は破棄されます。

SMSTARTノードとSMSTOPノードはChainを返しますが、実際の値は返しません。したがって、SMSTART/SMSTOPノードが使用されないChainの一部である場合、これらのノードはスケジューリングの対象とならず、DAGから削除されます。これらのノードが削除されるのを防ぐために、CopyFromRegからの結果がSMSTART/SMSTOPの実行後にのみ使用できるようにする必要があります。

これには、CopyToReg -> CopyFromRegのシーケンスを使用できます。これにより、値が仮想レジスタに移動/移動され、これらのノードがSMSTART/SMSTOPとチェーンされて、結果値を計算する式の一部になります。結果のCOPYノードは、レジスタアロケータによって削除されます。

以下の例は、Chainによって結果がリンクされておらず、値によってリンクされているDAGでこれがどのように使用されるかを示しています。

            t0: ch,glue = AArch64ISD::SMSTOP ...
          t1: ch,glue = ISD::CALL ....
        t2: res,ch,glue = CopyFromReg t1, ...
      t3: ch,glue = AArch64ISD::SMSTART t2:1, ....   <- this is now part of the expression that returns the result value.
    t4: ch = CopyToReg t3, Register:f64 %vreg, t2
  t5: res,ch = CopyFromReg t4, Register:f64 %vreg
t6: res = FADD t5, t9

関数開始時にDAGにSMSTARTを挿入する必要があるローカルストリーミング関数でも、これが必要です。

__attribute__((arm_locally_streaming)) を持つ関数

関数がarm_locally_streamingとしてマークされている場合、プロローグ/エピローグのランタイムSVEベクター長は、関数の本体のベクター長と異なる場合があります。これは、スタックフレームを設定した後にsmstartを呼び出し、同様にスタックフレームの割り当てを解除する前にsmstopを呼び出すためです。

ローカル変数を割り当てるために正しいSVEベクター長を使用することを確実にするために、CPUがまだストリーミングモードになっていない場合でも、ADDSVL命令を使用してストリーミングベクター長を使用してスタックスロットを割り当てることができます。

これはローカル変数でのみ機能し、呼び出し先保存スロットでは機能しません。これは、LLVMが1つのスタックフレームで2つの異なるスケーラブルベクター長を混在させることをサポートしていないためです。つまり、関数がarm_locally_streamingとしてマークされており、プロローグでSVE呼び出し先保存をスピルする必要がある場合は、現在サポートされていません。ただし、arm_locally_streaming関数はベクター長に依存する値を受け取ったり返したりできないため、ユーザーが介入しない限り、これは発生しにくいです。それ以外の場合は、'aarch64_sve_pcs'を使用してSVE PCSを強制し、さらにarm_locally_streamingを使用して、この問題が発生する必要があります。この組み合わせは、Clangで診断を発行することで防止できます。

arm_locally_streaming属性を持つ関数のプロローグ/エピローグがどのように見えるかの例を以下に示します。

#define N 64

void __attribute__((arm_streaming_compatible)) some_use(svfloat32_t *);

// Use a float argument type, to check the value isn't clobbered by smstart.
// Use a float return type to check the value isn't clobbered by smstop.
float __attribute__((noinline, arm_locally_streaming)) foo(float arg) {
  // Create local for SVE vector to check local is created with correct
  // size when not yet in streaming mode (ADDSVL).
  float array[N];
  svfloat32_t vector;

  some_use(&vector);
  svst1_f32(svptrue_b32(), &array[0], vector);
  return array[N - 1] + arg;
}

スタック領域の割り当てにはADDSVLを使用し、戻り値/引数の値を上書きすることを避ける必要があります。

_Z3foof:                                // @_Z3foof
// %bb.0:                               // %entry
        stp     d15, d14, [sp, #-96]!           // 16-byte Folded Spill
        stp     d13, d12, [sp, #16]             // 16-byte Folded Spill
        stp     d11, d10, [sp, #32]             // 16-byte Folded Spill
        stp     d9, d8, [sp, #48]               // 16-byte Folded Spill
        stp     x29, x30, [sp, #64]             // 16-byte Folded Spill
        add     x29, sp, #64
        str     x28, [sp, #80]                  // 8-byte Folded Spill
        addsvl  sp, sp, #-1
        sub     sp, sp, #256
        str     s0, [x29, #28]                  // 4-byte Folded Spill
        smstart sm
        sub     x0, x29, #64
        addsvl  x0, x0, #-1
        bl      _Z10some_usePu13__SVFloat32_t
        sub     x8, x29, #64
        ptrue   p0.s
        ld1w    { z0.s }, p0/z, [x8, #-1, mul vl]
        ldr     s1, [x29, #28]                  // 4-byte Folded Reload
        st1w    { z0.s }, p0, [sp]
        ldr     s0, [sp, #252]
        fadd    s0, s0, s1
        str     s0, [x29, #28]                  // 4-byte Folded Spill
        smstop  sm
        ldr     s0, [x29, #28]                  // 4-byte Folded Reload
        addsvl  sp, sp, #1
        add     sp, sp, #256
        ldp     x29, x30, [sp, #64]             // 16-byte Folded Reload
        ldp     d9, d8, [sp, #48]               // 16-byte Folded Reload
        ldp     d11, d10, [sp, #32]             // 16-byte Folded Reload
        ldp     d13, d12, [sp, #16]             // 16-byte Folded Reload
        ldr     x28, [sp, #80]                  // 8-byte Folded Reload
        ldp     d15, d14, [sp], #96             // 16-byte Folded Reload
        ret

ストリーミングモードでの不正な命令の使用を防ぐ

  • ストリーミングモード(PSTATE.SM=1)でプログラムを実行する場合、SVE/SVE2命令のサブセットと、ほとんどのAdvSIMD/NEON命令は無効です。

  • 通常モード(PSTATE.SM=0)でプログラムを実行する場合、SME命令のサブセットは無効です。

  • ストリーミング互換関数は、PSTATE.SM=0またはPSTATE.SM=1のいずれの場合でも有効な命令のみを使用する必要があります。

PSTATE.SMの値は、フィーチャーフラグではなく、関数属性によって制御されます。これは、'+sme'に対してコンパイルでき、要求されたストリーミングモードでは無効な場合でも、コンパイラーが任意の命令をコード生成することを意味します。コンパイラーは、ランタイムで特定の操作が利用可能であるという前提でコンパイラーが変換を行わないように、関数属性を使用する必要があります。

フィーチャーフラグでこれをモデル化しないことに意識的な選択をしました。これは、どちらのモードでもインラインアセンブリ(ユーザーがsmstart/smstopを手動で配置する)を引き続きサポートしたいためであり、TableGenの制限のため、個々の命令レベルで実装することが非常に複雑になったためです(D120261D121208を参照してください)。

最初のステップとして、関数にaarch64_pstate_sm_enabledaarch64_pstate_sm_body、またはaarch64_pstate_sm_compatible属性のいずれかがある場合、ベクター命令の使用を避けるために、ベクトル化(LoopVectorize/SLP)を完全に無効にします。

後で、ストリーミング互換命令のサブセットを使用したスケーラブルな自動ベクトル化を有効にするために、これらの制限を緩和することを目指しますが、そのためにはCostModel、Legalization、SelectionDAGの低減に対する変更が必要です。

また、関数にストリーミングモード属性が設定されている場合、Clangで診断を発行して、非ストリーミング(互換ではない)操作(ACLEイントリンシックなどを使用)の使用を防ぎます。

その他の考慮事項

  • コールサイトがPSTATE.SMを切り替える必要がある場合、または呼び出し先の関数本体が呼び出し元とは異なるストリーミングモードで実行される場合は、インライン化を無効にする必要があります。これは、関数呼び出しがストリーミングモードの変更の境界であるため必要です。

  • テールコール最適化は、呼び出しサイトがPSTATE.SMを切り替える必要がある場合、呼び出し元がPSTATE.SMの元の値を復元できるように無効にする必要があります。

3. PSTATE.ZA の処理

PSTATE.SMとは対照的に、PSTATE.ZAを有効にしても、SVEベクター長には影響せず、FP/AdvSIMD/SVEレジスタを上書きすることもありません。つまり、イントリンシックを使用してPSTATE.ZAを切り替えても安全です。これにより、プライベートZA関数(ZA状態を直接的または間接的に上書きする可能性のある関数)の呼び出しに対する遅延保存メカニズムの設定が簡単になります。

aarch64_new_zaでマークされた関数を処理するために、SelectionDAGの直前に実行される新しいLLVM IRパス(SMEABIPass)を導入しました。このパスで処理されたこのような関数には、aarch64_expanded_pstate_zaがマークされます。

遅延保存の設定

遅延保存のコミット

例外処理とZA

4. 型

AArch64 Predicate-as-Counter 型

概要:

predicate-as-counter型は、AArch64 SVE述語レジスタに保持されているpredicate-as-counter値の型を表します。このような値には、アクティブなレーン数、要素幅、および生成されたマスクを反転する必要があるかどうかを示すビットに関する情報が含まれています。ACLEイントリンシックを使用して、predicate-as-counter値を述語ベクトルとの間で移動する必要があります。

型には特定の制限があります。

  • 型は、関数のパラメーターと戻り値に使用できます。

  • この型でサポートされるLLVM操作は、loadstorephiselect、およびalloca命令に限定されます。

predicate-as-counter型は、スケーラブルな型です。

構文:

target("aarch64.svcount")

5. 参考文献

  1. SME ACLE プルリクエスト

  2. SME ABI プルリクエスト