Passing boolean settings to module to parse fast kernels via macros

I have a module that builds high performance kernels, based on user settings. I would like to construct them using settings that are provided by the user that imports the Kernels module. What would be the most logical way to pass the variables that I now set as global constants from the user side to the module, such that the settings are known at the time the macro is parsed? I am new to Julia and probably overlook something obvious, or do something in a non-Julian way. This is a highly simplified minimal working example of my code:

module Kernels

export kernel

# These should be removed.
const do_advection = true
const do_diffusion = false
const do_source = true

macro make_kernel()
    ex_rhs = :( 0 )
    if do_advection
        ex_rhs = :($ex_rhs .+ 1)
    end
    if do_diffusion
        ex_rhs = :($ex_rhs .+ 2)
    end
    if do_source
        ex_rhs = :($ex_rhs .+ 4)
    end

    ex = :( a .+= $ex_rhs )

    println(ex)
    return esc(ex)
end

function kernel(a)
    @make_kernel
end

end


using .Kernels

# I would like to be able to set the settings here.

a = zeros(10)
kernel(a)
println(a)

Macros are expanded during parsing, so if you want to make them user configurable I’d pass any custom settings you wish to use in front of the expression you want the macro to operate on as a key=val expression.

An alternative would be (if you want the setting to be global) to use a Ref instead of a plain value.

2 Likes

But how? I have followed your suggestion, yet I do not know how to get my settings into the macro in the code below:

module Kernels

export kernel

macro make_kernel(do_advection, do_diffusion, do_source)
    ex_rhs = :( 0 )
    if do_advection
        ex_rhs = :($ex_rhs .+ 1)
    end
    if do_diffusion
        ex_rhs = :($ex_rhs .+ 2)
    end
    if do_source
        ex_rhs = :($ex_rhs .+ 4)
    end

    ex = :( a .+= $ex_rhs )

    println(ex)
    return esc(ex)
end

function kernel(a, do_advection, do_diffusion, do_source)
    @make_kernel(do_advection, do_diffusion, do_source)
end

end


using .Kernels

const do_advection = true
const do_diffusion = true
const do_source = true

a = zeros(10)
kernel(a, do_advection, do_diffusion, do_source)
println(a)

Most commonly just pass the parameters as parameters to the function (do not use macros) for that:

julia> Base.@kwdef struct Parameters
           do_diffusion::Bool = true
           do_source::Bool = true
       end
Parameters

julia> function make_kernel(;p=Parameters())
           if p.do_diffusion
               println("do diffusion")
           end
           if p.do_source
               println("do source")
           end
       end
make_kernel (generic function with 1 method)

julia> make_kernel() # default parameters
do diffusion
do source

julia> make_kernel(p=Parameters(do_diffusion=false))
do source


1 Like

How can I make a single line statement with that solution? Can you show how my example would look like? In the actual use case the statement is inside a triple nested loop. Following your solution, it would end up with three separate loops, and this is why I am using macros.

If what you want is to elimitate the conditional inside an inner loop, you may want to use multiple dispatch. For example:

julia> function my_kernel(;p=Parameters())
           for i in 1:2
               for j in 1:2
                   inner_function(Val(p.do_source),Val(p.do_diffusion))
               end
           end
       end
my_kernel (generic function with 2 methods)

julia> inner_function(::Val{true},::Val{true}) = println("do both")
inner_function (generic function with 1 method)

julia> inner_function(::Val{false},::Val{true}) = println("do diffusion")
inner_function (generic function with 2 methods)

julia> inner_function(::Val{true},::Val{false}) = println("do source")
inner_function (generic function with 3 methods)

julia> inner_function(::Val{false},::Val{false}) = println("do nothing")
inner_function (generic function with 4 methods)

julia> Base.@kwdef struct Parameters
           do_diffusion::Bool = true
           do_source::Bool = true
       end
Parameters

julia> my_kernel()
do both
do both
do both
do both

julia> my_kernel(p=Parameters(do_source=false))
do diffusion
do diffusion
do diffusion
do diffusion

julia> my_kernel(p=Parameters(do_source=true,do_diffusion=false))
do source
do source
do source
do source

julia> my_kernel(p=Parameters(do_source=false,do_diffusion=false))
do nothing
do nothing
do nothing
do nothing

This eliminates the conditional inside the loop.

I’m not sure if this is the best solution for your problem, though, probably more information would be needed for actually suggesting something more specific.

(to really get this to be performant (more than just adding a conditional), you would need to somehow pass the values as parameters to the my_kernel function. Probably add a function barrier there).

1 Like

The actual use case is here: https://github.com/Chiil/MicroHH.jl/blob/main/src/Dynamics.jl#L43. I made a macro-based finite difference stencil generator, but at the moment all processes are standard on. I wanted to make this more sophisticated by letting the user decide which terms are included in the expression, without losing performance. Given that I have four options at the moment, that would mean 2^4 functions to be defined following your solution, which is doable, but at the same time it would be great if that effort could be prevented.

My two cents is that then you can use a macro to create all the inner functions, instead of making that dependent on the user choices. But others may know better if that is a good alternative.