Tuning agent — inferasim-tune#
One-pager
What: an automated search that proposes serving recipes, scores them through the projector, and returns the best one subject to your latency budget. Why: the serving space is too large and too awkward to grid, and the interesting answer is almost always constrained rather than maximal. Cost: no GPU by default. It optimises against the projector, so it inherits the projector’s boundaries.
The sweep API grids a space you enumerate. The tuning agent navigates a space you describe, over 36 serving levers, against any of 24 projected metrics, under latency constraints it will not violate.
Run it#
inferasim-tune --inference \
--workload <workload.yaml> \
--target-cluster infera/projection/examples/tuning/target_cluster_mi355x_inference.yaml \
--out-dir runs/tune-1
–inference is not optional
The agent tunes training configurations by default. Serving is a different
search surface, selected either by the --inference flag or by
optimization.mode: inference in the target-cluster YAML. The packaged
target_cluster_mi355x_inference.yaml sets that key, so --inference is
redundant with it — but pass a training-mode cluster file without the flag and
you will get a training search that never mentions TTFT.
Two stages#
1. Deterministic seed sweep. A systematic warm start over the serving space,
ordered by expected impact: TP (latency) → batching and concurrency
(throughput) → KV quantization (capacity) → weight quantization → combined →
chunked prefill → speculative decoding → EP (for MoE). This stage is
reproducible and needs no LLM. --seed-budget caps it (default 12).
2. LLM-driven search. Continues from the warm-started incumbent, proposing recipes, reading back the result of each, and keeping notes across rounds. It sees the legal axes, the architecture, the cluster, the objective and its direction, the active latency budgets, and the trial history.
--seed-only (alias --no-agent) runs stage 1 alone, which is the right way to
get a reproducible baseline. --agent-only skips the seeds and runs the LLM
against an existing history.
Flags#
Flag |
Default |
Meaning |
|---|---|---|
|
required |
the workload YAML |
|
required |
the cluster and optimization YAML |
|
off |
tune serving rather than training; overrides |
|
|
where trials, summary and scratchpad are written |
|
|
evaluator depth — see Evaluation modes |
|
|
how |
|
off |
shorthand for |
|
off |
deterministic stage only, no LLM |
|
off |
skip seeds, search from existing history |
|
off |
reuse an existing trials file in |
|
|
seed candidates before the LLM takes over |
Objectives#
Set optimization.objective in the target-cluster YAML. The default is
decode_throughput_tps_per_gpu. Friendly aliases are accepted —
max_throughput, min_ttft, min_latency, min_itl, tpot,
max_concurrency, tput_per_gpu and others resolve to the canonical names
below.
This list is the direct answer to “what can it predict?” — anything here can be the thing being optimised, and everything here is reported for every trial.
Throughput
Objective |
Meaning |
|---|---|
|
total tokens/s/GPU, prompt plus generation — the headline ranking |
|
the same, fleet total |
|
generation tokens/s/GPU (the default) |
|
generation tokens/s, fleet |
|
prompt tokens/s/GPU |
|
prompt tokens/s, fleet |
Total and decode-only throughput are different orderings. At a 144:1 prompt-to-generation ratio the prompt decides the winner, so a prefill-heavy agentic fleet should not be ranked on decode throughput.
Latency and interactivity
Objective |
Meaning |
|---|---|
|
mean time to first token (minimize) |
|
mean inter-token latency / TPOT (minimize) |
|
mean end-to-end request latency (minimize) |
|
per-user generation rate |
|
per-request generation throughput |
|
uncontended decode step time (minimize) |
Capacity and memory
Objective |
Meaning |
|---|---|
|
sequences the KV pool holds |
|
concurrency the pool sustains |
|
HBM footprint (minimize) |
|
KV footprint (minimize) |
|
resident weight footprint (minimize) |
|
activation working set (minimize) |
Interference and communication
Objective |
Meaning |
|---|---|
|
prefill interference in decode (minimize) |
|
TPOT inflation from that interference (minimize) |
|
step time when a prefill chunk lands (minimize) |
|
collective time per phase (minimize) |
|
GPUs a replica costs (minimize) |
Metrics marked (minimize) are negated internally, so scoring is always “higher is better” regardless of direction.
Deliberately unsupported
avg_power_w, joules_per_output_token, joules_per_total_token, mfu,
tflops_per_s_per_gpu and iteration_ms are rejected rather than accepted and
scored as absent. The serving projection reports none of them, and an objective
that silently scores None reads as a failed search rather than as a missing
metric.
Latency SLOs — the constraint that makes it useful#
Set budgets in milliseconds under optimization.slo. A trial that misses any of
them is rejected exactly like an over-memory one.
optimization:
objective: max_throughput
slo:
ttft_ms: 500
tpot_ms: 25
request_latency_ms: null # null leaves it unconstrained
SLO key |
Constrains |
|---|---|
|
time to first token |
|
per-token latency |
|
end-to-end request latency |
This is the difference between a search that is useful and one that is not.
Without an SLO, max_throughput always walks to the largest batch that fits,
and the interactive configuration never appears in the results at all. With one,
the search becomes the constrained problem serving actually poses: maximise
throughput subject to a latency promise.
The rejection reason names the budget and the overshoot rather than just failing
the trial — TTFT 812.4 ms > 500.0 ms — because that string is what the LLM
planner reads back. It is the signal telling it to trade concurrency away rather
than to try another dtype.
What it is allowed to vary#
A proposal may set any subset of these; unspecified fields inherit the profile-anchored baseline.
Group |
Levers |
|---|---|
Parallel shape |
|
Load |
|
Precision |
|
Scheduler and graphs |
|
Speculation |
|
Collectives |
|
MoE |
|
Attention |
|
Disaggregation |
|
Legality is derived from the architecture and the cluster before any trial runs, so the search is told which TP/PP/EP/batch/dtype values are admissible instead of discovering it by failing.
prefix_cache_hit_rate is deliberately not a lever. It is a property of the
traffic, not a knob to tune — an agentic trace resends a long transcript and
reprefills almost none of it, so leaving it at 0 tunes for a workload nobody
runs. Set it under optimization.inference instead.
Configuration#
target_cluster:
name: "mi355x-inference"
num_nodes: 1
gpus_per_node: 8
gpu_arch: mi355x
hardware_config: examples/hardware_configs/mi355x.yaml
available_for_benchmark:
has_gpu: false
benchmark_gpus: 0
optimization:
mode: inference
objective: max_throughput
memory_safety_margin: 0.10
hbm_capacity_gb: 288.0
inference:
input_len: 4096
output_len: 256
max_concurrency: null # defaults to the trial batch size
prefix_cache_hit_rate: 0.0
slo:
ttft_ms: 500
tpot_ms: 25
budget:
max_proposals: 16
max_perf_calls: 16
max_benchmark_calls: 0
max_rounds: 1
max_rlm_iterations: 8
agent:
llm:
provider: litellm
timeout: 300
max_tokens: 8000
The budget block is the cost control: max_perf_calls caps projector
evaluations and max_benchmark_calls caps GPU measurements (default 0, i.e.
none). max_rounds and max_rlm_iterations bound the LLM loop.
Two notes on that file. agent.llm.provider appears in the packaged examples
but is not read — the provider comes from the model string. And
optimization.hbm_capacity_gb is where the memory ceiling comes from, combined
with memory_safety_margin: a trial projecting above
hbm_capacity_gb × (1 - margin) is rejected the same way an SLO miss is.
Packaged examples live in infera/projection/examples/tuning/ — note that only
inference target-cluster examples ship, so a training run needs its own.
Evaluation modes#
On the serving path only --mode dry is special-cased: it synthesises metrics
so the loop can be exercised without the projector at all. memory-real and
full both run a real inferasim inference projection.
–profiling-mode does not reach the serving path
--profiling-mode is read only by the training branch. On the inference branch
the switch is available_for_benchmark.has_gpu in the target-cluster YAML:
set it to true and the agent measures, leave it false and every trial is
scored on the cost model. This is why the packaged inference example ships
has_gpu: false — passing --profiling-mode benchmark alongside it changes
nothing.
When measurement is enabled, the agent prefers a cached anchor over spawning a run, controlled by environment rather than flags:
Variable |
Effect |
|---|---|
|
directory of cached anchors, looked up as |
|
force one artifact for every trial |
|
reject a trial rather than measuring when the cache misses |
INFERASIM_INFER_BENCH_CACHE_ONLY is the one to set if you want measured
fidelity without ever letting a search start a GPU run. When no cached artifact
matches, the agent falls back to the nearest anchor below the target
parallelism.
On the training path, --mode memory-real runs a real memory projection but
ranks on a deliberately coarse throughput heuristic rather than a
projection. The LLM is told as much; do not read it as a performance number.
What it writes#
In --out-dir (default ./tuning_runs/<target_cluster.name>):
File |
Contents |
|---|---|
|
one record per trial: index, timestamp, config, result, source, legality and the reason it was rejected |
|
the mode, the objective, and the best config with its result |
|
the agent’s durable notes — plans, hypotheses and observations that survive across rounds |
|
the workload overlay each trial was actually projected from |
inference_trials.jsonl is the artifact worth keeping. Every trial is in it,
including the rejected ones with the budget they missed, which makes the search
auditable rather than a single recommended number. Trials are deduplicated by
config signature, so re-running does not re-score work you already have.
The serving path writes no plot; trials.png is produced by the training path
only.
Requirements and limits#
The LLM stage needs
dspyandpython-dotenv— install withpip install ".[projection-tuning]". Without them the agent skips stage 2 with a warning and you get the deterministic seed sweep, which is exactly what--seed-onlydoes. Stage 1 results are still written and still usable.The model is any LiteLLM provider string under
agent.llm.model, defaulting toopenai/gpt-4o. The key is taken fromagent.llm.api_keyor, failing that,OPENAI_API_KEY→ANTHROPIC_API_KEY→LLM_API_KEY.LLM_MODELandOPENAI_API_BASEalso work. With no key you get stage 1 only.The LLM search stage drives DSPy’s interpreter, which wants
denoonPATH(~/.deno/binis also checked). This does not affect--seed-only.max_perf_callsandmax_benchmark_callsare enforced.max_proposalsis passed to the planner as guidance but is not a hard gate, so treatmax_perf_callsas the real cost ceiling.The agent scores through the projector, so it can only distinguish what the projector distinguishes. Flags that do not enter the serving spec project to the same number, and a search over them would produce confident noise. See Boundaries and verification.
optimization.axesis parsed but only governs the training search; it does not restrict the serving levers.
Next steps#
Enumerate a space yourself instead: Sweeps and tuning.
Ground the scorer in hardware first: Anchors and calibration.
Every lever the projector understands: Modeled features.