Burn 之 burn-ndarray:基于 ndarray 的纯 Rust CPU 后端(特性开关、BLAS 选项与 no_std 支持详解)
【免费下载链接】burnBurn is a next generation tensor library and Deep Learning Framework that doesn't compromise on flexibility, efficiency and portability.项目地址: https://gitcode.com/GitHub_Trending/bu/burn
本文以crates/burn-ndarray/README.md为骨架,系统讲解 Burn 框架中 ndarray 后端的定位、特性开关(Feature Flags)、BLAS 加速选项、no_std运行模式与平台支持矩阵,并结合仓库源码剖析后端类型、随机数种子机制、零拷贝存储与并行/SIMD 实现细节。需要特别注意:该 crate 自0.22.0起已被标记为废弃(deprecated),计划在未来的版本中移除,新项目应迁移到burn-flex或 CubeCL 系列后端;本文内容适用于当前仓库中该后端仍然存在的版本,读完后你既能掌握该后端的全部配置方式,也能理解 Burn 后端体系的工作机制,为迁移提供判断依据。
一、burn-ndarray 是什么,以及它的废弃状态
burn-ndarray是 Burn 张量库的一个 CPU 后端,底层使用 Rust 的ndarray数组库执行张量运算。从 README 的文档说明看,它"兼容 CPU,可以编译到几乎任何平台,包括wasm、arm和x86"(见 backend.rs 中NdArray结构体上的文档注释)。
在 lib.rs 中,整个 crate 通过属性宏被标记为废弃:
#![deprecated( since = "0.22.0", note = "burn-ndarray is deprecated and will be removed in a future release. Use burn-flex for pure-Rust CPU execution (std, no_std, WebAssembly), or one of the CubeCL backends (burn-cuda, burn-rocm, burn-wgpu, burn-cpu) for GPU acceleration." )]README 给出的迁移路径是:
- 纯 Rust CPU 执行:迁移到
burn-flex。它是一个从零实现(from-scratch)的后端,取代burn-ndarray,提供更快的 GEMM、零拷贝视图操作、原生量化,并完整支持std、no_std与 WebAssembly; - GPU 加速:迁移到 CubeCL 系列后端(CUDA、ROCm、Vulkan、Metal、WebGPU)。
README 指向 burn-flex/COMPARISON.md 获取逐算子(operation-by-operation)的迁移对照与基准测试。Cargo.toml 中的 crate 描述也同步了这一定位:[Deprecated] Ndarray backend for the Burn framework - use burn-flex, burn-cuda, burn-rocm, or burn-wgpu instead。
二、Feature Flags:BLAS 选项与 no_std 模式
README 的核心内容是特性开关说明。本章将原文的开关列表完整继承,并结合 Cargo.toml 中的实际依赖映射逐项展开。
2.1 默认特性
Cargo.toml 中的默认特性为:
default = ["std", "simd", "multi-threads"]即默认同时启用:
std:标准库支持。从源码结构看,它是一组"向下传递"的特性开关,包括burn-std/std、burn-backend/std、burn-ir/std、ndarray/std、matrixmultiply/std、rand/std、rand/std_rng、num-traits/std、macerator/std——注释明确要求所有依赖在std关闭时都必须支持 no_std;simd:SIMD 加速路径,依赖macerator、bytemuck、seq-macro、itertools;multi-threads:多线程并行,依赖rayon,并同时打开ndarray/rayon与matrixmultiply/threading。
另外还有:
tracing:为burn-std、burn-backend、burn-ir开启 tracing 日志支持;export_tests:空特性,Cargo 注释说明其用途是"作为部分 burn-cubecl 内核的参考实现"(serves as a ref impl for some burn-cubecl kernels)。从源码看,开启该特性后量化算子的参考行为会放宽——backend.rs 中export_tests编译路径下额外支持 Q4/Q2 子字节量化档位,作为数值等价的参照基准。
2.2 四个 BLAS 特性开关
README 列出的四个 BLAS 选项及其在 Cargo.toml 中的实际映射:
| Feature | 依赖映射 | 说明 |
|---|---|---|
blas-accelerate | blas-src/accelerate+ndarray/blas | Accelerate framework(仅 macOS) |
blas-netlib | blas-src/netlib+ndarray/blas | Netlib BLAS |
blas-openblas | blas-src/openblas+ndarray/blas+openblas-src | 静态链接 OpenBLAS |
blas-openblas-system | blas-src/openblas+ndarray/blas+openblas-src/system | 使用系统已有的 OpenBLAS |
所有 BLAS 特性都通过blas-srccrate 链接实际的 BLAS 实现,并同步打开ndarray/blas使ndarray的矩阵运算走 BLAS。blas-openblas与blas-openblas-system的区别在于后者通过openblas-src/system复用系统安装的 OpenBLAS,前者由openblas-src静态构建。
在 lib.rs 中可以看到对应的编译期接线:
#[cfg(any( feature = "blas-netlib", feature = "blas-openblas", feature = "blas-openblas-system", ))] extern crate blas_src;只有开启这三个(非 Accelerate 的)BLAS 特性时,crate 才会显式链接blas_src。
2.3 no_std 模式与固定种子
README 特别指出:
这个 crate 可以通过关闭默认的
std特性来在没有标准库(#![no_std])、仅带alloc的情况下使用。注意:在
no_std模式下,如果未通过Backend::seed方法初始化,随机数种子是固定的。
这一行为在源码中有精确对应:
- lib.rs 首行即为
#![cfg_attr(not(feature = "std"), no_std)],末尾extern crate alloc;确认只依赖alloc; - rand.rs 按特性选择随机数生成器:
std构建下NdArrayRng = StdRng,no_std构建下NdArrayRng = SmallRng; - 未显式播种时的
get_seeded_rng()在两个分支下行为不同:std下调用burn_std::rand::get_seeded_rng()使用 OS 熵源;no_std下则使用编译期随机种子:
#[cfg(not(feature = "std"))] pub fn get_seeded_rng() -> NdArrayRng { // Use compile-time random seed for no_std const SEED: u64 = const_random::const_random!(u64); SmallRng::seed_from_u64(SEED) }也就是说,在嵌入式等 no_std 环境中,如果你不通过Backend::seed(即NdArray::seed(device, seed),见 backend.rs)显式设置种子,同一份构建产物的随机数序列是确定性的。
一个典型的 no_std 依赖声明写法(基于本 crate 的特性结构):
[dependencies] burn-ndarray = { version = "0.22", default-features = false, features = ["simd"] }三、平台支持矩阵
README 给出的平台支持表(完整继承自原文档):
| Option | CPU | GPU | Linux | MacOS | Windows | Android | iOS | WASM |
|---|---|---|---|---|---|---|---|---|
| Pure Rust | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes |
| Accelerate | Yes | No | No | Yes | No | No | Yes | No |
| Netlib | Yes | No | Yes | Yes | Yes | No | No | No |
| Openblas | Yes | No | Yes | Yes | Yes | Yes | Yes | No |
从这张表可以读出两个关键约束:
- 该后端只覆盖 CPU 路径(所有选项 GPU 列均为 No),GPU 场景需要 CubeCL 后端;
- 引入 BLAS 库后,WASM 支持全部丢失(所有 BLAS 选项 WASM 列均为 No)——因此 WebAssembly 部署只能走纯 Rust 路径,且通常还需
no_std或关闭multi-threads(WASM 单线程环境下 rayon 无意义)。
四、后端类型与默认配置:源码视角
4.1NdArray后端结构
B 中,设备类型只有一个变体:
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum NdArrayDevice { /// The CPU device. #[default] Cpu, }NdArray是一个空结构体(unit struct),实现BackendTypes(backend.rs):
impl BackendTypes for NdArray { type Device = NdArrayDevice; type FloatTensorPrimitive = NdArrayTensor; type IntTensorPrimitive = NdArrayTensor; type BoolTensorPrimitive = NdArrayTensor; type QuantizedTensorPrimitive = NdArrayQTensor; type GraphPrimitive = burn_backend::GraphUnsupported; }从源码结构看有两个值得注意的点:
- 浮点、整数、布尔张量共用同一个
NdArrayTensor原始类型(内部按元素类型枚举分派),量化张量由独立的NdArrayQTensor承载; GraphPrimitive = GraphUnsupported意味着该后端不支持图捕获(graph capture)。结合fn ad_enabled(_device) -> bool { false }(backend.rs),训练场景下它只能配合burn-autodiff的动态求导使用,这也解释了为何文档将其定位为"可移植性优先"的 CPU 后端。
4.2 默认 DType 与支持的数值类型
NdArrayDevice::defaults()(backend.rs)声明的默认设备能力为:
DeviceSettings::new( DType::F32, // 默认浮点类型 E DType::I64, // 默认整型 I DType::Bool(BoolStore::Native), QuantConfig::new( QuantScheme::default().with_store(QuantStore::Native), Default::default(), ), )dtype_usage方法(backend.rs)完整列出了支持的数据类型:
- 通用支持:
F64、F32、Flex32、I64/I32/I16/I8、U64/U32/U16/U8、Bool(Native),以及对称模式(QuantMode::Symmetric)+ 原生存储(QuantStore::Native)下的Q8F/Q8S量化; - 明确不支持:
F16、BF16(半精度在纯 Rust 路径上被拒绝); - 量化的边界:两级量化方案(two-level scheme,即 per-block + per-tensor 缩放)只有在块缩放 dtype 不能跨满 f32 值域时才被接受——测试
should_support_the_two_level_schemes_it_can_quantize(backend.rs)中构造了per_block([4], ScaleDtype::UE4M3)+per_tensor(ScaleDtype::F32)的方案并验证quantize_dynamic成功产出 2 级缩放;而per_block([4], ScaleDtype::F32)则被supports_dtype拒绝。测试注释解释了这个设计动机:"声称支持却 panic 的方案,比直接拒绝更糟,因为 panic 会落在第一个张量上,而不是在选择方案的地方"。
should_support_dtypes测试(backend.rs)以断言形式固化了上述整张支持表,可作为权威参照。
4.3 张量原始类型与零拷贝存储
NdArrayTensor 是一个按元素类型区分的枚举(F64/F32/I64…/Bool),每个变体持有NdArrayStorage<E>。该存储支持两种形态:
- Owned:底层是
ArcArray<E, IxDyn>(ndarray的写时复制共享数组,见SharedArray类型别名,tensor.rs); - Borrowed:从外部数据(如 burnpack 文件、mmap)零拷贝借用,在发生变异操作之前保持零拷贝。
from_data的分派逻辑(tensor.rs)很值得细读:
pub fn from_data(data: TensorData) -> NdArrayTensor { // Only use Borrowed storage for non-native allocations (e.g., burnpack mmap/file). // For native Rust heap allocations (the common case), go directly to owned storage: // `from_data_owned` reclaims the Vec zero-copy via `into_vec`, while // Borrowed storage would trigger a full memcopy on every single operation. if data.bytes.property() != AllocationProperty::Native { match Self::try_from_data_borrowed(data) { Ok(tensor) => return tensor, Err(data) => return Self::from_data_owned(data), } } Self::from_data_owned(data) }即:只有非原生分配(AllocationProperty不是Native,典型来源是 burnpack 存储的 mmap/文件数据)才走 Borrowed 零拷贝路径;普通的 Rust 堆分配会直接走 Owned 路径(try_into_vec零拷贝回收Vec),避免每次操作都触发一次完整的内存拷贝。配套的单测zero_copy_creates_borrowed_storage_for_non_native与native_alloc_creates_owned_storage(tensor.rs)分别用"ZERO-COPY REGRESSION"与"PERF REGRESSION"级别的断言保护这两条路径,属于该后端回归测试的核心。
此外,into_data(tensor.rs)在连续(contiguous)时会用try_into_owned_nocopy直接取回底层Vec并裁剪 offset 与尾部多余数据,实现无损往返;对非连续数据则逐元素收集。
五、算子实现与并行、SIMD 策略
5.1 算子模块布局
ops/ 目录按算子族组织,包括:matmul.rs、conv.rs、deform_conv.rs、avgpool.rs/maxpool.rs/adaptive_avgpool.rs、interpolate.rs、grid_sample.rs、activation.rs、quantization.rs/qtensor.rs、bool_tensor.rs/int_tensor.rs等。matmul在开启 BLAS 特性时经ndarray/blas走 BLAS 内核,否则回落到matrixmultiplycrate 的纯 Rust 实现。
元素级操作的分派依赖 tensor.rs 中导出的一组宏:execute_with_dtype!(全类型)、execute_with_float_dtype!(仅 F64/F32)、execute_with_int_dtype!(整型)、execute_with_numeric_dtype!(浮点+整型)。其文档注释明确了一个使用约束:没有自动类型转换,左右操作数 dtype 不一致时会 panic(Data type mismatch (lhs: ..., rhs: ...)),这是该后端的一个已知行为边界。
5.2 多线程:run_par!家族
parallel.rs 定义了一组按multi-threads特性切换的并行宏:
run_par!:有 rayon 时用rayon::scope执行闭包,无 rayon 时顺序执行;iter_par!/iter_slice_par!/iter_range_par!:分别为迭代器、切片、区间提供into_par_iter()或退化的顺序迭代。
与 rayon 协作的可变访问由 sharing.rs 中的UnsafeSharedRef解决:它持有RawArrayViewMut而非&mut,因为"交出&mut会把它重标记为独占,从而使所有已在途(in-flight)的句柄失效,只有最新一个可用"。该文件附带的测试handles_stay_valid_while_another_is_alive验证了两个并发句柄各写不相交元素的模式正确性。
5.3 SIMD 路径
ops/simd/子目录包含avgpool.rs、maxpool.rs、conv.rs、unary.rs、binary.rs、cmp.rs等专用 SIMD 实现,配合simd特性(依赖macerator编译期生成 SIMD 内联代码)。可以推断,启用默认特性时这些热点算子会优先走 SIMD 路径。
六、迁移指引:什么时候离开 burn-ndarray
综合 README、lib.rs 与 Cargo.toml 三处一致的信息,决策依据如下:
| 场景 | 建议后端 |
|---|---|
| 新的纯 Rust CPU 项目(std/no_std/WASM) | burn-flex:更快的 GEMM、零拷贝视图、原生量化 |
| GPU 加速(CUDA/ROCm/Vulkan/Metal/WebGPU) | CubeCL 系列后端 |
| 存量项目仍依赖本 crate | 可继续使用至其移除,但注意自0.22.0起编译器会给出废弃警告 |
迁移前后的行为差异(逐算子基准与对照)见 burn-flex/COMPARISON.md;burn-flex自身还有 BENCHMARKS.md 与 ARCHITECTURE.md 可深入对比。
七、验证与回归:从哪里看测试
该后端的正确性验证分散在仓库的测试体系里:
- 内置单元测试:
backend.rs与tensor.rs文件底部的#[cfg(test)]模块,覆盖 DType 支持表、两级量化方案、from_data/into_data往返(1D~4D)、零拷贝 Borrowed/Owned 路径选择、数据长度不足时 panic(should_panic_when_data_bytes_shorter_than_shape)等; - 跨后端通用测试:仓库的 burn-backend-tests crate 提供张量、autodiff 等成体系的跨后端测试集(其
tests/tensor/、tests/autodiff/下数百个测试文件按算子组织),export_tests特性的存在(Cargo.toml)说明该 crate 还承担为部分burn-cubecl内核提供参考数值的角色。
查看这些测试只需在仓库中打开对应文件;由于仓库是只读的,本地实验请以阅读源码和测试断言为主,或以该后端为参考理解 Burn 后端接口的约定。
小结
burn-ndarray展示了 Burn 框架"一个 CPU 后端如何做到最大可移植性"的完整方案:以ndarray为存储底座,用特性开关正交地叠加 std/no_std、SIMD、多线程与四种 BLAS 实现,并明确给出平台支持边界(无 GPU、BLAS 与 WASM 互斥)。理解它的特性矩阵、种子机制与零拷贝存储设计,不仅有助于读懂这一(已废弃但仍在维护中的)后端,也为评估burn-flex与 CubeCL 后端的能力差异提供了直接参照——因为 README 指定的迁移目标正是以逐算子对照的方式接管它的职责。
【免费下载链接】burnBurn is a next generation tensor library and Deep Learning Framework that doesn't compromise on flexibility, efficiency and portability.项目地址: https://gitcode.com/GitHub_Trending/bu/burn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考