On this page:
2.3.1 Making arrays
device-matrix
device-vector
2.3.2 Inspecting arrays
device-array?
device-matrix?
device-vector?
shape
dtype
layout
strides
numel
2.3.3 Changing the layout
contiguous?
contiguous
2.3.4 Converting Racket data
list->device-vector
device-vector->list
list*->device-matrix
device-matrix->list*
flvector->device-vector
device-vector->flvector
9.3

2.3 Device arrays: raft/array🔗ℹ

 (require raft/array) package: raft

A device array is a matrix or a vector in GPU memory: a buffer read through a shape, strides, an offset and a dtype, one of 'float32, 'float64, 'int32 and 'int64. A matrix is row-major or column-major. raft re-exports this module; Device arrays is its guide.

The examples below share these values:

> (define samples
    '((5.1 3.5 1.4 0.2)
      (4.9 3.0 1.4 0.2)
      (7.0 3.2 4.7 1.4)
      (6.4 3.2 4.5 1.5)))
> (define X (list*->device-matrix samples #:dtype 'float32))

Memory. An array allocates through its #:resources, (current-device-resources) by default, and queues its work on their stream; it keeps them alive. Its finalizer frees the memory, and its size counts as phantom bytes.

Printing. An array prints its dtype, shape, layout and device, then its values, waiting for its stream. An axis longer than 20, and every axis of an array over 1000 elements, shows its first and last three entries around .... Printing never raises; unreadable values print as <values unavailable: ...>.

Errors. An unknown dtype or layout, or a negative extent, raises exn:fail:raft of kind 'logic. An argument of the wrong kind may raise a different error.

2.3.1 Making arrays🔗ℹ

procedure

(device-matrix rows    
  cols    
  [#:dtype dtype    
  #:layout layout    
  #:resources resources]) → device-matrix?
  rows : exact-nonnegative-integer?
  cols : exact-nonnegative-integer?
  dtype : (or/c 'float32 'float64 'int32 'int64) = 'float32
  layout : (or/c 'row-major 'col-major) = 'row-major
  resources : device-resources? = (current-device-resources)
Allocates a rows-by-cols matrix of dtype in layout, uninitialised.

Examples:
> (define out (device-matrix 1000 128))
> (list (shape out) (dtype out) (layout out))

'((1000 128) float32 row-major)

An output shaped after its input:

> (define (distance-matrix data k)
    (match-define (list n _) (shape data))
    (device-matrix n k #:dtype (dtype data)))
> (define distances (distance-matrix X 3))
> (list (shape distances) (dtype distances))

'((4 3) float32)

Column-major, with resources of its own:

> (with-device-resources ([r (device-resources)])
    (define coefficients (device-matrix 3 2 #:dtype 'float64 #:layout 'col-major
                                        #:resources r))
    (list (layout coefficients) (strides coefficients)))

'(col-major (1 3))

An unsupported element type raises:

> (device-matrix 2 2 #:dtype 'float16)

device-matrix: unsupported dtype float16; expected float32,

float64, int32 or int64

procedure

(device-vector n    
  [#:dtype dtype    
  #:resources resources]) → device-vector?
  n : exact-nonnegative-integer?
  dtype : (or/c 'float32 'float64 'int32 'int64) = 'float32
  resources : device-resources? = (current-device-resources)
Allocates an uninitialised vector of n elements of dtype. Its stride is 1, or 0 when empty, and it is in both layouts.

Examples:
> (define labels (device-vector (first (shape X)) #:dtype 'int32))
> (list (shape labels) (dtype labels) (strides labels))

'((4) int32 (1))

Per-sample weights:

> (define weights (device-vector 4 #:dtype 'float64))
> (list (numel weights) (dtype weights))

'(4 float64)

One element per column of a matrix:

> (define (per-column m)
    (device-vector (second (shape m)) #:dtype (dtype m)))
> (shape (per-column X))

'(4)

2.3.2 Inspecting arrays🔗ℹ

procedure

(device-array? v) → boolean?

  v : any/c
Returns #t if v is a device array.

Examples:
> (device-array? X)

#t

> (device-array? labels)

#t

> (device-array? samples)

#f

Copying data to the device only when it is not there:

> (define (on-device data)
    (if (device-array? data)
        data
        (list*->device-matrix data #:dtype 'float32)))
> (eq? (on-device X) X)

#t

> (shape (on-device '((1.0 2.0))))

'(1 2)

Counting inputs already on the GPU:

> (count device-array? (list X labels samples))

2

procedure

(device-matrix? v) → boolean?

  v : any/c
Returns #t if v is a device array with two axes.

Examples:
> (device-matrix? X)

#t

> (device-matrix? labels)

#f

Checking an input for k-means:

> (define (kmeans-input? v)
    (and (device-matrix? v)
         (eq? (layout v) 'row-major)
         (memq (dtype v) '(float32 float64))
         #t))
> (kmeans-input? X)

#t

> (kmeans-input? (contiguous X #:layout 'col-major))

#f

A row count that treats a vector as one row:

> (define (row-count a)
    (if (device-matrix? a) (first (shape a)) 1))
> (map row-count (list X labels))

'(4 1)

procedure

(device-vector? v) → boolean?

  v : any/c
Returns #t if v is a device array with one axis.

Examples:
> (device-vector? labels)

#t

> (device-vector? X)

#f

Either kind back to Racket data:

> (define (->racket a)
    (if (device-vector? a)
        (device-vector->list a)
        (device-matrix->list* a)))
> (->racket (list->device-vector '(1 2 3)))

'(1 2 3)

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

'((1 2) (3 4))

Weights that match the samples:

> (define (weights-fit? w m)
    (and (device-vector? w)
         (= (first (shape w)) (first (shape m)))))
> (weights-fit? weights X)

#t

> (weights-fit? labels (device-matrix 5 2))

#f

procedure

(shape a) → (listof exact-nonnegative-integer?)

  a : device-array?
Returns the extents of a: (list rows cols) or (list n).

Examples:
> (shape X)

'(4 4)

> (shape labels)

'(4)

Sizing an algorithm’s outputs:

> (match-define (list n d) (shape X))
> (define centroids (device-matrix 3 d))
> (list n d (shape centroids))

'(4 4 (3 4))

Comparing shapes:

> (define (same-shape? a b)
    (equal? (shape a) (shape b)))
> (same-shape? X (contiguous X #:layout 'col-major))

#t

> (same-shape? X centroids)

#f

procedure

(dtype a) → (or/c 'float32 'float64 'int32 'int64)

  a : device-array?
Returns the element type of a.

Examples:
> (dtype X)

'float32

> (dtype labels)

'int32

An output of its input’s type:

> (define (scratch-like a)
    (match-define (list rows cols) (shape a))
    (device-matrix rows cols #:dtype (dtype a)))
> (dtype (scratch-like (list*->device-matrix '((1 2)) #:dtype 'int32)))

'int32

The types inferred from Racket data:

> (map dtype (list (list->device-vector '(1 2 3))
                   (list->device-vector '(1 2.5))
                   (list->device-vector '(1/2))))

'(int64 float64 float64)

procedure

(layout a) → (or/c 'row-major 'col-major)

  a : device-array?
Returns 'row-major if each row of a is contiguous in memory, 'col-major if each column is. An array in both layouts (a vector, one row or column, or no elements) answers 'row-major.

Examples:
> (layout X)

'row-major

> (layout (contiguous X #:layout 'col-major))

'col-major

To ask whether an array is in a given layout, use contiguous?:

> (define column (device-matrix 5 1 #:layout 'col-major))
> (list (strides column) (layout column) (layout labels))

'((1 5) row-major row-major)

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

#t

A log line per input:

> (for ([a (list X labels)])
    (printf "~a ~a ~a\n" (dtype a) (shape a) (layout a)))

float32 (4 4) row-major

int32 (4) row-major

procedure

(strides a) → (listof exact-nonnegative-integer?)

  a : device-array?
Returns, for each axis of a, how many elements apart neighbours along it are in memory, not bytes apart. An empty matrix has strides 0.

Examples:
> (strides X)

'(4 1)

> (strides (contiguous X #:layout 'col-major))

'(1 4)

> (strides (device-matrix 0 4))

'(0 0)

Where element (i, j) sits:

> (define (element-index a i j)
    (+ (* i (first (strides a))) (* j (second (strides a)))))
> (element-index X 2 1)

9

> (element-index (contiguous X #:layout 'col-major) 2 1)

6

Strides in bytes, for an interface that counts bytes:

> (define (byte-strides a)
    (define size (if (memq (dtype a) '(float32 int32)) 4 8))
    (map (lambda (s) (* s size)) (strides a)))
> (byte-strides X)

'(16 4)

procedure

(numel a) → exact-nonnegative-integer?

  a : device-array?
Returns the number of elements of a.

Examples:
> (numel X)

16

> (numel labels)

4

Device memory taken:

> (define (megabytes a)
    (define size (if (memq (dtype a) '(float32 int32)) 4 8))
    (/ (* (numel a) size) (* 1024 1024.0)))
> (megabytes out)

0.48828125

Skipping work on an empty input:

> (define (mean-of v)
    (if (zero? (numel v))
        +nan.0
        (/ (apply + (device-vector->list v)) (numel v))))
> (mean-of (list->device-vector '(1.0 2.0 4.5)))

2.5

> (mean-of (list->device-vector '()))

+nan.0

2.3.3 Changing the layout🔗ℹ

procedure

(contiguous? a [#:layout layout]) → boolean?

  a : device-array?
  layout : (or/c 'row-major 'col-major) = 'row-major
Returns #t if a is laid out in layout. A vector, a matrix with one row or one column, and an empty matrix are in both. Any other layout symbol answers #f.

Examples:
> (contiguous? X)

#t

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

#f

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

#t

A single column needs no copy:

> (define one-feature (list*->device-matrix '((5.1) (4.9) (7.0)) #:dtype 'float32))
> (list (contiguous? one-feature) (contiguous? one-feature #:layout 'col-major))

'(#t #t)

> (eq? (contiguous one-feature #:layout 'col-major) one-feature)

#t

Refusing the wrong order:

> (define (require-fortran-order m)
    (unless (contiguous? m #:layout 'col-major)
      (error 'least-squares "expected a col-major matrix, given ~a" (layout m)))
    m)
> (shape (require-fortran-order (contiguous X #:layout 'col-major)))

'(4 4)

> (shape (require-fortran-order one-feature))

'(3 1)

> (require-fortran-order X)

least-squares: expected a col-major matrix, given row-major

procedure

(contiguous a [#:layout layout]) → device-matrix?

  a : device-matrix?
  layout : (or/c 'row-major 'col-major) = 'row-major
Returns a in layout: a itself if (contiguous? a #:layout layout), otherwise a copy made on the GPU on a’s stream and resources.

Examples:
> (define Xf (contiguous X #:layout 'col-major))
> (list (layout Xf) (strides Xf))

'(col-major (1 4))

> (equal? (device-matrix->list* Xf) (device-matrix->list* X))

#t

Already in layout, no copy:

> (eq? (contiguous Xf #:layout 'col-major) Xf)

#t

> (eq? (contiguous X) X)

#t

Back to row-major:

> (define Xr (contiguous Xf))
> (list (layout Xr) (kmeans-input? Xr))

'(row-major #t)

Integer matrices:

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

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

 [[1 2 3]

  [4 5 6]]>

2.3.4 Converting Racket data🔗ℹ

Without #:dtype, the element type is inferred: all exact integers give 'int64, anything else (or an empty list) 'float64. An integer type truncates toward zero. A value the type cannot hold (a complex number, an out-of-range integer, a non-finite value for an integer type, a finite value too large for a float type) raises exn:fail:raft. Coming back, the conversions wait for the array’s stream; float elements become flonums, integer elements exact integers. raft/compat adds other Racket containers by the same rules (Conversions: raft/compat).

procedure

(list->device-vector xs    
  [#:dtype dtype    
  #:resources resources]) → device-vector?
  xs : (listof real?)
  dtype : (or/c #f 'float32 'float64 'int32 'int64) = #f
  resources : device-resources? = (current-device-resources)
Returns a vector holding xs, of dtype or the inferred type.

Examples:
> (list->device-vector '(3 1 4 1 5))

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

> (list->device-vector '(0.25 0.5) #:dtype 'float32)

#<device-vector float32[2] cuda:0 [0.25  0.5]>

Rationals become floats; values the type cannot hold raise:

> (device-vector->list (list->device-vector '(1/2 1/4 3)))

'(0.5 0.25 3.0)

> (list->device-vector '(1 2+3i))

list->device-vector: cannot infer a dtype: 2+3i is not a

real number

> (list->device-vector '(3000000000) #:dtype 'int32)

list->device-vector: 3000000000 does not fit int32

Cluster labels:

> (define assigned (list->device-vector '(0 2 1 2) #:dtype 'int32))
> (list (dtype assigned) (shape assigned))

'(int32 (4))

procedure

(device-vector->list v) → (listof real?)

  v : device-vector?
Returns the elements of v as a list.

Example:
> (device-vector->list assigned)

'(0 2 1 2)

Cluster sizes from labels:

> (define (cluster-sizes labels k)
    (define all (device-vector->list labels))
    (for/list ([c (in-range k)])
      (count (lambda (l) (= l c)) all)))
> (cluster-sizes assigned 3)

'(1 1 2)

'float32 elements widen:

> (device-vector->list (list->device-vector '(0.1 0.5) #:dtype 'float32))

'(0.10000000149011612 0.5)

procedure

(list*->device-matrix rows    
  [#:dtype dtype    
  #:layout layout    
  #:resources resources]) → device-matrix?
  rows : (listof (listof real?))
  dtype : (or/c #f 'float32 'float64 'int32 'int64) = #f
  layout : (or/c 'row-major 'col-major) = 'row-major
  resources : device-resources? = (current-device-resources)
Returns a matrix whose rows are rows, of dtype or the inferred type, packed in layout on the host. A ragged row raises exn:fail:raft.

Examples:
> (list*->device-matrix '((1 2) (3 4)))

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

 [[1 2]

  [3 4]]>

> (list*->device-matrix samples #:dtype 'float32 #:layout 'col-major)

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

 [[5.1 3.5 1.4 0.2]

  [4.9 3.0 1.4 0.2]

  [7.0 3.2 4.7 1.4]

  [6.4 3.2 4.5 1.5]]>

Rows parsed from CSV lines:

> (define lines '("5.1,3.5,1.4,0.2" "7.0,3.2,4.7,1.4"))
> (define parsed
    (for/list ([line (in-list lines)])
      (map string->number (string-split line ","))))
> (shape (list*->device-matrix parsed #:dtype 'float32))

'(2 4)

A ragged row is named:

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

list*->device-matrix: row 2 has 2 elements, but row 0 has 3:

'(7 8)

procedure

(device-matrix->list* m) → (listof (listof real?))

  m : device-matrix?
Returns the rows of m as a list of lists, whatever its layout.

Example:
> (device-matrix->list* (list*->device-matrix '((1 2) (3 4))))

'((1 2) (3 4))

Column-major:

> (device-matrix->list* (list*->device-matrix '((1 2) (3 4)) #:layout 'col-major))

'((1 2) (3 4))

A table of centroids:

> (define found (list*->device-matrix '((5.0 3.4) (6.6 3.0)) #:dtype 'float64))
> (for ([row (in-list (device-matrix->list* found))]
        [c (in-naturals)])
    (printf "cluster ~a: ~a\n" c (string-join (map ~a row) " ")))

cluster 0: 5.0 3.4

cluster 1: 6.6 3.0

procedure

(flvector->device-vector xs 
  [#:dtype dtype 
  #:resources resources]) 
 → device-vector?
  xs : flvector?
  dtype : (or/c 'float32 'float64 'int32 'int64) = 'float64
  resources : device-resources? = (current-device-resources)
Returns a vector holding the elements of xs, of dtype. An integer type truncates toward zero.

Example:
> (flvector->device-vector (flvector 0.5 1.5 2.5))

#<device-vector float64[3] cuda:0 [0.5 1.5 2.5]>

As 'float32:

> (define roots
    (for/flvector ([i (in-range 5)])
      (flsqrt (->fl i))))
> (flvector->device-vector roots #:dtype 'float32)

#<device-vector float32[5] cuda:0 [      0.0       1.0 1.4142135 1.7320508       2.0]>

Truncated to integers:

> (device-vector->list (flvector->device-vector (flvector 0.7 2.2 -1.5) #:dtype 'int64))

'(0 2 -1)

procedure

(device-vector->flvector v) → flvector?

  v : device-vector?
Returns the elements of v as an flvector.

Example:
> (device-vector->flvector (flvector->device-vector (flvector 1.0 2.0)))

(flvector 1.0 2.0)

Other types widen to flonums:

> (device-vector->flvector (list->device-vector '(0.1) #:dtype 'float32))

(flvector 0.10000000149011612)

> (device-vector->flvector (list->device-vector '(1 2 3)))

(flvector 1.0 2.0 3.0)

Into flonum code:

> (define v (device-vector->flvector (list->device-vector '(3.0 4.0))))
> (flsqrt (for/fold ([sum 0.0]) ([x (in-flvector v)]) (fl+ sum (fl* x x))))

5.0