Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add max_price policy #641

Merged
merged 4 commits into from
Aug 15, 2023
Merged
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
2 changes: 1 addition & 1 deletion cli/dstack/_internal/backend/aws/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AWSPricing(Pricing):
def __init__(self, session: boto3.Session):
super().__init__()
self.session = session
self.pricing_client = self.session.client("pricing")
self.pricing_client = self.session.client("pricing", region_name="us-east-1")

def _fetch_ondemand(self, attributes: Dict[str, str]):
def get_ondemand_price(terms: dict) -> Dict[str, str]:
Expand Down
5 changes: 4 additions & 1 deletion cli/dstack/_internal/backend/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,7 @@ def get_instance_candidates(
]
instances = self.compute().get_supported_instances()
instances = [i for i in instances if _matches_requirements(i.resources, requirements)]
return self.pricing().get_prices(instances, spot_query)
offers = self.pricing().get_prices(instances, spot_query)
if requirements.max_price is not None:
offers = [o for o in offers if o.price <= requirements.max_price]
return offers
8 changes: 8 additions & 0 deletions cli/dstack/_internal/configurators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def get_parser(
help="Request a GPU for the run",
)

parser.add_argument(
"--max-price", metavar="PRICE", type=float, help="Maximum price per hour, $"
)

spot_group = parser.add_mutually_exclusive_group()
spot_group.add_argument(
"--spot", action="store_const", dest="spot_policy", const=job.SpotPolicy.SPOT
Expand Down Expand Up @@ -102,6 +106,9 @@ def apply_args(self, args: argparse.Namespace):
gpu.update(args.gpu)
self.profile.resources.gpu = ProfileGPU.parse_obj(gpu)

if args.max_price is not None:
self.profile.resources.max_price = args.max_price

if args.spot_policy is not None:
self.profile.spot_policy = args.spot_policy

Expand Down Expand Up @@ -277,6 +284,7 @@ def requirements(self) -> job.Requirements:
memory_mib=self.profile.resources.memory,
gpus=None,
shm_size_mib=self.profile.resources.shm_size,
max_price=self.profile.resources.max_price,
)
if self.profile.resources.gpu:
r.gpus = job.GpusRequirements(
Expand Down
21 changes: 12 additions & 9 deletions cli/dstack/_internal/core/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ class Gateway(BaseModel):


class GpusRequirements(BaseModel):
count: Optional[int] = None
memory_mib: Optional[int] = None
name: Optional[str] = None
count: Optional[int]
memory_mib: Optional[int]
name: Optional[str]


class Requirements(BaseModel):
cpus: Optional[int] = None
memory_mib: Optional[int] = None
gpus: Optional[GpusRequirements] = None
shm_size_mib: Optional[int] = None
spot: Optional[bool] = None
local: Optional[bool] = None
cpus: Optional[int]
memory_mib: Optional[int]
gpus: Optional[GpusRequirements]
shm_size_mib: Optional[int]
spot: Optional[bool]
local: Optional[bool]
max_price: Optional[float]

def pretty_format(self):
res = ""
Expand All @@ -53,6 +54,8 @@ def pretty_format(self):
res += f", {self.gpus.count}x{self.gpus.name or 'GPU'}"
if self.gpus.memory_mib:
res += f" {self.gpus.memory_mib / 1024:g}GB"
if self.max_price is not None:
res += f" under ${self.max_price:g} per hour"
return res


Expand Down
5 changes: 4 additions & 1 deletion cli/dstack/_internal/core/profile.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from typing import List, Optional, Union

from pydantic import Field, validator
from pydantic import Field, confloat, validator
from typing_extensions import Annotated, Literal

from dstack._internal.core.configuration import ForbidExtra
Expand Down Expand Up @@ -90,6 +90,9 @@ class ProfileResources(ForbidExtra):
),
]
cpu: int = DEFAULT_CPU
max_price: Annotated[
Optional[confloat(gt=0.0)], Field(description="The maximum price per hour, $")
]
_validate_mem = validator("memory", "shm_size", pre=True, allow_reuse=True)(parse_memory)

@validator("gpu", pre=True)
Expand Down
1 change: 1 addition & 0 deletions docs/docs/reference/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following arguments are optional:
- `-p PORT`, `--port PORT` – (Optional) Requests port or define mapping (`LOCAL_PORT:CONTAINER_PORT`)
- `-e ENV`, `--env ENV` – (Optional) Set environment variable (`NAME=value`)
- `--gpu` – (Optional) Request a GPU for the run. Specify any: name, count, memory (`NAME:COUNT:MEMORY` or `NAME` or `COUNT:MEMORY`, etc...)
- `--max-price` – (Optional) Maximum price per hour, $
- `ARGS` – (Optional) Use `ARGS` to pass custom run arguments

Spot policy (the arguments are mutually exclusive):
Expand Down
5 changes: 3 additions & 2 deletions docs/docs/reference/profiles.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ Below is a full reference of all available properties.
- `gpu` - (Optional) The minimum number of GPUs, their model name and memory
- `name` - (Optional) The name of the GPU model (e.g., `"K80"`, `"V100"`, `"A100"`, etc)
- `count` - (Optional) The minimum number of GPUs. Defaults to `1`.
- `memory` (Optional) The minimum size of GPU memory (e.g., `"16GB"`)
- `shm_size` (Optional) The size of shared memory (e.g., `"8GB"`). If you are using parallel communicating
- `memory` - (Optional) The minimum size of GPU memory (e.g., `"16GB"`)
- `shm_size` - (Optional) The size of shared memory (e.g., `"8GB"`). If you are using parallel communicating
processes (e.g., dataloaders in PyTorch), you may need to configure this.
- `max_price` - (Optional) Maximum price per hour, $
- `spot_policy` - (Optional) The policy for provisioning spot or on-demand instances: `spot`, `on-demand`, or `auto`. `spot` provisions a spot instance. `on-demand` provisions a on-demand instance. `auto` first tries to provision a spot instance and then tries on-demand if spot is not available. Defaults to `on-demand` for dev environments and to `auto` for tasks.
- `retry_policy` - (Optional) The policy for re-submitting the run.
- `retry` - (Optional) Whether to retry the run on failure or not. Default to `false`
Expand Down
13 changes: 7 additions & 6 deletions runner/internal/models/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ type App struct {
}

type Requirements struct {
GPUs GPU `yaml:"gpus,omitempty"`
CPUs int `yaml:"cpus,omitempty"`
Memory int `yaml:"memory_mib,omitempty"`
Spot bool `yaml:"spot,omitempty"`
ShmSize int64 `yaml:"shm_size_mib,omitempty"`
Local bool `json:"local"`
GPUs GPU `yaml:"gpus,omitempty"`
CPUs int `yaml:"cpus,omitempty"`
Memory int `yaml:"memory_mib,omitempty"`
Spot bool `yaml:"spot,omitempty"`
ShmSize int64 `yaml:"shm_size_mib,omitempty"`
MaxPrice float64 `yaml:"max_price,omitempty"`
Local bool `json:"local"`
}

type GPU struct {
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def get_long_description():
package_dir={"": "cli"},
packages=find_packages("cli"),
package_data={
"dstack._internal": ["schemas/*.json", "scripts/*.sh"],
"dstack._internal": ["schemas/*.json", "scripts/*.sh", "scripts/*.py"],
"dstack._internal.hub": [
"statics/*",
"statics/**/*",
Expand Down
Loading