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)
> (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)
> (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
> (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
> (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
> (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?
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?
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?
> (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?
> (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
a : device-array?
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
> (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
> (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)
> (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?
> (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)
> (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?
> (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)
> (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
v : device-vector?
> (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