@threads and @generated clash?

metaprogramming
multithreading

#1

Is it possible that @threads cannot be used in a @generated function? Is there a workaround?

@generated function test(::Val{N}) where N
   quote
      Base.Threads.@threads for i = 1:$N
         i^2
      end
   end
end

test(Val{5}())

# ERROR: generated function body is not pure. this likely means it contains a closure or comprehension.

#2

Yes, it is possible and it is true afaik. Make the part that gets @generated be uninvolved with threading and then thread the generated stuff.


#3

simply putting the @generated stuff into an inner function works fine.


#4

is this a fundamental limitation or just due to the current implementation of either @generated or @threads?


#5

It is part and parcel of the nature of @generated, what it means and how it does.
Do not consider it a limitation of @generated, think of it as better design choice.

@generated function ... return that_which_is_generated end

that_which_is_generated is not a conventionally returned value – it is an expression to be used in refining the way the your logic and intent governs the way your code dispatches and is dispatched. It is a more_deeply_type_aware_refinement of program source text that @generated returns.


#6

What is it that you want it to accomplish?


#7

Write an inner nested loop of (essentially) the form

for i1 = 1:N-M+1, i2 = i1+1:N-M+2, ..., iM = I{M-1}+1:N 
   # do something
end

where M is a type parameter, so the loop can just be written out. I am aiming for M in the range up to at least 5. I created a macro to generate this loop

"""
`@symm`: symmetrises a loop over a cartesian range. For example
for i1 = a0:a1-2, i2 = i1+1:a1-1, i3 = i2+1:a1
   dosomething(i1, i2, i3)
end
may be written as
@symm 3 for i = a0:a1
   dosomething(i[1], i[2], i[3])
end
here, `i` is a `CartesianIndex`.
"""
macro symm(N, ex)
   if N isa Symbol
      N = eval(N)
   end
   @assert ex.head == :for
   @assert length(ex.args) == 2
   ex_for = ex.args[1]
   ex_body = ex.args[2]
   # iteration symbol
   i = ex_for.args[1]
   # lower and upper bound
   a0 = ex_for.args[2].args[1]
   a1 = ex_for.args[2].args[2]
   # create the for-loop without body
   loopstr = "for $(i)1 = ($a0):(($a1)-$(N-1))"
   for n = 2:N
      loopstr *= ", $i$n = $i$(n-1)+1:(($a1)-$(N-n))"
   end
   loopstr *= "\n $i = SVector{$N, Int}($(i)1"
   for n = 2:N
      loopstr *= ", $i$n"
   end
   loopstr *= ") \n end"
   loopex = parse(loopstr)
   append!(loopex.args[2].args, ex_body.args)
   # return the expression
   esc(quote
      $loopex
   end)
end

It actually seems to be working ok, though whenever I mess around with meta-programming there end up being some lose ends left. So I’ll be grateful about any pointers.

Anyhow, since I call this macro from a function where M is passed in as a type parameter, I believe that I needed a @generated function here:

@generated function myfun!(::MyType{N}) where N
   quote
         # get some upper and lower iteration bounds 
         a0, a1 =  1, 20 # get these somewhere useful normally  
         @symm $N for J = a0:a1
              # compute with J 
         end 
   end
end

But I’ll be thrilled to learn if I’m wrong. Or indeed, if anybody can tell me a better approach altogether. (I probably could just create an iterator which might be only marginally slower, but I’ll admit it just seemed more fun to do it with a macro.)


#8

(do not make macros your “goto” solution when working in Julia – prefer the very helpful non-macro-writing ways to get from here to there.)


#9

In general I agree, but here I’m not so convinced that writing an iterator would have been more pleasant to be honest.


#10

Well – eye of the beholder, perhaps, yet we all like our efforts to be less brittle and more flexible and easier to adapt – that is the rationale.

As one becomes more conversant with Julia, there is more simplicity. Its compelling.


#11

about your code – I dunno


#12

anyhow - thank you for answering. And I may still replace the macro with an iterator, we’ll see . . .


#13

Yes, that’s due to the closure generated by the macro as shown in the error message. It’s a pretty deep limitation though it is potentially fixable. Note that @generated does not interfere at all with threading, it’s the closure used by threading that is affected.


#14

Thanks for clarifying!