| | |
| | |
| |
|
| | import functools |
| | import time |
| | from datetime import datetime, timedelta, timezone |
| | from pathlib import Path |
| |
|
| | import click |
| | import httpx |
| |
|
| | from dyff.client import Client, errors |
| | from dyff.schema.platform import * |
| | from dyff.schema.requests import * |
| |
|
| | from app.api.models import PredictionResponse |
| |
|
| | |
| |
|
| |
|
| | def _wait_for_status( |
| | get_entity_fn, target_status: str | list[str], *, timeout: timedelta |
| | ) -> str: |
| | if isinstance(target_status, str): |
| | target_status = [target_status] |
| | then = datetime.now(timezone.utc) |
| | while True: |
| | try: |
| | status = get_entity_fn().status |
| | if status in target_status: |
| | return status |
| | except errors.HTTPError as ex: |
| | if ex.status != 404: |
| | raise |
| | except httpx.HTTPStatusError as ex: |
| | if ex.response.status_code != 404: |
| | raise |
| | if (datetime.now(timezone.utc) - then) >= timeout: |
| | break |
| | time.sleep(5) |
| | raise AssertionError("timeout") |
| |
|
| |
|
| | def _common_options(f): |
| | @click.option( |
| | "--account", |
| | type=str, |
| | required=True, |
| | help="Your account ID", |
| | metavar="ID", |
| | ) |
| | @functools.wraps(f) |
| | def wrapper(*args, **kwargs): |
| | return f(*args, **kwargs) |
| | return wrapper |
| |
|
| |
|
| | @click.group() |
| | def cli(): |
| | pass |
| |
|
| |
|
| | @cli.command() |
| | @_common_options |
| | @click.option( |
| | "--name", |
| | type=str, |
| | required=True, |
| | help="The name of your detector model. For display and querying purposes only.", |
| | ) |
| | @click.option( |
| | "--image", |
| | type=str, |
| | default=None, |
| | help="The Docker image to upload (e.g., 'some/image:latest')." |
| | " Must exist in your local Docker deamon." |
| | " Required if --artifact is not specified.", |
| | ) |
| | @click.option( |
| | "--endpoint", |
| | type=str, |
| | default="predict", |
| | help="The endpoint to call on your service to make a prediction.", |
| | ) |
| | @click.option( |
| | "--volume", |
| | type=click.Path(exists=True, file_okay=False, readable=True, resolve_path=True, path_type=Path), |
| | default=None, |
| | help="A local directory path containing files to upload and mount in the running Docker container." |
| | " You should use this if your submission includes large files like neural network weights." |
| | ) |
| | @click.option( |
| | "--volume-mount", |
| | type=click.Path(exists=False, path_type=Path), |
| | default=None, |
| | help="The path to mount your uploaded directory in the running Docker container." |
| | " Must be an absolute path." |
| | " Required if --volume is specified.") |
| | @click.option( |
| | "--artifact", |
| | "artifact_id", |
| | type=str, |
| | default=None, |
| | help="The ID of the Artifact (i.e., Docker image) to use in the submission, if it already exists." |
| | " You can pass the artifact.id from a previous invocation.", |
| | metavar="ID", |
| | ) |
| | @click.option( |
| | "--model", |
| | "model_id", |
| | type=str, |
| | default=None, |
| | help="The ID of the Model (i.e., neural network weights) to use in the submission, if it already exists." |
| | " You can pass the model.id from a previous invocation.", |
| | metavar="ID", |
| | ) |
| | @click.option( |
| | "--gpu", |
| | is_flag=True, |
| | default=False, |
| | help="Request a GPU (NVIDIA L4) for the inference service.", |
| | ) |
| | def upload_submission( |
| | account: str, |
| | name: str, |
| | image: str | None, |
| | endpoint: str, |
| | volume: Path | None, |
| | volume_mount: Path | None, |
| | artifact_id: str | None, |
| | model_id: str | None, |
| | gpu: bool, |
| | ) -> None: |
| | dyffapi = Client() |
| |
|
| | |
| | if artifact_id is None: |
| | |
| | click.echo(f"creating Artifact ... {account}") |
| | artifact = dyffapi.artifacts.create(ArtifactCreateRequest(account=account)) |
| | click.echo(f"artifact.id: \"{artifact.id}\"") |
| | _wait_for_status( |
| | lambda: dyffapi.artifacts.get(artifact.id), |
| | "WaitingForUpload", |
| | timeout=timedelta(seconds=30), |
| | ) |
| |
|
| | |
| | click.echo("pushing Artifact ...") |
| | dyffapi.artifacts.push(artifact, source=f"docker-daemon:{image}") |
| | time.sleep(5) |
| |
|
| | |
| | dyffapi.artifacts.finalize(artifact.id) |
| | _wait_for_status( |
| | lambda: dyffapi.artifacts.get(artifact.id), |
| | "Ready", |
| | timeout=timedelta(seconds=30), |
| | ) |
| |
|
| | click.echo("... done") |
| | else: |
| | artifact = dyffapi.artifacts.get(artifact_id) |
| | assert artifact is not None |
| |
|
| | model: Model | None = None |
| | if model_id is None: |
| | if volume is not None: |
| | if volume_mount is None: |
| | raise click.UsageError("--volume-mount is required when --volume is used") |
| | |
| | click.echo("creating Model from local directory ...") |
| |
|
| | model = dyffapi.models.create_from_volume( |
| | volume, name="model_volume", account=account, resources=ModelResources() |
| | ) |
| | click.echo(f"model.id: \"{model.id}\"") |
| | _wait_for_status( |
| | lambda: dyffapi.models.get(model.id), |
| | "WaitingForUpload", |
| | timeout=timedelta(seconds=30), |
| | ) |
| |
|
| | click.echo("uploading Model ...") |
| | dyffapi.models.upload_volume(model, volume) |
| | _wait_for_status( |
| | lambda: dyffapi.models.get(model.id), |
| | "Ready", |
| | timeout=timedelta(seconds=30), |
| | ) |
| |
|
| | click.echo("... done") |
| | else: |
| | model = None |
| | else: |
| | model = dyffapi.models.get(model_id) |
| | assert model is not None |
| |
|
| | |
| | if volume_mount is not None: |
| | if model is None: |
| | raise click.UsageError("--volume-mount requires --volume or --model") |
| | if not volume_mount.is_absolute(): |
| | raise click.UsageError("--volume-mount must be an absolute path") |
| | volumeMounts=[ |
| | VolumeMount( |
| | kind=VolumeMountKind.data, |
| | name="model", |
| | mountPath=volume_mount, |
| | data=VolumeMountData( |
| | source=EntityIdentifier.of(model), |
| | ), |
| | ), |
| | ] |
| | else: |
| | volumeMounts = None |
| |
|
| | accelerator: Accelerator | None = None |
| | if gpu: |
| | accelerator = Accelerator( |
| | kind="GPU", |
| | gpu=AcceleratorGPU( |
| | hardwareTypes=["nvidia.com/gpu-l4"], |
| | count=1, |
| | ), |
| | ) |
| |
|
| | |
| | service_request = InferenceServiceCreateRequest( |
| | account=account, |
| | name=name, |
| | model=None, |
| | runner=InferenceServiceRunner( |
| | kind=InferenceServiceRunnerKind.CONTAINER, |
| | imageRef=EntityIdentifier.of(artifact), |
| | resources=ModelResources(), |
| | volumeMounts=volumeMounts, |
| | accelerator=accelerator, |
| | ), |
| | interface=InferenceInterface( |
| | endpoint=endpoint, |
| | outputSchema=DataSchema.make_output_schema(PredictionResponse), |
| | ), |
| | ) |
| | click.echo("creating InferenceService ...") |
| | service = dyffapi.inferenceservices.create(service_request) |
| | click.echo(f"service.id: \"{service.id}\"") |
| | click.echo("... done") |
| |
|
| |
|
| | @cli.command() |
| | @_common_options |
| | @click.option( |
| | "--task", |
| | "task_id", |
| | type=str, |
| | required=True, |
| | help="The Task ID to submit to.", |
| | metavar="ID", |
| | ) |
| | @click.option( |
| | "--team", |
| | "team_id", |
| | type=str, |
| | required=True, |
| | help="The Team ID making the submission.", |
| | metavar="ID", |
| | ) |
| | @click.option( |
| | "--service", |
| | "service_id", |
| | type=str, |
| | required=True, |
| | help="The InferenceService ID to submit.", |
| | metavar="ID", |
| | ) |
| | @click.option( |
| | "--challenge", |
| | "challenge_id", |
| | type=str, |
| | default="dc509a8c771b492b90c43012fde9a04f", |
| | help="The Challenge ID to submit to.", |
| | metavar="ID", |
| | ) |
| | def submit(account: str, task_id: str, team_id: str, service_id: str, challenge_id: str) -> None: |
| | dyffapi = Client() |
| |
|
| | challenge = dyffapi.challenges.get(challenge_id) |
| | |
| | challengetask = challenge.tasks[task_id] |
| |
|
| | team = dyffapi.teams.get(team_id) |
| |
|
| | service = dyffapi.inferenceservices.get(service_id) |
| |
|
| | submission = dyffapi.challenges.submit( |
| | challenge.id, |
| | challengetask.id, |
| | SubmissionCreateRequest( |
| | account=account, |
| | team=team.id, |
| | submission=EntityIdentifier(kind="InferenceService", id=service.id), |
| | ), |
| | ) |
| | click.echo(submission.model_dump_json(indent=2)) |
| | click.echo(f"submission.id: \"{submission.id}\"") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | cli(show_default=True) |