MLX-CL
Common Lisp bindings for MLX
Table of Contents
1. About
MLX-CL is a Common Lisp binding for MLX, together with a set of trivial data processing library.
NOTE: Github's org-mode renderer is buggy, please refer documentation.
NOTE: Although it may not be exactly true, but every exposed function,
class, variable in mlx-cl should came with feature-rich documentation.
If you've found documentation missing or not clear, it is DEFINITELY
an issue.
2. Tutorials
3. Usage
3.1. Installation
You need: a Common Lisp distribution (tested on SBCL 2.5.8, LispWorks Personal Edition 8.0.1), Quicklisp (Common Lisp package manager), and mlx-cl under where your quicklisp can find.
A possible installation process
Here's a possible installation process:
> mkdir -pv ~/common-lisp/
> git clone --recursive https://github.com/li-yiyang/mlx-cl.git ~/common-lisp/mlx-cl
...
> sbcl
This is SBCL 2.5.8, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
To load "trivial-indent":
Load 1 ASDF system:
trivial-indent
; Loading "trivial-indent"
* (ql:quickload :mlx-cl)
To load "mlx-cl":
Load 1 ASDF system:
mlx-cl
; Loading "mlx-cl"
..................................................
................................................
(:mlx-cl)
* (in-package :mlx-user)
#<package "MLX-USER">
* (+ 1 '(2 3 4))
array([3, 4, 5], dtype=float32)
3.2. Systems and Packages
The core functionalities are provided within system mlx-cl:
(ql:quickload :mlx-cl)
would loads the following packages:
mlx-cl: the core binding package, if you are about to develop new packages, you should(defpackage #:your-new-package-name (:use :mlx-cl))
note that the API of
mlx-cloverwritescl's functions, which makes(:use :cl :mlx-cl)difficultmlx-cl.fftsee FFT for examplesmlx-cl.linalgmlx-cl.randomsee Random for examplesmlx-userthe following documentation are assumed to be written undermlx-userpackage.The
mlx-userpackage is defined like:(defpackage #:mlx-user (:use :mlx-cl) (:local-nicknames (:fft :mlx-cl.fft) (:rnd :mlx-cl.random) (:linalg :mlx-cl.linalg)))
There are some subsystem(s) providing additional functionalities
above mlx-cl:
mlx-cl/image: see readme underimagedir
3.3. Basic Operations
See tests in test/core/api.lisp for more examples.
3.3.1. Convert
Convert from Lisp to
mlx-arrayTo convert betweenmlx-arrayand Lisp array (data containers), use generic functionmlx-array:scalar to
mlx-array(mlx-array 2)
array(2, dtype=int32)
list to
mlx-array(mlx-array '((1 2) (3 4)))
array([[1, 2], [3, 4]], dtype=uint64)array to
mlx-array(mlx-array #3A(((1 2) (3 4)) ((5 6) (7 8))))array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=uint64)note: use array, it's fast.
You can implement methods to tell lisp how to convert your data into
mlx-array.Use
lisp<-to convertmlx-arrayinto Lisp value.For example:
(lisp<- (mlx.rnd:randint 10 :shape #(5 5)))
#2A((3 1 2 9 9) (8 9 0 1 1) (5 6 7 7 9) (3 4 5 2 7) (8 1 2 3 7))
Use
(mlx-array pathname)to loadmlx-arrayfrompathname, how to load themlx-arraydepends on the extension name of the pathname.For example, subsystem
mlx-cl/io/npyprovides NPY file I/O functionalities.(ql:quickload :mlx-cl/io/npy)
If having a NPY file:
import numpy as np np.save("./res/tmp/example.npy", np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))(mlx-array "./res/tmp/example.npy")
array([[1, 2, 3], [4, 5, 6]], dtype=float32)You could write
mlx-arrayas NPY usingsavefunction:(->* (mlx-array #2A((1 2 3) (4 5 6)) :dtype :uint8) (save * :output "./res/tmp/example.npy"))
./res/tmp/example.npy
import numpy as np print(np.load("./res/tmp/example.npy"))See I/O for more detailed description.
3.3.2. Alloc mlx-array
- Generate by range:
(arange [start] stop [step] &key step dtype)(arange 10) ; (arange STOP)
array([0, 1, 2, ..., 7, 8, 9], dtype=float32)
(arange 5 10) ; (arange START STOP)
array([5, 6, 7, 8, 9], dtype=float32)
(arange 2 10 2) ; (arange START STOP STEP)
array([2, 4, 6, 8], dtype=float32)
(linspace start stop &optional num &key dtype)(linspace 0 10) ; num=50 by default
array([0, 0.204082, 0.408163, ..., 9.59184, 9.79592, 10], dtype=float32)
Generate array with constant values:
(zeros SHAPE &key DTYPE)(ones SHAPE &key DTYPE)(full SHAPE &optional VALUE &key DTYPE)
(full '(5 5) 2333)
array([[2333, 2333, 2333, 2333, 2333], [2333, 2333, 2333, 2333, 2333], [2333, 2333, 2333, 2333, 2333], [2333, 2333, 2333, 2333, 2333], [2333, 2333, 2333, 2333, 2333]], dtype=uint64)- Generate coordinate grids:
(meshgrid arrays &key SPARES INDEXING)(meshgrid (arange -2 2) (arange -2 2))(array([[-2, -1, 0, 1], [-2, -1, 0, 1], [-2, -1, 0, 1], [-2, -1, 0, 1]], dtype=float32) array([[-2, -2, -2, -2], [-1, -1, -1, -1], [0, 0, 0, 0], [1, 1, 1, 1]], dtype=float32))
3.3.3. Indexing mlx-array
at: API like Python'sarr[...](at array &rest indexs)(at* array &rest indexs)(alias for(lisp<- (at ...)))
the
indexscould be(~ [start=0] stop [step=1] &key step)(~ * * -1)(equal to(~ :step -1))(~ 0 * -1)(equal to(~ 0 -1 -1))(~ 5)(equal to(~ 0 5 1))(~ )or just~(equal to(~ 0 -1 -1))
- integer
- rational for first / last (negative) parts of axis
- keywords for shortcuts, for example:
:*for all:firstfor the first element on the corresponding axis:lastfor the last element the corresponding axis- use
(documentation keyword 'mlx:slice)to get the documentation of slice shortcuts documentations
Examples:
take the all
(:*) elements in first axis, second (2) element in second axis,[0, 2)elements in third axis:(let ((arr (reshape (arange 0 27) '(3 3 3)))) (at arr :* 1 (~ 0 2)))
array([[3, 4], [12, 13], [21, 22]], dtype=float32)this can also bewritten as:
(let ((arr (reshape (arange 0 27) '(3 3 3)))) (at arr :all :second 1/2))
array([[3, 4], [12, 13], [21, 22]], dtype=float32)which means take all (
:all) elements in first axis, second (:second) element in second axis, first half (1/2) in third axis.this is equal to calling MLX Python's API like:
import mlx.core as mx print(mx.arange(0, 27).reshape((3, 3, 3))[:,1,0:2])
array([[3, 4], [12, 13], [21, 22]], dtype=int32)take all element in the first axis, the
0, 2, 3th elements on the second axis:(let ((arr (reshape (arange 0 20) '(2 10)))) (at arr :* '(0 2 3)))
array([[0, 2, 3], [10, 12, 13]], dtype=float32)this is equal to calling MLX Python's API like:
import mlx.core as mx print(mx.arange(0, 20).reshape((2, 10))[:,[0, 2, 3]])
array([[0, 2, 3], [10, 12, 13]], dtype=int32)
Dev Note: You can use
defmlx-sliceto define alias of the slice shortcuts. For example, the shortcuts of:allcan be defined as:(defmlx-slice :all (shape) "Slice for all SHAPE. " (~ 0 shape 1))
The documentation of mlx slice can be show by:
(documentation :* 'at)
Get all elements of axis in mlx-array. Return (~ 0 SHAPE 1).
3.3.4. Operations
MLX-CL overwrites a few of Common Lisp's methods (cl:+, cl:-, cl:*, cl:/, …)
as generic functions. So if you don't worry about some speed lost, you can do
(+ 1 (* 2 3) (/ 10 5))
9 (4 bits, #x9, #o11, #b1001)
as if you are using cl:+, cl:-, cl:*, cl:/ (other functions are the same).
For those functions that are not supported in normal common lisp functions:
(+ 2 '(3 4 5))
array([5, 6, 7], dtype=float32)
they would be convert into mlx-array automatically. Use lisp<- to force
convert mlx-array as lisp value.
A example
the following example came from my Image Processing homework:
(defun gauss-kernel (sigma &optional m
&aux
(m-min (1+ (* 2 (ceiling (* 3 sigma)))))
(m-val (the (or null (integer 0)) (or m m-min))))
"Return a Gauss kernel matrix.
Definition:
gauss(x, y) = exp(- (x^2 + y^2) / (2 * sigma^2)) / (2 * pi * sigma^2)
"
(declare (type (real 0) sigma)
(type (or null integer) m))
(when (lisp<- (< m-val m-min))
(warn "Given m=~A is lower than m_min=~A. " m-val m-min))
(let ((half (/ (1- m-val) 2)))
(destructuring-bind (x y)
(meshgrid (arange (- half) (1+ half))
(arange (- half) (1+ half)))
(let ((ker (exp (- (/ (+ (square x) (square y))
(* 2 (square sigma)))))))
(/ ker (sum ker))))))
which would produce:
(gauss-kernel 0.3)
array([[1.47169e-05, 0.00380683, 1.47169e-05],
[0.00380683, 0.984714, 0.00380683],
[1.47169e-05, 0.00380683, 1.47169e-05]], dtype=float32)
3.4. Random
The package mlx-cl.random is a collection of PRNG processes in MLX.
(loop :repeat 3 :collect (list (rnd:uniform) (rnd:uniform :key 0)))
((array(0.0390083, dtype=float32) array(0.418457, dtype=float32)) (array(0.42697, dtype=float32) array(0.418457, dtype=float32)) (array(0.976773, dtype=float32) array(0.418457, dtype=float32)))
the API in mlx-cl.random use :key as the random seed locally,
or you could use mlx.rnd:seed to set a seed globally.
(rnd:seed 2333) (rnd:uniform 10 :shape '(4 4))
array([[1.27809, 2.80545, 6.38505, 6.99261],
[3.23088, 3.66074, 1.126, 0.481315],
[9.99442, 4.19935, 2.59734, 9.24549],
[1.32391, 1.80017, 5.53488, 4.57218]], dtype=float32)
3.5. FFT
The package mlx-cl.fft is a collection of FFT processes binding in MLX.
(fft:fft #(1 2 3 4))
array([10+0j, -2+2j, -2+0j, -2-2j], dtype=complex64)
and inverse FFT:
(fft:ifft (fft:fft #(1 2 3 4)))
array([1+-0j, 2+-0j, 3+0j, 4+-0j], dtype=complex64)
Note: in order to acclerate, mlx.fft:1dfft, mlx.fft:2dfft would be faster.
3.6. I/O
The high-level API of reading and writing mlx-array is:
(mlx-array pathname &key ...)(save mlx-array &key output ...)
Behind the scene, it calls:
(load-from source format &key)(save-to object output format &key)
Use these functions if failed to guess correct format keyword from
the pathname.
If you want to define your own format reading and writing functionalities,
you can use the: (defmlx-extension (format . extensions) &rest options).
Define the implementation of mlx FORMAT.
Add EXTENSIONS to `*mlx-supported-extensions*' as FORMAT.
Syntax:
(defmlx-extension (format . extensions)
((:load-path path :lambda-list ... :element-type ...)
load-doc
load-code)
((:load stream :lambda-list ... :element-type ...)
load-doc
load-code)
((:save-path arr path :lambda-list ... :element-type ...)
save-doc
save-code)
((:save arr stream :lambda-list ... :element-type ...)
save-doc
save-code))
Currently implemented io submodules are:
mlx-cl/io/npy: read and write NPY file via marcoheisig/numpy-file-formatmlx-cl/io/png: read and write PNG file via pngload and zpngmlx-cl/io/jpeg: read and write JPEG file via sharplispers/cl-jpegmlx-cl/io/tiff: read and write TIFF file via slyrus/retrospectiff
4. Contributing and Developing
DEV Note:
- You could load system
mlx-cl/devto quickly loadmlx-clandmlx-cl/test, together with a set of indent rules to use with SLY/SLIME in Emacs. There's some helpful Emacs scripts under
devwhich could help with developing MLX-CLread more in MLX-CL/DEV/ELISP