Skip to content

Strategy Parameters

This guide explains the Strategy Parameters system in CTBase. Parameters are singleton types that allow a strategy to specialize its metadata and default options depending on the execution context (e.g., CPU vs GPU).

Prerequisites

Read the Implementing a Strategy guide first. Parameters extend the strategy system with type-based specialization.

Concept

Strategy parameters are singleton types that enable:

  • Type-based dispatch so the same strategy struct can carry different defaults on CPU vs GPU

  • Compile-time specialization through Julia's type system

  • Registry-level routing so a method tuple like (:mysolver, :cpu) resolves to the right concrete type

Parameters are not runtime values — they exist purely for dispatch and metadata specialization.

Built-in Parameters

CTBase ships two built-in parameters:

julia
Strategies.id(Strategies.CPU)
:cpu
julia
Strategies.id(Strategies.GPU)
:gpu
julia
Strategies.description(Strategies.CPU)
"CPU-based computation"

describe shows full introspection for a parameter type:

julia
Strategies.describe(Strategies.CPU)
CPU (parameter)
├─ id: :cpu
├─ hierarchy: CPU → AbstractStrategyParameter
└─ description: CPU-based computation

Parameter Contract

Every parameter type must:

  1. Subtype AbstractStrategyParameter

  2. Be a singleton (no fields)

  3. Implement id(::Type{<:YourParameter}) returning a Symbol

  4. Implement description(::Type{<:YourParameter}) returning a String

julia
struct Distributed <: Strategies.AbstractStrategyParameter end
Strategies.id(::Type{Distributed}) = :distributed
Strategies.description(::Type{Distributed}) = "Distributed multi-node execution"
Strategies.describe(Distributed)
Distributed (parameter)
├─ id: :distributed
├─ hierarchy: Distributed → AbstractStrategyParameter
└─ description: Distributed multi-node execution

Parameter Validation

julia
Strategies.is_a_parameter(Strategies.CPU)   # true
true
julia
Strategies.is_a_parameter(Int)              # false
false
julia
Strategies.parameter_id(Strategies.CPU)        # :cpu
:cpu

validate_parameter_type checks the full contract and returns nothing if valid:

julia
Strategies.validate_parameter_type(Strategies.CPU)

Parameterized Strategy: Step-by-Step

A parameterized strategy is a generic struct over P <: AbstractStrategyParameter. The metadata method is specialized on Type{MyStrategy{P}}, letting Julia dispatch select the right defaults for each parameter.

Step 1 — Define the strategy family and struct

julia
abstract type AbstractFakeOptimizer <: Strategies.AbstractStrategy end

struct FakeOptimizer{P <: Strategies.AbstractStrategyParameter} <: AbstractFakeOptimizer
    options::Strategies.StrategyOptions
end

Step 2 — Implement id

All parameter variants share the same ID:

julia
Strategies.id(::Type{<:FakeOptimizer}) = :fake_optimizer

Step 3 — Parameter-specific default helpers

julia
__fake_default_precision(::Type{Strategies.CPU}) = :float64
__fake_default_precision(::Type{Strategies.GPU}) = :float32

Step 4 — Implement metadata specialized on the parameterized type

The dispatch is on ::Type{FakeOptimizer{P}} — not on a second argument:

julia
function Strategies.metadata(::Type{FakeOptimizer{P}}) where {P <: Strategies.AbstractStrategyParameter}
    return Strategies.StrategyMetadata(
        Options.OptionDefinition(
            name        = :precision,
            type        = Symbol,
            default     = __fake_default_precision(P),
            description = "Numerical precision (:float64 for CPU, :float32 for GPU)",
            computed    = true,
        ),
        Options.OptionDefinition(
            name        = :max_iter,
            type        = Int,
            default     = 1000,
            description = "Maximum number of iterations",
        ),
    )
end

Mark computed options with computed=true

An option whose default value depends on the parameter type P should be marked computed=true. It is evaluated at metadata construction time, not hard-coded. This flag is optional but strongly recommended: describe separates computed options by parameter (showing the actual default for each), making the parameter-specific behavior immediately visible to users.

Let's verify the metadata for each parameter — the :precision default differs:

julia
Strategies.metadata(FakeOptimizer{Strategies.CPU})
StrategyMetadata with 2 options:

├─ precision::Symbol (default: float64 [computed])
description: Numerical precision (:float64 for CPU, :float32 for GPU)

└─ max_iter::Int64 (default: 1000)
   description: Maximum number of iterations
julia
Strategies.metadata(FakeOptimizer{Strategies.GPU})
StrategyMetadata with 2 options:

├─ precision::Symbol (default: float32 [computed])
description: Numerical precision (:float64 for CPU, :float32 for GPU)

└─ max_iter::Int64 (default: 1000)
   description: Maximum number of iterations

Step 5 — Implement the constructor

The constructor is specialized on the parameterized type so build_strategy_options calls the right metadata:

julia
function FakeOptimizer{P}(; mode::Symbol = :strict, kwargs...) where {P <: Strategies.AbstractStrategyParameter}
    opts = Strategies.build_strategy_options(FakeOptimizer{P}; mode = mode, kwargs...)
    return FakeOptimizer{P}(opts)
end

Step 6 — Instantiate and inspect

julia
FakeOptimizer{Strategies.CPU}()
FakeOptimizer{CPU} (instance, id=:fake_optimizer)
├─ max_iter = 1000  [default]
└─ precision = float64  [computed]
Tip: use describe(FakeOptimizer) to see all available options.
julia
FakeOptimizer{Strategies.GPU}()
FakeOptimizer{GPU} (instance, id=:fake_optimizer)
├─ max_iter = 1000  [default]
└─ precision = float32  [computed]
Tip: use describe(FakeOptimizer) to see all available options.
julia
FakeOptimizer{Strategies.CPU}(max_iter = 500)
FakeOptimizer{CPU} (instance, id=:fake_optimizer)
├─ max_iter = 500  [user]
└─ precision = float64  [computed]
Tip: use describe(FakeOptimizer) to see all available options.

Option access works exactly like non-parameterized strategies:

julia
solver = FakeOptimizer{Strategies.GPU}(max_iter = 200)
FakeOptimizer{GPU} (instance, id=:fake_optimizer)
├─ max_iter = 200  [user]
└─ precision = float32  [computed]
Tip: use describe(FakeOptimizer) to see all available options.
julia
julia> solver[:precision]
:float32

julia> solver[:max_iter]
200

julia> Strategies.source(Strategies.options(solver), :max_iter)
:user

Registering Parameterized Strategies

In create_registry, a parameterized strategy is declared as a (StrategyType, [Param1, Param2, ...]) tuple. Non-parameterized strategies are listed as plain types:

julia
registry = Strategies.create_registry(
    AbstractFakeOptimizer => (
        (FakeOptimizer, [Strategies.CPU, Strategies.GPU]),
    ),
)
StrategyRegistry with 1 family and 2 parameters:
├─ AbstractFakeOptimizer
│  └─ FakeOptimizer (id=:fake_optimizer) [:cpu, :gpu]
└─ parameters: :cpuCPU, :gpuGPU

The registry expands this into one concrete type per parameter. strategy_ids deduplicates:

julia
Strategies.strategy_ids(AbstractFakeOptimizer, registry)
(:fake_optimizer,)
julia
Strategies.type_from_id(:fake_optimizer, AbstractFakeOptimizer, registry; parameter=Strategies.CPU)
Main.FakeOptimizer{CTBase.Strategies.CPU}
julia
Strategies.type_from_id(:fake_optimizer, AbstractFakeOptimizer, registry; parameter=Strategies.GPU)
Main.FakeOptimizer{CTBase.Strategies.GPU}

Building Strategies from the Registry

build_strategy accepts an optional parameter type as second argument:

julia
Strategies.build_strategy(:fake_optimizer, Strategies.CPU, AbstractFakeOptimizer, registry;
    max_iter = 300)
FakeOptimizer{CPU} (instance, id=:fake_optimizer)
├─ max_iter = 300  [user]
└─ precision = float64  [computed]
Tip: use describe(FakeOptimizer) to see all available options.
julia
Strategies.build_strategy(:fake_optimizer, Strategies.GPU, AbstractFakeOptimizer, registry)
FakeOptimizer{GPU} (instance, id=:fake_optimizer)
├─ max_iter = 1000  [default]
└─ precision = float32  [computed]
Tip: use describe(FakeOptimizer) to see all available options.

Method Tuple Routing

When using a method tuple (e.g., (:fake_optimizer, :cpu)), extract_global_parameter_from_method reads the parameter token from the registry:

julia
method = (:fake_optimizer, :cpu)
param = Strategies.extract_global_parameter_from_method(method, registry)
CTBase.Strategies.CPU
julia
id = Strategies.extract_id_from_method(method, AbstractFakeOptimizer, registry)
Strategies.build_strategy(id, param, AbstractFakeOptimizer, registry)
FakeOptimizer{CPU} (instance, id=:fake_optimizer)
├─ max_iter = 1000  [default]
└─ precision = float64  [computed]
Tip: use describe(FakeOptimizer) to see all available options.

Mixed Registries

A registry can mix parameterized and non-parameterized strategies in the same family:

julia
struct FallbackOptimizer <: AbstractFakeOptimizer
    options::Strategies.StrategyOptions
end

Strategies.id(::Type{<:FallbackOptimizer}) = :fallback

function Strategies.metadata(::Type{<:FallbackOptimizer})
    return Strategies.StrategyMetadata(
        Options.OptionDefinition(
            name = :max_iter, type = Int, default = 500,
            description = "Maximum iterations",
        ),
    )
end

function FallbackOptimizer(; mode::Symbol = :strict, kwargs...)
    opts = Strategies.build_strategy_options(FallbackOptimizer; mode = mode, kwargs...)
    return FallbackOptimizer(opts)
end

mixed_registry = Strategies.create_registry(
    AbstractFakeOptimizer => (
        FallbackOptimizer,
        (FakeOptimizer, [Strategies.CPU, Strategies.GPU]),
    ),
)
StrategyRegistry with 1 family and 2 parameters:
├─ AbstractFakeOptimizer
│  ├─ FallbackOptimizer (id=:fallback)
│  └─ FakeOptimizer (id=:fake_optimizer) [:cpu, :gpu]
└─ parameters: :cpuCPU, :gpuGPU
julia
Strategies.strategy_ids(AbstractFakeOptimizer, mixed_registry)
(:fallback, :fake_optimizer)

describe with a Registry

describe with a registry shows the full picture including available parameters:

julia
Strategies.describe(:fake_optimizer, registry)
FakeOptimizer (strategy)
├─ id: :fake_optimizer
├─ hierarchy: FakeOptimizer → AbstractFakeOptimizer → AbstractStrategy
├─ family: AbstractFakeOptimizer
├─ parameters: CPU, GPU

├─ computed options for CPU:
│  └─ precision::Symbol (default: float64 [computed])
│     description: Numerical precision (:float64 for CPU, :float32 for GPU)

├─ computed options for GPU:
│  └─ precision::Symbol (default: float32 [computed])
│     description: Numerical precision (:float64 for CPU, :float32 for GPU)

└─ common options (1 option):
   └─ max_iter::Int64 (default: 1000)
      description: Maximum number of iterations

Summary

AspectDescription
PurposeCompile-time specialization of strategy defaults and metadata
ContractSingleton struct + id + description implementations
Built-inCPU, GPU
Metadata dispatchmetadata(::Type{MyStrategy{P}}) where {P}
Registry syntax(MyStrategy, [CPU, GPU]) tuple inside the family tuple
Builderbuild_strategy(id, Param, Family, registry; kwargs...)
Validationvalidate_parameter_type, is_a_parameter, parameter_id

See Also