dstack 0.19.27__py3-none-any.whl → 0.19.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dstack might be problematic. Click here for more details.
- dstack/_internal/cli/commands/__init__.py +11 -8
- dstack/_internal/cli/commands/apply.py +6 -3
- dstack/_internal/cli/commands/completion.py +3 -1
- dstack/_internal/cli/commands/config.py +1 -0
- dstack/_internal/cli/commands/init.py +2 -2
- dstack/_internal/cli/commands/offer.py +1 -1
- dstack/_internal/cli/commands/project.py +1 -0
- dstack/_internal/cli/commands/server.py +2 -2
- dstack/_internal/cli/main.py +1 -1
- dstack/_internal/cli/services/configurators/base.py +2 -4
- dstack/_internal/cli/services/configurators/fleet.py +4 -5
- dstack/_internal/cli/services/configurators/gateway.py +3 -5
- dstack/_internal/cli/services/configurators/run.py +51 -27
- dstack/_internal/cli/services/configurators/volume.py +3 -5
- dstack/_internal/core/backends/aws/compute.py +51 -36
- dstack/_internal/core/backends/azure/compute.py +10 -7
- dstack/_internal/core/backends/base/compute.py +96 -14
- dstack/_internal/core/backends/base/offers.py +34 -4
- dstack/_internal/core/backends/cloudrift/compute.py +5 -7
- dstack/_internal/core/backends/cudo/compute.py +4 -2
- dstack/_internal/core/backends/datacrunch/compute.py +13 -11
- dstack/_internal/core/backends/digitalocean_base/compute.py +4 -5
- dstack/_internal/core/backends/gcp/compute.py +12 -7
- dstack/_internal/core/backends/hotaisle/compute.py +4 -7
- dstack/_internal/core/backends/kubernetes/compute.py +6 -4
- dstack/_internal/core/backends/lambdalabs/compute.py +4 -5
- dstack/_internal/core/backends/local/compute.py +1 -3
- dstack/_internal/core/backends/nebius/compute.py +10 -7
- dstack/_internal/core/backends/oci/compute.py +10 -7
- dstack/_internal/core/backends/runpod/compute.py +15 -6
- dstack/_internal/core/backends/template/compute.py.jinja +3 -1
- dstack/_internal/core/backends/tensordock/compute.py +1 -3
- dstack/_internal/core/backends/tensordock/models.py +2 -0
- dstack/_internal/core/backends/vastai/compute.py +7 -3
- dstack/_internal/core/backends/vultr/compute.py +5 -5
- dstack/_internal/core/compatibility/runs.py +2 -0
- dstack/_internal/core/models/common.py +67 -43
- dstack/_internal/core/models/configurations.py +88 -62
- dstack/_internal/core/models/fleets.py +41 -24
- dstack/_internal/core/models/instances.py +5 -5
- dstack/_internal/core/models/profiles.py +66 -47
- dstack/_internal/core/models/projects.py +8 -0
- dstack/_internal/core/models/repos/remote.py +21 -16
- dstack/_internal/core/models/resources.py +69 -65
- dstack/_internal/core/models/runs.py +17 -9
- dstack/_internal/server/app.py +5 -0
- dstack/_internal/server/background/tasks/process_fleets.py +8 -0
- dstack/_internal/server/background/tasks/process_instances.py +3 -2
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +97 -34
- dstack/_internal/server/models.py +6 -5
- dstack/_internal/server/schemas/gateways.py +10 -9
- dstack/_internal/server/services/backends/__init__.py +1 -1
- dstack/_internal/server/services/backends/handlers.py +2 -0
- dstack/_internal/server/services/docker.py +8 -7
- dstack/_internal/server/services/projects.py +63 -4
- dstack/_internal/server/services/runs.py +2 -0
- dstack/_internal/server/settings.py +46 -0
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/main-56191fbfe77f49b251de.css +3 -0
- dstack/_internal/server/statics/{main-4eecc75fbe64067eb1bc.js → main-c51afa7f243e24d3e446.js} +61115 -49101
- dstack/_internal/server/statics/{main-4eecc75fbe64067eb1bc.js.map → main-c51afa7f243e24d3e446.js.map} +1 -1
- dstack/_internal/utils/env.py +85 -11
- dstack/version.py +1 -1
- {dstack-0.19.27.dist-info → dstack-0.19.29.dist-info}/METADATA +1 -1
- {dstack-0.19.27.dist-info → dstack-0.19.29.dist-info}/RECORD +68 -73
- dstack/_internal/core/backends/tensordock/__init__.py +0 -0
- dstack/_internal/core/backends/tensordock/api_client.py +0 -104
- dstack/_internal/core/backends/tensordock/backend.py +0 -16
- dstack/_internal/core/backends/tensordock/configurator.py +0 -74
- dstack/_internal/server/statics/main-56191c63d516fd0041c4.css +0 -3
- dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +0 -3
- {dstack-0.19.27.dist-info → dstack-0.19.29.dist-info}/WHEEL +0 -0
- {dstack-0.19.27.dist-info → dstack-0.19.29.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.27.dist-info → dstack-0.19.29.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -5,7 +5,7 @@ import uuid
|
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
|
-
from sqlalchemy import and_, not_, or_, select
|
|
8
|
+
from sqlalchemy import and_, func, not_, or_, select
|
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
10
|
from sqlalchemy.orm import contains_eager, joinedload, load_only, noload, selectinload
|
|
11
11
|
|
|
@@ -54,6 +54,7 @@ from dstack._internal.server.models import (
|
|
|
54
54
|
from dstack._internal.server.services.backends import get_project_backend_by_type_or_error
|
|
55
55
|
from dstack._internal.server.services.fleets import (
|
|
56
56
|
fleet_model_to_fleet,
|
|
57
|
+
generate_fleet_name,
|
|
57
58
|
get_fleet_requirements,
|
|
58
59
|
get_next_instance_num,
|
|
59
60
|
)
|
|
@@ -71,7 +72,7 @@ from dstack._internal.server.services.jobs import (
|
|
|
71
72
|
get_job_configured_volumes,
|
|
72
73
|
get_job_runtime_data,
|
|
73
74
|
)
|
|
74
|
-
from dstack._internal.server.services.locking import get_locker
|
|
75
|
+
from dstack._internal.server.services.locking import get_locker, string_to_lock_id
|
|
75
76
|
from dstack._internal.server.services.logging import fmt
|
|
76
77
|
from dstack._internal.server.services.offers import get_offers_by_requirements
|
|
77
78
|
from dstack._internal.server.services.requirements.combine import (
|
|
@@ -87,7 +88,6 @@ from dstack._internal.server.services.volumes import (
|
|
|
87
88
|
)
|
|
88
89
|
from dstack._internal.server.utils import sentry_utils
|
|
89
90
|
from dstack._internal.utils import common as common_utils
|
|
90
|
-
from dstack._internal.utils import env as env_utils
|
|
91
91
|
from dstack._internal.utils.logging import get_logger
|
|
92
92
|
|
|
93
93
|
logger = get_logger(__name__)
|
|
@@ -188,6 +188,7 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
|
|
|
188
188
|
run_spec = run.run_spec
|
|
189
189
|
profile = run_spec.merged_profile
|
|
190
190
|
job = find_job(run.jobs, job_model.replica_num, job_model.job_num)
|
|
191
|
+
multinode = job.job_spec.jobs_per_replica > 1
|
|
191
192
|
|
|
192
193
|
# Master job chooses fleet for the run.
|
|
193
194
|
# Due to two-step processing, it's saved to job_model.fleet.
|
|
@@ -288,7 +289,8 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
|
|
|
288
289
|
instance_filters=instance_filters,
|
|
289
290
|
)
|
|
290
291
|
fleet_models = fleet_models_with_instances + fleet_models_without_instances
|
|
291
|
-
fleet_model, fleet_instances_with_offers = _find_optimal_fleet_with_offers(
|
|
292
|
+
fleet_model, fleet_instances_with_offers = await _find_optimal_fleet_with_offers(
|
|
293
|
+
project=project,
|
|
292
294
|
fleet_models=fleet_models,
|
|
293
295
|
run_model=run_model,
|
|
294
296
|
run_spec=run.run_spec,
|
|
@@ -310,6 +312,7 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
|
|
|
310
312
|
session=session,
|
|
311
313
|
instances_with_offers=fleet_instances_with_offers,
|
|
312
314
|
job_model=job_model,
|
|
315
|
+
multinode=multinode,
|
|
313
316
|
)
|
|
314
317
|
job_model.fleet = fleet_model
|
|
315
318
|
job_model.instance_assigned = True
|
|
@@ -363,7 +366,8 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
|
|
|
363
366
|
job_model.job_provisioning_data = job_provisioning_data.json()
|
|
364
367
|
job_model.status = JobStatus.PROVISIONING
|
|
365
368
|
if fleet_model is None:
|
|
366
|
-
fleet_model = _create_fleet_model_for_job(
|
|
369
|
+
fleet_model = await _create_fleet_model_for_job(
|
|
370
|
+
session=session,
|
|
367
371
|
project=project,
|
|
368
372
|
run=run,
|
|
369
373
|
)
|
|
@@ -385,7 +389,7 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
|
|
|
385
389
|
offer=offer,
|
|
386
390
|
instance_num=instance_num,
|
|
387
391
|
)
|
|
388
|
-
job_model.job_runtime_data = _prepare_job_runtime_data(offer).json()
|
|
392
|
+
job_model.job_runtime_data = _prepare_job_runtime_data(offer, multinode).json()
|
|
389
393
|
# Both this task and process_fleets can add instances to fleets.
|
|
390
394
|
# TODO: Ensure this does not violate nodes.max when it's enforced.
|
|
391
395
|
instance.fleet_id = fleet_model.id
|
|
@@ -489,7 +493,8 @@ async def _refetch_fleet_models_with_instances(
|
|
|
489
493
|
return fleet_models
|
|
490
494
|
|
|
491
495
|
|
|
492
|
-
def _find_optimal_fleet_with_offers(
|
|
496
|
+
async def _find_optimal_fleet_with_offers(
|
|
497
|
+
project: ProjectModel,
|
|
493
498
|
fleet_models: list[FleetModel],
|
|
494
499
|
run_model: RunModel,
|
|
495
500
|
run_spec: RunSpec,
|
|
@@ -499,58 +504,99 @@ def _find_optimal_fleet_with_offers(
|
|
|
499
504
|
) -> tuple[Optional[FleetModel], list[tuple[InstanceModel, InstanceOfferWithAvailability]]]:
|
|
500
505
|
if run_model.fleet is not None:
|
|
501
506
|
# Using the fleet that was already chosen by the master job
|
|
502
|
-
|
|
507
|
+
fleet_instances_with_pool_offers = _get_fleet_instances_with_pool_offers(
|
|
503
508
|
fleet_model=run_model.fleet,
|
|
504
509
|
run_spec=run_spec,
|
|
505
510
|
job=job,
|
|
506
511
|
master_job_provisioning_data=master_job_provisioning_data,
|
|
507
512
|
volumes=volumes,
|
|
508
513
|
)
|
|
509
|
-
return run_model.fleet,
|
|
514
|
+
return run_model.fleet, fleet_instances_with_pool_offers
|
|
510
515
|
|
|
511
516
|
if len(fleet_models) == 0:
|
|
512
517
|
return None, []
|
|
513
518
|
|
|
514
519
|
nodes_required_num = _get_nodes_required_num_for_run(run_spec)
|
|
515
|
-
# The current strategy is to
|
|
516
|
-
# the run without additional provisioning and choose the one with the cheapest offer.
|
|
517
|
-
#
|
|
520
|
+
# The current strategy is first to consider fleets that can accommodate
|
|
521
|
+
# the run without additional provisioning and choose the one with the cheapest pool offer.
|
|
522
|
+
# Then choose a fleet with the cheapest pool offer among all fleets with pool offers.
|
|
523
|
+
# If there are no fleets with pool offers, choose a fleet with a cheapest backend offer.
|
|
524
|
+
# Fallback to autocreated fleet if fleets have no pool or backend offers.
|
|
525
|
+
# TODO: Consider trying all backend offers and then choosing a fleet.
|
|
518
526
|
candidate_fleets_with_offers: list[
|
|
519
527
|
tuple[
|
|
520
528
|
Optional[FleetModel],
|
|
521
529
|
list[tuple[InstanceModel, InstanceOfferWithAvailability]],
|
|
522
530
|
int,
|
|
523
|
-
|
|
531
|
+
int,
|
|
532
|
+
tuple[int, float, float],
|
|
524
533
|
]
|
|
525
534
|
] = []
|
|
526
535
|
for candidate_fleet_model in fleet_models:
|
|
527
|
-
|
|
536
|
+
fleet_instances_with_pool_offers = _get_fleet_instances_with_pool_offers(
|
|
528
537
|
fleet_model=candidate_fleet_model,
|
|
529
538
|
run_spec=run_spec,
|
|
530
539
|
job=job,
|
|
531
540
|
master_job_provisioning_data=master_job_provisioning_data,
|
|
532
541
|
volumes=volumes,
|
|
533
542
|
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
543
|
+
fleet_has_available_capacity = nodes_required_num <= len(fleet_instances_with_pool_offers)
|
|
544
|
+
fleet_cheapest_pool_offer = math.inf
|
|
545
|
+
if len(fleet_instances_with_pool_offers) > 0:
|
|
546
|
+
fleet_cheapest_pool_offer = fleet_instances_with_pool_offers[0][1].price
|
|
547
|
+
|
|
548
|
+
candidate_fleet = fleet_model_to_fleet(candidate_fleet_model)
|
|
549
|
+
profile = combine_fleet_and_run_profiles(
|
|
550
|
+
candidate_fleet.spec.merged_profile, run_spec.merged_profile
|
|
551
|
+
)
|
|
552
|
+
fleet_requirements = get_fleet_requirements(candidate_fleet.spec)
|
|
553
|
+
requirements = combine_fleet_and_run_requirements(
|
|
554
|
+
fleet_requirements, job.job_spec.requirements
|
|
555
|
+
)
|
|
556
|
+
multinode = (
|
|
557
|
+
candidate_fleet.spec.configuration.placement == InstanceGroupPlacement.CLUSTER
|
|
558
|
+
or job.job_spec.jobs_per_replica > 1
|
|
559
|
+
)
|
|
560
|
+
fleet_backend_offers = []
|
|
561
|
+
if (
|
|
562
|
+
_check_can_create_new_instance_in_fleet(candidate_fleet)
|
|
563
|
+
and profile is not None
|
|
564
|
+
and requirements is not None
|
|
565
|
+
):
|
|
566
|
+
fleet_backend_offers = await get_offers_by_requirements(
|
|
567
|
+
project=project,
|
|
568
|
+
profile=profile,
|
|
569
|
+
requirements=requirements,
|
|
570
|
+
exclude_not_available=True,
|
|
571
|
+
multinode=multinode,
|
|
572
|
+
master_job_provisioning_data=master_job_provisioning_data,
|
|
573
|
+
volumes=volumes,
|
|
574
|
+
privileged=job.job_spec.privileged,
|
|
575
|
+
instance_mounts=check_run_spec_requires_instance_mounts(run_spec),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
fleet_cheapest_backend_offer = math.inf
|
|
579
|
+
if len(fleet_backend_offers) > 0:
|
|
580
|
+
fleet_cheapest_backend_offer = fleet_backend_offers[0][1].price
|
|
581
|
+
|
|
582
|
+
fleet_priority = (
|
|
583
|
+
not fleet_has_available_capacity,
|
|
584
|
+
fleet_cheapest_pool_offer,
|
|
585
|
+
fleet_cheapest_backend_offer,
|
|
586
|
+
)
|
|
542
587
|
candidate_fleets_with_offers.append(
|
|
543
588
|
(
|
|
544
589
|
candidate_fleet_model,
|
|
545
|
-
|
|
546
|
-
len(
|
|
590
|
+
fleet_instances_with_pool_offers,
|
|
591
|
+
len(fleet_instances_with_pool_offers),
|
|
592
|
+
len(fleet_backend_offers),
|
|
547
593
|
fleet_priority,
|
|
548
594
|
)
|
|
549
595
|
)
|
|
550
596
|
if run_spec.merged_profile.fleets is None and all(
|
|
551
|
-
t[2] == 0 for t in candidate_fleets_with_offers
|
|
597
|
+
t[2] == 0 and t[3] == 0 for t in candidate_fleets_with_offers
|
|
552
598
|
):
|
|
553
|
-
# If fleets are not specified and no fleets have available offers, create a new fleet.
|
|
599
|
+
# If fleets are not specified and no fleets have available pool or backend offers, create a new fleet.
|
|
554
600
|
# This is for compatibility with non-fleet-first UX when runs created new fleets
|
|
555
601
|
# if there are no instances to reuse.
|
|
556
602
|
return None, []
|
|
@@ -570,7 +616,7 @@ def _get_nodes_required_num_for_run(run_spec: RunSpec) -> int:
|
|
|
570
616
|
return nodes_required_num
|
|
571
617
|
|
|
572
618
|
|
|
573
|
-
def
|
|
619
|
+
def _get_fleet_instances_with_pool_offers(
|
|
574
620
|
fleet_model: FleetModel,
|
|
575
621
|
run_spec: RunSpec,
|
|
576
622
|
job: Job,
|
|
@@ -614,6 +660,7 @@ async def _assign_job_to_fleet_instance(
|
|
|
614
660
|
session: AsyncSession,
|
|
615
661
|
instances_with_offers: list[tuple[InstanceModel, InstanceOfferWithAvailability]],
|
|
616
662
|
job_model: JobModel,
|
|
663
|
+
multinode: bool,
|
|
617
664
|
) -> Optional[InstanceModel]:
|
|
618
665
|
if len(instances_with_offers) == 0:
|
|
619
666
|
return None
|
|
@@ -643,7 +690,7 @@ async def _assign_job_to_fleet_instance(
|
|
|
643
690
|
job_model.instance = instance
|
|
644
691
|
job_model.used_instance_id = instance.id
|
|
645
692
|
job_model.job_provisioning_data = instance.job_provisioning_data
|
|
646
|
-
job_model.job_runtime_data = _prepare_job_runtime_data(offer).json()
|
|
693
|
+
job_model.job_runtime_data = _prepare_job_runtime_data(offer, multinode).json()
|
|
647
694
|
return instance
|
|
648
695
|
|
|
649
696
|
|
|
@@ -752,7 +799,8 @@ def _check_can_create_new_instance_in_fleet(fleet: Fleet) -> bool:
|
|
|
752
799
|
return True
|
|
753
800
|
|
|
754
801
|
|
|
755
|
-
def _create_fleet_model_for_job(
|
|
802
|
+
async def _create_fleet_model_for_job(
|
|
803
|
+
session: AsyncSession,
|
|
756
804
|
project: ProjectModel,
|
|
757
805
|
run: Run,
|
|
758
806
|
) -> FleetModel:
|
|
@@ -760,9 +808,19 @@ def _create_fleet_model_for_job(
|
|
|
760
808
|
if run.run_spec.configuration.type == "task" and run.run_spec.configuration.nodes > 1:
|
|
761
809
|
placement = InstanceGroupPlacement.CLUSTER
|
|
762
810
|
nodes = _get_nodes_required_num_for_run(run.run_spec)
|
|
811
|
+
|
|
812
|
+
lock_namespace = f"fleet_names_{project.name}"
|
|
813
|
+
# TODO: Lock fleet names on SQLite.
|
|
814
|
+
# Needs some refactoring so that the lock is released after commit.
|
|
815
|
+
if get_db().dialect_name == "postgresql":
|
|
816
|
+
await session.execute(
|
|
817
|
+
select(func.pg_advisory_xact_lock(string_to_lock_id(lock_namespace)))
|
|
818
|
+
)
|
|
819
|
+
fleet_name = await generate_fleet_name(session=session, project=project)
|
|
820
|
+
|
|
763
821
|
spec = FleetSpec(
|
|
764
822
|
configuration=FleetConfiguration(
|
|
765
|
-
name=
|
|
823
|
+
name=fleet_name,
|
|
766
824
|
placement=placement,
|
|
767
825
|
reservation=run.run_spec.configuration.reservation,
|
|
768
826
|
nodes=FleetNodesSpec(
|
|
@@ -776,7 +834,7 @@ def _create_fleet_model_for_job(
|
|
|
776
834
|
)
|
|
777
835
|
fleet_model = FleetModel(
|
|
778
836
|
id=uuid.uuid4(),
|
|
779
|
-
name=
|
|
837
|
+
name=fleet_name,
|
|
780
838
|
project=project,
|
|
781
839
|
status=FleetStatus.ACTIVE,
|
|
782
840
|
spec=spec.json(),
|
|
@@ -839,12 +897,17 @@ def _create_instance_model_for_job(
|
|
|
839
897
|
return instance
|
|
840
898
|
|
|
841
899
|
|
|
842
|
-
def _prepare_job_runtime_data(
|
|
900
|
+
def _prepare_job_runtime_data(
|
|
901
|
+
offer: InstanceOfferWithAvailability, multinode: bool
|
|
902
|
+
) -> JobRuntimeData:
|
|
843
903
|
if offer.blocks == offer.total_blocks:
|
|
844
|
-
if
|
|
904
|
+
if settings.JOB_NETWORK_MODE == settings.JobNetworkMode.FORCED_BRIDGE:
|
|
845
905
|
network_mode = NetworkMode.BRIDGE
|
|
846
|
-
|
|
906
|
+
elif settings.JOB_NETWORK_MODE == settings.JobNetworkMode.HOST_WHEN_POSSIBLE:
|
|
847
907
|
network_mode = NetworkMode.HOST
|
|
908
|
+
else:
|
|
909
|
+
assert settings.JOB_NETWORK_MODE == settings.JobNetworkMode.HOST_FOR_MULTINODE_ONLY
|
|
910
|
+
network_mode = NetworkMode.HOST if multinode else NetworkMode.BRIDGE
|
|
848
911
|
return JobRuntimeData(
|
|
849
912
|
network_mode=network_mode,
|
|
850
913
|
offer=offer,
|
|
@@ -24,7 +24,7 @@ from sqlalchemy_utils import UUIDType
|
|
|
24
24
|
|
|
25
25
|
from dstack._internal.core.errors import DstackError
|
|
26
26
|
from dstack._internal.core.models.backends.base import BackendType
|
|
27
|
-
from dstack._internal.core.models.common import
|
|
27
|
+
from dstack._internal.core.models.common import CoreConfig, generate_dual_core_model
|
|
28
28
|
from dstack._internal.core.models.fleets import FleetStatus
|
|
29
29
|
from dstack._internal.core.models.gateways import GatewayStatus
|
|
30
30
|
from dstack._internal.core.models.health import HealthStatus
|
|
@@ -71,7 +71,11 @@ class NaiveDateTime(TypeDecorator):
|
|
|
71
71
|
return value.replace(tzinfo=timezone.utc)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
class
|
|
74
|
+
class DecryptedStringConfig(CoreConfig):
|
|
75
|
+
arbitrary_types_allowed = True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DecryptedString(generate_dual_core_model(DecryptedStringConfig)):
|
|
75
79
|
"""
|
|
76
80
|
A type for representing plaintext strings encrypted with `EncryptedString`.
|
|
77
81
|
Besides the string, stores information if the decryption was successful.
|
|
@@ -84,9 +88,6 @@ class DecryptedString(CoreModel):
|
|
|
84
88
|
decrypted: bool = True
|
|
85
89
|
exc: Optional[Exception] = None
|
|
86
90
|
|
|
87
|
-
class Config(CoreModel.Config):
|
|
88
|
-
arbitrary_types_allowed = True
|
|
89
|
-
|
|
90
91
|
def get_plaintext_or_error(self) -> str:
|
|
91
92
|
if self.decrypted and self.plaintext is not None:
|
|
92
93
|
return self.plaintext
|
|
@@ -3,24 +3,25 @@ from typing import Annotated, Any, Dict, List, Optional
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
5
|
from dstack._internal.core.models.backends.base import BackendType
|
|
6
|
-
from dstack._internal.core.models.common import CoreModel
|
|
6
|
+
from dstack._internal.core.models.common import CoreConfig, CoreModel, generate_dual_core_model
|
|
7
7
|
from dstack._internal.core.models.gateways import GatewayConfiguration
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class CreateGatewayRequestConfig(CoreConfig):
|
|
11
|
+
@staticmethod
|
|
12
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
13
|
+
del schema["properties"]["name"]
|
|
14
|
+
del schema["properties"]["backend_type"]
|
|
15
|
+
del schema["properties"]["region"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CreateGatewayRequest(generate_dual_core_model(CreateGatewayRequestConfig)):
|
|
11
19
|
configuration: GatewayConfiguration
|
|
12
20
|
# Deprecated and unused. Left for compatibility with 0.18 clients.
|
|
13
21
|
name: Annotated[Optional[str], Field(exclude=True)] = None
|
|
14
22
|
backend_type: Annotated[Optional[BackendType], Field(exclude=True)] = None
|
|
15
23
|
region: Annotated[Optional[str], Field(exclude=True)] = None
|
|
16
24
|
|
|
17
|
-
class Config(CoreModel.Config):
|
|
18
|
-
@staticmethod
|
|
19
|
-
def schema_extra(schema: Dict[str, Any]) -> None:
|
|
20
|
-
del schema["properties"]["name"]
|
|
21
|
-
del schema["properties"]["backend_type"]
|
|
22
|
-
del schema["properties"]["region"]
|
|
23
|
-
|
|
24
25
|
|
|
25
26
|
class GetGatewayRequest(CoreModel):
|
|
26
27
|
name: str
|
|
@@ -345,7 +345,7 @@ async def get_instance_offers(
|
|
|
345
345
|
Returns list of instances satisfying minimal resource requirements sorted by price
|
|
346
346
|
"""
|
|
347
347
|
logger.info("Requesting instance offers from backends: %s", [b.TYPE.value for b in backends])
|
|
348
|
-
tasks = [run_async(backend.compute().
|
|
348
|
+
tasks = [run_async(backend.compute().get_offers, requirements) for backend in backends]
|
|
349
349
|
offers_by_backend = []
|
|
350
350
|
for backend, result in zip(backends, await asyncio.gather(*tasks, return_exceptions=True)):
|
|
351
351
|
if isinstance(result, BackendError):
|
|
@@ -9,7 +9,11 @@ from pydantic import Field, ValidationError, validator
|
|
|
9
9
|
from typing_extensions import Annotated
|
|
10
10
|
|
|
11
11
|
from dstack._internal.core.errors import DockerRegistryError
|
|
12
|
-
from dstack._internal.core.models.common import
|
|
12
|
+
from dstack._internal.core.models.common import (
|
|
13
|
+
CoreModel,
|
|
14
|
+
FrozenCoreModel,
|
|
15
|
+
RegistryAuth,
|
|
16
|
+
)
|
|
13
17
|
from dstack._internal.server.utils.common import join_byte_stream_checked
|
|
14
18
|
from dstack._internal.utils.dxf import PatchedDXF
|
|
15
19
|
|
|
@@ -31,15 +35,12 @@ class DXFAuthAdapter:
|
|
|
31
35
|
)
|
|
32
36
|
|
|
33
37
|
|
|
34
|
-
class DockerImage(
|
|
38
|
+
class DockerImage(FrozenCoreModel):
|
|
35
39
|
image: str
|
|
36
|
-
registry: Optional[str]
|
|
40
|
+
registry: Optional[str] = None
|
|
37
41
|
repo: str
|
|
38
42
|
tag: str
|
|
39
|
-
digest: Optional[str]
|
|
40
|
-
|
|
41
|
-
class Config(CoreModel.Config):
|
|
42
|
-
frozen = True
|
|
43
|
+
digest: Optional[str] = None
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class ImageConfig(CoreModel):
|
|
@@ -13,9 +13,22 @@ from dstack._internal.core.backends.dstack.models import (
|
|
|
13
13
|
)
|
|
14
14
|
from dstack._internal.core.backends.models import BackendInfo
|
|
15
15
|
from dstack._internal.core.errors import ForbiddenError, ResourceExistsError, ServerClientError
|
|
16
|
-
from dstack._internal.core.models.projects import
|
|
16
|
+
from dstack._internal.core.models.projects import (
|
|
17
|
+
Member,
|
|
18
|
+
MemberPermissions,
|
|
19
|
+
Project,
|
|
20
|
+
ProjectHookConfig,
|
|
21
|
+
)
|
|
22
|
+
from dstack._internal.core.models.runs import RunStatus
|
|
17
23
|
from dstack._internal.core.models.users import GlobalRole, ProjectRole
|
|
18
|
-
from dstack._internal.server.models import
|
|
24
|
+
from dstack._internal.server.models import (
|
|
25
|
+
FleetModel,
|
|
26
|
+
MemberModel,
|
|
27
|
+
ProjectModel,
|
|
28
|
+
RunModel,
|
|
29
|
+
UserModel,
|
|
30
|
+
VolumeModel,
|
|
31
|
+
)
|
|
19
32
|
from dstack._internal.server.schemas.projects import MemberSetting
|
|
20
33
|
from dstack._internal.server.services import users
|
|
21
34
|
from dstack._internal.server.services.backends import (
|
|
@@ -112,6 +125,7 @@ async def create_project(
|
|
|
112
125
|
user: UserModel,
|
|
113
126
|
project_name: str,
|
|
114
127
|
is_public: bool = False,
|
|
128
|
+
config: Optional[ProjectHookConfig] = None,
|
|
115
129
|
) -> Project:
|
|
116
130
|
user_permissions = users.get_user_permissions(user)
|
|
117
131
|
if not user_permissions.can_create_projects:
|
|
@@ -139,7 +153,7 @@ async def create_project(
|
|
|
139
153
|
session=session, project_name=project_name
|
|
140
154
|
)
|
|
141
155
|
for hook in _CREATE_PROJECT_HOOKS:
|
|
142
|
-
await hook(session, project_model)
|
|
156
|
+
await hook(session, project_model, config)
|
|
143
157
|
# a hook may change project
|
|
144
158
|
session.expire(project_model)
|
|
145
159
|
project_model = await get_project_model_by_name_or_error(
|
|
@@ -178,6 +192,19 @@ async def delete_projects(
|
|
|
178
192
|
raise ForbiddenError()
|
|
179
193
|
if all(name in projects_names for name in user_project_names):
|
|
180
194
|
raise ServerClientError("Cannot delete the only project")
|
|
195
|
+
|
|
196
|
+
res = await session.execute(
|
|
197
|
+
select(ProjectModel.id).where(ProjectModel.name.in_(projects_names))
|
|
198
|
+
)
|
|
199
|
+
project_ids = res.scalars().all()
|
|
200
|
+
if len(project_ids) != len(projects_names):
|
|
201
|
+
raise ServerClientError("Failed to delete non-existent projects")
|
|
202
|
+
|
|
203
|
+
for project_id in project_ids:
|
|
204
|
+
# FIXME: The checks are not under lock,
|
|
205
|
+
# so there can be dangling active resources due to race conditions.
|
|
206
|
+
await _check_project_has_active_resources(session=session, project_id=project_id)
|
|
207
|
+
|
|
181
208
|
timestamp = str(int(get_current_datetime().timestamp()))
|
|
182
209
|
new_project_name = "_deleted_" + timestamp + ProjectModel.name
|
|
183
210
|
await session.execute(
|
|
@@ -588,7 +615,9 @@ def get_member_permissions(member_model: MemberModel) -> MemberPermissions:
|
|
|
588
615
|
_CREATE_PROJECT_HOOKS = []
|
|
589
616
|
|
|
590
617
|
|
|
591
|
-
def register_create_project_hook(
|
|
618
|
+
def register_create_project_hook(
|
|
619
|
+
func: Callable[[AsyncSession, ProjectModel, Optional[ProjectHookConfig]], Awaitable[None]],
|
|
620
|
+
):
|
|
592
621
|
_CREATE_PROJECT_HOOKS.append(func)
|
|
593
622
|
|
|
594
623
|
|
|
@@ -614,6 +643,36 @@ def _is_project_admin(
|
|
|
614
643
|
return False
|
|
615
644
|
|
|
616
645
|
|
|
646
|
+
async def _check_project_has_active_resources(session: AsyncSession, project_id: uuid.UUID):
|
|
647
|
+
res = await session.execute(
|
|
648
|
+
select(RunModel.run_name).where(
|
|
649
|
+
RunModel.project_id == project_id,
|
|
650
|
+
RunModel.status.not_in(RunStatus.finished_statuses()),
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
run_names = list(res.scalars().all())
|
|
654
|
+
if len(run_names) > 0:
|
|
655
|
+
raise ServerClientError(f"Failed to delete project with active runs: {run_names}")
|
|
656
|
+
res = await session.execute(
|
|
657
|
+
select(FleetModel.name).where(
|
|
658
|
+
FleetModel.project_id == project_id,
|
|
659
|
+
FleetModel.deleted.is_(False),
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
fleet_names = list(res.scalars().all())
|
|
663
|
+
if len(fleet_names) > 0:
|
|
664
|
+
raise ServerClientError(f"Failed to delete project with active fleets: {fleet_names}")
|
|
665
|
+
res = await session.execute(
|
|
666
|
+
select(VolumeModel.name).where(
|
|
667
|
+
VolumeModel.project_id == project_id,
|
|
668
|
+
VolumeModel.deleted.is_(False),
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
volume_names = list(res.scalars().all())
|
|
672
|
+
if len(volume_names) > 0:
|
|
673
|
+
raise ServerClientError(f"Failed to delete project with active volumes: {volume_names}")
|
|
674
|
+
|
|
675
|
+
|
|
617
676
|
async def remove_project_members(
|
|
618
677
|
session: AsyncSession,
|
|
619
678
|
user: UserModel,
|
|
@@ -1164,6 +1164,8 @@ async def process_terminating_run(session: AsyncSession, run_model: RunModel):
|
|
|
1164
1164
|
):
|
|
1165
1165
|
run_model.next_triggered_at = _get_next_triggered_at(run.run_spec)
|
|
1166
1166
|
run_model.status = RunStatus.PENDING
|
|
1167
|
+
# Unassign run from fleet so that the new fleet can be chosen on the next submission
|
|
1168
|
+
run_model.fleet = None
|
|
1167
1169
|
else:
|
|
1168
1170
|
run_model.status = run_model.termination_reason.to_status()
|
|
1169
1171
|
|
|
@@ -4,8 +4,14 @@ Environment variables read by the dstack server. Documented in reference/environ
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import warnings
|
|
7
|
+
from enum import Enum
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
10
|
+
from dstack._internal.utils.env import environ
|
|
11
|
+
from dstack._internal.utils.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
9
15
|
DSTACK_DIR_PATH = Path("~/.dstack/").expanduser()
|
|
10
16
|
|
|
11
17
|
SERVER_DIR_PATH = Path(os.getenv("DSTACK_SERVER_DIR", DSTACK_DIR_PATH / "server"))
|
|
@@ -136,3 +142,43 @@ UPDATE_DEFAULT_PROJECT = os.getenv("DSTACK_UPDATE_DEFAULT_PROJECT") is not None
|
|
|
136
142
|
DO_NOT_UPDATE_DEFAULT_PROJECT = os.getenv("DSTACK_DO_NOT_UPDATE_DEFAULT_PROJECT") is not None
|
|
137
143
|
SKIP_GATEWAY_UPDATE = os.getenv("DSTACK_SKIP_GATEWAY_UPDATE") is not None
|
|
138
144
|
ENABLE_PROMETHEUS_METRICS = os.getenv("DSTACK_ENABLE_PROMETHEUS_METRICS") is not None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class JobNetworkMode(Enum):
|
|
148
|
+
# "host" for multinode runs only, "bridge" otherwise. Opt-in new defaut
|
|
149
|
+
HOST_FOR_MULTINODE_ONLY = 1
|
|
150
|
+
# "bridge" if the job occupies only a part of the instance, "host" otherswise. Current default
|
|
151
|
+
HOST_WHEN_POSSIBLE = 2
|
|
152
|
+
# Always "bridge", even for multinode runs. Same as legacy DSTACK_FORCE_BRIDGE_NETWORK=true
|
|
153
|
+
FORCED_BRIDGE = 3
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_job_network_mode() -> JobNetworkMode:
|
|
157
|
+
# Current default
|
|
158
|
+
mode = JobNetworkMode.HOST_WHEN_POSSIBLE
|
|
159
|
+
bridge_var = "DSTACK_FORCE_BRIDGE_NETWORK"
|
|
160
|
+
force_bridge = environ.get_bool(bridge_var)
|
|
161
|
+
mode_var = "DSTACK_SERVER_JOB_NETWORK_MODE"
|
|
162
|
+
mode_from_env = environ.get_enum(mode_var, JobNetworkMode, value_type=int)
|
|
163
|
+
if mode_from_env is not None:
|
|
164
|
+
if force_bridge is not None:
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"{bridge_var} is deprecated since 0.19.27 and ignored when {mode_var} is set"
|
|
167
|
+
)
|
|
168
|
+
return mode_from_env
|
|
169
|
+
if force_bridge is not None:
|
|
170
|
+
if force_bridge:
|
|
171
|
+
mode = JobNetworkMode.FORCED_BRIDGE
|
|
172
|
+
logger.warning(
|
|
173
|
+
(
|
|
174
|
+
f"{bridge_var} is deprecated since 0.19.27."
|
|
175
|
+
f" Set {mode_var} to {mode.value} and remove {bridge_var}"
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
logger.warning(f"{bridge_var} is deprecated since 0.19.27. Remove {bridge_var}")
|
|
180
|
+
return mode
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
JOB_NETWORK_MODE = _get_job_network_mode()
|
|
184
|
+
del _get_job_network_mode
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
2
2
|
"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
3
|
-
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-4eecc75fbe64067eb1bc.js"></script><link href="/main-56191c63d516fd0041c4.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>
|
|
3
|
+
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-c51afa7f243e24d3e446.js"></script><link href="/main-56191fbfe77f49b251de.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-font-family='metro-web, Metro, -apple-system, "system-ui", "Segoe UI", Roboto' data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>
|