-
-
Notifications
You must be signed in to change notification settings - Fork 95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add einsum, closes #124 #363
Conversation
- full contraction is working - transposition is working - hopefully more? The above two are in tests
All features are now working. The code is a mess though :)
Excellent.
Feel free to use Arraymancer/src/private/ast_utils.nim Lines 42 to 44 in 25cf5e3
I don't mind adding float only at first, working with types in macros is tricky nim-lang/RFCs#44 but I usually manage to find a way for those to work so if you struggle just use floats at first.
We should probably have an assert or compile-time error or at the very-least a documentation paragraph that says "Please use the explicit form of einstein summation, for example
No problem, we can also consider this syntax einsum(res, a, b):
let res[i,k] = a[i,j] * b[j,k] instead of let res = einsum(a, b):
res[i,k] = a[i,j] * b[j,k]
Yes it's correct How do I run the test from the nimble file? Is every test_ file run in the tests directory? Add it to:
|
Also refactor out shape assertions into its own proc.
Introduces a probably questionable interpretation of the explicit macro usage by assigning explicit case to a `let` var of the chosen identifier. Unfortunately the following is invalid syntax: ```nim einsum(a): let b[j,i] = a[i,j] ``` otherwise we could let the user choose.
Ok, finally did some refactoring. Regarding the explicit / implicit syntax. Unfortunately the syntax you proposed: einsum(res, a, b):
let res[i,k] = a[i,j] * b[j,k] is not valid Nim syntax (throws a So right now I just introduced a dual behavior. Implicit is mapped to just the Not sure in general if in general the rather subtle, profound change in the way edit: I also started adding a file that contains examples that are supposed to fail. I know that |
The types of all tensors given as arguments to `einsum` must match. Thus we check whether they are all the same. If they are, we use the type of the first tensor.
In both cases `einsum` again just returns a tensor.
Instead of working with the given tensors, we now create local copies, which are made contiguous and (if required) converted to row major order. This way our iteration should be more efficient, in case column major tensors are present.
This way the right most indices of the accessor will be the inner most loops. Since we force row major ordering, those indices will be closest together.
There's still some stuff I would consider changing (I'm not happy with the main macro code, to be honest; but refactoring more seemed like writing procs with tons of arguments :/) and I'm not sure if the documentation will come out fine (almost no RST experience). Aside from that I consider this PR to be mostly done for now. We now create local tensors with edit: For a complex example, the code created now looks like this: let b = einsum(m, n):
b[p,s,t,u,v] = m[p,q,r,s] * n[t,u,q,v,r]
# will expand to
let b = block:
type
T0Mangle = getSubType(type(m))
when T0Mangle isnot getSubType(type(m)):
{.error: "All tensors must be of the same type! " &
$"m" & " is of " & "type " &
$typeName(getSubType(type(m))) & " while " &
$"m" & " is of type " &
$typeName(getSubType(type(m))) & "!".}
elif T0Mangle isnot getSubType(type(n)):
{.error: "All tensors must be of the same type! " &
$"m" & " is of " & "type " &
$typeName(getSubType(type(m))) & " while " &
$"n" & " is of type " &
$typeName(getSubType(type(n))) & "!".}
let mCont = asContiguous[getSubType(type(m))](m, layout = rowMajor, force = true)
let nCont = asContiguous[getSubType(type(n))](n, layout = rowMajor, force = true)
doAssert nCont.rank ==
5
var shapes = newSeq[int](5)
[]=(shapes, 0,
mCont.shape[0])
[]=(shapes, 1,
mCont.shape[3])
[]=(shapes, 2,
nCont.shape[0])
[]=(shapes, 3,
nCont.shape[1])
[]=(shapes, 4,
nCont.shape[3])
var shapesContr = newSeq[int](2)
[]=(shapesContr, 0,
nCont.shape[2])
[]=(shapesContr, 1,
nCont.shape[4])
var tmp = newTensor[getSubType(type(m))](shapes)
for p in 0 ..<
shapes[0]:
for s in 0 ..<
shapes[1]:
for t in 0 ..<
shapes[2]:
for u in 0 ..<
shapes[3]:
for v in 0 ..<
shapes[4]:
var res: getSubType(type(m))
for q in 0 ..<
shapesContr[0]:
for r in 0 ..<
shapesContr[1]:
res += mCont[p, q, r, s] * nCont[t, u, q, v, r]
tmp[p, s, t, u, v] = res
tmp |
Regarding name_mangling you are probably mixing that up with laser: https://github.com/numforge/laser/blob/bf751f4bbec3d178cd3a80da73e446658d0f8dff/laser/openmp.nim#L13-L25 var mangling_rng {.compileTime.} = initRand(0x1337DEADBEEF)
var current_suffix {.compileTime.} = ""
proc omp_suffix*(genNew: static bool = false): string {.compileTime.} =
## genNew:
## if false, return the last suffix
## else return a fresh one
# This is exported because you cannot bind the symbol early enough
# for exportc
if genNew:
current_suffix = mangling_rng.rand(high(uint32)).toHex
result = current_suffix I don't even mangle the layers in the neural network DSL: Arraymancer/src/nn_dsl/dsl_types.nim Lines 83 to 85 in 25cf5e3
|
This avoids problems, if the user hands a tensor with the identifier `tmp`, `shape`, `shapeContr` or `res`.
Oh, then I saw it in laser. I now use |
This PR adds the Einstein summation via the
einsum
macro to Arraymancer.It's still a WIP, but all features described here:
https://rockt.github.io/2018/04/30/einsum
are implemented and tested in the tests (as a bonus, taking the diagonal of a tensor also works, which according to the article does not in
pytorch
;) ).The major thing still missing is actually taking the type of the input tensor into account. At the moment I just map to
float
tensors.The code has to be clean up and refactored in parts. Documentation also has to be added.
In principle the macro works as follows. Assuming we have two (theoretically also only one or more tensors) tensors
a
,b
, we can use Einstein summation as follows:to express a matrix multiplication implicitly or:
to assign the resulting indices explicitly. In the former case we calculate the indices that will be contracted automatically and assign the result along the axis specified by the order in which non contracting indices appear. The explicit usage allows to also transpose the result directly (e.g. exchange
i, k
->k, i
in theres
LHS above). In some cases the explicit form is strictly necessary, e.g. if one wants to only iterate over (not contract) an axis, but keep it in the result. All tests are done in implicit and explicit form. If no implicit form is tested, it means the test case does not make sense without the constraints from the explicit LHS (or would yield a different result, e.g. look at the Hadamard product) in a hypothetical implicit form:normal Einstein summation demands a full contraction of the tensors. Only by explicitly stating to keep both axes:
can it be expressed. This means that the implicit form favors actual Einstein summation, i.e. an index appearing more than once is summed over.
Note that the identifier used on the LHS of the statement in the explicit case does not actually matter. It can be the same as what you assign to, but it can be something else entirely too.
The implementation simply writes nested for loops over the non contracting axes and within over the contracting axes and assigns the result.
I open the PR already, because I'm unsure about the file structure of this.
tensors
subdirectory correct?test_
file run in the tests directory?