backend/native チュートリアル

このチュートリアルでは、ネイティブバックエンドを直接呼び出す方法を示します。型付きのカーネルを実行し、生の外部関数から Int64 のカーネルや最小値・最大値のカーネルを使い、プランをネイティブリクエストに変換し、C スケジューラでワークフローを実行します。

クイックスタート

ネイティブターゲット向けにビルドするパッケージからバックエンドをインポートします。

import {
  "Luna-Flow/luna_thread/backend/native",
}

supported_targets = "native"

3 つのワーカーとサイズ 2 のチャンクで累積和を計算します。

test "native quick start" {
  let input : FixedArray[Int] = [2, 4, 6, 8, 10, 12]
  debug_inspect(
    @native.execute_scan_sum_i32(input, 3, 2),
    content="Ok(<FixedArray: [2, 6, 12, 20, 30, 42]>)",
  )
}

日常的なタスク

Int64 のデータの最小値と最大値を求める

生の関数は長さを明示的に受け取り、結果を直接返します。

test "min and max" {
  let samples : FixedArray[Int64] = [12L, -7L, 40L, 3L, 0L, 25L]
  let n = samples.length()
  assert_eq(@native.ffi_execute_reduce_min_i64(samples, n, 2, 3), -7L)
  assert_eq(@native.ffi_execute_reduce_max_i64(samples, n, 2, 3), 40L)
}

自分の出力配列に書き込む

マップとスキャンは渡された配列に書き込み、ステータスコードを返します。成功なら 0 です。

test "output array" {
  let input : FixedArray[Int64] = [1L, 2L, 3L, 4L]
  let output : FixedArray[Int64] = FixedArray::make(4, 0L)
  let status = @native.ffi_execute_map_i64(input, 4, 2, 2, output)
  assert_eq(status, 0)
  debug_inspect(output, content="<FixedArray: [2, 4, 6, 8]>")
}

プランをリクエストに変換する

*_from_plan 関数はプランを検査し、バックエンドの型付きリクエストに変換します。

test "request from plan" {
  let policy = @shared.make_execution_policy(worker_count=2, chunk_size=4).unwrap()
  let plan = @plan.scan("prefix", @plan.i64_type(), 8, policy~)
  guard @native.scan_request_from_plan(plan) is Ok(request) else {
    fail("expected a request")
  }
  assert_true(@native.scan_request_is_valid(request))
  assert_eq(request.element_count, 8)
  assert_eq(@native.scan_request_value_type(request), 1)
}

ワークフローを実行する

submit_workflow_async はワーカースレッドでグラフを開始し、wait_workflow は終わるまでブロックします。backend/native API で述べたブリッジの既知の不具合のため、この例はドキュメントのテストとしてコンパイルされていません。

let policy = @shared.make_execution_policy(worker_count=2).unwrap()
let graph = @workflow.Workflow::new("fork-join", policy~)
  .add_node(@workflow.spawn_node(1, "spawn"))
  .add_node(@workflow.join_node(2, "join"))
  .add_edge(@workflow.Edge::new(1, 2, @workflow.control_dependency()))
guard @native.submit_workflow_async(graph) is Ok(handle) else { return }
let result = @native.wait_workflow(handle)
@native.drop_workflow(handle)
// result.state is Completed, result.completed_nodes is 2

さらに進んで

カーネルはどれも同じ 3 つの数 nn、ww、cc を受け取ります。それらで入力をどう分けるか、成功したとき結果が ww と cc によらない理由、オーバーフローをいつ検出するかはネイティブバックエンドの設計で導いています。MoonBit の外で OpenMP 付きの C ランタイムをリンクするには、アーキテクチャガイドのとおり CMake で native/ をビルドしてください。

よくある落とし穴

  • どのカーネルにも w≥⌈n/c⌉w \ge \lceil n / c \rceil、w≤nw \le n、c≤nc \le n が必要です。満たさなければ InvalidArgument で失敗します(リダクションは 0 を返します)。
  • 生の関数は渡された length をそのまま信用します。配列の実際の長さより大きい値を渡すと、末尾を越えて読み出します。
  • moon のビルドでは OpenMP が有効になりません。supports_openmp() が true を返しても、カーネルは呼び出し元のスレッドで実行されます。
  • プランから作ったリクエストのバッファは空で、まだそれを実行するものはありません。
  • ブロックしたまま起こされないノードを持つワークフローは終わらず、wait_workflow も戻りません。

次のステップ

backend/native API には生の関数も含めすべての関数が載っています。backend/native の設計ではスレッドモデルとメモリモデルを説明しています。core チュートリアルでは同じカーネルをファサード経由で使います。