You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adding device objects for selecting GPU backends (and defaulting to CPU if none exists). (#2297)
* Adding structs for cpu and gpu devices.
* Adding implementation of `Flux.get_device()`, which returns the most
appropriate GPU backend (or CPU, if nothing is available).
* Adding docstrings for the new device types, and the `get_device` function.
* Adding `CPU` to the list of supported backends. Made corresponding
changes in `gpu(x)`. Adding more details in docstring of `get_device`.
* Using `julia-repl` instead of `jldoctest`, and `@info` instead of `@warn`.
* Adding `DataLoader` functionality to device objects.
* Removing pkgids and defining new functions to check whether backend is
available and functional.
* Correcting typographical errors, and removing useless imports.
* Adding `deviceID` to each device struct, and moving struct definitions
to package extensions.
* Adding tutorial for using device objects in manual.
* Adding docstring for `get_device` in manual, and renaming internal
functions.
* Minor change in docs.
* Removing structs from package extensions as it is bad practice.
* Adding more docstrings in manual.
* Removing redundant log messages.
* Adding kwarg to `get_device` for verbose output.
* Setting `deviceID` to `nothing` if GPU is not functional.
* Adding basic tests for device objects.
* Fixing minor errors in package extensions and tests.
* Minor fix in tests + docs.
* Moving device tests to extensions, and adding a basic data transfer
test.
* Moving all device tests in single file per extension.
More information for conditional use of GPUs in CUDA.jl can be found in its [documentation](https://cuda.juliagpu.org/stable/installation/conditional/#Conditional-use), and information about the specific use of the variable is described in the [Nvidia CUDA blog post](https://developer.nvidia.com/blog/cuda-pro-tip-control-gpu-visibility-cuda_visible_devices/).
233
233
234
+
## Using device objects
235
+
236
+
As a more convenient syntax, Flux allows the usage of GPU `device` objects which can be used to easily transfer models to GPUs (and defaulting to using the CPU if no GPU backend is available). This syntax has a few advantages including automatic selection of the GPU backend and type stability of data movement. To do this, the [`Flux.get_device`](@ref) function can be used.
237
+
238
+
`Flux.get_device` first checks for a GPU preference, and if possible returns a device for the preference backend. For instance, consider the following example, where we load the [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) package to use an NVIDIA GPU (`"CUDA"` is the default preference):
239
+
240
+
```julia-repl
241
+
julia> using Flux, CUDA;
242
+
243
+
julia> device = Flux.get_device(; verbose=true) # returns handle to an NVIDIA GPU
244
+
[ Info: Using backend set in preferences: CUDA.
245
+
(::Flux.FluxCUDADevice) (generic function with 1 method)
246
+
247
+
julia> device.deviceID # check the id of the GPU
248
+
CuDevice(0): NVIDIA GeForce GTX 1650
249
+
250
+
julia> model = Dense(2 => 3);
251
+
252
+
julia> model.weight # the model initially lives in CPU memory
253
+
3×2 Matrix{Float32}:
254
+
-0.984794 -0.904345
255
+
0.720379 -0.486398
256
+
0.851011 -0.586942
257
+
258
+
julia> model = model |> device # transfer model to the GPU
259
+
Dense(2 => 3) # 9 parameters
260
+
261
+
julia> model.weight
262
+
3×2 CuArray{Float32, 2, CUDA.Mem.DeviceBuffer}:
263
+
-0.984794 -0.904345
264
+
0.720379 -0.486398
265
+
0.851011 -0.586942
266
+
267
+
```
268
+
269
+
The device preference can also be set via the [`Flux.gpu_backend!`](@ref) function. For instance, below we first set our device preference to `"CPU"`:
270
+
271
+
```julia-repl
272
+
julia> using Flux; Flux.gpu_backend!("CPU")
273
+
┌ Info: New GPU backend set: CPU.
274
+
└ Restart your Julia session for this change to take effect!
275
+
```
276
+
277
+
Then, after restarting the Julia session, `Flux.get_device` returns a handle to the `"CPU"`:
278
+
279
+
```julia-repl
280
+
julia> using Flux, CUDA; # even if CUDA is loaded, we'll still get a CPU device
281
+
282
+
julia> device = Flux.get_device(; verbose=true) # get a CPU device
283
+
[ Info: Using backend set in preferences: CPU.
284
+
(::Flux.FluxCPUDevice) (generic function with 1 method)
285
+
286
+
julia> model = Dense(2 => 3);
287
+
288
+
julia> model = model |> device
289
+
Dense(2 => 3) # 9 parameters
290
+
291
+
julia> model.weight # no change; model still lives on CPU
292
+
3×2 Matrix{Float32}:
293
+
-0.942968 0.856258
294
+
0.440009 0.714106
295
+
-0.419192 -0.471838
296
+
```
297
+
Clearly, this means that the same code will work for any GPU backend and the CPU.
298
+
299
+
If the preference backend isn't available or isn't functional, then [`Flux.get_device`](@ref) looks for a CUDA, AMD or Metal backend, and returns a corresponding device (if the backend is available and functional). Otherwise, a CPU device is returned. In the below example, the GPU preference is `"CUDA"`:
300
+
301
+
```julia-repl
302
+
julia> using Flux; # preference is CUDA, but CUDA.jl not loaded
303
+
304
+
julia> device = Flux.get_device(; verbose=true) # this will resort to automatic device selection
305
+
[ Info: Using backend set in preferences: CUDA.
306
+
┌ Warning: Trying to use backend: CUDA but it's trigger package is not loaded.
307
+
│ Please load the package and call this function again to respect the preferences backend.
308
+
└ @ Flux ~/fluxml/Flux.jl/src/functor.jl:637
309
+
[ Info: Using backend: CPU.
310
+
(::Flux.FluxCPUDevice) (generic function with 1 method)
311
+
```
312
+
For detailed information about how the backend is selected, check the documentation for [`Flux.get_device`](@ref).
@@ -444,3 +455,208 @@ function gpu(d::MLUtils.DataLoader)
444
455
d.rng,
445
456
)
446
457
end
458
+
459
+
# Defining device interfaces.
460
+
"""
461
+
Flux.AbstractDevice <: Function
462
+
463
+
An abstract type representing `device` objects for different GPU backends. The currently supported backends are `"CUDA"`, `"AMD"`, `"Metal"` and `"CPU"`; the `"CPU"` backend is the fallback case when no GPU is available. GPU extensions of Flux define subtypes of this type.
464
+
465
+
"""
466
+
abstract type AbstractDevice <:Functionend
467
+
468
+
function (device::AbstractDevice)(d::MLUtils.DataLoader)
Get all supported backends for Flux, in order of preference.
547
+
548
+
# Example
549
+
550
+
```jldoctest
551
+
julia> using Flux;
552
+
553
+
julia> Flux.supported_devices()
554
+
("CUDA", "AMD", "Metal", "CPU")
555
+
```
556
+
"""
557
+
supported_devices() = GPU_BACKENDS
558
+
559
+
"""
560
+
Flux.get_device(; verbose=false)::AbstractDevice
561
+
562
+
Returns a `device` object for the most appropriate backend for the current Julia session.
563
+
564
+
First, the function checks whether a backend preference has been set via the [`Flux.gpu_backend!`](@ref) function. If so, an attempt is made to load this backend. If the corresponding trigger package has been loaded and the backend is functional, a `device` corresponding to the given backend is loaded. Otherwise, the backend is chosen automatically. To update the backend preference, use [`Flux.gpu_backend!`](@ref).
565
+
566
+
If there is no preference, then for each of the `"CUDA"`, `"AMD"`, `"Metal"` and `"CPU"` backends in the given order, this function checks whether the given backend has been loaded via the corresponding trigger package, and whether the backend is functional. If so, the `device` corresponding to the backend is returned. If no GPU backend is available, a `Flux.FluxCPUDevice` is returned.
567
+
568
+
If `verbose` is set to `true`, then the function prints informative log messages.
569
+
570
+
# Examples
571
+
For the example given below, the backend preference was set to `"AMD"` via the [`gpu_backend!`](@ref) function.
572
+
573
+
```julia-repl
574
+
julia> using Flux;
575
+
576
+
julia> model = Dense(2 => 3)
577
+
Dense(2 => 3) # 9 parameters
578
+
579
+
julia> device = Flux.get_device(; verbose=true) # this will just load the CPU device
580
+
[ Info: Using backend set in preferences: AMD.
581
+
┌ Warning: Trying to use backend: AMD but it's trigger package is not loaded.
582
+
│ Please load the package and call this function again to respect the preferences backend.
583
+
└ @ Flux ~/fluxml/Flux.jl/src/functor.jl:638
584
+
[ Info: Using backend: CPU.
585
+
(::Flux.FluxCPUDevice) (generic function with 1 method)
586
+
587
+
julia> model = model |> device
588
+
Dense(2 => 3) # 9 parameters
589
+
590
+
julia> model.weight
591
+
3×2 Matrix{Float32}:
592
+
-0.304362 -0.700477
593
+
-0.861201 0.67825
594
+
-0.176017 0.234188
595
+
```
596
+
597
+
Here is the same example, but using `"CUDA"`:
598
+
599
+
```julia-repl
600
+
julia> using Flux, CUDA;
601
+
602
+
julia> model = Dense(2 => 3)
603
+
Dense(2 => 3) # 9 parameters
604
+
605
+
julia> device = Flux.get_device(; verbose=true)
606
+
[ Info: Using backend set in preferences: AMD.
607
+
┌ Warning: Trying to use backend: AMD but it's trigger package is not loaded.
608
+
│ Please load the package and call this function again to respect the preferences backend.
609
+
└ @ Flux ~/fluxml/Flux.jl/src/functor.jl:637
610
+
[ Info: Using backend: CUDA.
611
+
(::Flux.FluxCUDADevice) (generic function with 1 method)
0 commit comments