On this page:
1.2.1 Using Arrays
1.2.2 Resources
9.3

1.2 Concepts🔗ℹ

This library provides an array data structure that can run on CUDA devices. There are three key types of objects:

  • buffer: one RMM allocation in GPU memory, with its device, byte size and phantom bytes. It owns the memory, and its finalizer frees it.

  • array: a device vector or matrix, a typed and shaped view of a buffer (dtype, shape, strides, offset).

  • resources: a raft::handle_t on one GPU, owning a CUDA stream (an ordered queue of GPU work) and the cuBLAS, cuSOLVER and cuSPARSE handles. Every array and every operation runs on resources, by default the thread’s.

How do these differ from Racket arrays or vectors?

  • The memory is on the GPU, managed by RMM, not in the Racket heap. Moving data in or out is a copy.

  • Work is asynchronous. Operations queue on the resources’ stream and return; the program waits when a value comes back to Racket or on resources-sync!.

  • Dtype and layout are fixed. The dtype is 'float32, 'float64, 'int32 or 'int64; the layout is row-major, each row contiguous, or column-major, each column contiguous. Nothing converts silently: a wrong layout raises, and contiguous copies.

  • Strides count elements. A 2-by-3 matrix has strides '(3 1) row-major and '(1 2) column-major.

  • The collector cannot see device memory. A buffer’s phantom bytes make the device memory it holds bring collections.

image

An array’s memory, from RMM’s pool through the Racket collector and back.

You never free an array. The finalizer is the default and is correct on its own: each buffer is released once, on its own device and stream, from whatever OS thread runs it. It has no timeline, though: it runs at some collection after the last use. The with- forms give one. with-device-resources releases resources when its body returns, raises, escapes or yields, and with-array-views holds arrays for exactly one native call. The finalizer stays the backstop.

1.2.1 Using Arrays🔗ℹ

A conversion from Racket data infers the dtype, 'int64 for exact integers and 'float64 otherwise, unless #:dtype names one, and packs it in #:layout. An array prints its dtype, shape, layout, device and values:

> (list->device-vector '(1.5 -2.25 0.0))

#<device-vector float64[3] cuda:0 [  1.5 -2.25   0.0]>

> (list->device-vector '(3 1 4) #:dtype 'int32)

#<device-vector int32[3] cuda:0 [3 1 4]>

> (list*->device-matrix '((1 2 3) (4 5 6)))

#<device-matrix int64[2×3] row-major cuda:0

 [[1 2 3]

  [4 5 6]]>

> (define m (list*->device-matrix '((1 2 3) (4 5 6)) #:dtype 'float32 #:layout 'col-major))
> m

#<device-matrix float32[2×3] col-major cuda:0

 [[1.0 2.0 3.0]

  [4.0 5.0 6.0]]>

device-matrix and device-vector allocate without initialising, for a library to write into:

> (define out (device-matrix 1000 128 #:dtype 'float64))
> (list (shape out) (dtype out) (layout out))

'((1000 128) float64 row-major)

> (define labels (device-vector 1000 #:dtype 'int32))
> (list (shape labels) (dtype labels))

'((1000) int32)

What an array answers, and how its values come back:

> (list (shape m) (dtype m) (layout m) (strides m) (numel m))

'((2 3) float32 col-major (1 2) 6)

> (list (contiguous? m) (contiguous? m #:layout 'col-major))

'(#f #t)

> (strides (contiguous m))

'(3 1)

> (device-matrix->list* m)

'((1.0 2.0 3.0) (4.0 5.0 6.0))

> (device-vector->flvector (list->device-vector '(0.5 1.5)))

(flvector 0.5 1.5)

Device arrays: raft/array documents every operation, and Device arrays prepares a dataset with them. raft/compat converts vectors, nested vectors, f32vectors, f64vectors, byte strings and math/array arrays by the same rules (Other forms of Racket data), and a native binding hands arrays to its C++ inside with-array-views (raft/unsafe).

1.2.2 Resources🔗ℹ

device-resources makes resources with a stream of their own; current-device-resources is the thread’s default for a device, which every operation uses without #:resources:

> (define r (device-resources))
> r

#<device-resources device 0>

> (resources-device r)

0

> (eq? (current-device-resources) (current-device-resources))

#t

> (eq? r (current-device-resources))

#f

with-device-resources releases resources when its body exits, and resources-sync! waits for the work queued on them:

> (with-device-resources ([scoped (device-resources #:device 0)])
    (resources-sync! scoped)
    (resources-device scoped))

0

Resources, devices and errors: raft/core documents every operation, and Resources and the GPU builds a program around them.