跳转至

Choreo 语法设计

引言

Choreo 是一种嵌入式领域专用语言(eDSL),旨在简化加速器硬件上的数据搬运编排,减轻编写高性能 kernel 的工程师的日常负担,包括需受硬件能力约束的 tiling 策略等数据搬运复杂性。借助 Choreo,编程过程更易上手,工程师可更高效地优化这些关键环节。

将 Choreo 嵌入 C++

Choreo 为嵌入 C++ 的 DSL 代码。Choreo 编译器将 Choreo 函数做 source-to-source 翻译为 C++,并隐式包含 choreo.h,使翻译结果能与其余 C++ 代码协同工作。下例展示用法。

// some C++ code

__co__ void choreo_function() {
  // choreo code
}

// another C++ code
void foo() {
  choreo_function();
}
注意:Choreo 函数以关键字 \__co\__ 为前缀;函数体内的代码均由 Choreo 编译器翻译。其余 C++ 代码按声明名调用 Choreo 函数。

此外,引入若干 Choreo 专有类型,以保证 Choreo 函数实参与调用方一致。因若干重要编程要素尚未介绍,相关细节延至数据类型说明之后再述。

变量与类型

Choreo 引入四类基本类型范畴:scalar-type、spanned-type、integer-tuple-type(ituple-type)与 bounded-type,各司其职。

  • Scalar Type:满足程序控制需求,由 Integer TypeBoolean Type 构成。
  • Spanned Type:表示用于计算的数据类型(通常为张量)。除引用原始数据外,spanned type 还将数据与由多维范围表示的形状相关联;该「多维范围」称为 mdspan,对 tiling 等场景很有用,后文即将介绍。
  • Integer Tuple(I-Tuple)Type:表示一组整数值;常见用法是为多维数据引入各维上界。
  • Bounded (Integer/ITuple) Type:用于简化数据(子区域)引用的特殊类型,并与循环构造配合以迭代处理数据。

四类中,scalarituple 较易接受,因其可对应通用语言中的元素。而 spanned typebounded type 具有领域特异性,下文详述。

Scalar Types

如前所述,Choreo 的 Scalar types 包含 Integer TypeBoolean TypeInteger Type 类似 C++ 的 intint32_t,为有符号 32 位整数,取值范围为 $-2^{31}$ 至 $2^{31}-1$。下例展示其用于数据与函数声明:

int a;
__co__ int foo(int b);
支持整数算术、移位与逻辑运算,语法与 C++ 内建运算一致。 Choreo 不提供无符号标量整数,亦不提供 8/16/64 位标量整数等价物;该设计动机在于:程序控制未必需要这些类型,通常 32 位有符号整数已足够。

Choreo 中 Boolean Type 类似 C++ 的 bool;布尔上的运算及与整数之间的转换,与 C++ 一致。

Spanned Types

Spanned type 为复合类型,由 fundamental typemulti-dimensional-span(mdspan)type 组成。fundamental typemdspan 均非完整类型,即二者单独均无法为带存储的数据定型。在 Choreo 中称之为 partial types

将类型设计为二者组合,是为了能单独操纵 mdspan。在 ML 场景中,数据多为多维组织;程序员常将数据分块并在存储层次间搬运,因而操纵多维数据的 shape(由 mdspan 表示)至关重要。

下文将说明如何定义这些 partial types,以及如何将其组合为完整类型,以声明/定义多维数据。

Partial Type:mdspan

与多数类型系统不同,mdspan 作为类型实体出现时仅为 partial。称其为 partial,是因为 Choreo 程序无法将数据单独定义为 mdspan 类型;但 mdspan 本身可单独定义。该设计基于如下观察:loop tiling/blocking 主要关心多维数据的形状,因此 Choreo 允许程序员在不论关联何种 fundamental type 的情况下操纵 mdspan,以简化与形状相关的编程。

同时,choreo 编译器可对 mdspan 做类型检查,以尽早(在适用时于执行前)发现代码错误。

在 Choreo 中,mdspan 由 [] 括起,维上界以 , 分隔。例如

mdspan sp : [7, 8];           // defined a mdspan of 2-dimensions
mdspan<4> mds : [a, 3, 4, 1]; // 'a' is an existing integer
d : [c, 4, 28];               // 'd' is not explicit annotated. Type is deduced.
如图所示,可用 mdspan 关键字显式定义 mdspan,并可选用尖括号中的总维数;亦可无类型标注定义 mdspan,如上例中的 d。(注意:mdsd 属于依赖类型,因其类型依赖于 ab 等求值;程序员可为 ab 提供运行时取值,但需付出若干运行时检查代价。)

对 C++ 程序员而言,可将 mdspan 视作多维数组的性状(trait),描述多层范围;例如上例中 sp 定义两层范围,分别为 0..6 与 0..7。

定义 mdspan 后,可通过 () 运算符取得各维整型尺寸。

sp : [7, 8];
int b = sp(0) + sp(1);   // 'b' equals to 15 (7 + 8)
上例中,sp(0) 表示 mdspan sp 第一维的尺寸;各维尺寸本质上为整型,可用于整数算术。

由此可从已有 mdspan 派生新的 mdspan,如下:

sp : [6, 8];
spn : [1, sp(0)/2, sp(1)/4]; // define a new mdspan from the existing one.
该机制便于在 Choreo 代码中对多维 span 做 tiling。鉴于高性能 kernel 构造中 tiling 不可或缺,Choreo 提供语法糖以进一步简化:
sp : [6, 8];
spn : sp [1, (0)/2, (1)/4];  // spn is defined as [1, 3, 2]
语义与前一写法相同,语法更简洁,体现 Choreo 的设计取向之一:在可行时用最少代码实现功能。

注意:mdspan 仅可定义,不得修改已有 mdspan;且同一 mdspan 只能定义一次。

除上述按逐维方式定义 mdspan 外,Choreo 还支持其他定义方式;在介绍 i-tuple 时再说明。

完整定型(Fully-Typing)

单独 mdspan 无法用于定义计算数据。在 Choreo 函数中,数据定义须完整定型,由 fundamental type 与 mdspan 共同组成。下例说明:

ndims : [20, 15];
f32 [10, 10] d0;
f16 [ndims] d1;
Choreo 支持的 fundamental types 包括:

  • Unsigned Integers:u8/u16/u32
  • Signed Integers:s8/s16/s32
  • Floating-points:f16/bf16/f32

注意:Choreo 中 s32int 不同。s32 为 fundamental type,不能用于 fully typing。

存储限定符(Storage Qualifier)

Spanned 类型数据在 Choreo 中通常较大;程序员可将其在加速器不同 memory hierarchy 间搬运以充分利用硬件。

在 choreo 中定义三种 storage qualifier 以标注所定义数据:

  • global
  • shared
  • 以及 local

用法如下:

ndims : [20, 15];
local f32 [10, 10] d0;
shared f16 [ndims] d1;

默认情况下,若无 storage qualifier,则所定义数据视为来自 global memory。

I-Tuple Types

整数元组为整数的无序集合;如前所述,常用作(下标)索引。

定义 i-tuple 时,将元素置于 {} 之间。例如

ituple index = {5, 4, 3, 2, 1};  // It defines a tuple of 5 elements
index = {a, b};  // 'a' and 'b' are existing integers

mdspani-tuple 的运算

Choreo 允许对 i-tuplemdspan 进行特殊运算。下例对 mdspan 施加固定 tiling:

sp : [6, 8];
tiling_factor = {3, 2};
spn : sp / tiling_factor;   // spn is defined as [2, 4];
Choreo 中可通过此类 Tuple-Span Operations 定义 mdspan。支持的运算包括:

  • mdspan $/$ i-tuple
  • mdspan $+$ i-tuple
  • mdspan $\%$ i-tuple
  • mdspan $$ i-tuple*
  • mdspan $-$ i-tuple

这些运算本质上均可通过 mdspan 的逐维定义实现;tuple-span operations 有助于写出更可读的代码,亦为 Choreo 所追求的目标。

Bounded Types

Bounded types 包含 Bounded ScalarBounded ITuple。Bounded Scalar 取值范围为 $[0, ub)$,其中 ub 为上界。因此若将整数 p 设为 bounded,须同时关联具体上界。该关联须在 parallel-bywith-inControl Structures 内显式编写,后文介绍。

类似地,ITuple 为一组 Integer,亦可关联一组上界。具体而言,Choreo 中 Bounded ITuplemdspan 值关联,以确定一组上界。后续章节将给出详细语法。

控制结构

Choreo 在并行化、循环等方面与 C++ 有显著差异。

并行区域:parallel-by

在 CPU 等系统上,可通过异步线程实现并行执行。而在 Choreo 中,采用单指令多数据(SPMD)模型作为并行化手段,类似部分 OpenMP parallel 指令及 OpenCL/CUDA 等并行语言。

但构造并行区域的语法不同:采用 C 风格花括号,将并行执行的代码置于 parallel-by 块内。

parallel p by 6 {
  // SPMD code
}

上例展示如何用 Choreo 关键字 parallelby 创建并行区域:假定有 6 个执行线程,各线程执行相同的 SPMD 代码,但 p 取值不同。若熟悉 CUDA 编程,可将 p 类比为 thread index;若更熟悉顺序 C/C++,也可将 p 视为迭代 6 次的循环变量(但各次迭代的执行次序任意)。

除并行外,parallel-by 还有一层含义:语句中的 p 为与上界 $[0, 6)$ 关联的整数,称为 bounded integer,而非普通整数。在 chunkat 等特殊运算中(后文说明),需要 bounded-integer 才能正确工作,因为上界对其计算必不可少。

with-in 块与 where 子句

parallel-by 类似,with-in 也可将 i-tuple 绑定到 mdspan。下例说明:

with index in [10, 10] {
  // index is ituple with 2 elements
}
此处 index 为含 2 个元素的 i-tuple。有时希望为两元素命名,可使用如下语法:
with {x, y} in [10, 10] {...}
或同时命名 i-tuple 与其元素:
with index = {x, y} in [10, 10] {...}
此类情形下,称 indexbounded ituple

with-in 表面类似 parallel-by,但行为不同:重要区别之一是 with-in 隐含并行;块内代码顺序执行,仅用于创建 bounded-ituple

此外,可追加 where 子句。例如:

with {m, n} in [M, N], {n_p, k} in [N_P, K] where n_p <-> n {
  // matmul implements with m,n,K. n_p is no long useful.
}
该片段要求在所有迭代中 nn_p 取值相同;因而在 with-in 块内,需要 n_p 处可用 n 替换,反之亦然。该机制对编写多种 AI kernel 很有用;应使用运算符 <-> 建立此类关系。

foreach

with-in 定义 bounded-ituple 之后,可对其或 bounded-integer 进行循环。在 Choreo 中写法简洁:

with x in [10] {
  foreach x {
    // do something with each x
  }
}

upper-bound 运算

异步运算:DMA 语句

除并行执行外,Choreo 允许一种固定形式的异步运算:DMA 语句。

概念上,DMA 语句与 SPMD 代码异步执行,类似 CPU 异步线程,但行为受 DMA 配置约束。(CPU 对异步线程的编程自由度更高。)

下例展示基本 DMA 语句:

global f32 [10] g_data;
local f32 [10] l_data;
f = dma.copy.async g_data => l_data;
// ... async operations
wait f;         // explicit wait
此处利用数据搬运引擎(DTE)发起线性拷贝,将位于 global memory 的数据搬至 local memory 的 data1。可注意到对 f 的赋值:其为异步 DMA 实体的句柄,在 Choreo 中称为 DMA 操作的 future。程序员可写显式同步语句 wait,使当前线程在 future 完成前阻塞;否则线程与 DMA 并行继续执行。

有时显式定义临时数据较为繁琐,Choreo 提供更简语法:

global f32 [10] data;
f = dma.copy data => local;
... f.data;  // retrieve the 'local' data from the future
此处不必指定 DMA 的目标具体地址,仅需指定目标 memory 类型。这在许多场景下有利:程序员常希望避免自行管理 scratchpad memory(SPM)。本例中 local memory 分配由编译器负责;要取得搬运后的数据,只需调用 futuredata 成员。此外,该 dma 未标记 .async,故无需也不能对 f 执行 wait

DMA 操作细节繁复,编程须谨慎,应参考 DMA 手册做决策。尽管如此,Choreo 编译器仍提供大量静态与运行时检查,帮助避免常见错误。

Bounded-ituple/integer 与 chunkat 运算

chunkat 作用于 spanned 数据,在已有数据上构造新的 mdspan;在部分系统中亦称 subview。因 chunkat 接受 bounded-ituple 与 bounded-integer 作为参数,在 Choreo 中命名不同。

global f32 [6, 10, 100] data;
parallel p by 6 {
  with index = {x, y} in [10, 10] {
    // for every data move, the stride into 'data' is
    //    stride = p*1000 + x * 100 + y * 10
    //
    // for each chunk, the dimensioned size for the movement is {1, 1, 10}
    f = dma.copy data.chunkat(p, index) => local;
  }
}
上例展示 chunkat 的典型用法:spanned 数据类型为 f32 [6, 10, 100]chunkat 接收 integer pituple index 两个参数,假定将数据划分为 $6 \times 10 \times 10$ 块,对应 pindex 所关联的范围;每次取一块并搬运至 local 存储,沿最低维包含 10 个连续元素。

函数调用与调用 Choreo 函数

Choreo 函数不得调用另一 Choreo 函数;但在 Choreo 函数内调用 C++ kernel 属常见用法。

void bar() {...}    // C++ kernel function
__co__ void foo() {
  parallel p by 6 {
    call bar();     // Call the C++ function
  }
}
上例使用 Choreo 关键字 call 调用既有 C++ 函数 bar,较为直观。

Choreo 与 C++ 函数间的参数传递

向 Choreo 传参或反向传递时,需包含 Choreo 头文件 choreo.h。通常将原始 C++ 指针与维信息结合,构造 Choreo spanned 数据。下例说明:

#include "choreo.h"

void bar(const float* data, unsigned size) {}

__co__ void foo(f32 mdspan<2> d) {
  parallel p by 6 {
    call bar(d, |d|);     // Call the C++ function
  }
}

void foobar(float* a) {
  foo(choreo::make_spanview<2>(a, {1, 2}));
}
示例中使用 choreo 工具函数(模板)make_spanview 包装数据,不发生拷贝。在 Choreo 函数 foo 中,参数 f32 mdspan<2> 为对应实体;因存在隐式转换,可将 d 直接作为 C++ 函数 bar 的第一实参;运算 |d| 得到 spanned 数据 d 的总元素个数,用于调用 bar

同样,Scalar Type 数据也可与 Choreo 函数互传;但 ituple 仅在 Choreo 函数内部使用。

小结

Choreo 为 SPMD 编程提供新思路:偏好 C++ 风格写法,并嵌入 C++;其核心在于减轻底层编程负担,尤其是通过 DMA 在各 memory 层之间编排数据搬运。有时亦被称为 dataflow 编程 DSL。我们开发该工具以支撑日常构建高性能 kernel 的工作,希望程序员少纠缠语言构造细节,更多关注高层概念。

若您觉得其行为符合预期,欢迎反馈以便持续改进。