本系列文章将从以下几方面讲述UDO
-
UDO概述
介绍
Qualcomm® 神经处理 SDK 允许用户以用户定义操作(以下简称 UDO)的形式插入自定义神经网络操作,这些操作可能并非运行时引擎本身所支持的。这些操作可以是 TensorFlow 等常用训练框架中定义的操作,也可以是基于框架扩展构建的自定义操作,但 Qualcomm® 神经处理 SDK 中不提供。它们可以在任何支持的硬件加速器上原生执行。Qualcomm® 神经处理 SDK 提供了基础架构,可以无缝执行这些操作,与执行内部支持的操作相比,其开销极小甚至为零。
UDO 包剖析
Qualcomm® 神经处理 SDK 允许用户以动态库的形式提供 UDO 实现,这些动态库可以被查询、加载和执行,从而使用其中定义的内核执行推理。Qualcomm® 神经处理 SDK 提出了“UDO 包”的概念,用户可以通过该概念轻松表达 UDO 不同组件之间的关联。这一概念是所有支持用户创建用于网络推理的 UDO 包的工具的核心。然而,需要注意的是,Qualcomm® 神经处理 SDK 在运行时仍然直接与各种 UDO 库交互,而不是与 UDO 包结构交互。因此,用户可以自由地构建独立的库,而不必严格受制于“包”的概念。下图说明了 UDO 包的概念:如图所示,UDO 软件包由一个注册组件和一个实现组件组成。它们通常分别用一个注册库和一组实现库来表示,每个硬件加速器都有一个实现库,每个实现库都对应一个可用的实现内核。用户可以根据需要将这两个组件构建到一个库中。注册库包含指定所有用户定义操作及其所针对的硬件核心的方法。它还包含允许在网络创建时验证操作完整性的方法。注册库在 ARM CPU 上加载并执行。
特定于硬件的实现库公开了其他几种方法,用于实现操作实例的创建、执行、分析和销毁。这些方法通过相应软件平台支持的编程结构实现,例如用于 GPU 的 OpenCL 和用于 DSP 的 Hexagon-NN SDK。虽然特定于核心的实现文件在源代码上可能完全不同,但它们都需要使用 $SNPE_ROOT/include/SNPE/SnpeUdo 中定义的一组 C API 与 Qualcomm® 神经处理 SDK 进行交互。有关这些 API 的完整详细信息,请参阅 Qualcomm® 神经处理 SDK API。
UDO 工作流
Qualcomm® Neural Processing SDK 建议在开发 UDO 并将其集成到运行时时采用以下工作流程:
工作流程的第一步是确定模型中需要表示为用户定义操作的操作,并通过配置文件描述其属性。此文件的格式和内容在定义 UDO中进行了描述。
下一步将生成 UDO 包的组件,具体方法是创建 UDO 内核的源文件,并根据相应的工具链对其进行编译,从而生成特定于 GPU 和 DSP 等硬件内核的动态库。Qualcomm® 神经处理 SDK 提供了一个名为 的工具
snpe-udo-package-generator
,可帮助用户创建用于与 Qualcomm® 神经处理 SDK UDO API 交互的通用框架代码,并为用户留下一些占位符,方便他们填写内核实现。它还会为 x86、Android 等常见目标以及配置文件中指定的每个目标的运行时生成 makefile。有关包生成的更多信息,请参阅创建 UDO 包。有关为特定运行时编译 UDO 包的详细信息,请参阅 编译 UDO 包。第一步中创建的配置文件也需要与实际训练好的模型一起由 Qualcomm® Neural Processing SDK 模型转换工具使用,以便使用文件中的定义解释用户定义的操作。然后,可以使用诸如
snpe-dlc-info
探测模型中 UDO 属性之类的工具检查生成的 DLC 文件。有关使用 UDO 创建(以及选择性地量化)DLC 的详细信息,请参阅使用 UDO 准备模型。或者,也可以使用 Qualcomm® Neural Processing SDK 量化工具对带有 UDO 的模型进行量化,以便与 DSP 等定点运行时一起使用。量化器工具会估算网络中所有层(包括 UDO)激活的量化范围。由于该工具在 x86 主机上离线运行,因此需要为 UDO 提供 CPU 实现才能对整个网络进行推理。工作流程图中的虚线也说明了这一点。 有关量化过程的详细信息,请参阅使用 UDO 量化 DLC 。此工作流程的最后一步是能够使用 UDO 实际执行网络模型。Qualcomm® 神经处理 SDK 应用程序使用 UDO 包在对特定网络模型运行推理的流程中注册 UDO 实现。需要注意的是,这些 UDO 可以由多个 Qualcomm® 神经处理 SDK 实例同时执行,而不会出现竞争条件,从而提高网络推理的整体吞吐量。有关 UDO 包注册流程的更多详细信息,请参阅使用 UDO 运行模型。
如果 UDO 的 DSP 实现库未签名,无法在签名进程域(Qualcomm® 神经处理 SDK 应用程序的默认进程域)上执行,则需要请求使用未签名进程域。未签名进程域仅适用于 DSP 目标,并允许 Qualcomm® 神经处理 SDK 使用未签名的 UDO 实现库。要了解如何在 Qualcomm® 神经处理 SDK 应用程序中使用未签名进程域,请参阅使用 UDO 运行模型。
UDO 向后兼容性
本节指定了 UDO 包的限制:
-
在特定的 Qualcomm® Neural Processing SDK 发布版本上为 DSP V68 或更高版本编译的 UDO 需要与相同的发布版本一起使用,不能与不同的发布版本一起使用。
-
用户需要使用与特定 Qualcomm® Neural Processing SDK 版本兼容的正确的 Qualcomm® AI Direct SDK 重新编译为 DSP V68 生成的 UDO 包。
定义 UDO
用户定义操作 (UDO) 允许用户将其自定义操作与 Qualcomm® 神经处理 SDK 集成,以便在任何支持的硬件加速器上执行。UDO 机制接受自定义操作(定义如下)的规范,并处理该信息以处理包含该自定义操作的模型。本节介绍如何指定此类 UDO。
UDO配置规范
用户可以使用配置规范文件来表达其自定义操作的属性。此 UDO 配置(以下简称“UDO 配置”)是对操作的描述,可以使用可扩展标记语言 (XML)(以下简称“XML OpDef 配置”)或 JavaScript 对象表示法 (JSON) 语法和格式(以下简称“JSON UDO 配置”)创建。配置文件语法定义了描述 UDO 操作信息的字段。这些字段是预先确定的,最终将被解析为构成 UDO 所需的信息。所提供的信息应为通用的,并且独立于特定模型,这意味着特定于模型的参数或名称不必包含在配置中。这些信息将用于在框架模型中识别操作,并最终序列化到 DLC 模型中。这意味着配置中的任何更改都需要重新生成 DLC 模型,以确保序列化正确的信息。以下部分将介绍配置文件规范。
XML OpDef 配置描述
XML OpDef 配置描述了包中包含的操作以及包信息,例如包名称、版本和域。包信息和操作根据预定义的 XML 模式(如下所述)进行描述,该模式需要有关操作输入、输出和参数的信息。
XML OpDef 模式分解
本节概述了 XML OpDef 配置中用于定义操作定义 (Op Defs) 的架构。Op Defs 指定构成操作的输入、输出、参数和描述性元数据。该架构使用可扩展标记语言 (XML) 和 XML 架构定义 (XSD) 进行形式化。
操作定义模式
下图描述了这些 Op Def 实体之间的关系。
在上面的 OpDef Schema 图中,以 @ 为前缀的成员是 XML 属性,没有前缀的成员是 XML 元素。
操作防御
OpDef 是一个 XML 元素,用于在最高级别描述操作。它包含以下元素。包含内容的元素是必需的,空元素是可选的。
<OpDef> <Name>OpName</Name> <Description> <Content></Content> <Code></Code> </Description> <Reference Source="" Url=""></Reference> <!--Requires at least one input--> <Input> <Name>in[0]</Name> <Mandatory>true</Mandatory> <Constraint id="" Type=""></Constraint> <Datatype>FLOAT_32</Datatype> <Shape> <Rank>1D</Rank> <Layout></Layout> <Text></Text> </Shape> <Default></Default> <Repeated></Repeated> <IsStaticTensor></IsStaticTensor> </Input> <!--Requires at least one output--> <Output> <Name>out[0]</Name> <Mandatory>true</Mandatory> <Constraint id="" Type=""></Constraint> <Datatype>FLOAT_32</Datatype> <Shape> <Rank>1D</Rank> <Layout></Layout> <Text></Text> </Shape> <Repeated></Repeated> </Output> <!--Parameters are optional--> <Parameter> <Name>param</Name> <Mandatory>false</Mandatory> <Constraint id="" Type=""></Constraint> <Datatype>INT_32</Datatype> <Shape> <Rank>1D</Rank> <Layout></Layout> <Text></Text> </Shape> <Default></Default> <Enumeration> <Enum></Enum> </Enumeration> </Parameter> <UseDefaultTranslation></UseDefaultTranslation> <SupportedBackend>DSP_V68</SupportedBackend> </OpDef>
-
名称:操作的名称。
-
描述:可选;通过内容和代码序列描述操作。
-
内容:描述操作的字符串。
-
代码:表示描述操作的代码的字符串,例如,output_height = input_height - crop_top - crop_bottom
-
-
参考:可选;为操作定义一个或多个参考。
-
来源:操作来源的属性。例如 Tensorflow、ONNX 等。
-
Url:源 URL 的属性。
-
-
输入:定义操作的一个或多个输入。输入是张量的扩展,具有以下附加字段:
-
IsStaticTensor:可选的布尔值标志,如果设置为 True,则表示输入张量是包含或引用静态数据的参数。如果未设置,则该张量将被视为动态输入。
-
Repeated:可选布尔值,指定此输入是否重复。用于具有可变输入的操作,例如Concat。7
-
-
输出:定义操作的一个或多个输出。输出是张量的扩展 ,具有以下附加字段:
-
Repeated:可选布尔值,指定此输出是否重复。用于具有可变输出的操作,例如MultiClassNms。7
-
-
参数:可选;定义操作的一个或多个参数。参数是额外定义的张量的扩展。
-
枚举:枚举参数的可选字段。枚举由名为 Enum 的子字段组成,其内容给出了表示给定值的枚举名称。值按照枚举指定的顺序分配。
-
-
SupportedBackend:指定支持此操作的一个或多个后端的字段。当后端对某个操作有共同的定义时使用。如果同一操作在不同后端的字段有所不同,请使用SupplementalOpDef 并将该字段标记为 BACKEND_SPECIFIC。
-
UseDefaultTranslation:布尔值字段,如果设置为 true,则表示自定义操作将覆盖 QNN 原生操作。自定义操作类型必须与 QNN 原生操作的类型匹配,才能实现准确转换。如果设置为 false,则自定义操作将转换为通用的用户定义操作。在 false 模式下,自定义操作类型必须与源框架类型匹配。
创建 OpDef 的关键组件是输入、输出和参数,它们都是张量的扩展。张量元素定义如下:
-
名称:张量的名称。
-
描述:可选:通过内容和代码序列描述操作:
-
内容:描述操作的字符串。
-
代码:表示描述张量的代码的字符串。例如 output_height = input_height - crop_top - crop_bottom
-
-
约束:可选:定义给定张量的一个或多个约束。约束以字符串形式在元素主体中给出。8
-
id:指定约束的ID。用于覆盖补充操作定义的约束。
-
类型:约束的类型。有效类型包括:
-
数字:以基数为特征的约束,例如,输入数量 >= 1
-
形状:以张量维度限制为特征的约束,例如,秩 >= 1
-
值:用张量的值来表征的约束,例如,张量只能为正。
-
数据类型:一种约束,其特征是对数据类型的限制。通常用于可以具有多种数据类型但必须与其他张量具有相同数据类型的张量。
-
描述:不符合任何其他类别的约束的约束。
-
-
-
必填:布尔值;表示是否必须提供/定义张量。9
-
数据类型:定义张量允许的数据类型。必须是以下之一:
-
FLOAT_16
-
FLOAT_32
-
FIXED_4
-
FIXED_8
-
FIXED_16
-
单位
-
单位:16
-
UINT_32
-
细绳
-
BACKEND_SPECIFIC 用于指示数据类型依赖于后端。必须与SupplementalOpDef结合使用才能指定具体的数据类型。
-
-
形状:指定张量的形状。
-
秩:张量的秩,以枚举形式表示,具有以下值:
-
SCALAR:标量
-
1D:向量
-
2D:矩阵
-
3D:3D 张量或图像
-
4D:4D 张量或批处理图像
-
ND:通用 ND 张量 N >= 0
-
-
Layout:可选;指定张量的布局。必须是以下之一:
-
国家卫生和社区委员会
-
国家卫生和妇女福利部
-
UNDEFINED 用于标识布局既不是 NHCW 也不是 NHWC
-
BACKEND_SPECIFIC 用于指示布局依赖于后端。必须与 SupplementalOpDef结合使用才能指定具体的布局。
-
-
文本:可选;张量形状的字符串描述。
-
-
默认值:可选;表示张量默认值的字符串。可以是以下之一
-
张量:使用括号或方括号创建列表,例如 [[1, 2], [3, 4]]
-
标量:提供标量值,例如 1、1.1、-1
-
布尔值:提供 0(假)或 1(真)
-
字符串:任何其他字符串。如果文本无法解析为上述类别之一,则将存储为字符串。
-
补充操作防御
SupplementalOpDef 是 XML 元素,用于定义跨后端可变的内容。SupplementalOpDef 扩展了 OpDef 中定义的内容,但限制了可覆盖的字段。SupplementalOpDef 的结构如下:包含内容的元素是必需的,空元素是可选的。
<SupplementalOpDef> <Name>OpName</Name> <!--Only supplemented Inputs are required--> <Input> <Name>in[0]</Name> <Constraint id="" Type=""></Constraint> <Datatype></Datatype> <Shape> <Layout></Layout> <Text></Text> </Shape> <OnlyDefaultSupported></OnlyDefaultSupported> </Input> <!--Only supplemented Outputs are required--> <Output> <Name>out[0]</Name> <Constraint id="" Type=""></Constraint> <Datatype></Datatype> <Shape> <Layout></Layout> <Text></Text> </Shape> <OnlyDefaultSupported></OnlyDefaultSupported> </Output> <!--Only supplemented Params are required--> <Parameter> <Name>param</Name> <Constraint id="" Type=""></Constraint> <Datatype></Datatype> <Shape> <Layout></Layout> <Text></Text> </Shape> <OnlyDefaultSupported></OnlyDefaultSupported> </Parameter> </SupplementalOpDef>
-
名称:操作的名称。
-
输入:可选;为操作扩展一个或多个输入。补充输入是补充张量。
-
输出:可选;为操作扩展一个或多个输出。补充输出是补充张量。
-
参数:可选;用于扩展操作的一个或多个参数。补充参数是 补充张量 (Supplemental Tensors)。
输入、输出和参数都是补充张量,只能指定某些字段。补充张量中除名称外的所有字段都是可选的。
-
名称:张量的名称。必须与正在扩展的原始 OpDef 中的张量名称相对应。
-
约束:可选;定义给定张量的一个或多个约束。约束以字符串形式在元素主体中给出。8
-
id:指定约束的ID。用于覆盖补充操作定义的约束。
-
类型:约束的类型。有效类型包括:
-
数字:以基数为特征的约束。例如,输入数量 >= 1
-
形状:以张量维度限制为特征的约束。例如,秩 >= 1
-
值:用张量的值来表征的约束。例如,张量只能为正。
-
数据类型:一种约束,其特征是对数据类型的限制。通常用于可以具有多种数据类型但必须与其他张量具有相同数据类型的张量。
-
描述:不符合任何其他类别的约束的约束。
-
-
-
数据类型:定义张量允许的数据类型。必须是以下之一:
-
FLOAT_16
-
FLOAT_32
-
FIXED_4
-
FIXED_8
-
FIXED_16
-
单位
-
单位:16
-
UINT_32
-
细绳
-
-
形状:指定张量的形状。
-
Layout:可选;指定张量的布局。必须是以下之一:
-
国家卫生和社区委员会
-
国家卫生和妇女福利部
-
UNDEFINED 用于标识布局既不是 NHCW 也不是 NHWC
-
BACKEND_SPECIFIC 用于指示布局依赖于后端。必须与 SupplementalOpDef 结合使用才能指定具体的布局。
-
-
文本:可选;张量形状的字符串描述。
-
-
OnlyDefaultSupported:可选;布尔值,指示后端是否仅支持此张量相应 OpDef 中定义的默认值。
操作定义列表
OpDefList 是一个由一系列OpDef 元素组成的 XML 元素。OpDefList 与后端无关,仅用作多个 OpDef 的包装器。
<OpDefList> <!--One or more OpDef--> <OpDef> <!--OpDef defined above --> </OpDef> </OpDefList>
补充操作定义列表
SupplementalOpDefList 是一个 XML 元素,由一系列 SupplementalOpDef元素组成。此外,SupplementalOpDefList 还包含以下字段。
<SupplementalOpDefList Backend="HTP"> <SupportedOps> <OpName></OpName> </SupportedOps> <SupplementalOpDef> <!--SupplementalOpDef defined above--> </SupplementalOpDef> </SupplementalOpDefList>
-
后端:指定 SupplementalOpDef 正在补充哪个后端。
-
SupportedOps: OpName 元素序列。每个 OpName 对应相应 OpDefList 中定义的一个操作,并指示后端支持该操作。此信息可能与 OpDef 元素的 SupportedBackend 字段重复。
OpDefCollection
OpDefCollection 是配置文件的根 XML 元素,用于与 snpe-udo-package-generator 配合使用。它包含指定所有用户包所需的所有信息。OpDefCollection 包含以下内容:
<OpDefCollection xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:noNamespaceSchemaLocation="OpDef.xsd" PackageName="ExamplePackage" Domain="example" Version="1.0" > <!--One OpDefList--> <OpDefList> <!--OpDefList defined above--> </OpDefList> <!--SupplementalOpDefLists are not required--> <SupplementalOpDefList Backend="HTP"> <!--SupplementalOpDefList defined above--> </SupplementalOpDefList> </OpDefCollection>
-
PackageName:指定用户 OpPackage 的名称。由于包是按后端分配的,因此实际包名称将是此处指定的值加上 <Backend> 后缀,例如 MyPackageNameHtp。
-
域:指定包的域。
-
版本:指定包的版本。
-
OpDefList:一个OpDefList指定包的所有操作。
-
SupplementalOpDefList:可选;指定一个或多个SupplementalOpDefList 来指定每个后端的信息。
一个 OpDefCollection 元素可用于生成多个每个后端包。
JSON UDO 配置描述
上述 UDO 配置文件的细节如下所述。
{ "UdoPackage_0": { "Operators": [ { "type": "", "inputs":[ {"name":"", "per_core_data_types":{"CPU":"FLOAT_32", "GPU":"FLOAT_32", "DSP":"UINT_8"}, "static": true, "tensor_layout": "NHWC"}, {"name":"", "data_type": "FLOAT_32", "static": true, "tensor_layout": "NHWC"}, ], "outputs":[ {"name":"", "per_core_data_types":{"CPU":"FLOAT_32", "GPU":"FLOAT_32", "DSP":"UINT_8"}}, {"name":"", "data_type": "FLOAT_32"} ], "scalar_params": [ {"name":"scalar_param_1", "data_type": "INT_32"} ], "tensor_params": [ {"name":"tensor_param_1", "data_type": "FLOAT_32", "tensor_layout": "NHWC"}, ], "core_types": ["CPU", "GPU", "DSP"], "dsp_arch_types": ["v66", "v68", "v69", "v73"] } ], "UDO_PACKAGE_NAME": "MyCustomUdoPackage" } }
以上描述只是一个通用的配置文件,用于帮助定义用户可以填写的字段。必填字段会提供特定的值,而可选字段则用空字符串表示。请注意,可选字段仅表示如果未提供该字段,则使用默认值,否则将使用空字符串。每个可用字段的完整详细信息如下,按层次结构描述:
-
UdoPackage:每个 UDO 包都可以描述为 “UdoPackage_i”,其中i表示包的生成顺序。用户也可以自由使用空字符串,但必须使用字典结构。1
-
操作员:这是特定 UdoPackage 的子节点,指示存在的操作员数量 。5
-
类型:定义操作的类型。
-
输入:操作的输入张量列表。每个输入都是一个字典对象。2
-
name:可选字段,用于描述输入张量的名称。由于输入张量的名称是可变的,因此用户无需提供此字段。
-
per_core_data_type:一个字典对象,指定每个核心中此输入张量的数据类型。或者,如果用户希望所有指定核心具有相同的数据类型,则可以指定选项“data_type”后跟数据类型。支持的数据类型包括:
-
FLOAT_16
-
FLOAT_32
-
FIXED_4
-
FIXED_8
-
FIXED_16
-
单位
-
单位:16
-
UINT_32
-
细绳
-
-
static:如果输入数据是静态的(即数据在模型中提供),则必须设置该布尔值。如果输入张量包含数据,则需要设置此字段;否则,输入将被动态处理,并且数据不会被序列化。
-
tensor_layout:一个字符串字段,用于描述输入张量的规范维度格式。支持的值为:4
-
NCHW
-
国家卫生和社区委员会
-
-
-
输出:操作的输出张量列表。2
-
scalar_params:标量值属性列表。3
-
name:描述标量参数名称的必填字段。
-
data_type:描述此标量参数支持的数据类型的必填字段。
-
-
tensor_params:张量值属性列表。2 3
-
core_types:此特定操作所需的 IP 核。支持的 core_types 包括:
-
中央处理器
-
图形处理器
-
数字信号处理器
-
-
dsp_arch_types: DSP 核心类型的预期 DSP 架构类型。支持的 dsp_arch_types 包括:
-
v65
-
v66
-
v68
-
v69
-
v73
-
-
-
UDO_PACKAGE_NAME: UDO 包的名称,可以是任何有效的字符串。1
创建 UDO 配置
用户应尽力填写上述配置中描述的字段,以充分描述 UDO。在某些情况下,此配置所需的信息可以轻松从有关操作的框架文档中获取。然而,可能存在一些细微的注意事项,因此建议用户确保输入、输出和参数得到正确的分类和描述。一个潜在的问题是,如果仅根据文档编写配置,输入可能会被错误地归类为参数,反之亦然。在这种情况下,一个有用的技巧是使用诸如 Netron(可在此处找到: https: //github.com/lutzroeder/netron)之类的开源工具来可视化模型,以帮助正确构建 UDO 配置。
一旦创建了足够描述的配置,它就可以用作框架转换器的参数,如使用 UDO 准备模型中所述。
笔记:
-
一个配置文件中可以定义多个 UDO 包。用户需要注意的是,此处指定的包名称必须与创建相应包时使用的包名称一致。
-
每个输入、输出和张量参数都被归类为同一类型的张量对象,这意味着所有字段都是共享的。由于配置是对操作的通用描述,因此输入和输出的名称不是必需的。一个算子必须至少有一个输入和输出。
-
对于参数来说,名称字段始终是必需的。
-
张量布局是一种约定俗成的规则,用于指示张量内数据的排列方式。因此, NHWC张量布局意味着数据以 的形式组织 ,其中通道是变化最快的维度。请注意,这是 Qualcomm® 神经处理 SDK 的默认布局,如果选择其他张量布局,这可能会对包含 UDO 的模型产生影响。尤其需要注意的是,如果 选择NCHW张量布局,则可能需要将数据和/或张量参数重塑为 Qualcomm® 神经处理 SDK 的默认值,以保持维度理解。如果用户遇到这种情况,他们可能会注意到在 UDO 层之前引入了中间置换层,而 UDO 层最终将为相关张量提供数据。这些注意事项应该以转换器警告、调试消息或工具中描述的可视化工具的输出形式显示。有关张量布局的更多详细信息,用户可以查阅文档的“输入图像格式”部分。
(batch x height x width x channel)
-
对于 CPU、GPU 和 DSP 核心类型,每个 UDO 包可以定义任意数量的操作。但是,提供的框架代码经过定制,每个包中只定义一个操作。一个细微的区别是,生成的 DSP V65 或 DSP V66 实现源代码要求每个实现库对应一个操作。而在 CPU、GPU 和 DSP V68 及更高版本的情况下,一个库中可以包含任意数量的操作。
-
张量的数据类型决定了张量中包含的数据在 DLC 中的存储方式,以及 Qualcomm® 神经处理 SDK 在运行时执行期间移交的内存类型。虽然张量会以 UDO 定义指定的确切数据类型存储在 DLC 中,但根据所选的内核类型,用户预期接收的内存类型可能会受到运行时限制。用户应访问以下部分:编译 UDO 包, 了解更多详细信息。
-
DSP 和 HTP 后端目前不支持输入/输出数量未知的操作。
-
约束纯粹是描述性字段,不需要数学表达式。目前尚未强制执行约束。
-
输出不是强制性的,但不能有默认值,如果没有提供则假定为 NULL。
-