Skip to content

Remove unnecessary assignment to fix type instability in ForwardDiff.hessian! - #316

Merged
jrevels merged 1 commit into
JuliaDiff:masterfrom
chriselrod:master
Jun 18, 2018
Merged

Remove unnecessary assignment to fix type instability in ForwardDiff.hessian!#316
jrevels merged 1 commit into
JuliaDiff:masterfrom
chriselrod:master

Conversation

@chriselrod

@chriselrod chriselrod commented Apr 11, 2018

Copy link
Copy Markdown
Contributor

Before:

julia> using Traceur

julia> @trace ForwardDiff.hessian!(hr2, f2, max_density_params, cfg2)
(ForwardDiff.hessian!)(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#f2, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}, ::Val{true}) at /home/chris/.julia/v0.6/ForwardDiff/src/hessian.jl:47
  result is assigned as DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}} at line 47
  result is assigned as Core.Box at line 47
  dynamic dispatch to ($(QuoteNode(DiffResults.hessian)))((Core.getfield)(_9::Core.Box, :contents)) at line 54
  dynamic dispatch to ($(QuoteNode(DiffResults.gradient)))((Core.getfield)(_9::Core.Box, :contents)) at line 54
  returns Any
(ForwardDiff.hessian!)(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#f2, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}) at /home/chris/.julia/v0.6/ForwardDiff/src/hessian.jl:47
  returns Any
MutableDiffResult(14774.342219725911, ([1.50629e-8, 2.16231e-7, -1.52464e-7, -1.84819e-8, -4.70467e-8, -1.71915e-7, 1.96066e-7, -1.81974e-7], [86.4397 1.3505 -86.697 42.0846 18.541 41.7884 60.8334 80.3928; 1.3505 11.59 38.7777 -5.84322 8.49931 13.6476 20.3259 26.363; -86.697 38.7777 734.801 -4.81614 -134.535 -161.619 -167.768 -166.47; 42.0846 -5.84322 -4.81614 54.096 -17.7162 -23.4584 -21.8144 -20.9364; 18.541 8.49931 -134.535 -17.7162 256.49 -0.0 -0.0 -0.0; 41.7884 13.6476 -161.619 -23.4584 -0.0 502.43 -0.0 -0.0; 60.8334 20.3259 -167.768 -21.8144 -0.0 -0.0 694.565 -0.0; 80.3928 26.363 -166.47 -20.9364 -0.0 -0.0 -0.0 825.356]))


julia> @code_warntype ForwardDiff.hessian!(hr2, f2, max_density_params, cfg2)
Variables:
  #self#::ForwardDiff.#hessian!
  result::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}
  f::#f2
  x::Array{Float64,1}
  cfg::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}

Body:
  begin 
      return $(Expr(:invoke, MethodInstance for hessian!(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::Function, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}, ::Val{true}), :(#self#), :(result), :(f), :(x), :(cfg), :($(QuoteNode(Val{true}())))))
  end::Any

After:

julia> using Traceur

julia> @trace ForwardDiff.hessian!(hr2, f2, max_density_params, cfg2)
MutableDiffResult(14774.342219725911, ([1.50629e-8, 2.16231e-7, -1.52464e-7, -1.84819e-8, -4.70467e-8, -1.71915e-7, 1.96066e-7, -1.81974e-7], [86.4397 1.3505 -86.697 42.0846 18.541 41.7884 60.8334 80.3928; 1.3505 11.59 38.7777 -5.84322 8.49931 13.6476 20.3259 26.363; -86.697 38.7777 734.801 -4.81614 -134.535 -161.619 -167.768 -166.47; 42.0846 -5.84322 -4.81614 54.096 -17.7162 -23.4584 -21.8144 -20.9364; 18.541 8.49931 -134.535 -17.7162 256.49 -0.0 -0.0 -0.0; 41.7884 13.6476 -161.619 -23.4584 -0.0 502.43 -0.0 -0.0; 60.8334 20.3259 -167.768 -21.8144 -0.0 -0.0 694.565 -0.0; 80.3928 26.363 -166.47 -20.9364 -0.0 -0.0 -0.0 825.356]))

julia> @code_warntype ForwardDiff.hessian!(hr2, f2, max_density_params, cfg2)
Variables:
  #self#::ForwardDiff.#hessian!
  result::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}
  f::#f2
  x::Array{Float64,1}
  cfg::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}

Body:
  begin 
      return $(Expr(:invoke, MethodInstance for hessian!(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::Function, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f2,Float64},Float64,8,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},8},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f2,Float64},Float64,8},1}}}, ::Val{true}), :(#self#), :(result), :(f), :(x), :(cfg), :($(QuoteNode(Val{true}())))))
  end::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}

The actual performance difference is negligible, although if the type instabilities "infected" other parts of your code, it could be a problem. Before:

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
BenchmarkTools.Trial: 
  memory estimate:  6.08 KiB
  allocs estimate:  14
  --------------
  minimum time:     17.984 μs (0.00% GC)
  median time:      18.906 μs (0.00% GC)
  mean time:        19.678 μs (1.30% GC)
  maximum time:     1.341 ms (96.11% GC)
  --------------
  samples:          10000
  evals/sample:     1

after

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
BenchmarkTools.Trial: 
  memory estimate:  6.05 KiB
  allocs estimate:  12
  --------------
  minimum time:     18.235 μs (0.00% GC)
  median time:      19.136 μs (0.00% GC)
  mean time:        19.949 μs (1.60% GC)
  maximum time:     1.660 ms (96.77% GC)
  --------------
  samples:          10000
  evals/sample:     1

These benchmarks were prior to merging the commits for specializing gradients on input functions, after which:

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
BenchmarkTools.Trial: 
  memory estimate:  5.39 KiB
  allocs estimate:  11
  --------------
  minimum time:     17.653 μs (0.00% GC)
  median time:      20.097 μs (0.00% GC)
  mean time:        20.798 μs (1.66% GC)
  maximum time:     1.791 ms (96.75% GC)
  --------------
  samples:          10000
  evals/sample:     1

@KristofferC

Copy link
Copy Markdown
Collaborator

function extract_gradient!(::Type{T}, result::DiffResult, dual::Dual) where {T}
result = DiffResults.value!(result, value(T, dual))
result = DiffResults.gradient!(result, partials(T, dual))
return result
end

same thing?

@chriselrod

chriselrod commented Apr 11, 2018

Copy link
Copy Markdown
Contributor Author

It looks like the exact same problem can happen, but testing below, inference seems fine on 0.6 (for this toy example):

julia> const S = randn(10,4) |> x -> x' * x;

julia> f(x) = x' * S * x / 2

julia> x = randn(4);

julia> using ForwardDiff, Traceur

julia> hr = DiffResults.HessianResult(x);

julia> gcfg = ForwardDiff.GradientConfig(f, x);

julia> hcfg = ForwardDiff.HessianConfig(f, hr, x);

julia> @trace ForwardDiff.gradient!(hr, f, x, gcfg);

julia> @trace ForwardDiff.hessian!(hr, f, x, hcfg);
WARNING: Method definition hessian!(DiffResults.DiffResult{O, V, D} where D<:Tuple where V where O, Any, AbstractArray{T, N} where N where T) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47.
WARNING: Method definition hessian!(DiffResults.DiffResult{O, V, D} where D<:Tuple where V where O, Any, AbstractArray{T, N} where N where T, ForwardDiff.HessianConfig{T, V, N, DG, DJ} where DJ where DG where N where V) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47.
WARNING: Method definition hessian!(DiffResults.DiffResult{O, V, D} where D<:Tuple where V where O, Any, AbstractArray{T, N} where N where T, ForwardDiff.HessianConfig{T, V, N, DG, DJ} where DJ where DG where N where V, Base.Val{CHK}) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47.
WARNING: Method definition extract_gradient!(Type{T}, DiffResults.DiffResult{O, V, D} where D<:Tuple where V where O, Real) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:63 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:63.
WARNING: Method definition extract_gradient!(Type{T}, DiffResults.DiffResult{O, V, D} where D<:Tuple where V where O, ForwardDiff.Dual{T, V, N} where N where V<:Real where T) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:70 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:70.
WARNING: Method definition vector_mode_gradient!(Any, F, Any, ForwardDiff.GradientConfig{T, V, N, D} where D where N where V) in module ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:102 overwritten at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:102.
(ForwardDiff.hessian!)(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#f, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f,Float64},Float64,10,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},10},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},1}}}, ::Val{true}) at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47
  result is assigned as DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}} at line 47
  result is assigned as Core.Box at line 47
  dynamic dispatch to (DiffResults.hessian)((Core.getfield)(_9::Core.Box, :contents)) at line 54
  dynamic dispatch to (DiffResults.gradient)((Core.getfield)(_9::Core.Box, :contents)) at line 54
  returns Any
(ForwardDiff.hessian!)(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#f, ::Array{Float64,1}, ::ForwardDiff.HessianConfig{ForwardDiff.Tag{#f,Float64},Float64,10,Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},10},1},Tuple{Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},1},Array{ForwardDiff.Dual{ForwardDiff.Tag{#f,Float64},Float64,10},1}}}) at /home/celrod/.julia/v0.6/ForwardDiff/src/hessian.jl:47
  returns Any

julia> ForwardDiff.chunksize(gcfg) == length(x)
true

julia> ydual = ForwardDiff.vector_mode_dual_eval(f, x, gcfg);

julia> @which ForwardDiff.extract_gradient!(ForwardDiff.Tag{typeof(f),Float64}, hr, ydual)
extract_gradient!(::Type{T}, result::DiffResults.DiffResult, dual::ForwardDiff.Dual) where T in ForwardDiff at /home/celrod/.julia/v0.6/ForwardDiff/src/gradient.jl:70

julia> @trace ForwardDiff.extract_gradient!(ForwardDiff.Tag{typeof(f),Float64}, hr, ydual)
MutableDiffResult(14.576177213652889, ([-21.5326, -1.40267, -2.76614, -4.14998], [16.7264 -0.141016 1.7357 5.34906; -0.141016 8.72789 -1.63618 -4.21548; 1.7357 -1.63618 2.34548 1.39758; 5.34906 -4.21548 1.39758 8.88728]))

Perhaps things are simple enough to inference to succeed, but nesting gradients didn't cause a problem either:

julia> g(x) = sum(abs2, DiffResults.gradient(ForwardDiff.gradient!(gr, f, x, gcfg3)))
g (generic function with 1 method)

julia> gcfg2 = ForwardDiff.GradientConfig(g, x);

julia> const gr = DiffResults.GradientResult(gcfg2.duals);

julia> const gcfg3 = ForwardDiff.GradientConfig(f, gcfg2.duals);

julia> @trace ForwardDiff.gradient!(hr, g, x, gcfg2)
MutableDiffResult(390.562342477784, ([556.839, -55.7623, -647.817, -61.8146], [11.211 0.709601 -9.07282 -1.09975; 0.709601 4.64805 1.76466 -0.0891536; -9.07282 1.76466 13.865 0.435675; -1.09975 -0.0891536 0.435675 5.1435]))

julia> g2(x) = sum(abs2, DiffResults.gradient(ForwardDiff.gradient!(gr3, f, x, gcfg6)))
g2 (generic function with 1 method)

julia> h(x) = sum(abs2, DiffResults.gradient(ForwardDiff.gradient!(gr2, g2, x, gcfg5)))
h (generic function with 1 method)

julia> gcfg4 = ForwardDiff.GradientConfig(h, x);

julia> const gr2 = DiffResults.GradientResult(gcfg4.duals);

julia> const gcfg5 = ForwardDiff.GradientConfig(g2, gcfg4.duals);

julia> const gr3 = DiffResults.GradientResult(gcfg5.duals);

julia> const gcfg6 = ForwardDiff.GradientConfig(f, gcfg5.duals);

julia> @trace ForwardDiff.gradient!(hr, h, x, gcfg4)
MutableDiffResult(736667.2051329048, ([1.06113e6, -83656.7, -1.23535e6, -1.02606e5], [11.211 0.709601 -9.07282 -1.09975; 0.709601 4.64805 1.76466 -0.0891536; -9.07282 1.76466 13.865 0.435675; -1.09975 -0.0891536 0.435675 5.1435]))

julia> @code_warntype ForwardDiff.gradient!(hr, h, x, gcfg4, Val(true))
Variables:
  #self# <optimized out>
  result::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}
  f::#h
  x::Array{Float64,1}
  cfg::ForwardDiff.GradientConfig{ForwardDiff.Tag{#h,Float64},Float64,4,Array{ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4},1}}
  #temp# <optimized out>

Body:
  begin 
      goto 2
      2:  # line 34:
      unless (4 === (Base.arraylen)(x::Array{Float64,1})::Int64)::Bool goto 8 # line 35:
      $(Expr(:invoke, MethodInstance for vector_mode_gradient!(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#h, ::Array{Float64,1}, ::ForwardDiff.GradientConfig{ForwardDiff.Tag{#h,Float64},Float64,4,Array{ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4},1}}), :(ForwardDiff.vector_mode_gradient!), :(result), :(f), :(x), :(cfg)))
      goto 11
      8:  # line 37:
      $(Expr(:invoke, MethodInstance for chunk_mode_gradient!(::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}, ::#h, ::Array{Float64,1}, ::ForwardDiff.GradientConfig{ForwardDiff.Tag{#h,Float64},Float64,4,Array{ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4},1}}), :(ForwardDiff.chunk_mode_gradient!), :(result), :(f), :(x), :(cfg)))
      11:  # line 39:
      return result::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}
  end::DiffResults.MutableDiffResult{2,Float64,Tuple{Array{Float64,1},Array{Float64,2}}}

julia> eltype(gcfg6)
ForwardDiff.Dual{ForwardDiff.Tag{#f,ForwardDiff.Dual{ForwardDiff.Tag{#g2,ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4}},ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4},4}},ForwardDiff.Dual{ForwardDiff.Tag{#g2,ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4}},ForwardDiff.Dual{ForwardDiff.Tag{#h,Float64},Float64,4},4},4}

So it seems to be inferring correctly.
Because the DiffResults.value! function is not always mutating:
https://github.com/JuliaDiff/DiffResults.jl/blob/d49bd6d78c1f600bca237467b375941bb5a3caf8/src/DiffResults.jl#L161

We should either leave it as is, or to be safe, rename the assigned result every time:

function extract_gradient!(::Type{T}, result::DiffResult, dual::Dual) where {T}
    result2 = DiffResults.value!(result, value(T, dual))
    result3 = DiffResults.gradient!(result2, partials(T, dual))
    return result3
end

@jrevels

jrevels commented Apr 11, 2018

Copy link
Copy Markdown
Member

The original example is happening because result is a closed-over binding that's getting reassigned, so it's hitting JuliaLang/julia#15276. Examples where the binding is not referencing a closed-over variable thus should not exhibit the problem, as you've found.

This is really a Julia compiler problem rather than a ForwardDiff problem, but if you care to put in the effort for a workaround here, the most straightforward strategy would probably be to manually instantiate a callable struct with the correct type parameters instead of using the closure.

@chriselrod

chriselrod commented Apr 14, 2018

Copy link
Copy Markdown
Contributor Author

I'll manually instantiate a struct. Will get to it soon.

@chriselrod

chriselrod commented Apr 14, 2018

Copy link
Copy Markdown
Contributor Author

I was considering a few more complicated routes to avoid allocating the mutable struct GradF more than once, but because this would require both a DiffResult and a HessianConfig, it'd need some API changes.
So I figured I'd just go with your suggestion.

EDIT:
Prior to the last commit:

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
BenchmarkTools.Trial: 
  memory estimate:  5.39 KiB
  allocs estimate:  11
  --------------
  minimum time:     16.030 μs (0.00% GC)
  median time:      18.445 μs (0.00% GC)
  mean time:        18.849 μs (1.43% GC)
  maximum time:     1.451 ms (96.41% GC)
  --------------
  samples:          10000
  evals/sample:     1

following it:

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
BenchmarkTools.Trial: 
  memory estimate:  5.39 KiB
  allocs estimate:  11
  --------------
  minimum time:     15.869 μs (0.00% GC)
  median time:      16.461 μs (0.00% GC)
  mean time:        17.133 μs (1.56% GC)
  maximum time:     1.396 ms (95.93% GC)
  --------------
  samples:          10000
  evals/sample:     1

....and 0.7:

julia> @benchmark ForwardDiff.hessian!($hr2, f2, $max_density_params, $cfg2)
┌ Warning: `indmin` is deprecated, use `argmin` instead.
│   caller = minimum at trials.jl:112 [inlined]
└ @ Core trials.jl:112
┌ Warning: `indmax` is deprecated, use `argmax` instead.
│   caller = maximum at trials.jl:117 [inlined]
└ @ Core trials.jl:117
BenchmarkTools.Trial: 
  memory estimate:  768 bytes
  allocs estimate:  4
  --------------
  minimum time:     12.273 μs (0.00% GC)
  median time:      12.563 μs (0.00% GC)
  mean time:        12.751 μs (0.00% GC)
  maximum time:     60.073 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1

@jrevels

jrevels commented Jun 18, 2018

Copy link
Copy Markdown
Member

Sorry for the late response, thanks for this!

@jrevels
jrevels merged commit a375e2f into JuliaDiff:master Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants