Skip to main content
Ctrl+K
Knit - Home Knit - Home
  • Knit

Guides

  • Quickstart
  • Tutorial
    • Part I — The guided tour
    • Part II — Refining the experiment
    • Part III — Searching for the best architecture
  • Stitch Guide
    • Bootstrap
    • Commands
    • Parameters
    • Types
    • Setup
    • Resources
    • Spack
    • Jobs
    • Apps
    • Parameter search
    • Recording
    • Results & artifacts
    • Provenance
    • Cleanup
    • Bundle & export
    • Profiles
    • Metadata
    • Query
    • AI
    • Logging & output
    • Plugins
    • Introspection
    • Agent skills

API Reference

  • Public API
  • Private API
  • Repository
  • Open issue
  • .rst

Parameter search

Contents

  • Declare a parameter’s search domain
  • Run a parameter sweep
  • Optimize a job with Bayesian search
    • Model the objective instead of optimizing it

Parameter search#

Exploring a declared parameter space: exhaustive sweeps with knit sweep and Bayesian optimization with the knit-ax plugin.

Declare a parameter’s search domain#

Attach a value set or range to a parameter with –domain for sweeps and optimization.

APIs: knit_with_optional, knit_with_required, knit_parameter_space, knit_parameter_space_json

A tunable parameter can carry a search domain — the set or range of values a parameter sweep or a Bayesian-optimization run may explore. Declare it with --domain <descriptor> on knit_with_optional / knit_with_required, quoting the descriptor so the shell does not brace-expand it:

# Declare each tunable parameter's search domain with --domain. Quote the
# descriptor: the shell would otherwise brace-expand {a,b,c}. --log marks a real
# range as log-scaled (a hint a Bayesian-optimization proposer reads; a sweep
# ignores it). The domain is validated against the parameter's type the first
# time it is read, not at registration.
@command "train" "Train a model over a search space."
@with_optional "lr:real"        "0.01" "Learning rate." --domain '[1e-5..1.0]' --log
@with_optional "batch:integer"  "32"   "Batch size."    --domain '{16,32,64,128}'
@with_optional "layers:integer" "4"    "Hidden layers." --domain '[2..12]'
@with_optional "data:string"    ""     "Training data." --domain '@glob:data/*.csv'
_train() {
    echo "lr=$(knit_get_parameter lr "$@") batch=$(knit_get_parameter batch "$@")"
}
@done
# Declare each tunable parameter's search domain with --domain. Quote the
# descriptor: the shell would otherwise brace-expand {a,b,c}. --log marks a real
# range as log-scaled (a hint a Bayesian-optimization proposer reads; a sweep
# ignores it). The domain is validated against the parameter's type the first
# time it is read, not at registration.
knit_register "train" _train "Train a model over a search space."
knit_with_optional "lr:real"        "0.01" "Learning rate." --domain '[1e-5..1.0]' --log
knit_with_optional "batch:integer"  "32"   "Batch size."    --domain '{16,32,64,128}'
knit_with_optional "layers:integer" "4"    "Hidden layers." --domain '[2..12]'
knit_with_optional "data:string"    ""     "Training data." --domain '@glob:data/*.csv'
_train() {
    echo "lr=$(knit_get_parameter lr "$@") batch=$(knit_get_parameter batch "$@")"
}
knit_done

The descriptor grammar is one set of literals: {a,b,c} is a value list, [lo..hi] / [lo..hi:step] a range, @glob:<pattern> a filesystem glob, and @file:<path> one value per line. --log adds a log-scale hint to a real range (a Bayesian-optimization proposer reads it; a sweep ignores it). The domain must match the parameter’s type — a range needs integer or real, a glob needs string, a list suits any type — and it is validated the first time it is read, not at registration, so a type mismatch surfaces at first use.

Discover the declared domains of a command with knit describe --space, which annotates each parameter with its domain (and emits a structured space object in the JSON and YAML formats). A search proposer reads the same domains programmatically through knit_parameter_space (structured, by nameref) and knit_parameter_space_json.

Run a parameter sweep#

Run a command, job, or app once per point in a cartesian product of values with knit sweep.

APIs: sweep, submit:drain, knit_with_optional, knit_parameter_space, knit_command_parameters

knit sweep runs a downstream command once per point in the cartesian product of a set of values per parameter. It is the exhaustive, no-model proposer: you write the exact invocation you would run once, and mark any value you want to vary in sweep-syntax. The sweep is opaque to the downstream command — it scans the tokens after -- for sweep-syntax, treats each as an axis, passes everything else through verbatim, and re-emits the reconstructed line once per combination.

A value is a swept axis when it is {a,b,c} (a list), [lo..hi] / [lo..hi:step] (a range), @glob:<pattern> or @file:<path>; quote the braces and brackets so the shell does not expand them. Everything else passes through. The downstream command carries an ordinary job here:

# An ordinary job whose tunable parameter carries a search --domain, so the sweep
# can draw its values from the declaration (--fill domain / --n @domain) as well as
# from an explicit set on the command line (--n '{1,2,4}').
@job "sim" "Run one trivial simulation."
@without_setup
@with_optional "n:integer"      "1" "Number of steps." --domain '{1,2,4}'
@with_table
@with_output   "result:integer" "0" "Twice the step count (a stand-in result)."
_sim() {
    local n
    n=$(knit_get_parameter "n" "$@")
    knit_output "result" "$(( n * 2 ))"
    printf 'sim: n=%s result=%s\n' "${n}" "$(( n * 2 ))"
}
@done
# An ordinary job whose tunable parameter carries a search --domain, so the sweep
# can draw its values from the declaration (--fill domain / --n @domain) as well as
# from an explicit set on the command line (--n '{1,2,4}').
knit_register_job "sim" _sim "Run one trivial simulation."
knit_without_setup
knit_with_optional "n:integer"      "1" "Number of steps." --domain '{1,2,4}'
knit_with_table
knit_with_output   "result:integer" "0" "Twice the step count (a stand-in result)."
_sim() {
    local n
    n=$(knit_get_parameter "n" "$@")
    knit_output "result" "$(( n * 2 ))"
    printf 'sim: n=%s result=%s\n' "${n}" "$(( n * 2 ))"
}
knit_done

Ephemeral: run the product now. Write the real invocation after -- and mark the swept values. The dispatcher you use (submit, run, or a plain command) selects what each point does; the sweep launches them in-process, keeping at most --max-inflight in flight (default 1):

# a job per point (3 submissions), grouped as a campaign:
$ ./exp.sh sweep -- submit --group mc --nodes {1,2} -- sim --n '{1,2,4}'
# an app per point, inside one allocation:
$ ./exp.sh sweep -- run --procs {1,2,4} -- stencil --tile '{64,128}'
# a plain command per point:
$ ./exp.sh sweep -- sim --n '{1,2,4}'

A --group you put on the submit line tags the jobs the sweep produces, so the campaign’s jobs share a jobs.group you can later drain, inspect, or remove as a unit. The sweep does not forward its own --group onto that line: a --group on sweep labels only the sweeps (parent) row, and the two are kept independent so you can gather jobs from several submits — by hand or under one or more sweeps — under one job-level group. Independently of any group, every point records an automatic call edge back to the sweep’s row, so a query can always walk a campaign to its children. --on-failure stop aborts on the first failed child (continue, the default, runs the whole product). --dry-run prints each reconstructed command line and exits without recording or running — the quickest way to see exactly what a sweep would do.

Vary a declared domain. When a parameter declares a --domain (see Declare a parameter’s search domain), the sweep can draw its values from the declaration instead of the command line: --param @domain opts one parameter in, --fill domain opts in every unspecified parameter with an enumerable domain, and --param @default opts one back out. A real-interval range is bounds-only and cannot be enumerated, so sweeping one is a hard error with a fix message. This is the one point where the sweep introspects the target, through the same accessors a Bayesian-optimization proposer uses (knit_parameter_space / knit_command_parameters):

$ ./exp.sh sweep --fill domain -- submit -- sim          # sim --n over its {1,2,4} domain
$ ./exp.sh sweep -- submit -- sim --n @domain            # same, opting --n in explicitly

Durable: prepare now, drain later (resumable). Swap -- submit for -- prepare and the sweep writes the whole product as a durable to-do list — one prepared job per point, no scheduler contact — then a separate submit drain releases them. This is the form to use for a large or long campaign, or whenever you want to resume after an interruption: a drainer that dies leaves the un-started points still prepared, so re-running drain resumes exactly the remainder (see Drain a prepared batch with submit drain and Retry failed or killed jobs):

# 1. prepare the product once (durable, grouped -- --group on prepare labels
#    the prepared jobs so the drain can select them):
$ ./exp.sh sweep -- prepare --group mc -- sim --n '{1,2,4}'
# 2. drain it -- from the login node, or inside a pilot job:
$ ./exp.sh submit drain --group mc --max-inflight 8 --reconcile --retry killed,failed

Resumability is not a sweep feature at all — it is a property of the prepare/drain queue the sweep writes into. Draining inside a knit_with_pilot job packs the swept jobs onto the pilot’s nodes with no second queue wait; a pilot killed at walltime is resumed by re-submitting it, where --reconcile first corrects the sub-jobs the hard kill stranded at running and --retry re-queues them. The sweep’s own --max-inflight / --on-failure govern only the ephemeral forms; durable concurrency, recovery, and failure policy are drain’s.

Optimize a job with Bayesian search#

Tune a job’s recorded output with the knit-ax plugin’s Bayesian ask/tell loop over a declared search domain, or fit a surrogate model and query it with ax predict.

APIs: knit_import, knit_with_optional, knit_with_output, knit_with_table, knit_get_parameter, knit_output

The knit-ax plugin runs Bayesian optimization over a knit command. Where knit sweep tries every point in a product, ax fits a model to the results so far and asks it where to look next — the right proposer when each trial is expensive and the space is large. Import the plugin, then write an ordinary job whose tunable parameter carries a search --domain and whose figure of merit is a declared output:

# An ordinary job with one tunable real knob carrying a search --domain and a
# declared numeric output. Each trial, "ax optimize" proposes an x within the
# domain, runs the job, reads the recorded output, and feeds it back to the
# model. The objective is closed-form here so the example is self-contained; a
# real job would run a simulation and record its figure of merit.
@job "quad" "Evaluate the surrogate objective."
@without_setup
@with_optional "x:real"     "0" "The knob to optimize." --domain '[0..12]'
# @job already declares the job's table (named "quad"); do not add @with_table,
# which would re-point it at the "submit:quad" command name and split recording
# from where the objective is read.
@with_output   "score:real" "0" "Objective = 100 - (x - 6)^2 (max 100 at x = 6)."
_quad() {
    local x score
    x=$(knit_get_parameter "x" "$@")
    score=$(awk -v x="${x}" 'BEGIN { printf "%.6f", 100 - (x - 6) * (x - 6) }')
    knit_output "score" "${score}"
    printf 'quad: x=%s score=%s\n' "${x}" "${score}"
}
@done
# An ordinary job with one tunable real knob carrying a search --domain and a
# declared numeric output. Each trial, "ax optimize" proposes an x within the
# domain, runs the job, reads the recorded output, and feeds it back to the
# model. The objective is closed-form here so the example is self-contained; a
# real job would run a simulation and record its figure of merit.
knit_register_job "quad" _quad "Evaluate the surrogate objective."
knit_without_setup
knit_with_optional "x:real"     "0" "The knob to optimize." --domain '[0..12]'
# @job already declares the job's table (named "quad"); do not add @with_table,
# which would re-point it at the "submit:quad" command name and split recording
# from where the objective is read.
knit_with_output   "score:real" "0" "Objective = 100 - (x - 6)^2 (max 100 at x = 6)."
_quad() {
    local x score
    x=$(knit_get_parameter "x" "$@")
    score=$(awk -v x="${x}" 'BEGIN { printf "%.6f", 100 - (x - 6) * (x - 6) }')
    knit_output "score" "${score}"
    printf 'quad: x=%s score=%s\n' "${x}" "${score}"
}
knit_done

A run is a campaign: a named, resumable record stored on disk under campaigns/<name>/ (a sibling of setups/ and jobs/, so knit bundle packs it). Two commands drive it, mirroring prepare then submit drain: ax prepare validates the request, resolves the objective, builds the search space, and records the campaign without running a trial; ax optimize starts — or resumes — the ask/tell loop toward the budget. First run and every resume are the same call, so a campaign interrupted at walltime continues from its persisted state:

# define the campaign (runs nothing); the objective is a declared output,
# the varying parameter is drawn from its domain with @domain. --max-trials
# is the campaign's lifetime budget:
$ ./exp.sh ax prepare --campaign tune --objective score --maximize \
      --max-trials 20 --seed 1 -- submit -- quad --x @domain
# drive the loop (re-run to resume); --max-inflight sets this call's concurrency:
$ ./exp.sh ax optimize --campaign tune --max-inflight 4
$ ./exp.sh ax list                  # one line per campaign: status, trials, best
$ ./exp.sh ax show --campaign tune  # one campaign's detail and current optimum
# reached the budget but want to keep searching? raise it, then optimize again:
$ ./exp.sh ax extend --campaign tune --max-trials 40
$ ./exp.sh ax optimize --campaign tune

Each trial is a real knit submit into the campaign’s group, so the trials are ordinary tracked jobs: on ax optimize, --max-inflight keeps that many in flight (per call, default 1) and --trials K runs at most K more this call (default: run to the campaign’s --max-trials); every trial records a call edge back to the campaign’s optimizations row, so a query can walk a campaign to its children. ax reads each trial’s objective from the declared output column, feeds it to the Ax model, and records the best trial on the campaign row and in campaigns/<name>/result.json. --minimize (the default) or --maximize sets the sense; --sobol-trials sets how many quasi-random trials explore before the model engages; --on-failure penalize tells the model a fixed penalty for a failed trial instead of dropping it.

The same campaign optimizes an app inside one allocation: swap -- submit for -- run and give the launch fields, as in -- run --procs 8 -- bench --msg-size @domain. App trials share the allocation, so they run sequentially (--max-inflight is job-mode only), and the objective is read from the app’s own table. Declare a parameter’s domain with --domain (see Declare a parameter’s search domain); ax and knit sweep read the very same domains, so a space written for one proposer serves the other unchanged.

Model the objective instead of optimizing it#

Sometimes the goal is not the best point but a model of how the objective behaves across the whole space. Prepare with --model in place of --minimize / --maximize: the campaign then fits a surrogate by active learning, sending each trial where the model is least certain rather than toward an optimum. A model campaign records no best, so ax show lists its varying axes instead:

$ ./exp.sh ax prepare --campaign surf --objective score --model \
      --max-trials 20 -- submit -- quad --x @domain
$ ./exp.sh ax optimize --campaign surf   # spends the budget on active learning
$ ./exp.sh ax show --campaign surf
...
axes:       quad.x

Ask the fitted model for the objective at a point with ax predict. Write the point as the prepare command with a concrete value in each varying position; the value must be in its domain (a member of a {...} set, or inside a [lo..hi] range). It prints the predicted value by default; --variance, --std, --interval (the 95% confidence interval), and --json add detail:

$ ./exp.sh ax predict --campaign surf -- submit -- quad --x 6
93.8
$ ./exp.sh ax predict --campaign surf --std --interval -- submit -- quad --x 6
93.8  (±6.7, 95% CI 80.7–106.9)

ax predict works on any campaign that has run trials — a --model campaign or a --minimize / --maximize one — and reads the model only, recording nothing. ax show --diagnostics cross-validates the fitted surrogate and reports its fit quality (R² / RMSE); it refits the model, so it is opt-in.

previous

Apps

next

Recording

Contents
  • Declare a parameter’s search domain
  • Run a parameter sweep
  • Optimize a job with Bayesian search
    • Model the objective instead of optimizing it

By Matthieu Dorier

© Copyright 2026, UChicago Argonne, LLC.