burn-tensor 0.22.0

Tensor library with user-friendly APIs and automatic differentiation support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
pub use burn_std::{
    DeviceError, DeviceSettings, ExecutionError, backtrace::BackTrace, device::DeviceId,
};

#[cfg(feature = "cubecl")]
pub use burn_backend::cubecl::{
    AdapterLuid, DeviceIdentity, MemoryAccess, PciAddress, PciVendor, PhysicalDevice,
    ThroughputError, ThroughputKey, ThroughputMode, ThroughputValue,
};
use burn_backend::{Backend, DeviceOps};
pub use burn_backend::{
    MemoryPoolUsage, ProfileDuration, ProfileOptions, ProfileTicks, SlicedPoolReport, TimingMethod,
};
#[allow(unused)]
use burn_dispatch::DispatchDeviceId;
#[cfg(feature = "autodiff")]
use burn_dispatch::GradientCheckpointingStrategy;
#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
use burn_dispatch::devices::RuntimeId;
#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
use burn_dispatch::devices::WgpuBackend;
use burn_dispatch::{Dispatch, DispatchDevice};
use burn_std::{BoolDType, FloatDType, IntDType, TensorData};

#[cfg(feature = "capture")]
pub use burn_dispatch::backends::capture::{
    CaptureError, CaptureScope, CapturedGraph, CompletedCaptureScope, TensorId,
};

#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;

/// A high-level device handle for tensor operations.
///
/// [`Device`] provides a unified interface to interact with the underlying compute backend.
///
/// Autodiff support is configured on the device rather than through a separate type parameter.
/// Tensors inherit that context when created and can later change it independently.
#[cfg_attr(
    feature = "autodiff",
    doc = "Wrap a device with [`.autodiff()`](Device::autodiff) to enable automatic differentiation with the device."
)]
#[cfg_attr(
    not(feature = "autodiff"),
    doc = "Enable the `autodiff` feature to add automatic differentiation support to devices."
)]
///
/// # Backend selection
///
/// Default Cargo features do not enable an execution backend. Select one explicitly,
/// for example with the `wgpu` or `flex` feature; there is no implicit CPU fallback.
/// Backend-free builds can expose tensor/model APIs, but cannot create an execution device.
///
/// [`Device::default()`] selects the first enabled backend in this order:
/// CUDA, Metal, ROCm, Vulkan, WebGPU, wgpu, CPU, LibTorch, Flex, Remote, NdArray.
/// Use an explicit factory method when the choice must be independent of Cargo feature unification.
/// Without an execution backend, `Device::default()` panics with configuration guidance.
///
/// Enable the desired backend via Cargo feature flags, then call the
/// corresponding factory method:
///
/// ```rust,ignore
/// // Default CUDA device (requires the `cuda` feature).
/// let device = Device::cuda(DeviceIndex::Default);
///
/// // CUDA device at hardware index 1.
/// let device = Device::cuda(1);
///
/// // WGPU with explicit selector (requires `wgpu`/`vulkan`/`metal`/`webgpu`).
/// let device = Device::wgpu(DeviceKind::DiscreteGpu(0));
///
/// // Default device for whichever backend is enabled.
/// let device = Default::default();
/// ```
///
/// Available factory methods (each gated by its matching Cargo feature):
/// `Device::cpu`, `Device::cuda` / `Device::rocm` / `Device::libtorch_cuda`
/// (take an integer index or a [`DeviceIndex`]), `Device::wgpu` /
/// `Device::vulkan` / `Device::metal` / `Device::webgpu` (take a
/// [`DeviceKind`]), `Device::flex`, `Device::ndarray`, `Device::libtorch`,
/// `Device::libtorch_mps`, `Device::libtorch_vulkan`, `Device::capture`.
///
/// # Autodiff
///
/// Requires `autodiff` feature.
///
/// Gradient computation is opt-in for a device:
///
/// ```rust,ignore
/// let device = Device::default().autodiff();
///
/// // Tensors created on this device can participate in an autodiff graph.
/// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device).require_grad();
/// ```
pub struct Device {
    blob: device_opaque::Opaque,
}

// Aligned, type-erased storage for `DispatchDevice`. See `crate::macros` for
// why this indirection exists (it keeps the dispatch type tree out of
// downstream MIR).
burn_std::obfuscate!(
    type: DispatchDevice,
    module: device_opaque,
    derives: [Send, Sync]
);

impl Clone for Device {
    fn clone(&self) -> Self {
        Self::new(self.as_dispatch().clone())
    }
}

impl Default for Device {
    fn default() -> Self {
        Self::new(DispatchDevice::default())
    }
}

impl core::fmt::Debug for Device {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "Device<{:?}>", self.as_dispatch())
    }
}

// Manually implement both `eq` and `ne` to add documentation on equality.
#[allow(clippy::partialeq_ne_impl)]
impl PartialEq for Device {
    /// Compares devices based on hardware identity.
    ///
    /// Returns `true` if both devices represent the same compute resource.
    /// Note that this comparison ignores autodiff and checkpointing settings. Inspect
    /// [`Device::is_autodiff`] and `Device::gradient_checkpointing_strategy()` when execution
    /// context also matters.
    fn eq(&self, other: &Self) -> bool {
        self.as_dispatch() == other.as_dispatch()
    }

    /// Compares devices based on hardware identity.
    ///
    /// Returns `false` if both devices represent the same compute resource,
    /// even if one has autodiff enabled and the other does not.
    fn ne(&self, other: &Self) -> bool {
        !self.eq(other)
    }
}

impl Eq for Device {}

impl Device {
    /// Wrap a backend-specific device in a unified [`Device`].
    ///
    /// Used by:
    /// - the backend-specific factory methods below (`Device::cuda`, etc.)
    ///   — these are the recommended entry points for downstream code;
    /// - burn-tensor's bridge ops, which already hold a [`DispatchDevice`]
    ///   and just need to wrap it;
    /// - direct callers (tests, type-erased helpers) that have a concrete
    ///   backend device type at hand.
    ///
    /// Anything convertible into [`DispatchDevice`] is accepted, including
    /// `DispatchDevice` itself.
    pub fn new(device: impl Into<DispatchDevice>) -> Self {
        Self {
            blob: device_opaque::Opaque::new(device.into()),
        }
    }

    /// Borrow the underlying [`DispatchDevice`].
    ///
    /// The inverse of [`Device::new`]. Useful to backend-extension authors who need to dispatch on
    /// the concrete backend variant (e.g. matching `DispatchDevice::Remote(_)`).
    pub fn as_dispatch(&self) -> &DispatchDevice {
        self.blob.as_ref()
    }

    /// Crate-internal owning extraction of the underlying dispatch device.
    pub(crate) fn into_dispatch(self) -> DispatchDevice {
        self.blob.into_inner()
    }
}

impl<D: Into<DispatchDevice>> From<D> for Device {
    fn from(device: D) -> Self {
        Self::new(device)
    }
}

/// Selector for the hardware index of a backend whose devices are simply
/// indexed (e.g. CUDA, ROCm).
///
/// Backend factory methods that take an index (`Device::cuda`, `Device::rocm`,
/// `Device::libtorch_cuda`) accept `impl Into<DeviceIndex>`, so the common
/// shorthand is to pass a plain integer literal:
///
/// ```rust,ignore
/// Device::cuda(0);                    // hardware index 0
/// Device::cuda(DeviceIndex::Default); // backend-chosen default
/// ```
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
pub enum DeviceIndex {
    /// Target a specific hardware device by its index.
    Specified(usize),
    /// Let the backend pick its default device (typically index `0`).
    #[default]
    Default,
}

impl DeviceIndex {
    /// Construct a [`DeviceIndex::Specified`] from anything convertible into
    /// a `usize`.
    pub fn new(index: impl Into<usize>) -> Self {
        Self::Specified(index.into())
    }

    /// Resolve to a concrete hardware index, defaulting to `0` for
    /// [`DeviceIndex::Default`]. Backend factory methods are each gated by a
    /// Cargo feature, so this looks dead when none of them are enabled.
    #[allow(dead_code)]
    pub(crate) fn resolve(self) -> usize {
        match self {
            DeviceIndex::Specified(i) => i,
            DeviceIndex::Default => 0,
        }
    }
}

impl From<usize> for DeviceIndex {
    fn from(i: usize) -> Self {
        Self::Specified(i)
    }
}

impl From<u32> for DeviceIndex {
    fn from(i: u32) -> Self {
        Self::Specified(i as usize)
    }
}

impl From<u64> for DeviceIndex {
    fn from(i: u64) -> Self {
        Self::Specified(i as usize)
    }
}

impl From<i32> for DeviceIndex {
    fn from(i: i32) -> Self {
        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
    }
}

impl From<i64> for DeviceIndex {
    fn from(i: i64) -> Self {
        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
    }
}

/// Selector for the more flexible backends whose device handle is a tagged
/// enum (e.g. WGPU, which can target a discrete/integrated/virtual GPU, a CPU
/// adapter, an externally-created wgpu setup, or just "best available").
///
/// The variants mirror `WgpuDeviceKind` from cubecl so the mapping is direct, but
/// it is kept as a burn-owned enum so callers don't have to depend on cubecl.
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
pub enum DeviceKind {
    /// Discrete GPU with the given index. The index is the index of the discrete GPU in the list
    /// of all discrete GPUs found on the system.
    DiscreteGpu(usize),

    /// Integrated GPU with the given index. The index is the index of the integrated GPU in the
    /// list of all integrated GPUs found on the system.
    IntegratedGpu(usize),

    /// Virtual GPU with the given index. The index is the index of the virtual GPU in the list of
    /// all virtual GPUs found on the system.
    VirtualGpu(usize),

    /// CPU.
    Cpu,

    /// The best available device found with the current graphics API.
    ///
    /// This will prioritize GPUs wgpu recognizes as "high power". Additionally, you can override this using
    /// the `CUBECL_WGPU_DEFAULT_DEVICE` environment variable. This variable is spelled as if i was a `WgpuDevice`,
    /// so for example `CUBECL_WGPU_DEFAULT_DEVICE=IntegratedGpu(1)` or `CUBECL_WGPU_DEFAULT_DEVICE=Cpu`
    #[default]
    DefaultDevice,

    /// Use an externally created, existing, wgpu setup. This is helpful when using `CubeCL` in conjunction
    /// with some existing wgpu setup (eg. egui or bevy), as resources can be transferred in & out of `CubeCL`.
    ///
    /// # Notes
    ///
    /// This identifies an already registered runtime. To register an application's
    /// setup, use `Device::wgpu_options().setup(setup).init()` and retain the
    /// returned device. Clone that device to share it; do not invent an existing ID.
    Existing(u32),
}

impl Device {
    /// Create a reusable graph-capture device.
    ///
    /// Operations on tensors moved to this device are recorded rather than executed. Use
    /// [`Device::capture_scope`] to delimit each capture and declare its graph boundaries.
    #[cfg(feature = "capture")]
    pub fn capture() -> Self {
        Self::new(DispatchDevice::capture())
    }

    /// Capture the operations performed by `capture` on this device.
    ///
    /// The closure receives a [`CaptureScope`] and must return the token produced by
    /// [`CaptureScope::complete`], containing the ordered runtime input and output tensor IDs.
    /// Requiring this return value prevents a capture from being finalized without an explicit
    /// boundary declaration. Completing the scope immediately rejects further tensor operations;
    /// the device can then be reused for later, independent scopes after the closure returns.
    ///
    /// Returns [`CaptureError::InvalidDevice`] if this is not a capture device, and
    /// [`CaptureError::AlreadyActive`] if another scope is active on the same device.
    #[cfg(feature = "capture")]
    pub fn capture_scope(
        &self,
        capture: impl FnOnce(CaptureScope) -> CompletedCaptureScope,
    ) -> Result<CapturedGraph, CaptureError> {
        match self.as_dispatch() {
            DispatchDevice::Capture(device) => device.capture_scope(capture),
            #[allow(unreachable_patterns)] // Capture can be the only enabled backend.
            _ => Err(CaptureError::InvalidDevice),
        }
    }

    /// Default CPU device backed by CubeCL's CPU backend.
    #[cfg(feature = "cpu")]
    pub fn cpu() -> Self {
        Self::new(burn_dispatch::devices::CpuDevice::default())
    }

    /// CUDA device at the given hardware index.
    ///
    /// Accepts a plain integer (e.g. `Device::cuda(0)`) or a
    /// [`DeviceIndex`] — use [`DeviceIndex::Default`] to let the backend
    /// pick.
    #[cfg(feature = "cuda")]
    pub fn cuda(index: impl Into<DeviceIndex>) -> Self {
        Self::new(burn_dispatch::devices::CudaDevice::new(
            index.into().resolve(),
        ))
    }

    /// ROCm/HIP device at the given hardware index.
    ///
    /// Same selector semantics as [`Device::cuda`].
    #[cfg(feature = "rocm")]
    pub fn rocm(index: impl Into<DeviceIndex>) -> Self {
        Self::new(burn_dispatch::devices::RocmDevice::new(
            index.into().resolve(),
        ))
    }

    /// Flex backend device.
    #[cfg(feature = "flex")]
    pub fn flex() -> Self {
        Self::new(burn_dispatch::devices::FlexDevice)
    }

    /// Default NdArray (CPU) device.
    #[cfg(feature = "ndarray")]
    #[deprecated(
        since = "0.22.0",
        note = "burn-ndarray is deprecated and will be removed in a future release. Use `Device::flex()` for pure-Rust CPU execution instead."
    )]
    #[allow(deprecated)] // constructing the deprecated device is this constructor's job
    pub fn ndarray() -> Self {
        Self::new(burn_dispatch::devices::NdArrayDevice::default())
    }

    /// LibTorch CPU device.
    #[cfg(feature = "tch")]
    #[deprecated(
        since = "0.22.0",
        note = "burn-tch is deprecated and will be removed in a future release. Use a CubeCL backend (`Device::cuda`, `Device::rocm`, `Device::metal`, `Device::vulkan`, `Device::cpu`) or `Device::flex()` instead."
    )]
    #[allow(deprecated)] // constructing the deprecated device is this constructor's job
    pub fn libtorch() -> Self {
        Self::new(burn_dispatch::devices::LibTorchDevice::Cpu)
    }

    /// LibTorch CUDA device at the given hardware index.
    #[cfg(feature = "tch")]
    #[deprecated(
        since = "0.22.0",
        note = "burn-tch is deprecated and will be removed in a future release. Use a CubeCL backend (`Device::cuda`, `Device::rocm`, `Device::metal`, `Device::vulkan`, `Device::cpu`) or `Device::flex()` instead."
    )]
    #[allow(deprecated)] // constructing the deprecated device is this constructor's job
    pub fn libtorch_cuda(index: impl Into<DeviceIndex>) -> Self {
        Self::new(burn_dispatch::devices::LibTorchDevice::Cuda(
            index.into().resolve(),
        ))
    }

    /// LibTorch Metal Performance Shaders (MPS) device.
    #[cfg(feature = "tch")]
    #[deprecated(
        since = "0.22.0",
        note = "burn-tch is deprecated and will be removed in a future release. Use a CubeCL backend (`Device::cuda`, `Device::rocm`, `Device::metal`, `Device::vulkan`, `Device::cpu`) or `Device::flex()` instead."
    )]
    #[allow(deprecated)] // constructing the deprecated device is this constructor's job
    pub fn libtorch_mps() -> Self {
        Self::new(burn_dispatch::devices::LibTorchDevice::Mps)
    }

    /// LibTorch Vulkan device.
    #[cfg(feature = "tch")]
    #[deprecated(
        since = "0.22.0",
        note = "burn-tch is deprecated and will be removed in a future release. Use a CubeCL backend (`Device::cuda`, `Device::rocm`, `Device::metal`, `Device::vulkan`, `Device::cpu`) or `Device::flex()` instead."
    )]
    #[allow(deprecated)] // constructing the deprecated device is this constructor's job
    pub fn libtorch_vulkan() -> Self {
        Self::new(burn_dispatch::devices::LibTorchDevice::Vulkan)
    }

    /// WGPU device, selected via [`DeviceKind`].
    ///
    /// [`AutoCompiler`](burn_dispatch::devices::AutoCompiler) dispatches to the most
    /// appropriate shader language (WGSL, SPIR-V, or MSL) based on the enabled features.
    ///
    /// For [`DeviceKind::DefaultDevice`], the adapter is picked by `wgpu`'s
    /// selection heuristics (high-power GPU preferred, or overridden by
    /// `CUBECL_WGPU_DEFAULT_DEVICE`).
    ///
    /// The graphics API — and so the shader compiler — is the runtime's to settle, from the
    /// enabled features and what the machine offers. `Device::vulkan`, `Device::metal` and
    /// `Device::webgpu` pin one instead: the same adapter on two APIs is two devices.
    ///
    /// This constructor is lazy. Use [`Device::wgpu_options`] to initialize with runtime
    /// options or share an existing wgpu setup. In a browser, initialize asynchronously
    /// with [`init_async`](crate::wgpu::WgpuOptions::init_async) before creating tensors.
    #[cfg(feature = "wgpu")]
    pub fn wgpu(device_kind: DeviceKind) -> Self {
        Self::new(wgpu_device(
            device_kind,
            burn_dispatch::devices::WgpuBackend::Auto,
        ))
    }

    /// Vulkan-backed WGPU device, selected via [`DeviceKind`] — and so `SPIR-V`, where the
    /// build and the adapter allow it.
    ///
    /// Pinned to Vulkan: the device comes up there or not at all, rather than quietly on
    /// whichever API [`Device::wgpu`] would settle on.
    #[cfg(feature = "vulkan")]
    pub fn vulkan(device_kind: DeviceKind) -> Self {
        Self::new(wgpu_device(
            device_kind,
            burn_dispatch::devices::WgpuBackend::Vulkan,
        ))
    }

    /// Metal-backed WGPU device, selected via [`DeviceKind`], requiring native MSL support.
    ///
    /// Pinned to Metal, as `Device::vulkan` is to Vulkan. Runtime initialization panics if
    /// native MSL is unavailable on the selected device. Use [`Device::wgpu`] for automatic
    /// compiler selection with WGSL fallback.
    #[cfg(feature = "metal")]
    pub fn metal(device_kind: DeviceKind) -> Self {
        Self::new(wgpu_device(
            device_kind,
            burn_dispatch::devices::WgpuBackend::Metal,
        ))
    }

    /// WebGPU-backed device, selected via [`DeviceKind`] — the browser's own. Pinned to
    /// WebGPU, as `Device::vulkan` is to Vulkan.
    #[cfg(feature = "webgpu")]
    pub fn webgpu(device_kind: DeviceKind) -> Self {
        Self::new(wgpu_device(
            device_kind,
            burn_dispatch::devices::WgpuBackend::WebGpu,
        ))
    }

    /// Enables autodiff on this device.
    ///
    /// Tensors created on the returned device inherit its autodiff context. A tensor can later
    /// change that context independently with [`Tensor::autodiff`](crate::Tensor::autodiff) or
    /// [`Tensor::without_autodiff`](crate::Tensor::without_autodiff).
    ///
    /// Calling this method on a device that already has autodiff enabled returns it unchanged,
    /// preserving its gradient-checkpointing strategy. This operation is idempotent.
    /// Calling it repeatedly doesn't enable higher-order differentiation; only first-order
    /// autodiff is supported.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let device = Device::default().autodiff();
    /// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device).require_grad();
    /// let gradients = x.backward();
    /// ```
    ///
    #[cfg(feature = "autodiff")]
    #[must_use]
    pub fn autodiff(self) -> Self {
        match self.into_dispatch() {
            device @ DispatchDevice::Autodiff(_) => Self::new(device),
            other => Self::new(DispatchDevice::autodiff(other)),
        }
    }

    /// Returns this device's gradient-checkpointing strategy when autodiff is enabled.
    #[cfg(feature = "autodiff")]
    pub fn gradient_checkpointing_strategy(&self) -> Option<GradientCheckpointingStrategy> {
        match self.as_dispatch() {
            DispatchDevice::Autodiff(device) => Some(device.gradient_checkpointing_strategy()),
            _ => None,
        }
    }

    /// Enables gradient checkpointing on the autodiff device.
    ///
    /// Gradient checkpointing recomputes activations during backpropagation for operations
    /// marked as memory-bound, while compute-bound operations still cache their
    /// output. This reduces peak memory usage at the cost of additional computation
    /// for memory-bound ops.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let device = Device::default().autodiff().gradient_checkpointing();
    /// ```
    ///
    /// # Panics
    ///
    /// Panics if autodiff is not enabled on this device.
    #[cfg(feature = "autodiff")]
    #[must_use]
    pub fn gradient_checkpointing(self) -> Self {
        self.with_gradient_checkpointing_strategy(GradientCheckpointingStrategy::Balanced)
    }

    /// Sets the gradient-checkpointing strategy on this autodiff device.
    #[cfg(feature = "autodiff")]
    #[must_use]
    pub(crate) fn with_gradient_checkpointing_strategy(
        self,
        strategy: GradientCheckpointingStrategy,
    ) -> Self {
        match self.into_dispatch() {
            DispatchDevice::Autodiff(device) => Self::new(
                DispatchDevice::autodiff_with_gradient_checkpointing(device.inner(), strategy),
            ),
            _ => panic!(
                "Gradient checkpointing requires autodiff; use Device::autodiff().gradient_checkpointing()"
            ),
        }
    }

    /// Returns this device without its autodiff association.
    ///
    /// If autodiff is not enabled, the device is returned unchanged. This operation is idempotent.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let device = Device::default().autodiff();
    /// let inference_device = device.without_autodiff();
    ///
    /// assert!(!inference_device.is_autodiff());
    /// ```
    #[must_use]
    pub fn without_autodiff(self) -> Self {
        if self.is_autodiff() {
            Self::new(self.into_dispatch().inner())
        } else {
            self
        }
    }

    /// Applies `source`'s autodiff association and gradient-checkpointing strategy to this device,
    /// discarding this device's own.
    #[must_use]
    #[doc(hidden)]
    pub fn with_autodiff_context_from(self, source: &Self) -> Self {
        #[cfg(feature = "autodiff")]
        {
            match source.gradient_checkpointing_strategy() {
                Some(strategy) => self
                    .autodiff()
                    .with_gradient_checkpointing_strategy(strategy),
                None => self.without_autodiff(),
            }
        }

        #[cfg(not(feature = "autodiff"))]
        {
            let _ = source;
            self
        }
    }

    /// Returns this device without its autodiff association.
    ///
    /// This is equivalent to [`without_autodiff`](Self::without_autodiff). `inner` reflects the
    /// historical backend-decorator model, while `without_autodiff` describes the device's runtime
    /// property directly.
    #[must_use]
    pub fn inner(self) -> Self {
        self.without_autodiff()
    }

    /// Synchronize the device, waiting for all pending operations to complete.
    ///
    /// # Errors
    ///
    /// Returns an [`ExecutionError`] if an operation failed to execute.
    pub fn sync(&self) -> Result<(), ExecutionError> {
        Dispatch::sync(self.as_dispatch())
    }

    /// Flush the device's pending operations, handing them off for execution without waiting
    /// for them to complete.
    ///
    /// Backends that buffer work hold registered operations in a local queue until enough
    /// accumulate: the fusion backend batches ops to build optimizations, and the remote backend
    /// batches them before sending them over the network. `flush` forces that queue out now — the
    /// fusion backend processes its pending optimizations and the remote backend sends its batch to
    /// the server.
    ///
    /// Unlike [`sync`](Self::sync), this does not block on results — it only ensures buffered
    /// operations are dispatched instead of sitting idle. Eager backends, which execute each
    /// operation as it is registered, have nothing buffered and treat this as a no-op.
    ///
    /// # Errors
    ///
    /// Returns an [`ExecutionError`] when the buffered operations cannot be dispatched, e.g. on a
    /// device that is poisoned.
    pub fn flush(&self) -> Result<(), ExecutionError> {
        Dispatch::flush(self.as_dispatch())
    }

    /// Measure how long this device spends on the work `func` puts on it, in
    /// device time.
    ///
    /// The measurement is a [`ProfileDuration`]: a future the device answers
    /// once it has run both ends of the window, so nothing here waits on the
    /// device, and windows nest — an inner `profile` costs the outer one
    /// nothing. Collect them and [`resolve`](ProfileDuration::resolve) once
    /// the run is over.
    ///
    /// The window spans the stream from the call to `func`'s return. Work the
    /// stream still owed from before falls in; work a backend queues past the
    /// end falls out — the fusion backend holds a closure's last operations
    /// back to batch them, so a window over lazy work alone can read as
    /// empty. Ending the closure with a read, or
    /// [`profile_with`](Self::profile_with) and
    /// [`ProfileOptions::flush`], closes the window over all of it. A window
    /// that nothing ran in reads as no time.
    ///
    /// A backend with no device clock (ndarray, LibTorch, a remote device
    /// whose server has none) measures wall-clock time between two syncs
    /// instead: that one waits, and an inner window's syncs are charged to
    /// the outer.
    ///
    /// ```rust,ignore
    /// let (output, duration) = device.profile(|| model.forward(input))?;
    /// // Later, once the run is over:
    /// let ticks = duration.resolve().await.expect("the window carried work");
    /// println!("forward: {:?}", ticks.duration());
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an [`ExecutionError`] when the device refuses to open or close
    /// the window — a remote device does from a browser thread, which cannot
    /// wait on the server.
    pub fn profile<O: Send + 'static>(
        &self,
        func: impl FnOnce() -> O + Send,
    ) -> Result<(O, ProfileDuration), ExecutionError> {
        self.profile_with(ProfileOptions::default(), func)
    }

    /// [`profile`](Self::profile) with [`ProfileOptions`].
    pub fn profile_with<O: Send + 'static>(
        &self,
        options: ProfileOptions,
        func: impl FnOnce() -> O + Send,
    ) -> Result<(O, ProfileDuration), ExecutionError> {
        Dispatch::profile(self.as_dispatch(), options, func)
    }

    /// Seeds the random number generator for this device.
    ///
    /// Seeding before tensor operations that involve randomness (e.g. [`Tensor::random`](crate::Tensor::random))
    /// makes those operations reproducible in a single-threaded program.
    ///
    /// # Note
    ///
    /// Depending on the backend, the seed may be applied globally rather than scoped
    /// to this specific device. It is guaranteed that at least this device will be seeded.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let device = Default::default();
    /// device.seed(42);
    /// let t = Tensor::<1>::random([8], Distribution::Default, &device);
    /// ```
    pub fn seed(&self, seed: u64) {
        Dispatch::seed(self.as_dispatch(), seed)
    }

    /// Returns whether this device is associated with autodiff.
    ///
    /// This is device context inherited by newly created tensors, not a statement that any tensor
    /// participates in a graph or retains gradients.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let device = Default::default();
    /// assert!(!device.is_autodiff());
    ///
    /// let ad_device = device.autodiff();
    /// assert!(ad_device.is_autodiff());
    /// ```
    pub fn is_autodiff(&self) -> bool {
        Dispatch::ad_enabled(self.as_dispatch())
    }

    /// Returns `true` if this device supports `dtype` for general computation:
    /// storage, conversion, *and* arithmetic.
    ///
    /// A type can be less than generally supported — bf16 on a Vulkan device,
    /// for example, is often storable and convertible but has no arithmetic
    /// (SPIR-V's `SPV_KHR_bfloat16` permits only conversions, dot products,
    /// and cooperative-matrix use). Computing in such a type produces
    /// backend-dependent garbage, so check before selecting a reduced
    /// precision:
    ///
    /// ```rust,ignore
    /// let dtype = if device.supports_dtype(FloatDType::BF16) {
    ///     FloatDType::BF16
    /// } else {
    ///     FloatDType::F32
    /// };
    /// ```
    pub fn supports_dtype(&self, dtype: impl Into<burn_std::DType>) -> bool {
        Dispatch::supports_dtype(self.as_dispatch(), dtype.into())
    }

    /// Sets the current allocation mode to persistent.
    pub fn memory_persistent_allocations<
        Output: Send,
        Input: Send,
        Func: Fn(Input) -> Output + Send,
    >(
        &self,
        input: Input,
        func: Func,
    ) -> Output {
        Dispatch::memory_persistent_allocations(self.as_dispatch(), input, func)
    }

    /// Triggers a memory cleanup on this device.
    ///
    /// The amount of memory reclaimed depends on the allocator implementation.
    /// Calling this method does not guarantee that any memory will be freed.
    pub fn memory_cleanup(&self) {
        Dispatch::memory_cleanup(self.as_dispatch());
    }

    /// This device's dynamic pools, in the order they were installed. `None` on
    /// a backend that does not report them.
    pub fn memory_pool_report(&self) -> Option<Vec<SlicedPoolReport>> {
        Dispatch::memory_pool_report(self.as_dispatch())
    }

    /// What this device's allocator currently holds. `None` on a backend that
    /// does not report it.
    pub fn memory_pool_usage(&self) -> Option<MemoryPoolUsage> {
        Dispatch::memory_pool_usage(self.as_dispatch())
    }

    /// Prepares the given data for transfer between the CPU and accelerator devices such as GPUs.
    ///
    /// Depending on the backend, the data may be transferred to pinned memory
    /// or another transfer-optimized format to improve transfer performance.
    pub fn staging<'a, Iter>(&self, data: Iter)
    where
        Iter: Iterator<Item = &'a mut TensorData>,
    {
        Dispatch::staging(data, self.as_dispatch());
    }

    /// Returns the [`DeviceSettings`] for this device.
    ///
    /// Settings include the default float, integer, and boolean data types used when creating
    /// tensors on this device.
    ///
    /// Before initialization, returns a snapshot of the backend defaults without locking them.
    /// Another thread may configure the device afterward, so subsequent tensor operations may use
    /// different settings. [Configure the device](Device::configure) before relying on its defaults.
    pub fn settings(&self) -> DeviceSettings {
        burn_backend::get_device_settings::<Dispatch>(self.as_dispatch())
    }

    /// Returns settings for tensor operations, initializing them to defaults if unset.
    pub(crate) fn get_or_init_settings(&self) -> DeviceSettings {
        burn_backend::get_or_init_device_settings::<Dispatch>(self.as_dispatch())
    }

    /// Configures the [settings](DeviceSettings) for this device.
    ///
    /// This configures the dtype used when no explicit type is specified at tensor
    /// creation time.
    ///
    /// Settings can only be initialized once per device. Configure defaults before creating
    /// tensors or initializing model parameters: tensor creation locks them to the backend's
    /// defaults, even with an explicit dtype, and any later call returns
    /// [`DeviceError::AlreadyInitialized`]. Querying [`settings`](Device::settings) does not lock them.
    ///
    /// Individual tensors can still use an explicit supported dtype at creation or be converted
    /// with [`Tensor::cast`](crate::Tensor::cast); neither changes the defaults.
    ///
    /// # Errors
    ///
    /// Returns [`DeviceError::UnsupportedDType`] if a requested dtype is unsupported.
    /// Returns [`DeviceError::AlreadyInitialized`] if settings have already been initialized
    /// for this device, either by a prior call or by a tensor operation.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use burn_tensor::{Device, FloatDType, Int, IntDType, Tensor};
    ///
    /// let mut device = Device::cuda(0);
    /// device.configure((FloatDType::F16, IntDType::I32))?;
    ///
    /// // Float tensors will now use F16
    /// let floats = Tensor::<2>::zeros([2, 3], &device);
    /// // Int tensors will now use I32
    /// let ints = Tensor::<2, Int>::zeros([2, 3], &device);
    /// ```
    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
        let mut config = config.into();

        let defaults = self.as_dispatch().defaults();

        let float_dtype = config.float_dtype.take().unwrap_or(defaults.float_dtype);
        let int_dtype = config.int_dtype.take().unwrap_or(defaults.int_dtype);
        let bool_dtype = config.bool_dtype.take().unwrap_or(defaults.bool_dtype);

        burn_backend::set_default_dtypes::<Dispatch>(
            self.as_dispatch(),
            float_dtype,
            int_dtype,
            bool_dtype,
        )
    }

    /// Retrieves all available [`Device`]s that match the given [`DeviceType`] filter.
    ///
    /// Backends enumerate the hardware found on this machine, and `DeviceType::Remote` every
    /// device a remote server hosts:
    ///
    /// ```rust,ignore
    /// // Every CUDA device on this machine.
    /// let local = Device::enumerate(DeviceType::Cuda);
    ///
    /// // Filters combine with `|`.
    /// let both = Device::enumerate(DeviceType::Cuda | DeviceType::Remote(host));
    /// ```
    ///
    /// # Panics
    ///
    /// Where `RemoteHost::devices` returns an error for a `DeviceType::Remote`, and on wasm for any
    /// `DeviceType::Remote`, which a browser can only list with `RemoteHost::devices_async`.
    pub fn enumerate(filter: impl Into<DeviceFilter>) -> Devices {
        #[allow(unused)]
        let mut devices = Vec::new();

        #[allow(clippy::never_loop)] // at least one backend is expected to be enabled.
        for device_type in filter.into() {
            #[allow(unused)]
            let type_id = match device_type {
                // The cubecl runtimes are one dispatch backend, so `DispatchDeviceId::Cube`
                // would list every runtime's devices at once. These name the runtime the
                // caller asked for instead. `Metal`, `Vulkan` and `WebGpu` pin wgpu to the
                // requested graphics API; `Wgpu` leaves the API to auto-selection.
                #[cfg(feature = "cpu")]
                DeviceType::Cpu => {
                    push_cube(&mut devices, RuntimeId::Cpu);
                    continue;
                }
                #[cfg(feature = "cuda")]
                DeviceType::Cuda => {
                    push_cube(&mut devices, RuntimeId::Cuda);
                    continue;
                }
                #[cfg(feature = "rocm")]
                DeviceType::Rocm => {
                    push_cube(&mut devices, RuntimeId::Hip);
                    continue;
                }
                #[cfg(feature = "wgpu")]
                DeviceType::Wgpu => {
                    push_cube(&mut devices, RuntimeId::Wgpu);
                    continue;
                }
                #[cfg(feature = "metal")]
                DeviceType::Metal => {
                    push_wgpu(&mut devices, WgpuBackend::Metal);
                    continue;
                }
                #[cfg(feature = "vulkan")]
                DeviceType::Vulkan => {
                    push_wgpu(&mut devices, WgpuBackend::Vulkan);
                    continue;
                }
                #[cfg(feature = "webgpu")]
                DeviceType::WebGpu => {
                    push_wgpu(&mut devices, WgpuBackend::WebGpu);
                    continue;
                }
                #[cfg(feature = "flex")]
                DeviceType::Flex => DispatchDeviceId::Flex,
                #[cfg(feature = "ndarray")]
                DeviceType::NdArray => DispatchDeviceId::NdArray,
                #[cfg(feature = "tch")]
                DeviceType::LibTorch => DispatchDeviceId::LibTorch,
                #[cfg(feature = "remote")]
                DeviceType::Remote(host) => {
                    devices.extend(host.enumerate());
                    continue;
                }
            };

            #[allow(unreachable_code)] // need to have one backend enabled, so it is reachable
            for device in Dispatch::enumerate(type_id) {
                devices.push(Device::new(device))
            }
        }

        Devices(devices)
    }

    /// Who this device is: the part's name, what its kernels are compiled for and, for a card
    /// on a bus, the card itself. `None` for a backend that does not report one. Opens the
    /// device.
    #[cfg(feature = "cubecl")]
    pub fn identity(&self) -> Option<DeviceIdentity> {
        self.as_dispatch().identity()
    }

    /// Every card this build can reach, once each, with the devices that reach it.
    ///
    /// A card visible to two runtimes (an NVIDIA GPU under CUDA and under Vulkan) is one entry
    /// holding both devices, so a caller placing work on cards never puts two stages on one
    /// card by another name. Opens every device of every runtime, so call it once and keep
    /// the answer.
    #[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
    pub fn enumerate_physical() -> Vec<PhysicalGpu> {
        let mut gpus: Vec<PhysicalGpu> = Vec::new();
        for device in Dispatch::enumerate(DispatchDeviceId::Cube) {
            let device = Device::new(device);
            let Some(DeviceIdentity {
                name,
                physical: Some(physical),
                ..
            }) = device.identity()
            else {
                continue;
            };
            match gpus
                .iter_mut()
                .find(|gpu| gpu.physical.is_same_card(&physical))
            {
                Some(gpu) => {
                    gpu.physical.fill_from(&physical);
                    gpu.devices.0.push(device);
                }
                None => gpus.push(PhysicalGpu {
                    physical,
                    name,
                    devices: Devices(vec![device]),
                }),
            }
        }
        gpus
    }

    /// Measure peak compute and memory throughput for this device.
    ///
    /// Runs cubecl-std's throughput benchmarks for each [`ThroughputKey`],
    /// returning one [`ThroughputStat`] per key (in the same order). A key the
    /// device has no peak for keeps its place, carrying the [`ThroughputError`]
    /// saying why. Only cubecl-backed devices (cuda, wgpu, ...) report
    /// measurements; other backends return an empty vector.
    #[cfg(feature = "cubecl")]
    pub fn performance_stats(&self, keys: &[ThroughputKey]) -> Vec<ThroughputStat> {
        self.as_dispatch()
            .performance_stats(keys)
            .into_iter()
            .zip(keys.iter().copied())
            .map(|(value, key)| ThroughputStat { key, value })
            .collect()
    }
}

/// One card and every device that reaches it, from [`Device::enumerate_physical`].
#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
#[derive(Debug, Clone)]
pub struct PhysicalGpu {
    /// The card's PCI address, Windows LUID and vendor, from whichever of its runtimes reported
    /// each.
    pub physical: PhysicalDevice,
    /// The part as the first runtime that reached it names it.
    pub name: String,
    /// Every device that runs on this card, one per runtime that reaches it.
    pub devices: Devices,
}

/// A single peak-throughput measurement produced by [`Device::performance_stats`].
#[cfg(feature = "cubecl")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThroughputStat {
    /// The measurement key (mode + dtype) that was benchmarked.
    pub key: ThroughputKey,
    /// The measured throughput for that key, or why the device has none.
    pub value: Result<ThroughputValue, ThroughputError>,
}

/// Short, column-friendly name for a throughput mode.
#[cfg(feature = "cubecl")]
fn mode_label(mode: &ThroughputMode) -> &'static str {
    match mode {
        ThroughputMode::ComputeDirect { .. } => "compute-direct",
        ThroughputMode::ComputeCmma { .. } => "compute-cmma",
        ThroughputMode::Memory(spec) => match spec.access {
            MemoryAccess::Copy => "memory",
            MemoryAccess::Read => "memory-read",
            MemoryAccess::Write => "memory-write",
        },
        ThroughputMode::Launch => "launch",
    }
}

#[cfg(feature = "cubecl")]
impl core::fmt::Display for ThroughputStat {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        // Width/alignment flags are ignored on `ThroughputMode`/`ElemType` directly
        // (their fmt impls don't call `f.pad`), so render them to `String`s first —
        // `str`'s `Display` honors padding. The mode is labelled by hand rather than
        // derived through `Debug`: its variants carry payloads that would blow out the column.
        let mode = mode_label(&self.key.mode);

        // `ThroughputKey::dtype()` reports f32 for the modes that don't compute with an
        // element type, so blank the column there rather than print a misleading type.
        let dtype = match self.key.mode {
            ThroughputMode::ComputeDirect { dtype } | ThroughputMode::ComputeCmma { dtype, .. } => {
                alloc::format!("{dtype}")
            }
            ThroughputMode::Memory(_) | ThroughputMode::Launch => alloc::string::String::new(),
        };

        let value = match &self.value {
            Ok(value) => value.format(&self.key),
            Err(error) => alloc::format!("{error}"),
        };

        write!(f, "{mode:<14} {dtype:<5} {value}")
    }
}

/// Append every device of the cubecl `runtime` to `devices`, skipping any already listed.
///
/// A filter can name the same runtime twice (e.g. two `DeviceType::Cuda` entries),
/// and a caller enumerating hardware should see each device once.
#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
fn push_cube(devices: &mut Vec<Device>, runtime: RuntimeId) {
    for device in Dispatch::enumerate_cube(runtime) {
        let device = Device::new(device);
        if !devices.contains(&device) {
            devices.push(device);
        }
    }
}

/// Append every wgpu device on `backend`, skipping any already listed.
#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
fn push_wgpu(devices: &mut Vec<Device>, backend: WgpuBackend) {
    push_wgpu_with(
        devices,
        backend,
        burn_dispatch::devices::CubeDevice::enumerate,
    );
}

#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
fn push_wgpu_with(
    devices: &mut Vec<Device>,
    backend: WgpuBackend,
    mut enumerate: impl FnMut(DeviceId) -> Vec<DeviceId>,
) {
    use burn_dispatch::devices::{CubeDevice, WgpuDevice, WgpuDeviceKind};

    // Enumeration preserves the seed's kind and graphics API. Ask for each concrete
    // kind on the requested API: Auto may expose different kinds or fewer adapters.
    // DefaultDevice is an alias, and Existing devices cannot be discovered.
    for kind in [
        WgpuDeviceKind::DiscreteGpu(0),
        WgpuDeviceKind::IntegratedGpu(0),
        WgpuDeviceKind::VirtualGpu(0),
        WgpuDeviceKind::Cpu,
        WgpuDeviceKind::Other(0),
    ] {
        let seed = CubeDevice::Wgpu(WgpuDevice::new(kind).on(backend));
        for id in enumerate(seed.to_id()) {
            let device = Device::new(CubeDevice::from_id(id));
            if !devices.contains(&device) {
                devices.push(device);
            }
        }
    }
}

/// Map our backend-agnostic [`DeviceKind`] onto cubecl's `WgpuDevice`, on `backend`.
///
/// Shared by [`Device::wgpu`], which leaves the graphics API to the runtime, and
/// [`Device::vulkan`], [`Device::metal`] and [`Device::webgpu`], which each pin theirs.
#[cfg(feature = "wgpu")]
pub(crate) fn wgpu_device(
    device_kind: DeviceKind,
    backend: burn_dispatch::devices::WgpuBackend,
) -> burn_dispatch::devices::WgpuDevice {
    use burn_dispatch::devices::{WgpuDevice, WgpuDeviceKind};

    let kind = match device_kind {
        DeviceKind::DiscreteGpu(i) => WgpuDeviceKind::DiscreteGpu(i),
        DeviceKind::IntegratedGpu(i) => WgpuDeviceKind::IntegratedGpu(i),
        DeviceKind::VirtualGpu(i) => WgpuDeviceKind::VirtualGpu(i),
        DeviceKind::Cpu => WgpuDeviceKind::Cpu,
        DeviceKind::DefaultDevice => WgpuDeviceKind::DefaultDevice,
        DeviceKind::Existing(id) => WgpuDeviceKind::Existing(id),
    };

    WgpuDevice::new(kind).on(backend)
}

/// Represents the devices that can be used.
///
/// `DeviceType` is used to filter the available device types for [`Device::enumerate`]. Each
/// variant selects a backend's hardware on this machine, except `Remote`, which selects a remote
/// server's devices.
///
/// Variants combine into a [`DeviceFilter`] with the `|` operator, so a single
/// [`Device::enumerate`] call can span several backends and remote servers.
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceType {
    #[cfg(feature = "cpu")]
    Cpu,
    #[cfg(feature = "cuda")]
    Cuda,
    #[cfg(feature = "rocm")]
    Rocm,
    #[cfg(feature = "wgpu")]
    Wgpu,
    #[cfg(feature = "metal")]
    Metal,
    #[cfg(feature = "vulkan")]
    Vulkan,
    #[cfg(feature = "webgpu")]
    WebGpu,
    #[cfg(feature = "flex")]
    Flex,
    #[cfg(feature = "ndarray")]
    NdArray,
    #[cfg(feature = "tch")]
    LibTorch,
    /// Every device the remote server `host` hosts, connected on first use like any listed device.
    /// [`RemoteHost::devices`](crate::remote::RemoteHost::devices) does the same and returns an
    /// error where `enumerate` panics.
    #[cfg(feature = "remote")]
    Remote(crate::remote::RemoteHost),
}

/// A set of [`DeviceType`]s passed to [`Device::enumerate`].
///
/// Built from a single [`DeviceType`], a `Vec<DeviceType>`, or by combining variants with the
/// `|` operator (`DeviceType::Cuda | DeviceType::Cpu`).
#[derive(Debug, Clone, Default)]
pub struct DeviceFilter(Vec<DeviceType>);

impl DeviceFilter {
    /// Create an empty filter.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a [`DeviceType`] to the filter.
    pub fn with(mut self, device_type: DeviceType) -> Self {
        self.0.push(device_type);
        self
    }
}

impl From<DeviceType> for DeviceFilter {
    fn from(value: DeviceType) -> Self {
        DeviceFilter(vec![value])
    }
}

impl From<Vec<DeviceType>> for DeviceFilter {
    fn from(value: Vec<DeviceType>) -> Self {
        DeviceFilter(value)
    }
}

impl IntoIterator for DeviceFilter {
    type Item = DeviceType;
    type IntoIter = alloc::vec::IntoIter<DeviceType>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl core::ops::BitOr for DeviceType {
    type Output = DeviceFilter;
    fn bitor(self, rhs: Self) -> DeviceFilter {
        DeviceFilter(vec![self, rhs])
    }
}

impl core::ops::BitOr<DeviceType> for DeviceFilter {
    type Output = DeviceFilter;
    fn bitor(mut self, rhs: DeviceType) -> DeviceFilter {
        self.0.push(rhs);
        self
    }
}

/// Configuration options used to initialize a device.
///
/// Unlike [`DeviceSettings`], this type represents partial user-provided
/// configuration and does not require all settings to be specified.
///
/// Any unspecified options will be resolved to device-specific defaults
/// when the device is initialized.
///
/// Use [`Device::configure`] to apply this configuration to a device.
#[derive(new, Debug, Clone, Default)]
pub struct DeviceConfig {
    /// Default floating-point data type.
    pub float_dtype: Option<FloatDType>,

    /// Default integer data type.
    pub int_dtype: Option<IntDType>,

    /// Default boolean data type.
    pub bool_dtype: Option<BoolDType>,
    // TODO: maybe quantization, but for now we keep this as device defaults
}

impl DeviceConfig {
    /// Sets the default floating-point data type for tensors created on the device.
    pub fn float_dtype(mut self, dtype: impl Into<FloatDType>) -> Self {
        self.float_dtype = Some(dtype.into());
        self
    }

    /// Sets the default integer data type for tensors created on the device.
    pub fn int_dtype(mut self, dtype: impl Into<IntDType>) -> Self {
        self.int_dtype = Some(dtype.into());
        self
    }

    /// Sets the default boolean data type storage precision for tensors created on the device.
    pub fn bool_dtype(mut self, dtype: impl Into<BoolDType>) -> Self {
        self.bool_dtype = Some(dtype.into());
        self
    }
}

impl From<FloatDType> for DeviceConfig {
    fn from(value: FloatDType) -> Self {
        DeviceConfig::new(Some(value), None, None)
    }
}

impl From<IntDType> for DeviceConfig {
    fn from(value: IntDType) -> Self {
        DeviceConfig::new(None, Some(value), None)
    }
}

impl From<BoolDType> for DeviceConfig {
    fn from(value: BoolDType) -> Self {
        DeviceConfig::new(None, None, Some(value))
    }
}

impl From<(FloatDType, IntDType)> for DeviceConfig {
    fn from(value: (FloatDType, IntDType)) -> Self {
        DeviceConfig::new(Some(value.0), Some(value.1), None)
    }
}

/// A collection of [`Device`]s returned by [`Device::enumerate`].
///
/// This type provides bulk operations and transformations over multiple
/// devices, such as enabling autodiff or configuring the device settings.
///
/// # Example
///
/// ```rust,ignore
/// let mut devices = Device::enumerate(DeviceType::Cuda)
///     .autodiff();
///
/// devices.configure(
///     DeviceConfig::default().float_dtype(FloatDType::F16),
/// )?;
/// ```
///
/// `Devices` dereferences to a slice of [`Device`], so it can be iterated,
/// indexed, and passed anywhere a `&[Device]` is expected.
#[derive(Debug, Clone)]
pub struct Devices(Vec<Device>);

impl Devices {
    /// Enables autodiff across all contained devices.
    ///
    /// Devices that already have autodiff enabled are left unchanged, preserving their
    /// gradient-checkpointing strategy.
    ///
    /// See [`Device::autodiff`].
    #[cfg(feature = "autodiff")]
    #[must_use]
    pub fn autodiff(mut self) -> Self {
        for device in &mut self.0 {
            *device = core::mem::take(device).autodiff();
        }

        self
    }

    /// Removes the autodiff association from every contained device.
    ///
    /// See [`Device::without_autodiff`].
    #[must_use]
    pub fn without_autodiff(mut self) -> Self {
        for device in &mut self.0 {
            *device = core::mem::take(device).without_autodiff();
        }

        self
    }

    /// Configures the [settings](DeviceSettings) for all devices.
    ///
    /// This configures the dtype used when no explicit type is specified at tensor
    /// creation time.
    ///
    /// Settings can only be initialized once per device. Configure defaults before creating
    /// tensors or initializing model parameters; tensor creation locks them, even with an explicit
    /// dtype. Querying [`Device::settings`] does not lock them.
    ///
    /// Stops at the first error; devices configured before it keep their settings.
    ///
    /// See [`Device::configure`].
    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
        let config = config.into();
        for device in &mut self.0 {
            device.configure(config.clone())?;
        }
        Ok(())
    }

    /// Returns the `Vec` of [`Device`]s.
    pub fn into_vec(self) -> Vec<Device> {
        self.0
    }
}

// Loop over `&Devices` or `Devices` seamlessly
impl FromIterator<Device> for Devices {
    fn from_iter<I: IntoIterator<Item = Device>>(devices: I) -> Self {
        Self(devices.into_iter().collect())
    }
}

impl IntoIterator for Devices {
    type Item = Device;
    type IntoIter = alloc::vec::IntoIter<Device>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl core::ops::Deref for Devices {
    type Target = [Device];
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[cfg(all(test, feature = "capture"))]
mod capture_tests {
    use super::*;

    #[cfg(all(feature = "flex", feature = "autodiff"))]
    #[test]
    #[should_panic(expected = "Cannot move a tensor to a capture device with autodiff enabled")]
    fn capture_transfer_rejects_autodiff_context() {
        let tensor = crate::Tensor::<1, crate::Int>::from_ints([1, 2], &Device::flex().autodiff());
        let capture = Device::capture();
        let _ = capture.capture_scope(|scope| {
            let _ = tensor.to_device(&capture);
            scope.complete([], [])
        });
    }

    #[cfg(all(feature = "flex", feature = "autodiff"))]
    #[test]
    #[should_panic(expected = "Cannot move a tensor to a capture device with autodiff enabled")]
    fn capture_float_transfer_rejects_autodiff_context() {
        let tensor =
            crate::Tensor::<1>::from_floats([1.0, 2.0], &Device::flex().autodiff()).require_grad();
        let capture = Device::capture();
        let _ = capture.capture_scope(|scope| {
            let _ = tensor.to_device(&capture);
            scope.complete([], [])
        });
    }

    #[test]
    fn user_facing_capture_device_supports_repeated_scopes() {
        let device = Device::capture();

        let first = device
            .capture_scope(|scope| scope.complete([], []))
            .unwrap();
        let second = device
            .capture_scope(|scope| scope.complete([], []))
            .unwrap();

        assert!(first.graph.operations.is_empty());
        assert!(second.graph.operations.is_empty());
    }

    #[test]
    fn capture_scope_rejects_a_non_capture_device() {
        let device = Device::default();

        let result = device.capture_scope(|scope| scope.complete([], []));

        assert!(matches!(result, Err(CaptureError::InvalidDevice)));
    }

    #[test]
    fn capture_device_reports_recordable_dtype_support() {
        let device = Device::capture();

        let captured = device.capture_scope(|scope| {
            assert!(device.supports_dtype(FloatDType::F32));
            assert!(device.supports_dtype(FloatDType::F64));
            assert!(device.supports_dtype(FloatDType::BF16));
            assert!(device.supports_dtype(IntDType::I32));
            assert!(device.supports_dtype(BoolDType::Native));
            scope.complete([], [])
        });

        assert!(captured.is_ok());
    }
}

#[cfg(all(test, feature = "flex", feature = "autodiff"))]
mod autodiff_move_tests {
    use crate::{Bool, Device, GradientCheckpointingStrategy, Int, Tensor, TensorData};

    // A tensor without autodiff (e.g. a gradient) can be moved onto an autodiff device; it lands
    // on the underlying hardware and stays outside autodiff.
    #[test]
    fn move_non_autodiff_float_tensor_to_autodiff_device() {
        let device = Device::default();
        let ad_device = device.clone().autodiff();

        let t = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &device);
        let moved = t.to_device(&ad_device);

        assert!(!moved.is_autodiff());
        assert!(!moved.is_tracked());
        assert_eq!(
            moved.try_into_vec_as::<f32>().unwrap(),
            vec![1.0, 2.0, 3.0, 4.0]
        );
    }

    #[test]
    fn move_autodiff_tensor_preserves_association_strategy_and_graph() {
        let device = Device::default();
        let ad_device = device.clone().autodiff().gradient_checkpointing();

        let tensor = Tensor::<1>::from_floats([1.0, 2.0], &ad_device).require_grad();
        let moved = tensor.clone().to_device(&device);

        assert!(moved.is_autodiff());
        assert!(moved.is_tracked());
        // The transfer is a differentiable operation, so its result is a derived tensor whose own
        // gradient isn't retained by default.
        assert!(!moved.is_require_grad());
        assert_eq!(
            moved.gradient_checkpointing_strategy(),
            Some(GradientCheckpointingStrategy::Balanced)
        );

        let grads = moved.sum().backward();
        tensor
            .grad(&grads)
            .expect("the transfer should preserve the graph")
            .into_data()
            .assert_eq(&TensorData::from([1.0f32, 1.0]), true);
    }

    #[test]
    fn move_non_float_tensors_preserves_context() {
        let device = Device::default();
        let ad_device = device.clone().autodiff().gradient_checkpointing();

        let plain_int = Tensor::<1, Int>::from_ints([1, 2], &device).to_device(&ad_device);
        assert!(!plain_int.is_autodiff());

        let autodiff_bool =
            Tensor::<1, Bool>::from_bool([true, false], &ad_device).to_device(&device);
        assert!(autodiff_bool.is_autodiff());
        assert_eq!(
            autodiff_bool.gradient_checkpointing_strategy(),
            Some(GradientCheckpointingStrategy::Balanced)
        );
    }

    #[test]
    #[should_panic(
        expected = "Gradient checkpointing requires autodiff; use Device::autodiff().gradient_checkpointing()"
    )]
    fn gradient_checkpointing_requires_autodiff() {
        let _ = Device::default().gradient_checkpointing();
    }
}

#[cfg(all(test, feature = "cuda", feature = "wgpu"))]
mod tests {
    use super::*;

    /// `cargo test -p burn-tensor --features cuda,wgpu a_card_reached_by_two_runtimes -- --ignored`
    #[test]
    #[ignore = "needs an NVIDIA card reachable through CUDA and Vulkan"]
    fn a_card_reached_by_two_runtimes_is_listed_once() {
        let gpus = Device::enumerate_physical();
        let nvidia: Vec<_> = gpus
            .iter()
            .filter(|gpu| gpu.physical.vendor == Some(PciVendor::Nvidia))
            .collect();
        assert!(!nvidia.is_empty(), "no NVIDIA card in {gpus:?}");
        for gpu in &nvidia {
            assert!(
                gpu.physical.pci_address.is_some(),
                "{} has no PCI address",
                gpu.name
            );
            assert!(
                gpu.devices.len() >= 2,
                "{} is reached by {:?} only",
                gpu.name,
                gpu.devices
            );
        }
        for (i, gpu) in gpus.iter().enumerate() {
            for other in &gpus[i + 1..] {
                assert!(
                    !gpu.physical.is_same_card(&other.physical),
                    "one card listed twice: {gpu:?} and {other:?}"
                );
            }
        }
    }
}

#[cfg(all(test, any(feature = "metal", feature = "vulkan", feature = "webgpu")))]
mod enumerate_wgpu_tests {
    use super::*;
    use burn_dispatch::devices::{CubeDevice, WgpuDevice, WgpuDeviceKind};

    fn cube_device(kind: WgpuDeviceKind, backend: WgpuBackend) -> CubeDevice {
        CubeDevice::Wgpu(WgpuDevice::new(kind).on(backend))
    }

    #[test]
    fn enumerate_requested_api_independently_of_auto() {
        for backend in [WgpuBackend::Metal, WgpuBackend::Vulkan, WgpuBackend::WebGpu] {
            // An existing Auto device must not limit discovery on the requested API
            // or be deduplicated with a device pinned to that API.
            let auto = cube_device(WgpuDeviceKind::DiscreteGpu(0), WgpuBackend::Auto);
            let expected = [
                WgpuDeviceKind::DiscreteGpu(0),
                WgpuDeviceKind::DiscreteGpu(1),
                WgpuDeviceKind::IntegratedGpu(0),
                WgpuDeviceKind::VirtualGpu(0),
                WgpuDeviceKind::Cpu,
                WgpuDeviceKind::Other(0),
            ]
            .map(|kind| cube_device(kind, backend));
            let enumerate = |seed: DeviceId| {
                let CubeDevice::Wgpu(seed_device) = CubeDevice::from_id(seed) else {
                    panic!("expected a wgpu seed");
                };
                assert_eq!(seed_device.backend, backend);
                expected
                    .iter()
                    .filter(|device| device.to_id().type_id == seed.type_id)
                    .map(CubeDevice::to_id)
                    .collect()
            };

            let mut devices = vec![Device::new(auto.clone())];
            push_wgpu_with(&mut devices, backend, enumerate);
            let expected_devices: Vec<_> = core::iter::once(&auto)
                .chain(expected.iter())
                .cloned()
                .map(Device::new)
                .collect();
            assert_eq!(devices, expected_devices);

            push_wgpu_with(&mut devices, backend, enumerate);
            assert_eq!(
                devices, expected_devices,
                "repeated filters must not duplicate devices"
            );
        }
    }

    #[test]
    fn enumerate_unavailable_api_is_empty() {
        let mut devices = Vec::new();
        push_wgpu_with(&mut devices, WgpuBackend::Vulkan, |_| Vec::new());
        assert!(devices.is_empty());
    }

    #[cfg(not(target_family = "wasm"))]
    #[test]
    fn enumerate_pins_the_graphics_api() {
        let cases = [
            #[cfg(feature = "metal")]
            (DeviceType::Metal, WgpuBackend::Metal),
            #[cfg(feature = "vulkan")]
            (DeviceType::Vulkan, WgpuBackend::Vulkan),
            #[cfg(feature = "webgpu")]
            (DeviceType::WebGpu, WgpuBackend::WebGpu),
        ];
        for (device_type, backend) in cases {
            for device in Device::enumerate(device_type).iter() {
                match device.as_dispatch() {
                    DispatchDevice::Cube(CubeDevice::Wgpu(wgpu)) => {
                        assert_eq!(wgpu.backend, backend);
                    }
                    other => panic!("expected a wgpu device, got {other:?}"),
                }
            }
        }
    }
}