core 教程

本教程展示如何借助 luna_thread 门面从 MoonBit 运行并行整数内核,如何把同样的工作描述为由库验证的计划,以及如何构建并检查工作流图。所有内容都在原生目标上运行。

快速开始

添加模块:

moon add Luna-Flow/luna_thread@0.1.0

在一个为原生目标构建的包中导入门面:

import {
  "Luna-Flow/luna_thread",
}

supported_targets = "native"

用两个工作线程对数组求和,每个线程处理至多三个元素的块:

test "quick start" {
  let input : FixedArray[Int] = [1, 2, 3, 4, 5, 6]
  let total = @luna_thread.execute_reduce_sum_i32(
    input,
    worker_count=2,
    chunk_size=3,
  )
  inspect(total, content="21")
}

moon test --target native 报告:

Total tests: 1, passed: 1, failed: 0.

日常任务

选择工作线程数和块大小

内核把 nn 个元素分成 ⌈n/c⌉\lceil n / c \rceil 个至多 cc 个元素的块,每个块需要一个工作线程,所以先选 cc,再取 w=⌈n/c⌉w = \lceil n / c \rceil。两者都不能超过 nn。

fn chunked_sum(input : FixedArray[Int], chunk_size : Int) -> Int {
  let n = input.length()
  let workers = (n + chunk_size - 1) / chunk_size
  @luna_thread.execute_reduce_sum_i32(input, worker_count=workers, chunk_size~)
}

test "choose the parameters" {
  let input : FixedArray[Int] = [10, 20, 30, 40, 50, 60, 70]
  inspect(chunked_sum(input, 3), content="280")
  inspect(chunked_sum(input, 7), content="280")
}

把元素翻倍并处理溢出

execute_map_i32 把每个元素翻倍,并把溢出作为错误报告,而不是回绕:

test "map with overflow" {
  let small : FixedArray[Int] = [1, -2, 3, 4]
  debug_inspect(
    @luna_thread.execute_map_i32(small, worker_count=2, chunk_size=2),
    content="Ok(<FixedArray: [2, -4, 6, 8]>)",
  )
  let large : FixedArray[Int] = [1, 2147483647]
  debug_inspect(
    @luna_thread.execute_map_i32(large, worker_count=2, chunk_size=1),
    content="Err(Overflow)",
  )
}

计算前缀和

execute_scan_sum_i32 返回累计总和:

test "prefix sums" {
  let deposits : FixedArray[Int] = [100, -30, 50, 20, -10]
  match @luna_thread.execute_scan_sum_i32(deposits, worker_count=2, chunk_size=3) {
    Ok(balance) => debug_inspect(balance, content="<FixedArray: [100, 70, 120, 140, 130]>")
    Err(error) => fail("scan failed: \{Repr(error)}")
  }
}

把工作描述为计划并验证它

计划记录应该运行什么,但不运行它。验证会告诉你 v1 运行时是否支持它:

test "plans" {
  let policy = @luna_thread.make_policy(worker_count=2, chunk_size=3).unwrap()
  let plan = @luna_thread.reduce(
    "total",
    @luna_thread.i32_type(),
    6,
    @luna_thread.sum_reduction(),
    policy~,
  )
  assert_true(@luna_thread.is_ready(plan))
  let doubles = @luna_thread.map("halve", @luna_thread.f64_type(), 6, policy~)
  debug_inspect(@luna_thread.validate(doubles), content="[UnsupportedValueType]")
}

构建并检查工作流

工作流是由任务组成的图。这个工作流先派生,再计算上面的计划,最后汇合,并用边固定顺序:

test "workflow" {
  let policy = @luna_thread.make_policy(worker_count=2, chunk_size=3).unwrap()
  let plan = @luna_thread.reduce(
    "total",
    @luna_thread.i32_type(),
    6,
    @luna_thread.sum_reduction(),
    policy~,
  )
  let graph = @luna_thread.workflow("sum-job", policy~)
    .add_node(@luna_thread.spawn_task(1, "spawn"))
    .add_node(@luna_thread.compute_task(2, "sum", plan))
    .add_node(@luna_thread.join_task(3, "join"))
    .add_edge(@workflow.Edge::new(1, 2, @workflow.control_dependency()))
    .add_edge(@workflow.Edge::new(2, 3, @workflow.control_dependency()))
  assert_true(@luna_thread.workflow_is_ready(graph))
  let submission = @luna_thread.submit_workflow(graph)
  inspect(submission.accepted(), content="true")
}

为了使用 Edge,导入中还需要 "Luna-Flow/luna_thread/workflow"。submit_workflow 验证并记录提交;它不会运行图。

进阶

要在真实线程上运行工作流的同步协议,请用 submit_workflow_async 提交,然后调用 wait_workflow 和 drop_workflow。由于 C 桥接层的一个已知缺陷,异步路径是实验性的;使用前请阅读 core API 中的警告。

对于其他元素类型和内核,backend/native 暴露了原始的 C 入口,包括 Int64 以及最小值和最大值归约;参见原生后端教程。若要更精细地控制计划和图,请直接使用 plan 和 workflow 包:plan 教程和 workflow 教程逐字段地构建计划和图。只描述工作的包可以导入这两个包,并在所有目标上构建。

常见陷阱

  • 默认值 worker_count=1 和 chunk_size=1 只适用于单元素输入。对于更长的输入,内核返回 Err(InvalidArgument),execute_reduce_sum_i32 返回 0。
  • execute_reduce_sum_i32 在和为零以及任何失败时都返回 0。需要区分二者时,请自行检查输入和参数,或者改用扫描并取最后一个元素。
  • 由于部分和会被检查,Int 求和是否溢出可能取决于分块方式:[-1, 0, 2147483647, 1] 在 chunk_size=4 时能求和,在 chunk_size=2 时却失败。
  • is_ready 不检查“每块一个工作线程”的条件,因此就绪的计划仍可能被内核拒绝。
  • javascript_policy() 在 v1 中会中止,make_policy 会拒绝 JavaScript 后端和异步模式。
  • Workflow::add_node 和其他构建方法会就地修改工作流。
  • 门面只能在原生目标上构建。导入它的包需要 supported_targets = "native",或者只在原生目标上构建。

下一步

core API 列出了每个门面函数及其精确语义。core 设计解释了为什么门面是一组带默认值的自由函数,原生后端设计解释了内核和工作流调度器如何使用线程和内存。架构指南展示了各个包和 C 运行时如何组合在一起。