On Friday, 1 February 2013 02:23:34 UTC+8, Timothy Baldridge wrote:
At this point I'm wondering if we should define what the end goal of this project is. Thus far we've basically said "An abstract API for performing matrix operations". But what does that actually mean?
I've tried my best to articulate it in the README / Wiki - improvement patches welcome!
core.matrix needs to be more than just an API. In addition it needs:
1. Default implementations that are good enough to do meaningful numerical computing in Clojure
2. A plug-in system that supports other matrix implementations under the same API (an SPI, if you like....)
1) makes core.matrix useful in its own right without extra dependencies and gives us something comparable to NumPy
2) makes it extensible, so we can support an ecosystem of implementations (including many already very good implementations available from the Java world) and do more advanced stuff (e.g. the plan based workflows)
A plan based workflow implementation doesn't necessarily need to be part of core.matrix itself, but it should absolutely be compatible with the same API / SPI if at all possible.
When I see many of the operations currently in the API, I start thinking of my experience with working with video/audio processing. For a stereo audio stream is really a matrix of doubles [channels x number of samples]. A video frame is a matrix of [width x height x elements per pixel].
Many of the operations we're looking at in this library apply quite cleanly to these sort of operations. For instance, mixing two audio streams:
(add (scale audio-stream1 mix-amount-1) (scale audio-stream2 mix-amount-2))
In this example, it would be a poor implementation to have each operation return a new matrix for each operation as that would kill the CPU cache not to mention the cost of memory allocation.
You always have the in-place operations. scale!, add! and suchlike are designed for efficient in-place updates of this type.
I think we also need an "add-multiple!" function or something similar - multiply and add is a sufficiently common case that I think it deserves it's own API function, and it is important because it can often be optimised by the underlying implementation.
Perhaps the easy answer is to do something like we currently have for sub-views. Perhaps on some implementations add, scale, etc. Would return a lazy view who's contents are only calculated once a mget operations is called.
But it does sound like some people are talking of a more involved implementation that would allow for a more functional and less declarative approach. For instance, something as simple as map:
(defkernel scale [v amount]
(* v amount))
(matrix-map (partial scale 0.5) my-matrix)
An implementation that supports something like that will lock us out of LLVM, and the GPU, and keep us firmly in the GPU camp (for the most part), as dealing with closures opens up a huge can of worms when it comes to generating code for a GPU for instance.
I think we need to avoid converting to closures for exactly this reason. I think we actually want some form of expression format that can be maniplulated like an AST. My contrived version:
(defexpression scale [v amount]
(* v amount)) ;;; returns an expression AST object.
(def scale-half (partial-expression [v] (scale v 0.5))) ;;; returns a new expression
(apply-expression! scale-half my-matrix)
If we make the expression object as a defrecord/deftype that implements IFn, then we could also do:
(scale-half my-matrix)
How to execute the expression is then an implementation choice:
- A default implementation could turn the expression into a closure and use "emap" to map over all elements. Slower but general purpose.
- A GPU implementation could compile the expression into GPU code
- An LLVM implementation could compile down to native code and run this, exploiting SSE instructions etc.