Migrating to Burn 0.22
Burn 0.22 selects backends at runtime through Device. Tensor operations follow Tensor → Bridge →
Dispatch → Backend; ordinary models and tensor functions no longer carry a backend type parameter.
Backend implementations and extensions still use low-level backend traits.
Cargo features and toolchain
The minimum supported Rust version is 1.95. NdArray and LibTorch are deprecated, the Candle backend
is removed, and backend tracing requires the opt-in tracing feature. Enable a feature for each
backend constructor you use; burn has no default execution backend.
| 0.21 | 0.22 |
|---|---|
server | remote-server |
remote (WebSocket) | remote (Iroh); add remote-websocket for WebSocket |
sqlite through dataset or train | sqlite, enabled explicitly (sqlite-bundled is an alias) |
Linear algebra in burn::tensor::linalg | linalg feature; burn::linalg extension traits |
Signal processing in burn::tensor::signal | signal feature; burn::signal or burn::tensor::signal. See Signal Processing Functions |
burn-store dependency for SafeTensors and PyTorch | safetensors and pytorch features (imply store) |
candle, candle-cuda, candle-metal | Removed |
router, dispatch, distributed, collective, record-item-custom-serde | Removed; runtime dispatch and burn::tensor::distributed are built in |
With default-features = false, enable optim explicitly if you use burn::optim or
burn::lr_scheduler; train also enables it.
Reinforcement learning is opt-in. Enable the rl feature on burn to use burn::rl and the RL
learner in burn::train. If you depend on burn-train directly, rl is no longer one of its
default features.
Types and devices
| Previous API | 0.22 API |
|---|---|
Tensor<B, D> / Tensor<B, D, Int> | Tensor<D> / Tensor<D, Int> |
Model<B>, Linear<B>, Module<B> | Model, Linear, Module |
B::Device | burn::tensor::Device |
| A backend type alias at the application entry point | A constructor such as Device::wgpu(...), Device::cuda(0), or Device::flex() |
Autodiff<B> as the application's backend | device.autodiff() before model/input initialization |
| Backend element type parameters | Device dtype defaults, explicit creation dtypes, and tensor.cast(...) |
tensor.into_scalar() | tensor.into_scalar::<f32>(), or an inferred element type |
| Backend-generic optimizers | ModuleOptimizer, returned by optimizer configuration init() |
burn::module::Initializer | burn::nn::Initializer |
Device-level operations previously called through B: Backend, such as seeding and synchronization,
are now methods on Device. See Using a Device.
Device::flush() returns Result<(), ExecutionError>: it fails when the buffered operations cannot
be dispatched, e.g. on a poisoned device. Propagate the error with ? or handle it.
When upgrading a model, remove its backend parameter and the corresponding parameters on fields and methods. The rank and kind remain part of the tensor type:
use burn::{
module::Module,
nn::{Linear, LinearConfig},
tensor::{Device, Tensor},
};
#[derive(Module, Debug)]
struct Model {
linear: Linear,
}
impl Model {
fn new(device: &Device) -> Self {
Self { linear: LinearConfig::new(4, 2).init(device) }
}
fn forward(&self, input: Tensor<2>) -> Tensor<2> {
self.linear.forward(input)
}
}
// Requires the flex and autodiff features.
let device = Device::flex().autodiff();
let model = Model::new(&device);
let input = Tensor::<2>::ones([8, 4], &device);
let gradients = model.forward(input).sum().backward();
assert!(model.linear.weight.grad(&gradients).is_some());
Configure device dtype defaults before creating tensors. Configuration is shared by the compute device and can only be initialized once. See Backend and Device.
get_device_settings no longer initializes or locks defaults. Use
burn_backend::get_or_init_device_settings to preserve the 0.21 behavior, or device.settings() to
query settings in application code. Tensor creation still locks defaults, even with an explicit
dtype.
Prefer explicit device constructors during migration. Device::default() chooses from compiled-in
backends, not from available hardware. Enabling an additional backend through Cargo feature
unification can therefore change the default. This also affects implicit device selection by
Tensor::from(...) and dataloaders without set_device(...).
Autodiff is runtime state
Removing B: AutodiffBackend moves precondition checks to runtime. Enabling autodiff permits graph
recording; it does not make every tensor require gradients.
| Question | API |
|---|---|
| Does this tensor have autodiff enabled? | is_autodiff() |
| Does it participate in a graph? | is_tracked() on float tensors |
| Will its gradient be retained? | is_require_grad() on float tensors |
| Which checkpointing strategy does it carry? | gradient_checkpointing_strategy() |
Call require_grad() on source leaves before computing the output. Calling it on a plain float
tensor or tracked intermediate panics. Derived tensors can be tracked without retaining their own
gradient. set_require_grad(false) cuts the graph, while detach() cuts the graph and preserves a
leaf's retention setting. Use without_autodiff() to remove the association entirely; inner()
remains an alias. autodiff() is the clearer spelling of from_inner(...).
backward() consumes reachable graph steps even though it borrows the output. Clones share the
tape; is_tracked() remains true after consumption. Recompute a forward pass for another backward,
or combine losses sharing intermediates before backward. See
Autodiff.
Moving tensors and switching module state
to_device() preserves a tensor's source autodiff and checkpointing context. Moving a plain tensor
to an autodiff device does not enable autodiff; moving an autodiff tensor to a plain device does not
disable it. Device equality ignores those settings.
For initialized module parameters, to_device() preserves gradient connections to source
parameters. Its tracked output parameters are intermediates that cannot themselves be optimized. An
uninitialized, unshared parameter instead initializes directly on the destination and remains a
leaf. Cloning a lazy parameter shares its initialization state; moving a shared parameter
initializes it on the source first. Use fork(&destination) for independent destination leaves
regardless of initialization state. Starting from a plain or validation module, use
model.train().fork(&destination) to enable training explicitly.
AutodiffModule has been merged into Module. Replace its imports and bounds with Module;
replace module from_inner(module) calls with module.train(). Tensor from_inner remains
available. A Module bound does not establish that a value is currently training. valid() and
train() return the same type. valid() disables autodiff and training flags in a snapshot;
train() restores configured trainability and flags. Explicit no_grad() and freeze() settings
persist. freeze() also disables module-owned training flags, whereas no_grad() only changes
parameter gradients.
Keep the original training model when using model.valid() for validation. The snapshot discards
tensor checkpointing strategies, which train() does not restore.
Dropout additionally checks its input tensor's autodiff context, so create model inputs on the training device even when their gradients are not needed. See Module.
Migrating checkpoints
Records now use burnpack and ModuleRecord; the old Recorder, PrecisionSettings, generated
record types, and #[derive(Record)] are removed. Burn 0.22 cannot directly load their MessagePack
(.mpk), binary (.bin), or JSON files, including compressed variants. Renaming a file to .bpk
does not convert it.
For burnpack checkpoints, loading and saving no longer require a recorder. Weights are loaded onto the model's devices.
| Previous API | 0.22 API |
|---|---|
model.load_file(path, &recorder, &device)? | model.try_load_file(path)? |
model.save_file(path, &recorder)? | model.save_file(path)? |
Use try_load_file to handle loading errors; load_file now panics on failure.
Loaded weights retain the checkpoint's dtype by default. To use the model's dtype instead, load a
ModuleRecord and call cast_to_module_dtype() before applying it. See
Saving and Loading Models.
To migrate model weights, load the checkpoint in an older Burn project that can read it, using the
original model definition, recorder, and precision settings. Export the loaded model through
burn-store, then import those weights into the corresponding 0.22 model.
For example, for a checkpoint readable by Burn 0.21, add burn-store = "0.21" to that project's
dependencies. After initializing the original model and device, load and export its parameters:
// Run in the Burn 0.21 project.
use burn::{
module::Module,
record::{FullPrecisionSettings, NamedMpkFileRecorder},
};
use burn_store::{ModuleSnapshot, SafetensorsStore};
// Match the recorder and precision settings used to save the checkpoint.
// This example reads model.mpk; load_file adds the recorder's extension.
let recorder = NamedMpkFileRecorder::<FullPrecisionSettings>::default();
let model = model.load_file("model", &recorder, &device)?;
let mut store = SafetensorsStore::from_file("model.safetensors");
model.save_into(&mut store)?;
If the checkpoint was saved with CompactRecorder, use HalfPrecisionSettings instead. For other
formats, use the corresponding recorder. Checkpoints from earlier releases may require an
intermediate migration before 0.21 can read them.
In the Burn 0.22 project, add burn-store = "0.22", initialize the migrated model, and load the
exported weights:
// Run in the Burn 0.22 project, with an initialized mutable model.
use burn_store::{ModuleSnapshot, SafetensorsStore};
let mut store = SafetensorsStore::from_file("model.safetensors");
model.load_from(&mut store)?;
Burn-produced SafeTensors files need no PyTorch adapter. Keep parameter paths and shapes consistent between the models, check the loaded dtypes, and compare outputs on the same input after conversion. See Saving and Loading Models for key remapping and other loading options.
If you already exported weights through burn-store, use SafetensorsStore or BurnpackStore for
the corresponding format. Burnpack exports from 0.21 use compatible tensor metadata, but this does
not guarantee compatibility with every earlier .bpk file: older releases may encode dtypes
differently.
This procedure transfers model parameters. It does not migrate legacy optimizer or learning rate scheduler records, so it does not resume the full training checkpoint. Start with new optimizer and scheduler state, or implement a separate conversion if preserving that state is required. See Record for the 0.22 record APIs.
LSTM
Lstm::forget_gate is now Option<GateController>: Some(gate) for an uncoupled LSTM and None
when input_forget is true. This also applies to BiLstm::forward.forget_gate and
BiLstm::reverse.forget_gate. Update direct access to use as_ref() or as_mut(), and wrap
replacement gates in Some(gate).
Set input_forget through LstmConfig::with_input_forget(...) or
BiLstmConfig::with_input_forget(...) before calling .init(), matching the setting used during
training. Initialization now determines whether the separate forget gate is created; changing the
field afterward does not add or remove its parameters.
Older coupled checkpoints contain redundant forget-gate tensors. For a burnpack checkpoint, allow these unused tensors when loading, then save the module again to omit them:
use burn::{module::Module, nn::LstmConfig, store::ModuleRecord};
// Match the original model's dimensions, bias, and other configuration settings.
let lstm = LstmConfig::new(d_input, d_hidden, bias)
.with_input_forget(true)
.init(&device);
let record = ModuleRecord::load("lstm.bpk")?.allow_unused(true);
let lstm = lstm.try_load_record(record)?;
lstm.save_file("lstm-migrated.bpk")?;
The same loading procedure applies to BiLSTM. Uncoupled checkpoints keep the same parameter paths
and do not require allow_unused(true). For older recorder formats, first follow
Migrating checkpoints to convert the file format.
Datasets and dataloaders
Dataset access is now fallible. Update custom datasets and training loops to handle these return types:
| API | Previous | 0.22 |
|---|---|---|
Dataset::get | Option<I> | Result<I, E> |
| Dataset iterator item | I | Result<I, E> |
| Dataloader iterator item | Batch | Result<Batch, DatasetError> |
In custom datasets, return Ok(item) on success and Err(error) for retrieval failures.
Out-of-bounds access must panic. The error type E defaults to DatasetError.
In custom training loops, handle each batch's result before using it, for example with
let batch = batch?;. See Dataset for examples and dataloader
compatibility with custom error types.
For specialized dataset APIs:
- Windows:
window()returnsResult<Option<Vec<I>>, DatasetError>. Handle the error before checking whether a window fits. Window iterator items are also wrapped inResult. - Images: replace
Vec<PixelDepth>withPixelDatawhen constructingImageDatasetItem. Match itsU8,U16, orF32variant for the packed values, or useimage.iter()for individualPixelDepthvalues. See Images. - SQLite: enable
sqliteexplicitly. Update error matches:SqliteDatasetError::Sqlwrapsturso::Error, andRowandDeserializeare new variants.
Training
Update your training configuration:
| Previous API | 0.22 API |
|---|---|
with_file_checkpointer(CompactRecorder::new()) | with_default_checkpointers() |
renderer(renderer) | renderer(Box::new(renderer)) |
AurocMetric::new() | AurocMetric::binary(), AurocMetric::multiclass(reduction), or AurocMetric::multilabel(reduction) |
AurocInput | ClassificationOutput or MultiLabelClassificationOutput |
evaluator.eval(..) returning the renderer | EvaluationResult; read its renderer field |
Default checkpointers save the model, optimizer, and scheduler as burnpack files. AUROC's multiclass
and multilabel constructors take a ClassReduction. See Learner for
training configuration.
Training and evaluation now stop instead of panicking when a metric cannot read its tensors, a
dataloader fails, a checkpointer fails, or a multi-device worker panics. LearningResult,
RLResult, and EvaluationResult report the reason for an early stopping:
error: theTrainingErrorthat stopped it.TrainingError::is_device_poisoned()tells whether the device is poisoned and, consequentially, if the process needs to be restarted.interrupted: theInterruptionrequested throughInterrupter::stop, if the run stopped without an error.
Check error after launch or eval to detect a failed run. If you destructure these results or
build them with struct literals, add the two fields.
Review configurations and numerical baselines affected by these behavior changes:
- Cosine annealing: cycles no longer restart abruptly.
num_itersspecifies the descent to the minimum, after which the rate rises along the cosine curve. Update schedules that relied on warm restarts. See Cosine annealing. - Padding: cross-entropy loss and accuracy exclude padded targets from their denominators, including the weight sum for weighted loss. An entirely padded cross-entropy batch returns NaN.
Tensor data and numeric semantics
Replace the deprecated TensorData vector methods and handle their errors:
| Previous API | 0.22 API |
|---|---|
data.to_vec::<E>() | data.try_to_vec::<E>() |
data.into_vec::<E>() | data.try_into_vec::<E>() |
These methods return Result<Vec<E>, DataError> and require E to match the stored dtype. For
conversion, use try_to_vec_as::<E>() or try_into_vec_as::<E>() on TensorData or Tensor.
Update error matches for the revised DataError variants and Tensor::try_into_scalar's
TensorReadError.
ExecutionError has a new DevicePoisoned variant for faults the device cannot recover from, such
as an illegal memory access; add it to exhaustive matches. ExecutionError::is_device_poisoned()
detects it.
TensorData fields are private, so its byte length always matches its shape and dtype (quantized
data is not checked yet). Replace field access with the accessors:
| Previous API | 0.22 API |
|---|---|
data.shape | data.shape() (returns &Shape) |
data.dtype | data.dtype() |
data.bytes (borrowed) | data.bytes() or data.as_bytes() |
data.bytes (moved) | data.into_bytes(), or data.into_parts() for all three |
&mut data.bytes | TensorData::with_bytes_mut(..) (length must not change) |
TensorData { bytes, shape, dtype } | TensorData::try_from_bytes(bytes, shape, dtype)? |
TensorData::from_bytes and from_bytes_vec now panic when the byte length does not match the
shape and dtype. Use try_from_bytes or try_from_bytes_vec for untrusted input; they return
DataError::InvalidByteLength, the same check deserialization applies.
Other source changes:
-
Dimensions: negative indices are supported. Annotate untyped empty inputs, such as
flip([] as [isize; 0])orsqueeze_dims(&[] as &[isize]). -
Convolution:
ConvOptions::paddingstores(before, after)pairs. KeepConvOptions::new(..)for symmetric padding; replace deprecatedPaddedConvOptionswithConvOptions::new_with_padding(..)for asymmetric padding. -
Interpolation:
module::interpolate(x, output_size, options)is nowmodule::interpolate(x, options). Set the size withoptions.with_output_size([h, w]), or useoptions.with_scale_factor([sh, sw])to scale the input size. -
Pooling: the functional
max_pool1d,max_pool2d,avg_pool1d,avg_pool2d, and their_with_indicesvariants takeMaxPoolOptionsorAvgPoolOptionsinstead of positional arguments. Only the kernel size is required; stride defaults to the kernel size, padding to 0, dilation to 1,ceil_modeto false, andcount_include_padto true. 1D options use single-element arrays. Usewith_padding_pairs(..)for asymmetric padding:// Before max_pool2d(x, [3, 3], [2, 2], [1, 1], [1, 1], false); avg_pool1d(x, 3, 1, 1, false, false); // After max_pool2d(x, MaxPoolOptions::new([3, 3]).with_stride([2, 2]).with_padding([1, 1])); avg_pool1d( x, AvgPoolOptions::new([3]) .with_stride([1]) .with_padding([1]) .with_count_include_pad(false), ); -
Quantization: replace
with_level(..)andwith_param(..)withper_tensor(ScaleDtype)orper_block(block, ScaleDtype). See Quantization. -
Softplus: use
SoftplusConfig::new().with_beta(beta).with_threshold(threshold)instead of struct literals to account for the newthresholdfield (default: 20).
Update numerical expectations for these cases:
| Case | 0.22 behavior |
|---|---|
NaN in a max, min, or max_abs reduction | Returns NaN |
NaN in an argmax or argmin reduction | Returns the first NaN's index |
NaN in cummax or cummin | Returns NaN from that position onward |
| Reducing a zero-length axis | sum: 0; prod: 1; any: false; all: true; float mean: NaN; max/min: panic |
Empty axes in max_abs_dims or *_norm_dims | Applies the elementwise transformation without reducing |
Positive shift in roll or roll_dim | Moves elements toward higher indices, matching torch.roll; 0.21 moved them lower |
Custom integrations
The following sections apply when you implement Burn traits yourself or use lower-level APIs. Skip them if your project only uses the built-in modules, optimizers, metrics, and stores.
Modules
For handwritten implementations, consult the Module trait documentation for the required methods;
#[derive(Module)] generates them automatically.
Other module API changes:
BatchNormandDropoutnow includeParam<Flag>training controls; use their config builders instead of struct literals.- Replace
ParamId::serialize()/deserialize()withDisplay/FromStr. - Replace
Reinitializerwithburn::nn::Initializerfor new parameters or aModuleMapperfor existing ones. UseParam::mapto preserve IDs and configured trainability, and keep trainable tensors as gradient-retaining leaves. See the mapper example.
Parameter is sealed through ParameterValue. Replace custom Param<T> types with tensor
parameters, Param<Flag> for training controls, or ordinary module fields for other state.
Optimizers and schedulers
Replace SimpleOptimizer / OptimizerAdaptor with the per-tensor Optimizer trait and its
ModuleOptimizer wrapper. Use RecordState and the new record types instead of #[derive(Record)]
for optimizer and scheduler state. See Optimizer.
For custom LrScheduler implementations, remove the associated Record type and backend-generic
record methods. Implement:
to_record(&self) -> LrSchedulerRecordload_record(&mut self, record: LrSchedulerRecord)— loading now mutates the scheduler.
Derive Clone to obtain the blanket LrSchedulerClone implementation.
Custom metrics
Update the metric lifecycle:
- Implement
Metric::compute(&mut self)to compute the epoch value after per-batch updates. - Return
Option<NumericEntry>fromNumeric::value()andrunning_value(). UseNonewhen the metric is only defined at the end of an epoch. - Return the computed epoch value from
final_value(). - Return
Result<SerializedEntry, TensorReadError>fromupdateandcompute. Read tensors withtry_into_data()ortry_into_scalar()and propagate errors with?; wrap other return values inOk(..).ConfusionStatsState::compute_updatealso returns aResult.
Custom training outputs implement ItemLazy::sync(self) -> Result<Self, ExecutionError>: propagate
device.flush()? and wrap the returned output in Ok(..). When an output cannot be synced, the
event processor reports it once, as a EventProcessorFailure::Sync, and no metric processes that event.
See Custom Metric for an implementation example.
Renderers and event processors
Implement TrainingProgressLogger and EvaluationProgressLogger for custom MetricsRenderer
types. Move progress handling from the removed render_train, render_valid, and render_test
methods to logger callbacks. The old TrainingProgress, EvaluationProgress, and ProgressType
types are removed.
Update custom event matches:
| Event | Required change |
|---|---|
LearnerEvent::Start | Match struct fields total_epochs and starting_epoch |
EvaluatorEvent::Start | Match struct field total_tests |
LearnerEvent::StartSplit / EndSplit | Handle the new split lifecycle events |
EvaluatorEvent::StartTest / EndTest | Handle the new test lifecycle events |
Event processor methods return Result<(), EventProcessorError>: process_train, process_valid,
flush, and process_test. A EventProcessorError lists every failure: a metric that failed
(EventProcessorFailure::Metric, with its name and split) or an event that could not be synced
(EventProcessorFailure::Sync). EventProcessorEvaluation gains a flush method with a default implementation. Custom
processors return Ok(()) on success.
In a custom SupervisedLearningStrategy, handle each processor result: pass it to
interrupter.fail_on_error(..) to stop training cleanly, or call unwrap() to panic as before.
Report dataset errors with interrupter.fail(err) rather than interrupter.stop(..), so the run
reports them as errors. MultiDevicesTrainStep::step returns MultiDeviceStepError instead of
DatasetError.
Distributed training
Remove the distributed and collective feature flags and the burn-collective dependency.
Collective operations are available through burn::tensor::distributed.
- Replace
DistributedSession/DistributedRuntimewithDistributedContext. - Construct DDP strategies with
ExecutionStrategy::ddp(devices, DistributedConfig { .. }). - Call
.into_vec()onDevice::enumerate(..)when aVec<Device>is required.
Check runtime support before using collectives: CubeCL all-reduce currently requires CUDA, including on remote servers. See Distributed Computing.
Remote backend
A remote device is a Device, connected through a RemoteHost that names its server. Connecting
returns a Result, and a server returns a ServeError instead of panicking:
| 0.21 API | 0.22 API |
|---|---|
RemoteDevice::new("ws://host:3000") | Device::remote_options(&RemoteHost::websocket("ws://host:3000")).init()? |
| Listing a server's devices | Device::enumerate(DeviceType::Remote(host)), or host.devices()? to handle the error |
burn::server::start_websocket::<B>(device, port) | RemoteServer::new([device]).serve(WebSocketTransport::new(port))? |
start_websocket_async::<B>(device, port).await | RemoteServer::new([device]).serve_async(WebSocketTransport::new(port)).await? |
serve installs the server's logging and handles Ctrl+C and SIGTERM. serve_async does neither:
the application owns its subscriber and its signals, and dropping the future stops the server.
Iroh, now the default transport, reaches a server by its id across any network:
RemoteHost::iroh(server_id) on the client, and
IrohTransport::new(IrohIdentity::load_or_create(path)?) on the server. A server hosts exactly the
devices it is given, and a backend outside Burn's own serves through
burn_remote::server::BackendServer::<B>. See
Distributed Computing.
Storage adapters and checkpointers
Update custom burn-store integrations:
| Previous API | 0.22 API |
|---|---|
TensorSnapshot | burn_pack::Tensor, including in collect and apply |
get_snapshot | get_tensor |
get_all_snapshots | get_all_tensors |
ModuleAdapter::adapt now takes a burn_pack::Tensor and ModuleContext<'_>, and returns the
tensor. Remove the burnpack feature flag; support is always enabled. PyTorch readers now use the
pytorch-reader crate. See Saving and Loading Models.
For custom training checkpoints, pass implementations of these traits to
SupervisedTraining::with_custom_checkpointers(..):
Checkpointer<ModuleRecord>Checkpointer<OptimizerRecord>Checkpointer<LrSchedulerRecord>
Backend extensions
To migrate custom operations:
- Enable
extensionfor low-level access throughburn::backend. - Annotate your backend trait with
#[backend_extension(...)]and implement it on the supported backends. Use theCubeselector for CubeCL runtimes such as WGPU and CUDA. - Expose a
Tensor<D>wrapper usingDispatchandinto_dispatch()/from_dispatch().
The macro generates routing. Custom derivatives still need an autodiff implementation. See Backend Extension for a complete example and Fusion requirements.
Update low-level tensor access:
- Specify the backend when constructing a tensor:
Tensor::from_primitive::<B>(primitive). - Replace
into_primitivewithtry_into_primitive::<B>()and handle backend mismatches. Shared CubeCL backend aliases do not establish which runtime the tensor uses. - Remove backend parameters from
TensorKindbounds. The trait is sealed and itsPrimitiveassociated type is removed; use the conversion methods above for primitives andKINDto inspect the tensor kind. - Use accessors for the now-private
AutodiffTensorfields.
Out-of-tree backends are not supported in Burn 0.22. #[backend_extension] adds operations to
Burn's supported backends; it does not currently allow registering new backends.