I have written a simple function in a .jl file that I can successfully differentiate using forward. However I am new to Julia and I do not understand how to see the generated source code for the differentiated function. I've tried all sorts of things like #code_lowered Zygote.forward(maxPool, [1.0, 2.0]) and #code_lowered Zygote.forward(maxPool) but they just show me the call to forward itself.
How can I see the code that Zygote generates for the forward and reverse passes?
using Pkg
using Zygote, ForwardDiff
function size1d(v)
return size(v)[1]
end
function max(a, b)
if a > b
a
else
b
end
end
function maxPool(v)
return [max(v[2 * i - 1], v[2 * i])
for i in 1:div(size1d(v), 2)]
end
v = [1.0, 2.0, 3.0, 4.0]
df = [20.0, 30.0]
println("maxPool(v):")
println(maxPool(v))
println()
println("maxAdjoint:")
maxAdjoint = Zygote.forward(max, 3.0, 4.0)[2]
println(maxAdjoint(1.0))
println()
println("maxPoolAdjoint:")
maxPoolAdjoint = Zygote.forward(maxPool, v)[2]
println(maxPoolAdjoint(df))
Zygote has its own macro Zygote.#code_adjoint for showing the lowered adjoint code, i.e. the code that generates the gradient of a function in reverse mode. I'm not sure about forward mode though.
Here's a simple example in reverse mode:
julia> using Zygote
julia> f(x) = 2x + 1
f (generic function with 1 method)
julia> #code_lowered f(1)
CodeInfo(
1 ─ %1 = 2 * x
│ %2 = %1 + 1
└── return %2
)
julia> Zygote.#code_adjoint f(1)
Zygote.Adjoint(1: (%3, %4 :: Zygote.Context, %1, %2)
%5 = Zygote._forward(%4, Main.:*, 2, %2)
%6 = Base.getindex(%5, 1)
%7 = Base.getindex(%5, 2)
%8 = Zygote._forward(%4, Main.:+, %6, 1)
%9 = Base.getindex(%8, 1)
%10 = Base.getindex(%8, 2)
return %9
, 1: (%1)
%2 = (#10)(%1)
%3 = Zygote.gradindex(%2, 2)
%4 = (#7)(%3)
%5 = Zygote.gradindex(%4, 3)
%6 = Zygote.tuple(nothing, %5)
return %6
)
We might worry from the length and apparent complexity of this lowered adjoint code that the gradient is slow, but we can check the LLVM code to make sure everything ends up being elided away:
julia> #code_llvm f'(1)
; # /Users/mason/.julia/packages/Zygote/SAZMM/src/compiler/interface.jl:50 within `#34'
define i64 #"julia_#34_18250"(i64) {
top:
ret i64 2
}
Related
I want to have a curried version of a function. So, I write the code as follows:
f(x::Int64, y::Int64) = x + y
f(x::Int64) = (y::Int64) -> f(x, y)
But I am not sure if Julia considers this an example of a type-unstable definition. On the face of it, one of the methods returns an anonymous function, while another returns an Int64. Yet, when the curried version is applied, the final result is also an Int64.
So, my questions are:
Is this code type-stable?
If not, is there a way to have a curried version of a function without writing type-unstable code?
Thanks in advance.
Yes, it is.
According to the official doc, you can investigate it by using the #code_warntype macro:
julia> #code_warntype f(1, 5)
MethodInstance for f(::Int64, ::Int64)
from f(x::Int64, y::Int64) in Main at REPL[2]:1
Arguments
#self#::Core.Const(f)
x::Int64
y::Int64
Body::Int64
1 ─ %1 = (x + y)::Int64
└── return %1
The arguments of this function have the exact type Int64, and as we can see in the Body::Int64, the inferred return type function is Int64.
Furthermore, we have f(x) which is based on the type-stable function f(x, y):
julia> #code_warntype f(1)
MethodInstance for f(::Int64)
from f(x::Int64) in Main at REPL[15]:1
Arguments
#self#::Core.Const(f)
x::Int64
Locals
#3::var"#3#4"{Int64}
Body::var"#3#4"{Int64}
1 ─ %1 = Main.:(var"#3#4")::Core.Const(var"#3#4")
│ %2 = Core.typeof(x)::Core.Const(Int64)
│ %3 = Core.apply_type(%1, %2)::Core.Const(var"#3#4"{Int64})
│ (#3 = %new(%3, x))
└── return #3
Here as well, there's not any unstable defined parameter type.
Look at the following as an example of an unstable-typed function:
julia> unstF(X) = x*5
unstF (generic function with 1 method)
julia> #code_warntype unstF(1)
MethodInstance for unstF(::Int64)
from unstF(X) in Main at REPL[17]:1
Arguments
#self#::Core.Const(unstF)
X::Int64
Body::Any
1 ─ %1 = (Main.x * 5)::Any
└── return %1
If you try this in the REPL, you'll see the Any appears with a red color. Since we have the Body::Any (Any with the red color), we can conclude that the returned object by this function is a non-concrete type object. Because the compiler doesn't know what is the x (Note that the input is X). So the result can be Anything! So this function is type-unstable for (here, for integer inputs. Note that you should investigate the type-stability of your function by your desired input(s). E.g., #code_warntype f(5) and #code_warntype f(5.) should be observed if I can pass it Float64 or Int64 either).
i am new in Julia. i read a doc about julia static-analysis. it gives a function.
function foo(x,y)
z = x + y
return 2 * z
end
and use the julia introspection function code_typed get output:
code_typed(foo,(Int64,Int64))
1-element Array{Any,1}:
:($(Expr(:lambda, {:x,:y}, {{:z},{{:x,Int64,0},{:y,Int64,0},{:z,Int64,18}},{}},
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64))))
it has an Expr . but when i call code_typed, the output is :
code_typed(foo, (Int64,Int64))
1-element Vector{Any}:
CodeInfo(
1 ─ %1 = Base.add_int(x, y)::Int64
│ %2 = Base.mul_int(2, %1)::Int64
└── return %2
) => Int64
it has a CodeInfo.it is different with the output in doc.
does julia have some change? and how can i get the Exprs according to my function and argtypes?
That code snippet appears to be taken from https://www.aosabook.org/en/500L/static-analysis.html, which was published in 2016 (about two years before the release of Julia 1.0) and references Julia version 0.3 from 2015.
Julia 1.0 gives
julia> code_typed(foo,(Int64,Int64))
1-element Array{Any,1}:
CodeInfo(
2 1 ─ %1 = (Base.add_int)(x, y)::Int64 │╻ +
3 │ %2 = (Base.mul_int)(2, %1)::Int64 │╻ *
└── return %2 │
) => Int64
while more current versions such as 1.6 and 1.7 give
julia> code_typed(foo,(Int64,Int64))
1-element Vector{Any}:
CodeInfo(
1 ─ %1 = Base.add_int(x, y)::Int64
│ %2 = Base.mul_int(2, %1)::Int64
└── return %2
) => Int64
(a much more minor change, but nonetheless)
If for any reason, you want this result in the form of an array or vector of Exprs, you seem to be able to get this using (e.g.)
julia> t = code_typed(foo,(Int64,Int64));
julia> t[1].first.code
3-element Vector{Any}:
:(Base.add_int(_2, _3))
:(Base.mul_int(2, %1))
:(return %2)
though this is likely considered an implementation detail and liable to change between minor versions of Julia.
I have read that Julia has Macros but I am unsure if the Macros Julia provide are the ones I am thinking about.
I have the following expression:
Global.data[
Dates.value(Dates.Year(dtCursor)) - 2000,
Dates.value(Dates.Month(dtCursor)),
Dates.value(Dates.Day(dtCursor)),
Dates.value(Dates.Hour(dtCursor)) + 1,
Dates.value(Dates.Minute(dtCursor)) + 1,
1
]
And I repeat this a lot. I am wondering if I could have a macro that with dtCursor as parameter (it might be other variables in other cases) types all that for me. I am therefore looking for the Macro expansion functionality which was traditionally found in Macro assemblers.
I definitively do not want to include this as a function as this code is executed tens of thousands of times and therefore I do not want to add the overhead of a function call.
I have tried:
macro readData(_dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year(_dtCursor)) - 2000,
Dates.value(Dates.Month(_dtCursor)),
Dates.value(Dates.Day(_dtCursor)),
Dates.value(Dates.Hour(_dtCursor)) + 1,
Dates.value(Dates.Minute(_dtCursor)) + 1,
value
]
)
end
And later be invoked by:
println(#readData(dtCursor, 1))
Where dtCursor is a DateTime variable.
But I am getting:
ERROR: LoadError: UndefVarError: _dtCursor not defined
I have read https://docs.julialang.org/en/v1/manual/metaprogramming/index.html#man-macros-1 but a bit of help understanding what to do in this case is really welcomed.
Use a function
I definitively do not want to include this as a function as this code is executed tens of thousands of times and therefore I do not want to add the overhead of a function call.
You are definitively wrong.
You might be right in some languages, but not in JuliaLang.
(I do think this is a very useful questiom though because can highlight for others not to do this 😀)
That function-call in-lines away, and even if it didn't we have other tools (#inline) we would want to use before using a macro.
Macro's are for syntactic transformations.
If you are not doing a syntactic tranformation think again before using macros.
Here is a link to a good point made during by Steven G. Johnson at his keynote in juliacon:
"Functions are mostly good enough for Jeff Bezanson. Don't try to outsmart Jeff Bezason"
How to write it as a Macro and as a Function
The following answers your original question question
using Dates
using BenchmarkTools
macro readData(_dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year($(esc(_dtCursor)))) - 2000,
Dates.value(Dates.Month($(esc(_dtCursor)))),
Dates.value(Dates.Day($(esc(_dtCursor)))),
Dates.value(Dates.Hour($(esc(_dtCursor)))) + 1,
Dates.value(Dates.Minute($(esc(_dtCursor)))) + 1,
$value
]
)
end
function readData(_dtCursor, value)
Global.data[
Dates.value(Dates.Year(_dtCursor)) - 2000,
Dates.value(Dates.Month(_dtCursor)),
Dates.value(Dates.Day(_dtCursor)),
Dates.value(Dates.Hour(_dtCursor)) + 1,
Dates.value(Dates.Minute(_dtCursor)) + 1,
value
]
end
Benchmark it.
You say this is going to be run on 10,000s of times.
So I will benchmark on 100_000 uses, just to be safe.
const Global = (; data=[join((y, m, d, h, M, s)," ") for y in 2000:2010, m in 1:3, d in 1:20, h in 1:10, M in 1:30, s in 1:30]);
size(Global.data)
length(Global.data)
const sample_dts = map(1:100_000) do _
y, m, d, h, M, s = rand.(axes(Global.data))
dt = DateTime(y+2000, m, d, h-1, M-1)
end;
func_demo() = [readData(dt, 3) for dt in sample_dts];
macro_demo() = [#readData(dt, 3) for dt in sample_dts];
#btime func_demo()
#btime macro_demo()
They benchmark as identical
julia> #btime macro_demo();
5.409 ms (3 allocations: 781.34 KiB)
julia> #btime func_demo();
5.393 ms (3 allocations: 781.34 KiB)
Infact they specialize into (basically) the same code.
julia> #code_typed macro_demo()
CodeInfo(
1 ─ %1 = Main.sample_dts::Core.Compiler.Const(DateTime[2002-01-18T04:19:00, 2001-01-19T08:22:00, 2006-02-08T04:07:00, 2011-01-08T09:03:00, 2006-02-10T06:18:00, 2002-03-12T00:05:00, 2011-02-20T08:29:00, 2011-02-20T07:12:00, 2005-01-13T03:22:00, 2006-01-01T00:29:00 …
2005-03-10T04:29:00, 2002-03-12T09:11:00, 2002-03-11T00:28:00, 2007-02-12T02:26:00, 2003-02-15T07:29:00, 2009-01-01T02:02:00, 2009-
01-03T02:11:00, 2001-02-16T03:16:00, 2004-01-17T05:12:00, 2010-02-02T05:10:00], false)
│ %2 = %new(Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))}, getfield(Main, Symbol("##50#51"))(), %1)::Base.Gen
erator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))}
│ %3 = invoke Base.collect(%2::Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##50#51"))})::Array{String,1}
└── return %3
) => Array{String,1}
julia> #code_typed getfield(Main, Symbol("##50#51")).instance(1) # check the internals
│ %1 = %1 = Main.Global::Core.Compiler.Const((#==GIANT Inlined Const ==#)
│ %2 = Base.getfield(%1, :data)::Array{String,6}
│ %3 = Base.sub_int(dt, 2000)::Int64
│ %4 = Base.add_int(dt, 1)::Int64
│ %5 = Base.add_int(dt, 1)::Int64
│ %6 = Base.arrayref(true, %2, %3, dt, dt, %4, %5, 3)::String
└── return %6
) => String
julia> #code_typed func_demo()
CodeInfo(
1 ─ %1 = Main.sample_dts::Core.Compiler.Const(DateTime[2002-01-18T04:19:00, 2001-01-19T08:22:00, 2006-02-08T04:07:00, 2011-01-08T09:03:00, 2006-02-10T06:18:00, 2002-03-12T00:05:00, 2011-02-20T08:29:00, 2011-02-20T07:12:00, 2005-01-13T03:22:00, 2006-01-01T00:29:00 … 2005-03-10T04:29:00, 2002-03-12T09:11:00, 2002-03-11T00:28:00, 2007-02-12T02:26:00, 2003-02-15T07:29:00, 2009-01-01T02:02:00, 2009-
01-03T02:11:00, 2001-02-16T03:16:00, 2004-01-17T05:12:00, 2010-02-02T05:10:00], false)
│ %2 = %new(Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))}, getfield(Main, Symbol("##43#44"))(), %1)::Base.Gen
erator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))}
│ %3 = invoke Base.collect(%2::Base.Generator{Array{DateTime,1},getfield(Main, Symbol("##43#44"))})::Array{String,1}
└── return %3
) => Array{String,1}
julia> #code_typed getfield(Main, Symbol("##43#44")).instance(1)
CodeInfo(
1 ─ %1 = Main.Global::NamedTuple{(:data,),Tuple{Array{String,6}}}
│ %2 = Base.getfield(%1, :data)::Array{String,6}
│ %3 = Base.sub_int(dt, 2000)::Int64
│ %4 = Base.add_int(dt, 1)::Int64
│ %5 = Base.add_int(dt, 1)::Int64
│ %6 = Base.arrayref(true, %2, %3, dt, dt, %4, %5, 3)::String
└── return %6
) => String
There is a very minor difference in the generors function between the two.
Wheree the value became a Compliler.Const or a NamedTuple when inlining,
but after that goes the LLVM that difference goes way too I think
(Check #code_llvm if your really interested. But we are already super deap into the weeds.)
This is probably the wrong code to be optimizing in the first place.
A long with the guidance to benchmark any optimization you do.
One should also profile the code to decide what is worth optimizing.
A function that is only called 10,000s of times and not allocating giant arrays etc, probably not worth worrying too much about.
Especially if you are just worrying about function call overhead,
which is only a handful of CPU cycles.
You have to splice in the variables you pass as macro arguments:
julia> macro readData(dtCursor, value)
return :(
Global.data[
Dates.value(Dates.Year($dtCursor)) - 2000,
Dates.value(Dates.Month($dtCursor)),
Dates.value(Dates.Day($dtCursor)),
Dates.value(Dates.Hour($dtCursor)) + 1,
Dates.value(Dates.Minute($dtCursor)) + 1,
$value
]
)
end
#readData (macro with 1 method)
julia> #macroexpand #readData(dtCursor, 1)
:((Main.Global).data[(Main.Dates).value((Main.Dates).Year(Main.dtCursor)) - 2000, (Main.Dates).value((Main.Dates).Month(Main.dtCursor)), (Main.Dates).value((Main.Dates).Day(Main.dtCursor)), (Main.Dates).value((Main.Dates).Hour(Main.dtCursor)) + 1, (Main.Dates).value((Main.Dates).Minute(Main.dtCursor)) + 1, 1])
Furthermore, Julia macros are hygenic; that means that there will be no confusion about the name _dtCursor in the macro definition, and the name dtCursor at call site. One thing you might need to do is to escape the inputs, though.
Also, this might be an overkill. You should benchmark the macro version against the function version; maybe, there's enough inlining happening that the macro doesn't actually matter.
I have issue after calling my macro:
#introspectable square(x) = x * x
Then when calling
square(3)
i should be able to get 9, cause the function call has been specialized to execute an attribute of the structure which is Julia code, however when I enter the macro, the code seems to be directly evaluated.
What i have tried:
struct IntrospectableFunction
name
parameters
native_function
end
(f::IntrospectableFunction)(x) = f.native_function(x)
macro introspectable(expr)
name = expr.args[1].args[1]
parameters = tuple(expr.args[1].args[2:end]...)
body = expr.args[2].args[2]
:( global $name = IntrospectableFunction( :( name ), $parameters, :( body ) ))
end
#introspectable square(x) = x * x
square(3)
The answer should be 9 , however i get "Object of type symbol are not callable ". However if i replace :( body ) with x -> x * x i get the desired result, my objective is generalizing the macro-call.
I usually find it easier to work with expressions in macros (it is not the shortest way to write things, but, from my experience, it is much easier to control what gets generated).
Therefore I would rewrite your code as:
macro introspectable(expr)
name = expr.args[1].args[1]
parameters = expr.args[1].args[2:end]
anon = Expr(Symbol("->"), Expr(:tuple, parameters...), expr.args[2].args[2])
constr = Expr(:call, :IntrospectableFunction, QuoteNode(name), Tuple(parameters), anon)
esc(Expr(:global, Expr(Symbol("="), name, constr)))
end
Now, as you said you wanted generality I would define your functor like this:
(f::IntrospectableFunction)(x...) = f.native_function(x...)
(in this way you allow multiple positional arguments to be passed).
Now let us test our definitions:
julia> #introspectable square(x) = x * x
IntrospectableFunction(:square, (:x,), getfield(Main, Symbol("##3#4"))())
julia> square(3)
9
julia> #macroexpand #introspectable square(x) = x * x
:(global square = IntrospectableFunction(:square, (:x,), ((x,)->x * x)))
julia> #introspectable toarray(x,y) = [x,y]
IntrospectableFunction(:toarray, (:x, :y), getfield(Main, Symbol("##5#6"))())
julia> toarray("a", 10)
2-element Array{Any,1}:
"a"
10
julia> #macroexpand #introspectable toarray(x,y) = [x,y]
:(global toarray = IntrospectableFunction(:toarray, (:x, :y), ((x, y)->[x, y])))
julia> function localscopetest()
#introspectable globalfun(x...) = x
end
localscopetest (generic function with 1 method)
julia> localscopetest()
IntrospectableFunction(:globalfun, (:(x...),), getfield(Main, Symbol("##9#10"))())
julia> globalfun(1,2,3,4,5)
(1, 2, 3, 4, 5)
julia> function f()
v = 100
#introspectable localbinding(x) = (v, x)
end
f (generic function with 1 method)
julia> f()
IntrospectableFunction(:localbinding, (:x,), getfield(Main, Symbol("##11#12")){Int64}(100))
julia> localbinding("x")
(100, "x")
(note that it is useful to use #macroexpand to make sure our macro works as expected)
EDIT - how to handle a minimal multiple dispatch
I am writing a non-macro example because it is related to the data structure:
Use e.g. such a definition:
struct IntrospectableFunction
name::Symbol
method_array::Vector{Pair{Type{<:Tuple}, Function}}
end
function (f::IntrospectableFunction)(x...)
for m in f.method_array
if typeof(x) <: first(m)
return last(m)(x...)
end
end
error("signature not found")
end
and now you can write:
julia> square = IntrospectableFunction(:square, [Tuple{Any}=>x->x*x,Tuple{Any,Any}=>(x,y)->x*y])
IntrospectableFunction(:square, Pair{DataType,Function}[Tuple{Any}=>##9#11(), Tuple{Any,Any}=>##10#12()])
julia> square(3)
9
julia> square(2,3)
6
Keep in mind that the approach I present is not perfect and universal - it just serves to give a very simple example how you could do it.
Is there a way I can localize memoization (via Memoize.jl) inside a function? or at least delete the dictionaries created by memoization?
Clarification: suppose I define a define a function f(x, y). I want to start with a fresh table for every new value of y. That is, given y = y0, f( . , y0) iterates on itself for x, x-1, etc but given a new y = y1, I don't need to store the old table for y0, so that memory can be freed up. How can I do that?
Solution:
cachedfib() = begin
global dict = Dict()
global dict2 = Dict()
function _fib(n::Int, a::Int)
if !haskey(dict2, a)
dict2[a] = true
dict = Dict()
end
if haskey(dict, (n, a))
return dict[(n, a)]
elseif n < 2
dict[(0, a)] = 0
dict[(1, a)] = 1
return dict[(n, a)]
else
dict[(n, a)] = a*(_fib(n - 1, a) + _fib(n - 2, a))
return dict[(n, a)]
end
end
end
fib = cachedfib()
fib(10, 1)
fib(10, 2)
now call dict and dict2 and check that the dictionaries are refreshed every time the second argument changes. You can get even better performance when the parameters to store are integers and you use Array instead of Dict
To use memoization technique you can do it with let or closures. Have a look at my rapid implementation of factorial (with closure).
with_cached_factorial() = begin
local _cache = [1] #cache factorial(0)=1
function _factorial(n)
if n < length(_cache)
println("pull out from the cache factorial($n)=$(_cache[n+1])")
_cache[n+1]
else
fres = n * _factorial(n-1)
push!(_cache, fres)
println("put factorial($n)=$fres into the cache of the size=$(sizeof(_cache))") #a
fres
end
end
end
Now, just use it:
julia> myf = with_cached_factorial()
_factorial (generic function with 1 method)
julia> myf(3)
pull out from the cache factorial(0)=1
put factorial(1)=1 into the cache of the size=16
put factorial(2)=2 into the cache of the size=24
put factorial(3)=6 into the cache of the size=32
6
julia> myf(5)
pull out from the cache factorial(3)=6
put factorial(4)=24 into the cache of the size=40
put factorial(5)=120 into the cache of the size=48
120
julia> myf(10)
pull out from the cache factorial(5)=120
put factorial(6)=720 into the cache of the size=56
put factorial(7)=5040 into the cache of the size=64
put factorial(8)=40320 into the cache of the size=72
put factorial(9)=362880 into the cache of the size=80
put factorial(10)=3628800 into the cache of the size=88
3628800
let Aold = nothing
global foo
function foo(A::AbstractArray)
if A == Aold
println("Same as last array")
else
Aold = A
end
nothing
end
end
Results:
julia> A = rand(2,2)
2x2 Array{Float64,2}:
0.272936 0.153311
0.299549 0.703668
julia> B = rand(2,2)
2x2 Array{Float64,2}:
0.6762 0.377428
0.493344 0.240194
julia> foo(A)
julia> foo(A)
Same as last array
julia> foo(B)
julia> foo(A)
julia> foo(A)
Same as last array
julia> Aold
ERROR: UndefVarError: Aold not defined