MODULE C16Mat ;
(* Chapter 16: computation goes faster with threads when there are
cores to run them on -- a matrix product and a matrix inversion,
each done once on one thread and once on eight, and the two
answers compared BIT FOR BIT.
The shape of every threaded computation here is the same:
* the work is cut into bands that touch DISJOINT parts of the
result -- rows of the product, columns of the inverse;
* each worker claims a band through a monitor, computes it in
its own pool, and copies it into its own part of the result;
* the main thread waits on the monitor and then reads.
Nothing is shared and written by two threads, so there is nothing
to lock during the arithmetic, and the answer cannot depend on
which band finished first. Every band does the SAME arithmetic
in the SAME order as the serial version, which is why the check
at the end can demand identical bytes rather than a tolerance. *)
IMPORT Io ;
IMPORT Faults ;
IMPORT Fmt ;
IMPORT Mat ;
IMPORT Time ;
CONST
N = 500 ; (* 500 x 500: 125 million multiply-adds; sized
to run inside the tutorial's 10 s sandbox
wall on a slow host -- see chapter 16 *)
Bands = 8 ;
TYPE
(* the finish line and the band counter -- everything the workers
share AND change lives in the monitor, and nowhere else *)
Work = MONITOR RECORD
n : I64 ; (* workers finished *)
next : I64 ; (* the next band to claim *)
END ;
MulJob = RECORD
w : Work ;
a, b : PTR Mat.Matrix ; (* read by every worker, written by none *)
prod : PTR Mat.Matrix ; (* rows [r0, r1) of it belong to band k *)
END [SHARABLE] ;
InvJob = RECORD
w : Work ;
l : PTR Mat.Matrix ; (* the Cholesky factor, read by all *)
inv : PTR Mat.Matrix ; (* columns [c0, c1) of it belong to band k *)
END [SHARABLE] ;
(* --- the three bound procedures, and they are all tiny --- *)
PROCEDURE Claim (VAR w: Work) : I64 =
VAR k : I64 ;
BEGIN
k := w.next ;
w.next := w.next + 1 ;
RETURN k
END Claim ;
PROCEDURE Finish (VAR w: Work) =
BEGIN
w.n := w.n + 1 ;
SIGNAL (w)
END Finish ;
PROCEDURE AwaitAll (VAR w: Work ; want: I64) =
BEGIN
WHILE w.n < want DO WAIT (w) END
END AwaitAll ;
(* band k of N rows is [k*N/Bands, (k+1)*N/Bands) *)
PROCEDURE Lo (k: I64) : I64 =
BEGIN
RETURN k * N DIV Bands
END Lo ;
PROCEDURE Hi (k: I64) : I64 =
BEGIN
RETURN (k + 1) * N DIV Bands
END Hi ;
(* --- the product, one row band per worker --- *)
PROCEDURE MulWorker (VAR j: MulJob) =
VAR
wpool : POOL ; (* a pool per worker: the allocator is not locked *)
band, p : PTR Mat.Matrix IN wpool ;
k, r0, r1, r, c : I64 ;
BEGIN
k := Claim (j.w) ;
IF k < Bands THEN
r0 := Lo (k) ; r1 := Hi (k) ;
BEGIN
(* my rows of A, then the whole of B: the same MulM the serial
version calls, on a slice of the problem *)
band := Mat.New (wpool, r1 - r0, N) ;
FOR r := 0 TO r1 - r0 - 1 DO
FOR c := 0 TO N - 1 DO
Mat.Set (band, r, c, Mat.Get (j.a, r0 + r, c))
END
END ;
p := Mat.MulM (wpool, band, j.b) ;
(* into MY rows of the shared product, and no one else's *)
FOR r := 0 TO r1 - r0 - 1 DO
FOR c := 0 TO N - 1 DO
Mat.Set (j.prod, r0 + r, c, Mat.Get (p, r, c))
END
END
EXCEPT
| Faults.SizeError : Io.ErrLine ('a band had the wrong shape') ; Io.Halt (2)
END
END ;
Finish (j.w)
END MulWorker ;
(* --- the inverse: factor once, then solve one column band per
worker. Mat.SpdInverse is exactly Cholesky followed by CholSolve
on the identity, and CholSolve treats each column on its own, so a
band of identity columns solved here is the same arithmetic. --- *)
PROCEDURE InvWorker (VAR j: InvJob) =
VAR
wpool : POOL ;
id, x : PTR Mat.Matrix IN wpool ;
k, c0, c1, r, c : I64 ;
BEGIN
k := Claim (j.w) ;
IF k < Bands THEN
c0 := Lo (k) ; c1 := Hi (k) ;
BEGIN
id := Mat.New (wpool, N, c1 - c0) ; (* pool storage is zero *)
FOR c := 0 TO c1 - c0 - 1 DO Mat.Set (id, c0 + c, c, 1.0) END ;
x := Mat.CholSolve (wpool, j.l, id) ;
FOR r := 0 TO N - 1 DO
FOR c := 0 TO c1 - c0 - 1 DO
Mat.Set (j.inv, r, c0 + c, Mat.Get (x, r, c))
END
END
EXCEPT
| Faults.SizeError : Io.ErrLine ('a band had the wrong shape') ; Io.Halt (2)
END
END ;
Finish (j.w)
END InvWorker ;
PROCEDURE Identical (a, b: PTR Mat.Matrix) : BOOL =
VAR r, c : I64 ;
BEGIN
IF (Mat.Rows (a) # Mat.Rows (b)) OR (Mat.Cols (a) # Mat.Cols (b)) THEN
RETURN FALSE
END ;
FOR r := 0 TO Mat.Rows (a) - 1 DO
FOR c := 0 TO Mat.Cols (a) - 1 DO
IF Mat.Get (a, r, c) # Mat.Get (b, r, c) THEN RETURN FALSE END
END
END ;
RETURN TRUE
END Identical ;
(* how far a product is from the identity, as a single number *)
PROCEDURE FarFromIdentity (m: PTR Mat.Matrix) : F64 =
VAR
r, c : I64 ;
want, d, worst : F64 ;
BEGIN
worst := 0.0 ;
FOR r := 0 TO Mat.Rows (m) - 1 DO
FOR c := 0 TO Mat.Cols (m) - 1 DO
IF r = c THEN want := 1.0 ELSE want := 0.0 END ;
d := Mat.Get (m, r, c) - want ;
IF d < 0.0 THEN d := -d END ;
IF d > worst THEN worst := d END
END
END ;
RETURN worst
END FarFromIdentity ;
VAR
pool : POOL ;
a, b, at, s, prodS, invS, chk : PTR Mat.Matrix IN pool ;
mj : PTR MulJob IN pool ;
ij : PTR InvJob IN pool ;
r, c, i : I64 ;
t0, t1 : Time.Instant ;
serial, par : F64 ;
BEGIN
BEGIN
(* two matrices with a pattern rather than random numbers, so the
whole run is reproducible with no seed to carry *)
a := Mat.New (pool, N, N) ;
b := Mat.New (pool, N, N) ;
FOR r := 0 TO N - 1 DO
FOR c := 0 TO N - 1 DO
Mat.Set (a, r, c, F64 ((r * 7 + c * 3) MOD 11) / 10.0) ;
Mat.Set (b, r, c, F64 ((r * 5 + c * 2) MOD 13) / 10.0)
END
END ;
(* ---- 1. the product, serial then banded ---- *)
t0 := Time.Now () ;
prodS := Mat.MulM (pool, a, b) ;
t1 := Time.Now () ;
serial := Time.Elapsed (t0, t1) ;
mj := NEW (pool, MulJob) ;
mj.a := a ; mj.b := b ;
mj.prod := Mat.New (pool, N, N) ;
t0 := Time.Now () ;
FOR i := 1 TO Bands DO THREAD (MulWorker, mj) END ;
AwaitAll (mj.w, Bands) ;
t1 := Time.Now () ;
par := Time.Elapsed (t0, t1) ;
(* timings only when asked (any argument): a number that changes
from run to run has no place in gated output *)
IF Io.ArgCount () > 1 THEN
Io.ErrLine ('product: serial ' + Fmt.Fixed (serial, 3) + ' s, '
+ Fmt.I64Str (Bands) + ' bands ' + Fmt.Fixed (par, 3)
+ ' s, ' + Fmt.Fixed (serial / par, 1) + 'x')
END ;
IF Identical (prodS, mj.prod) THEN
Io.WriteLine ('product of two ' + Fmt.I64Str (N) + 'x' + Fmt.I64Str (N)
+ ' matrices in ' + Fmt.I64Str (Bands)
+ ' bands: identical to the serial product, bit for bit')
ELSE
Io.WriteLine ('product: the bands DIFFER from the serial product')
END ;
(* ---- 2. the inverse of an SPD matrix, serial then banded ----
S = A^T A + N I is symmetric positive definite by construction *)
at := Mat.Transpose (pool, a) ;
s := Mat.MulM (pool, at, a) ;
FOR r := 0 TO N - 1 DO
Mat.Set (s, r, r, Mat.Get (s, r, r) + F64 (N))
END ;
t0 := Time.Now () ;
invS := Mat.SpdInverse (pool, s) ;
t1 := Time.Now () ;
serial := Time.Elapsed (t0, t1) ;
ij := NEW (pool, InvJob) ;
t0 := Time.Now () ;
ij.l := Mat.Cholesky (pool, s) ; (* once, on this thread *)
ij.inv := Mat.New (pool, N, N) ;
FOR i := 1 TO Bands DO THREAD (InvWorker, ij) END ;
AwaitAll (ij.w, Bands) ;
t1 := Time.Now () ;
par := Time.Elapsed (t0, t1) ;
IF Io.ArgCount () > 1 THEN
Io.ErrLine ('inverse: serial ' + Fmt.Fixed (serial, 3) + ' s, '
+ Fmt.I64Str (Bands) + ' bands ' + Fmt.Fixed (par, 3)
+ ' s, ' + Fmt.Fixed (serial / par, 1) + 'x')
END ;
IF Identical (invS, ij.inv) THEN
Io.WriteLine ('inverse of a ' + Fmt.I64Str (N) + 'x' + Fmt.I64Str (N)
+ ' SPD matrix in ' + Fmt.I64Str (Bands)
+ ' column bands: identical to SpdInverse, bit for bit')
ELSE
Io.WriteLine ('inverse: the bands DIFFER from SpdInverse')
END ;
(* and it IS an inverse: S times it is the identity to rounding *)
chk := Mat.MulM (pool, s, ij.inv) ;
IF FarFromIdentity (chk) < 1.0e-9 THEN
Io.WriteLine ('S times the inverse is the identity to within 1e-9: yes')
ELSE
Io.WriteLine ('S times the inverse is the identity to within 1e-9: NO')
END
EXCEPT
| Faults.SizeError : Io.ErrLine ('a matrix had the wrong shape') ; Io.Halt (1)
| Mat.NotSPD : Io.ErrLine ('S is not positive definite') ; Io.Halt (1)
| ValueRange : Io.ErrLine ('a value out of range') ; Io.Halt (1)
END
END C16Mat.