Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions mkdocs/docs/concepts/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ base: Qwen/Qwen2.5-7B-Instruct
# The number of benchmarked trials
max_trials: 3

# The benchmark workload (defaults to 8, 1024, 1024)
concurrency: 16
input_tokens: 2048
output_tokens: 512

env:
- HF_TOKEN
```
Expand Down Expand Up @@ -66,9 +71,11 @@ $ dstack preset create -f preset.dstack.yml --resume a1b2c3d4

</div>

When resuming, `model`, `prompt`, `min_context_length`, `max_trials`, `concurrency`, `baseline`, and `fleets` are read from the original session, not from the configuration file. Editing them and resuming has no effect. To change any of them, create a new preset.

To stop a creation and its runs, use `dstack preset stop`.

!!! info "Claude configuration"
??? info "Claude configuration"
By default, preset creation uses the existing `claude` login. To use an Anthropic API key instead, set:

```shell
Expand All @@ -84,6 +91,11 @@ To stop a creation and its runs, use `dstack preset stop`.

Supported effort levels are `low`, `medium`, `high`, `xhigh`, and `max`.

!!! info "Recommended setup"
- Profile settings such as `spot_policy`, `max_price`, and `backends` are ignored during preset creation. Configure them on the fleet instead, and pass a specific fleet so the agent uses the offers you intend.
- The agent chooses the hardware that gets the best performance. If you have a preference on hardware (e.g. price or specific GPUs), use a fleet that is pre-configured correspondingly.
- If `min_context_length` is unset, the agent may choose a short context to get a better benchmark result. Set it explicitly.

## Configuration options

### Model
Expand All @@ -106,15 +118,21 @@ To stop a creation and its runs, use `dstack preset stop`.

### Trials

`max_trials` is required and sets how many benchmarked trials the agent runs before promoting the best one. Set `concurrency` to control the benchmark concurrency.
`max_trials` is required and sets how many benchmarked trials the agent runs before promoting the best one.

`concurrency`, `input_tokens`, and `output_tokens` define the benchmark workload, and default to 8, 1024, and 1024. The same values are used for every trial and for the final service benchmark, so results are comparable.

### Baseline

Set `baseline: true` to make the first trial a baseline: the agent serves the model the way the chosen serving framework recommends, without tuning it for performance. Later trials are optimization attempts.

### Context length

Set `context_length` to require a minimum supported context length.
Set `min_context_length` to require a minimum supported context length.

### Fleets

Set `fleets` to restrict creation and reuse to specific [fleets](fleets.md). Placement properties such as `backends`, `max_price`, and `spot_policy` constrain both creation and reuse too.
Set `fleets` to restrict creation and reuse to specific [fleets](fleets.md).

### Prompt

Expand Down Expand Up @@ -147,7 +165,7 @@ $ dstack preset apply -f preset.dstack.yml --id 532f3f4b
Spot policy on-demand
Max price off
Model Qwen/Qwen3.5-27B (base)
Preset 532f3f4b (ctx=8K con=8 387 tok/s TTFT 582ms)
Preset 532f3f4b (ctx=8K io=512/512 con=8 tps=387 ttft=582)

# BACKEND RESOURCES INSTANCE TYPE PRICE
1 runpod (EU-RO-1) cpu=12 mem=54GB disk=100GB gpu=RTXPRO4500:32GB:1 NVIDIA RTX PRO 4500 Blackwell $0.74
Expand All @@ -167,20 +185,30 @@ Use `dstack preset` to list presets:

```shell
$ dstack preset list
BASE ID GPU BENCHMARK STATUS SUBMITTED NAME
Qwen/Qwen2.5-0.5B
bc592b38 clauding (0/3) 23 sec ago qwen05
Qwen/Qwen3-32B
f91d6b60 RTX5090:32GB:1 con=8 576 tok/s TTFT 368ms verified (10/10) 2 days ago qwen3-32b
Qwen/Qwen3.5-27B
3c4d5e6f verifying (3/3) 2 min ago qwen35-27b-2
532f3f4b RTXPRO4500:32GB:1.. con=8 387 tok/s TTFT 582ms verified (4/4) yesterday qwen35-27b
d1c2e12b RTX5090:32GB:1 con=8 266 tok/s TTFT 2.15s verified (7/10) yesterday
ID BASE GPU WORKLOAD BEST STATUS SUBMITTED
bc592b38 Qwen/Qwen3.6-27B RTXPRO4500:32GB:1 ctx=32K io=1024/1024 con=8 tps=422 ttft=501 ▁▄█ trialing (3/10) 2 min ago
```

</div>

Presets are grouped by base model. In-progress creations appear too, with a live status like `clauding` or `verifying`. Pass `-w` to watch in realtime. Pass `-v` to include validation resources and all benchmark metrics, or `--json` for complete preset objects. Filter with `--base` or `--repo`.
By default, `dstack preset` shows creations that are still running, or the most recent one if none are. Pass `-a` to show every preset, or `-n` to show the last N:

<div class="termy">

```shell
$ dstack preset list -a
ID BASE GPU WORKLOAD BEST STATUS SUBMITTED
bc592b38 Qwen/Qwen3.6-27B RTXPRO4500:32GB:1 ctx=32K io=1024/1024 con=8 tps=422 ttft=501 ▁▄█ trialing (3/10) 2 min ago
f91d6b60 Qwen/Qwen3-32B RTX5090:32GB:1 ctx=32K io=1024/512 con=8 tps=576 ttft=368 ▁▁▅▅▄▅▇█ verified (8/8) 2 days ago
532f3f4b Qwen/Qwen3.5-27B RTXPRO4500:32GB:1 ctx=8K io=512/512 con=8 tps=387 ttft=582 ▁▇██ verified (4/4) yesterday
d1c2e12b Qwen/Qwen3.5-27B RTX5090:32GB:1 ctx=32K io=1024/128 con=8 tps=266 ttft=2154 █▆█▁███ verified (7/10) yesterday
```

</div>

The `BEST` column is the best trial so far, followed by one glyph per trial: height is throughput, `·` is a trial that produced no benchmark. The shape shows whether a run converged or wandered.

Pass `-w` to watch in realtime. Pass `-v` to include validation resources, the model repo, and the preset name, or `--json` for complete preset objects. Filter with `--base` or `--repo`.

### Delete presets

Expand Down
33 changes: 31 additions & 2 deletions src/dstack/_internal/cli/commands/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,26 @@ def _list(self, args: argparse.Namespace) -> None:
verbose = args.verbose
if not getattr(args, "watch", False):
presets, sessions = self._list_presets_and_sessions(base=base, repo=repo)
print_presets(presets, sessions=sessions, verbose=verbose)
print_presets(
presets,
sessions=sessions,
verbose=verbose,
all_presets=args.all_presets,
limit=args.limit,
)
return
with Live(console=console, refresh_per_second=LIVE_TABLE_REFRESH_RATE_PER_SEC) as live:
while True:
presets, sessions = self._list_presets_and_sessions(base=base, repo=repo)
live.update(get_presets_table(presets, sessions=sessions, verbose=verbose))
live.update(
get_presets_table(
presets,
sessions=sessions,
verbose=verbose,
all_presets=args.all_presets,
limit=args.limit,
)
)
time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS)

def _list_presets_and_sessions(
Expand Down Expand Up @@ -408,6 +422,21 @@ def _add_list_args(parser: argparse.ArgumentParser) -> None:
action="store_true",
help="Output in JSON format",
)
parser.add_argument(
"-a",
"--all",
action="store_true",
dest="all_presets",
help="Show all presets. By default, it only shows unfinished creations or the last one.",
)
parser.add_argument(
"-n",
"--last",
metavar="COUNT",
type=int,
dest="limit",
help="Show only the last N presets. Implies --all",
)
model_filter = parser.add_mutually_exclusive_group()
model_filter.add_argument(
"--base",
Expand Down
50 changes: 48 additions & 2 deletions src/dstack/_internal/cli/models/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from dstack._internal.utils.json_schema import add_extra_schema_types

DEFAULT_CONCURRENCY = 8
DEFAULT_INPUT_TOKENS = 1024
DEFAULT_OUTPUT_TOKENS = 1024
DEFAULT_BASELINE = False


class PresetModelRepo(CoreModel):
Expand Down Expand Up @@ -134,7 +137,7 @@ class PresetConfiguration(
)
),
] = None
context_length: Annotated[
min_context_length: Annotated[
Optional[PositiveInt], Field(description="The minimum required context length")
] = None
max_trials: Annotated[
Expand All @@ -155,6 +158,34 @@ class PresetConfiguration(
)
),
] = None
input_tokens: Annotated[
Optional[PositiveInt],
Field(
description=(
"The number of input tokens per request used for benchmarks during"
f" preset creation. Defaults to `{DEFAULT_INPUT_TOKENS}`"
)
),
] = None
output_tokens: Annotated[
Optional[PositiveInt],
Field(
description=(
"The number of output tokens per request used for benchmarks during"
f" preset creation. Defaults to `{DEFAULT_OUTPUT_TOKENS}`"
)
),
] = None
baseline: Annotated[
Optional[bool],
Field(
description=(
"Whether the first trial must be a baseline that serves the model with the"
" serving framework's recommended defaults instead of an optimization attempt."
" Defaults to `false`"
)
),
] = None
gateway: Annotated[
Optional[Union[bool, EntityReference, str]],
Field(
Expand All @@ -173,6 +204,18 @@ class PresetConfiguration(
def effective_concurrency(self) -> int:
return self.concurrency if self.concurrency is not None else DEFAULT_CONCURRENCY

@property
def effective_input_tokens(self) -> int:
return self.input_tokens if self.input_tokens is not None else DEFAULT_INPUT_TOKENS

@property
def effective_output_tokens(self) -> int:
return self.output_tokens if self.output_tokens is not None else DEFAULT_OUTPUT_TOKENS

@property
def effective_baseline(self) -> bool:
return self.baseline if self.baseline is not None else DEFAULT_BASELINE

@root_validator(pre=True)
def apply_model_shorthand(cls, values: Any) -> Any:
if not isinstance(values, dict):
Expand Down Expand Up @@ -211,9 +254,12 @@ class PresetConstraints(CoreModel):

run_name_prefix: str
model: PresetModelSpec
context_length: Optional[PositiveInt] = None
min_context_length: Optional[PositiveInt] = None
max_trials: PositiveInt
concurrency: PositiveInt
input_tokens: PositiveInt
output_tokens: PositiveInt
baseline: bool = False
fleets: list[str] = Field(min_items=1)
env: list[str] = []

Expand Down
8 changes: 8 additions & 0 deletions src/dstack/_internal/cli/services/presets/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ async def _session_tailers(
offset_key="trials",
echo=agent_session.echo,
),
_RecordMirror(
source=workspace.verifications_path,
target=agent_session.verifications_path,
redacted_values=redacted_values,
offset_store=offset_store,
offset_key="verifications",
echo=agent_session.echo,
),
]
tailer_tasks = [
asyncio.create_task(tailer.run()) for tailer in [progress_tailer, *record_mirrors]
Expand Down
13 changes: 8 additions & 5 deletions src/dstack/_internal/cli/services/presets/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dstack._internal.cli.services.configurators.run import ServiceConfigurator
from dstack._internal.cli.services.presets.output import (
format_preset_benchmark,
format_preset_objective,
)
from dstack._internal.cli.services.presets.store import PresetStore
from dstack._internal.core.errors import CLIError
Expand Down Expand Up @@ -58,11 +59,11 @@ def _validate_preset_matches(preset: Preset, *, configuration: PresetConfigurati
service_model = preset.service.model
if service_model is None or service_model.name.lower() != model_name.lower():
raise CLIError(f"Preset {preset.id} does not serve {model_name}")
if configuration.context_length is not None:
if preset.context_length < configuration.context_length:
if configuration.min_context_length is not None:
if preset.context_length < configuration.min_context_length:
raise CLIError(
f"Preset {preset.id} does not support context length"
f" {configuration.context_length}"
f" {configuration.min_context_length}"
)
if configuration.model.allows_variant_selection:
if preset.base.lower() != model_name.lower():
Expand Down Expand Up @@ -94,5 +95,7 @@ def _format_requested_model(configuration: PresetConfiguration) -> str:


def _format_selected_preset(preset: Preset) -> str:
details = format_preset_benchmark(preset, verbose=True)
return f"{escape(preset.id)} ([secondary]{details}[/])"
# The formatter dims its own keys; wrapping it again would flatten that.
# One line, so the objective and the result are joined rather than columned.
details = f"{format_preset_objective(preset)} {format_preset_benchmark(preset, verbose=True)}"
return f"{escape(preset.id)} ({details})"
10 changes: 8 additions & 2 deletions src/dstack/_internal/cli/services/presets/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,10 @@ async def _create_preset(
workspace=setup.workspace,
token=token,
)
prompt = get_preset_agent_system_prompt(user_prompt=setup.user_prompt)
prompt = get_preset_agent_system_prompt(
user_prompt=setup.user_prompt,
baseline=configuration.effective_baseline,
)
if setup.write_constraints:
if setup.user_prompt:
agent_session.write_user_prompt(setup.user_prompt)
Expand Down Expand Up @@ -849,9 +852,12 @@ def _build_constraints(
{
"run_name_prefix": build_name,
"model": json.loads(configuration.model.json(exclude_none=True)),
"context_length": configuration.context_length,
"min_context_length": configuration.min_context_length,
"max_trials": configuration.max_trials,
"concurrency": configuration.effective_concurrency,
"input_tokens": configuration.effective_input_tokens,
"output_tokens": configuration.effective_output_tokens,
"baseline": configuration.effective_baseline,
"fleets": list(allowed_fleets),
"env": list(configuration.env),
}
Expand Down
Loading
Loading