以下内容翻译自:CUTLASS 中的 Implicit GEMM Convolution
Implicit GEMM 是将卷积操作表述为 GEMM (广义矩阵-矩阵积)。卷积接受激活张量并对其应用滑动滤波器以产生输出张量。
此版本的 CUTLASS 包含几个与卷积相关的工件。
二维卷积可以映射到矩阵乘:组建一个包含激活张量元素的卷积矩阵,然后由滤波张量形成的矩阵乘以该矩阵。该算法的最早形式通过通常称为 im2col 的操作显式构造卷积矩阵。生成的矩阵按照滤波器大小复制每个激活元素,消耗额外的存储容量和内存带宽。
隐式 GEMM 算法是 CUDA 中分块、分层 GEMM 计算的一种变体:当数据从全局内存加载到共享内存时,通过周密地更新指针和谓词,它会动态形成卷积矩阵的分块。
一旦在共享内存中构成卷积矩阵,现有组件计算 warp-level GEMM 累加卷积结果并更新输出张量。
本节介绍用于 Turing Tensor Core 的高效隐式 GEMM 卷积 CUDA 内核的结构。
前向卷积层计算输出张量y = conv(x, w)
其中 x
(NHWC)、w
(KRSC) 和 y
(NPQK) 是4-D 张量。
该计算可以通过以下分析函数来描述。
y[n, p, q, k] = sum_c(sum_r(sum_s( x[n, f(p, r), g(q, s), c] * w[k, r, s, c] )))
其中函数f
和g
定义如下。
f(p, r) = p * stride_h + R - r - 1 + pad_h
g(q, s) = q * stride_w + S - s - 1 + pad_w
CUTLASS Utilities 中提供了 host 和 device 参考实现。
这个计算可以映射到矩阵乘积的元素,如下所示。
C = gemm(A, B)
其中
根据以下关系,输出矩阵 C i j C_{ij} Cij 的每个元素对应于输出张量y[n, p, q, k]
中的一个元素。
y[n, p, q, k] = Cij
其中,
i = q + Q * (p + P * n)
j = k
这些关系可以倒推如下。
k = j
n = i / (PQ)
residual = i % (PQ)
p = residual / Q
q = residual % Q
在 CRS 上迭代以累加结果的三重循环嵌套也可以通过以下关系被线性化并映射到内部 GEMM K K K 维度(不要与滤波器张量维度 K K K 混淆)。
gemm_k = s + S * (r + R * c)
逆向为
c = gemm_k / (RS)
residual = gemm_k % (RS)
r = residual / S
s = residual % S
给定这些等式,GEMM 三重循环嵌套可以使用张量索引进行扩充,如下所示。
int GEMM_M = N * P * Q;
int GEMM_N = K;
int GEMM_K = C * R * S;
for (int gemm_i = 0; gemm_i < GEMM_M; ++gemm_i) {
for (int gemm_j = 0; gemm_j < GEMM_N; ++gemm_j) {
int n = gemm_i / (PQ);
int npq_residual = gemm_i % (PQ);
int p = npq_residual / Q;
int q = npq_residual % Q;
Accumulator accum = 0;
for (int gemm_k = 0; gemm_k < GEMM_K; ++gemm_k) {
int k = gemm_j;
int c = gemm_k / (RS);
int crs_residual = gemm_k % (RS);
int r = crs_residual / S;
int s = crs_residual % S;
int h = f(p, r);
int w = g(q, s);
ElementA a = tensor_A.at({n, h, w, c});
ElementB b = tensor_B.at({k, r, s, c});
accum += a * b;
}
C[gemm_i * K + gemm_j] = accum;
}
}
CUTLASS GEMM implementation 在图块上显式迭代。因此,可以实现一个图块迭代器来分析地计算这些函数并加载适当的元素。然而,由此产生的模运算将是计算密集型的,并且开销将限制以 Turing Tensor Core 为目标的 GEMM 内核的性能。
以下部分描述了如何在以 Tensor Core 为目标的分层 GEMM 内核结构体中实施高效的实现。
为了获得最佳性能,建议使用以下参数:
这可实现128位向量内存访问,从而产生高效的 CUDA 内核。通过在conv::kernel::DefaultConv2dFprop
中设置 AlignmentA 和 AlignmentB,即使在张量核上也支持较小的对齐,但性能低于128位对齐张量。
CUTLASS 定义了接受大量模板参数的 CUDA C++ 模板,以通过操作、数据类型、图块配置、数学指令和融合输出操作来特化生成的内核。
在 turing_tensorop_conv2dfprop.cu,卷积操作定义如下:
/// Define an Implicit GEMM convolution forward propagation (fprop) kernel
using Conv2dFpropKernel = typename cutlass::conv::kernel::DefaultConv2dFprop<
ElementInputA, // data type of element a (mapped to activation for fprop)
LayoutInputA, // layout of element a (mapped to activation for fprop)
ElementInputB, // data type of element b (mapped to filters for fprop)
LayoutInputB, // layout of element b (mapped to filters for fprop)
ElementC, // data type of element c (mapped to output for fprop)
LayoutC, // layout of element c (mapped to output for fprop)
ElementAccumulator, // data type of internal accumulation
MMAOp, // opcode class tag
SmArch, // target SM architecture
ThreadblockShape, // shape of threadblock tile
WarpShape, // shape of warp-level GEMM tile
InstructionShape, // shape of target math instruction
EpilogueOp, // epilogue operator
SwizzleThreadBlock, // optional function to reorder threadblocks for locality
NumStages, // number of pipeline stages in threadblock-scoped GEMM
cutlass::arch::OpMultiplyAddSaturate, // math operation on data of element a and b
cutlass::conv::IteratorAlgorithm::kOptimized // global memory iterator algorithm
>::Kernel
该模板旨在通用并涵盖所有可行的配置。该示例指定了以下具体数据类型、布局和图块形状。
/// Define an Implicit GEMM convolution forward propagation (fprop) kernel
using Conv2dFpropKernel = typename cutlass::conv::kernel::DefaultConv2dFprop<
cutlass::int4b_t, // data type of element a (mapped to activation for fprop)
cutlass::layout::TensorNHWC, // layout of element a (mapped to activation for fprop)
cutlass::int4b_t, // data type of element b (mapped to filters for fprop)
cutlass::layout::TensorNHWC, // layout of element b (mapped to filters for fprop)
int32_t, // data type of element c (mapped to output for fprop)
cutlass::layout::TensorNHWC, // layout of element c (mapped to output for fprop)
int32_t, // data type of internal accumulation
cutlass::arch::OpClassTensorOp, // opcode class tag
cutlass::arch::Sm75, // target SM architecture
cutlass::gemm::GemmShape<128, 128, 128>, // shape of threadblock tile
cutlass::gemm::GemmShape<64, 64, 128>, // shape of warp-level GEMM tile
cutlass::gemm::GemmShape<8, 8, 32>, // shape of target math instruction
cutlass::epilogue::thread::LinearCombinationClamp<
int32_t, // data type of output matrix
8, // The number of elements per vectorized
// memory access. This becomes the vector width of
// math instructions in the epilogue too.
int32_t, // Data type of accumulator
float>; , // epilogue operator
SwizzleThreadBlock, // optional function to reorder threadblocks for locality
2, // number of pipeline stages in threadblock-scoped GEMM
cutlass::arch::OpMultiplyAddSaturate, // math operation on data of element a and b
cutlass::conv::IteratorAlgorithm::kOptimized // global memory iterator algorithm
>::Kernel
即使用4位整数输入和输出 (cutlass::int4b_t
) 计算2D 卷积前向传播。使用32位整数(int32_t
)进行内部累加,并对单精度浮点(float
)的输出进行元素线性组合运算。
线程块和线程束级别形状指的是 gemm_api.md 描述的分层分块 GEMM 计算。较大的图块可以更好地重用通过共享内存加载的数据,但启动的 CTA 较少,并且对于较小的问题规模可能无法完全占用 GPU。较小的图块配置实现了较低的峰值利用率,但对于实际工作负载,可以更好地匹配 GPU 内的 SM 数量。
以下代码将隐式 GEMM 操作的参数收集到一个结构体中。
//
// Define arguments for CUTLASS Convolution
//
// mode (kCrossCorrelation or kConvolution)
cutlass::conv::Mode mode = cutlass::conv::Mode::kCrossCorrelation;
// Split K dimension into 1 partitions
int split_k_slices = 1;
cutlass::conv::Conv2dProblemSize problem_size(
options.input_size,
options.filter_size,
options.padding,
options.conv_stride,
options.dilation,
options.output_size(),
mode,
split_k_slices);
typename ImplicitGemm::Arguments arguments{
problem_size,
tensor_a.device_ref(),
tensor_b.device_ref(),
tensor_c.device_ref(),
tensor_c.device_ref(),
{options.alpha, options.beta},
};
mode
标志指示是计算互相关还是卷积。参数input_size
、filter_size
、padding
、conv_stride
和dilation
指定输入和输出张量的纬度,并表征问题大小。
参数tensor_a.device_ref()
、tensor_b.device_ref()
和tensor_c.device_ref()
是
CUTLASS TensorRef<>
对象,其中包含 GPU 设备内存中张量数据的指针和步幅值。
以下代码初始化并启动设备上的隐式 GEMM 操作。以下代码初始化并在设备上启动隐式GEMM操作。 初始化参数结构体,接着将其用于查询设备端工作区要求,并在需要时在设备内存中分配空间。
然后,使用参数结构体和设备内存中的工作空间初始化隐式 GEMM 对象。该初始化步骤预先计算卷积核使用的内部查找表,必要时也可以清除设备端工作空间。
最后,调用初始化的隐式 GEMM 对象,在设备上启动内核。tensor_c
现在包含了隐式 GEMM 的结果。
ImplicitGemm implicit_gemm_op;
// Query workspace size
size_t workspace_size = implicit_gemm_op.get_workspace_size(arguments);
// Allocate workspace memory
cutlass::device_memory::allocation<uint8_t> workspace(workspace_size);
// Initialize the Implicit GEMM object
cutlass::Status status = implicit_gemm_op.initialize(arguments, workspace.get());
if (status != cutlass::Status::kSuccess) {
/* error */
}
//
// Launch initialized CUTLASS kernel
//
status = implicit_gemm_op();
if (status != cutlass::Status::kSuccess) {
/* error */
}
该示例演示了如何使用 CUTLASS Utilities 中定义的cutlass::HostTensor<>
将输入和输出张量写入 CSV 文件。
std::ofstream output_workspace(ss.str());
output_workspace
<< "Input = \n" << tensor_a.host_view() << "\n\n"
<< "Filters = \n" << tensor_b.host_view() << "\n\n";
// Copy device memory to host backing store
tensor_c.sync_host();
output_workspace << "Computed = \n" << tensor_c.host_view() << std::endl;
CUTLASS 定义了以下 CUDA C++模板来实现隐式 GEMM 卷积,这些模板将在后续章节中进行更详细的描述。
激活图块迭代器将激活图块加载到寄存器中。提供了两种实现方案:
滤波器图块迭代器将滤波器加载到寄存器中。 同样,提供了两种实现方式:
优化迭代器涵盖的改进包括:
例如,一个优化的激活迭代器使用 fast divmod 将 GEMM 的 M 维映射到 NPQ。
流水线主循环将线程块范围的图块从全局内存加载到共享内存中,然后应用 CUTRASS 线程束级 GEMM 操作从共享内存加载并向 Turing Tensor Core 发出指令。
存储到共享内存以及使用 Turing Tensor Core 执行 warp 宽度矩阵乘法运算的操作直接从 CUTLASS GEMM 组件应用。这包括以下几个部分:
Implicit GEMM Convolution 算法将 GEMM 的 K 维(CRS 的范围)划分为线程块图块,并将每个线程块图块分配给一个滤波器位置和一个通道区间。迭代完所有滤波器位置后,卷积算法前进到下一个通道区间并从滤波器r=0, s=0
开始。
主循环的每次迭代都会计算一个线程块图块的矩阵乘积,如 CUTLASS GEMM implementation 中所述。总而言之,激活和滤波器的线程块图块从全局内存中的张量加载并存储到共享内存中。线程块内的每个线程加载一个或多个向量并共同涵盖整个图块。
下图展示了 Implicit GEMM 主循环的一次特定迭代。线程块中的每个线程都映射到 Activation 和 Filter 张量中的若干元素向量。GEMM 的 M 维度中,每个索引对应于输出张量的唯一(N,P,Q)
索引,并且可以基于该索引以及滤波器位置(r,s)
来计算指针。
体现此功能的 CUTLASS 组件是 Conv2dFpropFilterTileAccessIteratorAnalytic。其构造函数计算 GEMM 的 M 维到 (N, P, Q) 的映射,at()
方法将线程执行的每个内存访问的线性偏移映射到的 Activation 张量中。此外,valid()
方法计算每个滤波器位置和每个内存访问的有效性,以指示内存访问是在张量的边界内还是在边界外。
operator++()
在连续维度和跨步维度上对线程执行的内存访问进行迭代。
// cutlass/conv/threadblock/conv2d_fprop_activation_tile_access_iterator_analytic.h
// Update iterator to thread's next contiguous, strided memory access
Conv2dFpropActivationTileAccessIteratorAnalytic &operator++() {
++iteration_contiguous_;
if (iteration_contiguous_ < ThreadMap::Iterations::kContiguous) {
return *this;
}
iteration_contiguous_ = 0;
++iteration_strided_;
if (iteration_strided_ < ThreadMap::Iterations::kStrided) {
return *this;
}
iteration_strided_ = 0;
return *this;
}
在访问完当前线程块图块的所有入口后,advance()
更新指向下一个图块的指针。添加到每个指针的偏移量遵循滤波器位置的遍历,执行以下操作之一:
(r, s, c)
前进到滤波器位置(r, s+1, c)
(r, S-1, c)
前进到滤波器位置(r+1, 0, c)
(R-1, S-1, c)
前进到滤波器位置(0, 0, c+32)
方法advance()
主体中的逻辑计算激活 GEMM-A 图块的上述三个更新:
// cutlass/conv/threadblock/conv2d_fprop_activation_tile_access_iterator_analytic.h
// Advance to the next access
void advance() {
// moves to the next tile
++filter_s_;
if (filter_s_ < problem_size_.S) {
return;
}
filter_s_ = 0;
++filter_r_;
if (filter_r_ < problem_size_.R) {
return;
}
filter_r_ = 0;
filter_c_ += Shape::kRow * problem_size_.split_k_slices;
}
类似的逻辑也适用于 Conv2dFpropFilterTileAccessIteratorAnalytic。
为了减少主循环体中的计算开销,可以在主机代码中预先计算指针偏移量,并作为其Params
结构体中的查找表提供给 CUDA 内核。如 Conv2dFpropFilterTileAccessIteratorOptimized 所示,将从滤波器位置计算偏移的逻辑 提取到了Params
构造函数中。
// cutlass/conv/threadblock/conv2d_params.h
struct Conv2dFpropActivationIteratorOptimizedParams<layout::TensorNHWC> {
...
// next S
inc_next[0] = conv_sign * (int64_t(layout.stride()[0]) * problem_size.dilation_w) * element_size_bits / 8;
// next R
inc_next[1] = conv_sign * (
int64_t(layout.stride()[1]) * problem_size.dilation_h
- (problem_size.S - 1) * layout.stride()[0] * problem_size.dilation_w
) * element_size_bits / 8;
// next C
inc_next[2] = (
threadblock_shape.column() * problem_size.split_k_slices
- conv_sign * int64_t(problem_size.R - 1) * layout.stride()[1] * problem_size.dilation_h
- conv_sign * int64_t(problem_size.S - 1) * layout.stride()[0] * problem_size.dilation_w
) * element_size_bits / 8;
...
}
这允许Conv2dFpropActivationTileAccessIteratorOptimized::advance()
中的设备代码中仅执行增量表的简单查找。
// cutlass/conv/threadblock/conv2d_fprop_activation_tile_access_iterator_optimized.h
CUTLASS_HOST_DEVICE
void advance() {
int next_idx = 0;
// moves to the next tile
++filter_s_;
if (filter_s_ == problem_size_.S) {
filter_s_ = 0;
++filter_r_;
if (filter_r_ < problem_size_.R) {
next_idx = 1;
}
else {
filter_r_ = 0;
next_idx = 2;
}
}
add_byte_offset_(params_.inc_next[next_idx]); // in addition to Conv2dFpropActivationTileAccessIteratorAnalytic::advance()
if (next_idx == 2) {
filter_c_ += params_.filter_c_delta;
}
}
Turing Tensor Core 通过在线程束内的所有线程之间共享数据来高效地计算矩阵乘法累加运算。支持以下操作:
Shape | A | B | C |
---|---|---|---|
8x8x32 | int4b_t | int4b_t | int32_t |
8x8x16 | int8b_t | int8b_t | int32_t |
16x8x8 | half | half | half |
16x8x8 | half | half | float |
从功能上讲,Turing 8x8x32矩阵乘法运算将 A、B 和 C 矩阵分布在线程束内的32个线程上,如下图所示。
CUDA 程序员可以通过 PTX 指令 mma.sync 访问此 Tensor Core 操作。CUTLASS 使用 cutlass/arch/mma_sm75.h 中定义的设备端内在函数封装内联 PTX,如下例所示。
unsigned A; // eight packed 4-bit integer elements
unsigned B; // eight packed 4-bit integer elements
int C[2]; // two 32-bit integer elements
int D[2]; // two 32-bit integer elements
asm volatile(
"mma.sync.aligned.m8n8k32.row.col.s32.s4.s4.s32 {%0,%1}, {%2}, {%3}, {%4,%5};\n"
: "=r"(D[0]), "=r"(D[1])
: "r"(A), "r"(B), "r"(C[0]), "r"(C[1]));
为了高效地将数据从共享内存加载到寄存器中,并在线程束之间的分布与上述匹配,Turing GPU 架构引入了 ldmatrix。ldmatrix 是最终的线程束协作指令,因为所有线程都向长度为128位的最多32个行向量提供地址。这些行从共享内存中取出,然后分布在每行四个线程的组中。线程内 SMEM 指针和目标寄存器的排列如下所示。图中突出显示了线程0以强调映射。
Turing Tensor Core 对 INT4数据进行乘法累加运算的计算矩阵的大小为8×8×32个元素。ldmatrix 每次操作最多取32行(或列)。可以发出16个 Tensor Core 操作来实现32×32×32矩阵乘积,并完美地消耗由两个 ldmatrix 指令加载的所有数据,如下图所示。通过增加内存指令数量并发出更多 Tensor Core 操作,可以实现更大的图块,最大可达64x64x32的线程束级矩阵操作。 限制是保存累加器元素的寄存器数量。
在前两节中,我们描述了如何从全局内存中的激活和滤波器张量加载数据来计算卷积,并且描述了 ldmatrix 和 mma.sync 的组合,以从共享内存中获取数据并发出 Tensor Core 操作。
为了确保数据移动效率,必须注意避免 bank 冲突。CUTLASS 使用置换后的共享内存布局来避免存储到共享内存时发生存 bank 冲突,并使用 ldmatrix 从共享内存高效加载。
下图说明了用于从全局内存加载激活和滤波器线程块图块以及共享内存中的置换布局的线程映射。
在图中,一个线程束宽度的内存访问以蓝色突出显示,单个线程加载一个128位向量。全局内存中的图块可以对应于激活或滤波器,并假设是通过四个线程加载连续通道进行“条带挖掘”的。
将共享内存可视化为一个"主行"矩阵,其中八列表示8个128位 bank。 如 CUTLASS GTC 2019 slides、recording 中所述,如果每个线程束满足以下条件,则访问共享内存将不会发生冲突:
为了实现无冲突存储,共享内存布局重新映射条带挖掘排列以转置向量,并对每个线程指针的列索引应用 XOR 运算。 具体来说,
int store_column = (lane_id % 8) ^ (lane_id / 8);
这种布局上的转换将有助于从共享内存中读取数据切片,以使用 Tensor Core 计算线程束级矩阵乘法。
下图显示了参与 ldmatrix 指令的前16个线程如何逻辑映射到共享内存中矩阵的c=0..31
切片。该切片在代码中称为k_group
,因为它对应于线程束级矩阵乘法的相同 K 索引。
图中下半部分为共享内存中的物理布局,线程根据 XOR 函数进行行列偏移。通过检查,我们可以观察到没有 bank 冲突,因为 T0 … T7 每个访问唯一的存储体,T8 … T15 也是如此,后续相同。
为了前进到共享内存中的下一个“k-group”,根据以下顺序使用 XOR 操作更新指针:
k=0
前进到k=1
k=1
前进到k=2
k=2
前进到k=3
k=3
前进到k=0
其中第一种转变如下所示。
CUTLASS warp-level GEMM API 定义了用于从置换共享内存加载数据切片并向 Tensor Core 发出操作的模板。
主循环终止后,线程束级 GEMM 的累加器图块存储线程束对输出张量的贡献。然而,线程块内的线程之间的数据分布专门用于使用 Tensor Core 的高效矩阵乘法累加运算,并且不利于全局内存的高效合并操作。需要进行数据重排。
Epilogue 是通过共享内存交换累加器元素、加载输出矩阵或张量的切片、应用线性缩放或偏置等元素操作,以及将结果存储到输出张量的组件。
CUTLASS 将其构建为几个组件:
alpha * AB + beta * C
得到最终输出的逐元素函数单元测试在一个独立的 CUDA 内核中验证上述每个组件的功能行为。这就给人们提供了一个便利的环境
Convolution unit tests
GEMM unit tests
Epilogue unit tests
本节介绍所提供的卷积示例,旨在引导读者了解 Implicit GEMM 卷积的 CUTLASS 实现。
示例 09_turing_tensorop_conv2dfprop 计算前向卷积层,其中输入和输出都是 4-b 整数。示例源代码可见于 examples/09_turing_tensorop_conv2dfprop/turing_tensorop_conv2dfprop.cu。
在构建示例之前,首先执行 quickstart.md 中构建任何 CUTLASS 组件的先决条件步骤。计算能力7.5指的是 Turing 架构,这项工作需要 CUDA 10.2 Toolkit 或更高版本,以使用 Turing Tensor Core 的mma
PTX instruction。
$ mkdir build && cd build
$ cmake .. -DCUTLASS_NVCC_ARCHS=75
要构建示例,请从构建目录执行make 09_turing_tensorop_conv2dfprop
。
$ make 09_turing_tensorop_conv2dfprop
$ ls examples/09_turing_tensorop_conv2dfprop
examples/09_turing_tensorop_conv2dfprop
此示例提供了一个简单的命令行界面,用于指定4位整数元素 (cutlass::int4b_t
) 的4D 张量的范围,将它们初始化为随机值,并计算卷积层的结果。可选地,输入和输出张量可以保存到.csv文件中,并且可以执行 CUTRASS 主机端参考检查以验证正确性。
使用--help
运行可以看到完整的用法语句:
$ ./examples/09_turing_tensorop_conv2dfprop/09_turing_tensorop_conv2dfprop --help
09_turing_tensorop_conv2dfprop example
This example uses Turing's Tensor Core operators on int4 data types to compute
forward convolution on tensors of layout NHWC.
Options:
--help If specified, displays this usage statement.
--n <int> Input tensor extent N
--h <int> Input tensor extent H
--w <int> Input tensor extent W
--c <int> Input tensor extent C
--k <int> Filter extent K
--r <int> Filter extent R
--s <int> Filter extent S
--alpha <float> Epilogue scalar alpha
--beta <float> Epilogue scalar beta
--ref-check If set (true), reference check on the host is computed
--perf-check If set (true), performance is measured.
--benchmark If set (true), performance benchmarking on several layers and batch-size.
--iterations <int> Number of profiling iterations to perform.
--save-workspace If set, workspace is written to a text file.
--tag <string> String to replicate across the first column in the results table
Examples:
$ ./examples/09_turing_tensorop_conv2dfprop/09_turing_tensorop_conv2dfprop --n=32 --h=224 --w=224 --c=128 --k=256 --r=1 --s=1
$ ./examples/09_turing_tensorop_conv2dfprop/09_turing_tensorop_conv2dfprop --n=1 --h=224 --w=224 --c=32 --k=32 --r=3 --s=3 --ref-check
请注意,此示例假设所有张量都是128b 对齐且格式为 NHWC。 因此,对于激活、滤波器和输出,维度 C 必须能被32整除。
如果传递了--benchmark
选项,则会针对不同的批处理大小对 ResNet50中的几个层进行分析。此示例输出是在使用 CUDA 10.2编译的 NVIDIA RTX 2080上计算的。
build$ ./examples/09_turing_tensorop_conv2dfprop/09_turing_tensorop_conv2dfprop --benchmark
卷积也可以由 CUTLASS Profiler 运行。
Copyright © 2017 - 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: BSD-3-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.