diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..a9e4ee246d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [dstackai] diff --git a/.github/workflows/build-artifacts.yml b/.github/workflows/build-artifacts.yml index c8b68db670..1f28825a81 100644 --- a/.github/workflows/build-artifacts.yml +++ b/.github/workflows/build-artifacts.yml @@ -64,7 +64,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -74,7 +74,7 @@ jobs: - name: Install dependencies run: uv sync --all-extras - name: Run pyright - uses: jakebailey/pyright-action@v2 + uses: jakebailey/pyright-action@v3 with: pylance-version: latest-release - name: Download frontend build @@ -100,10 +100,7 @@ jobs: defaults: run: working-directory: runner - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go @@ -126,11 +123,6 @@ jobs: if [[ "${{ inputs.go-integration-tests }}" == "true" ]]; then SHORT="" fi - # Skip failing integration tests on macOS for release builds. - # TODO: https://github.com/dstackai/dstack/issues/3005 - if [[ "${{ inputs.staging }}" == "false" && "${{ startsWith(matrix.os, 'macos') }}" == "true" ]]; then - SHORT="-short" - fi go version go fmt $(go list ./... | grep -v /vendor/) go vet $(go list ./... | grep -v /vendor/) @@ -171,33 +163,6 @@ jobs: runner/dstack-shim-${{ matrix.goos }}-${{ matrix.goarch }} retention-days: 1 - gateway-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up uv - uses: astral-sh/setup-uv@v5 - with: - python-version: 3.11 - - name: Build package - working-directory: gateway - run: | - echo "__version__ = \"${{ inputs.version }}\"" > src/dstack/gateway/version.py - # TODO: depend on a specific dstackai/dstack commit for staging builds? - if [[ "${{ inputs.staging }}" == "false" ]]; then - sed \ - -i.old \ - "s|@ https://github.com/dstackai/dstack/archive/refs/heads/master.tar.gz|== ${{ inputs.version }}|" \ - pyproject.toml - diff pyproject.toml pyproject.toml.old > /dev/null && echo "Could not set version" && exit 1 - fi - uv build - - uses: actions/upload-artifact@v4 - with: - name: dstack-gateway - path: gateway/dist/dstack_gateway-${{ inputs.version }}-py3-none-any.whl - retention-days: 1 - python-build: needs: [code-lint, frontend-build] runs-on: ubuntu-latest @@ -224,12 +189,14 @@ jobs: VERSION=${{ inputs.version }} IS_RELEASE=True fi - BASE_IMAGE=$(cat src/dstack/version.py | grep "base_image = ") - BASE_IMAGE_UBUNTU_VERSION=$(cat src/dstack/version.py | grep "base_image_ubuntu_version = ") + DOCKER_BASE_IMAGE=$(cat src/dstack/version.py | grep "docker_base_image = ") + DOCKER_BASE_IMAGE_UBUNTU_VERSION=$(cat src/dstack/version.py | grep "docker_base_image_ubuntu_version = ") + VM_BASE_IMAGE=$(cat src/dstack/version.py | grep "vm_base_image = ") echo "__version__ = \"$VERSION\"" > src/dstack/version.py echo "__is_release__ = $IS_RELEASE" >> src/dstack/version.py - echo $BASE_IMAGE >> src/dstack/version.py - echo $BASE_IMAGE_UBUNTU_VERSION >> src/dstack/version.py + echo $DOCKER_BASE_IMAGE >> src/dstack/version.py + echo $DOCKER_BASE_IMAGE_UBUNTU_VERSION >> src/dstack/version.py + echo $VM_BASE_IMAGE >> src/dstack/version.py cp README.md src uv build - uses: actions/upload-artifact@v4 @@ -251,8 +218,8 @@ jobs: - name: Generate json schema run: | mkdir /tmp/json-schemas - uv run python -c "from dstack._internal.core.models.configurations import DstackConfiguration; print(DstackConfiguration.schema_json())" > /tmp/json-schemas/configuration.json - uv run python -c "from dstack._internal.core.models.profiles import ProfilesConfig; print(ProfilesConfig.schema_json())" > /tmp/json-schemas/profiles.json + uv run python -c "from dstack._internal.core.models.configurations import DstackConfiguration; import json; print(json.dumps(DstackConfiguration.model_json_schema()))" > /tmp/json-schemas/configuration.json + uv run python -c "from dstack._internal.core.models.profiles import ProfilesConfig; import json; print(json.dumps(ProfilesConfig.model_json_schema()))" > /tmp/json-schemas/profiles.json - uses: actions/upload-artifact@v4 with: name: json-schemas diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 140ba82204..9df8040d4d 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,4 +1,8 @@ -name: Build Docs +name: Build Site + +# Reusable build of the full site: the React landing (website/) overlaid onto the +# MkDocs docs + blog build, via scripts/docs/build_site.sh. Produces the `site` artifact +# consumed by docs.yaml (deploy) and build.yml (PR build check). on: workflow_call: @@ -11,15 +15,21 @@ jobs: - uses: astral-sh/setup-uv@v5 with: python-version: 3.11 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: website/package-lock.json - name: Install dstack run: | uv sync --extra server - name: Build run: | sudo apt-get update && sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev - uv run mkdocs build -s + ./scripts/docs/build_site.sh - uses: actions/upload-artifact@v4 with: name: site path: site retention-days: 1 + include-hidden-files: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcd92e696f..77e50916c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: branches: - "master" paths-ignore: - - "docs/**" + - "mkdocs/**" - "mkdocs.yml" pull_request: branches: diff --git a/.github/workflows/docker-tt-smi.yml b/.github/workflows/docker-tt-smi.yml index d60a4cf487..444266ccdb 100644 --- a/.github/workflows/docker-tt-smi.yml +++ b/.github/workflows/docker-tt-smi.yml @@ -11,6 +11,11 @@ on: description: "TT SMI version" required: true default: "3.0.25" + tag_latest: + description: "Update 'latest'" + type: boolean + required: true + default: false jobs: build-tt-smi: @@ -38,8 +43,12 @@ jobs: --build-arg IMAGE_NAME=${IMAGE_NAME} \ --build-arg TT_SMI_VERSION=${{ inputs.tt_smi_version }} \ --build-arg BUILD_DATE=$(date --utc --iso-8601=seconds)Z \ - --tag ${IMAGE_NAME}:latest - VERSION=$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' ${IMAGE_NAME}) - docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:${VERSION} + --tag ${IMAGE_NAME}:${{ inputs.tt_smi_version }} + VERSION=$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' ${IMAGE_NAME}:${{ inputs.tt_smi_version }}) + docker tag ${IMAGE_NAME}:${{ inputs.tt_smi_version }} ${IMAGE_NAME}:${VERSION} docker push ${IMAGE_NAME}:${VERSION} - docker push ${IMAGE_NAME}:latest + - name: Tag and push latest + if: ${{ inputs.tag_latest }} + run: | + docker tag ${{ inputs.image_name }}:${{ inputs.tt_smi_version }} ${{ inputs.image_name }}:latest + docker push ${{ inputs.image_name }}:latest diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7a34328969..c17bf2cd95 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,58 +1,30 @@ -name: Build Docker & cloud images -run-name: Build Docker & cloud images ${{ inputs.image_version }}${{ inputs.staging && ' (staging)' || '' }} +name: Build Docker images +run-name: Build Docker images ${{ inputs.image_version }}${{ inputs.staging && ' (staging)' || '' }} on: workflow_dispatch: inputs: image_version: - description: "Docker image version" + description: "Image version" required: true staging: description: "Build staging images" type: boolean default: false - prepull_staging: - description: "Pre-pull staging Docker images" - type: boolean - default: false - build_docker: - description: "Build Docker images" - type: boolean - default: true - build_aws: - description: "Build AWS images" - type: boolean - default: true - build_azure: - description: "Build Azure images" - type: boolean - default: true - build_gcp: - description: "Build GCP images" - type: boolean - default: true - build_oci: - description: "Build OCI images" - type: boolean - default: true env: - PACKER_VERSION: "1.9.2" BUILD_DOCKER_REPO: ${{ inputs.staging && 'base-stgn' || 'base' }} - PREPULL_DOCKER_REPO: ${{ inputs.prepull_staging && 'base-stgn' || 'base' }} - VM_IMAGE_BUILD_PREFIX: ${{ inputs.staging && format('stgn-{0}-', github.run_number) || '' }} # staging ? prefix : '' jobs: build-docker: - if: inputs.build_docker defaults: run: working-directory: docker - runs-on: ubuntu-latest + runs-on: dstack-ubuntu-latest-32-cores strategy: matrix: flavor: ["base", "devel", "devel-efa"] - ubuntu_version: ["22"] + ubuntu_version: ["24"] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -73,192 +45,11 @@ jobs: df -h / - name: Build and upload to DockerHub run: | - if [ "${{ matrix.flavor }}" = "base" ]; then - FILE="base/Dockerfile" - elif [ "${{ matrix.flavor }}" = "devel" ]; then - FILE="base/Dockerfile" - else - FILE="base/efa/Dockerfile" - fi docker buildx build \ --platform linux/amd64 \ + --target ${{ matrix.flavor }} \ --tag dstackai/${{ env.BUILD_DOCKER_REPO }}:${{ inputs.image_version }}-${{ matrix.flavor }}-ubuntu${{ matrix.ubuntu_version }}.04 \ - --build-arg FLAVOR=${{ matrix.flavor }} \ --build-arg UBUNTU_VERSION=${{ matrix.ubuntu_version }} \ --provenance=false \ --push \ - -f $FILE . - - build-aws-images: - needs: build-docker - if: always() && inputs.build_aws && (needs.build-docker.result == 'success' || needs.build-docker.result == 'skipped') - defaults: - run: - working-directory: scripts/packer - runs-on: ubuntu-latest - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - strategy: - matrix: - variant: ["", "-cuda"] - steps: - - uses: actions/checkout@v4 - - name: Download packer - run: | - wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - chmod +x packer - - name: Run packer - run: | - ./packer build -var-file=versions.json $PROD_VARS -var image_repo=${{ env.PREPULL_DOCKER_REPO }} -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX aws-image${{ matrix.variant }}.json - env: - PROD_VARS: ${{ !inputs.staging && '-var-file=aws-vars-prod.json' || '' }} # production ? var-file : '' - - build-azure-images: - needs: build-docker - if: always() && inputs.build_azure && (needs.build-docker.result == 'success' || needs.build-docker.result == 'skipped') - defaults: - run: - working-directory: scripts/packer - runs-on: ubuntu-latest - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - VERSION: ${{ github.run_number }} - strategy: - matrix: - variant: ["", "-cuda", "-grid"] - steps: - - uses: actions/checkout@v4 - - uses: Azure/login@v2 - name: Log in to az - with: - creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' - - name: Download packer - run: | - wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - chmod +x packer - - name: Run packer - run: | - ./packer build -var-file=versions.json -var image_repo=${{ env.PREPULL_DOCKER_REPO }} -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX azure-image${{ matrix.variant }}.json - - name: Publish azure image - if: ${{ !inputs.staging }} - run: | - IMAGE_DEFINITION=${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} - IMAGE_NAME=${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} - ../publish_azure_image.sh $IMAGE_DEFINITION $IMAGE_NAME - - build-gcp-images: - needs: build-docker - if: always() && inputs.build_gcp && (needs.build-docker.result == 'success' || needs.build-docker.result == 'skipped') - defaults: - run: - working-directory: scripts/packer - runs-on: ubuntu-latest - strategy: - matrix: - variant: ["", "-cuda"] - permissions: - contents: "read" - id-token: "write" - steps: - - uses: actions/checkout@v4 - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - workload_identity_provider: "projects/531508670106/locations/global/workloadIdentityPools/github-identity-pool/providers/github-id-provider" - service_account: "github-actions@dstack.iam.gserviceaccount.com" - create_credentials_file: true - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2 - - name: Download packer - run: | - wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - chmod +x packer - - name: Run packer - run: | - ./packer build -var-file=versions.json -var image_repo=${{ env.PREPULL_DOCKER_REPO }} -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX gcp-image${{ matrix.variant }}.json - - name: Publish images - run: | - IMAGE_VERSION=${IMAGE_VERSION//./-} - gcloud compute images add-iam-policy-binding ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-$IMAGE_VERSION --member='allAuthenticatedUsers' --role='roles/compute.imageUser' - env: - IMAGE_VERSION: ${{ inputs.image_version }} - - build-oci-images: - needs: build-docker - if: always() && inputs.build_oci && (needs.build-docker.result == 'success' || needs.build-docker.result == 'skipped') - runs-on: ubuntu-latest - env: - OCI_COMPARTMENT: ocid1.compartment.oc1..aaaaaaaaxu2uq64unfa2imwkp37icxqv6f7gwp2mczdt2mukuqbkauwqmbtq - OCI_SUBNET: ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaaewxkaqsmbi2tig5sfw4eexzo3mkb4zrpm4gwvfhqdddnxicxe4fa - OCI_AVAILABILITY_DOMAIN: kZql:EU-FRANKFURT-1-AD-3 - OCI_REGION: eu-frankfurt-1 - strategy: - matrix: - variant: ["", "-cuda"] - steps: - - uses: actions/checkout@v4 - - name: Setup OCI config - run: | - mkdir ~/.oci - echo "$OCI_KEY_CONTENT" > ~/.oci/key.pem - echo [DEFAULT] > ~/.oci/config - echo region=$OCI_REGION >> ~/.oci/config - echo tenancy=$OCI_TENANCY >> ~/.oci/config - echo user=$OCI_USER >> ~/.oci/config - echo fingerprint=$OCI_FINGERPRINT >> ~/.oci/config - echo key_file=~/.oci/key.pem >> ~/.oci/config - env: - OCI_TENANCY: ${{ secrets.OCI_CLI_TENANCY }} - OCI_USER: ${{ secrets.OCI_CLI_USER }} - OCI_FINGERPRINT: ${{ secrets.OCI_CLI_FINGERPRINT }} - OCI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} - - name: Install packer - working-directory: scripts/packer - run: | - wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip - chmod +x packer - ./packer plugins install github.com/hashicorp/oracle - - name: Run packer - working-directory: scripts/packer - run: | - ./packer build \ - -var-file=versions.json \ - -var image_repo=${{ env.PREPULL_DOCKER_REPO }} \ - -var image_version=${{ inputs.image_version }} \ - -var build_prefix=$VM_IMAGE_BUILD_PREFIX \ - -var oci_compartment_ocid=$OCI_COMPARTMENT \ - -var oci_subnet_ocid=$OCI_SUBNET \ - -var oci_availability_domain=$OCI_AVAILABILITY_DOMAIN \ - oci-image${{ matrix.variant }}.json - - uses: astral-sh/setup-uv@v5 - with: - python-version: "3.12" - - name: Install dependencies for publishing - run: | - uv sync --extra oci - - name: Copy image to target regions - if: ${{ !inputs.staging }} - run: | - uv run scripts/oci_image_tools.py copy \ - --image ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} \ - --from $OCI_REGION \ - --compartment $OCI_COMPARTMENT - - name: Publish image in OCI Marketplace - if: ${{ !inputs.staging }} - run: | - uv run scripts/oci_image_tools.py publish \ - --image ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} \ - --compartment $OCI_COMPARTMENT \ - --version ${{ inputs.image_version }} \ - --description "Image for running workloads with dstack - https://dstack.ai/" \ - --os "Ubuntu 22.04" \ - --contact-name dstack \ - --contact-email hello@dstack.ai + -f base/Dockerfile . diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 3e0d5f3a75..78212c6fda 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,7 @@ -name: Build & Deploy Docs +name: Build & Deploy Site + +# Builds the full site (React landing + MkDocs docs + blog, see build-docs.yml) and +# cross-repo deploys it to the GitHub Pages repo serving dstack.ai. on: workflow_dispatch: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db006abbc4..0618b00367 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: with: version: ${{ needs.compute-version.outputs.version }} staging: false - go-integration-tests: true + go-integration-tests: false upload-pre-pypi-artifacts: needs: [compute-version, build-artifacts] @@ -66,7 +66,7 @@ jobs: path: dist - name: Upload Python package to PyPI run: | - uv publish --username ${{ secrets.PYPI_USERNAME }} --password ${{ secrets.PYPI_PASSWORD }} + uv publish --username ${{ secrets.PYPI_USERNAME }} --password ${{ secrets.PYPI_PASSWORD }} upload-post-pypi-artifacts: needs: [compute-version, pypi-upload] diff --git a/.github/workflows/upload-pre-pypi-artifacts.yml b/.github/workflows/upload-pre-pypi-artifacts.yml index 542f445e2e..91e94508bf 100644 --- a/.github/workflows/upload-pre-pypi-artifacts.yml +++ b/.github/workflows/upload-pre-pypi-artifacts.yml @@ -25,21 +25,6 @@ jobs: ignore-empty-workdir: true - name: Install AWS run: uv tool install awscli - - name: Download dstack-gateway - uses: actions/download-artifact@v4 - with: - name: dstack-gateway - path: gateway - - name: Upload dstack-gateway to S3 - working-directory: gateway - run: | - CHANNEL=release - if [ "${{ inputs.staging }}" = "true" ]; then - CHANNEL=stgn - fi - WHEEL=dstack_gateway-${{ inputs.version }}-py3-none-any.whl - aws s3 cp $WHEEL "s3://dstack-gateway-downloads/$CHANNEL/$WHEEL" - echo "${{ inputs.version }}" | aws s3 cp - "s3://dstack-gateway-downloads/$CHANNEL/latest-version" - name: Download dstack-runner uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/vm-images.yml b/.github/workflows/vm-images.yml new file mode 100644 index 0000000000..f86769834b --- /dev/null +++ b/.github/workflows/vm-images.yml @@ -0,0 +1,212 @@ +name: Build VM images +run-name: Build VM images ${{ inputs.image_version }}${{ inputs.staging && ' (staging)' || '' }} + +on: + workflow_dispatch: + inputs: + image_version: + description: "Image version" + required: true + staging: + description: "Build staging images" + type: boolean + default: false + build_aws: + description: "Build AWS images" + type: boolean + default: true + build_azure: + description: "Build Azure images" + type: boolean + default: true + azure_variant: + description: "Azure image variants to build" + type: choice + options: ["all", "standard", "cuda", "grid"] + default: "all" + azure_new_gallery_version: + description: "Rebuild Azure images that already exist and publish them as a new gallery version" + type: boolean + default: false + build_gcp: + description: "Build GCP images" + type: boolean + default: true + build_oci: + description: "Build OCI images" + type: boolean + default: true + +env: + PACKER_VERSION: "1.9.2" + VM_IMAGE_BUILD_PREFIX: ${{ inputs.staging && format('stgn-{0}-', github.run_number) || '' }} # staging ? prefix : '' + +jobs: + build-aws-images: + if: inputs.build_aws + defaults: + run: + working-directory: scripts/packer + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + strategy: + matrix: + variant: ["", "-cuda"] + steps: + - uses: actions/checkout@v4 + - name: Download packer + run: | + wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + chmod +x packer + - name: Run packer + run: | + ./packer build -var-file=versions.json $PROD_VARS -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX aws-image${{ matrix.variant }}.json + env: + PROD_VARS: ${{ !inputs.staging && '-var-file=aws-vars-prod.json' || '' }} # production ? var-file : '' + + build-azure-images: + if: inputs.build_azure + defaults: + run: + working-directory: scripts/packer + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + VERSION: ${{ github.run_number }} + strategy: + matrix: + variant: ${{ fromJSON('{"all":["","-cuda","-grid"],"standard":[""],"cuda":["-cuda"],"grid":["-grid"]}')[inputs.azure_variant] }} + steps: + - uses: actions/checkout@v4 + - uses: Azure/login@v2 + name: Log in to az + with: + creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + - name: Download packer + run: | + wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + chmod +x packer + - name: Run packer + run: | + ./packer build ${{ inputs.azure_new_gallery_version && '-force' || '' }} -var-file=versions.json -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX azure-image${{ matrix.variant }}.json + - name: Publish azure image + if: ${{ !inputs.staging }} + run: | + IMAGE_DEFINITION=${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} + IMAGE_NAME=${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} + ../publish_azure_image.sh $IMAGE_DEFINITION $IMAGE_NAME ${{ inputs.azure_new_gallery_version }} + + build-gcp-images: + if: inputs.build_gcp + defaults: + run: + working-directory: scripts/packer + runs-on: ubuntu-latest + strategy: + matrix: + variant: ["", "-cuda"] + permissions: + contents: "read" + id-token: "write" + steps: + - uses: actions/checkout@v4 + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: "projects/531508670106/locations/global/workloadIdentityPools/github-identity-pool/providers/github-id-provider" + service_account: "github-actions@dstack.iam.gserviceaccount.com" + create_credentials_file: true + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + - name: Download packer + run: | + wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + chmod +x packer + - name: Run packer + run: | + ./packer build -var-file=versions.json -var image_version=${{ inputs.image_version }} -var build_prefix=$VM_IMAGE_BUILD_PREFIX gcp-image${{ matrix.variant }}.json + - name: Publish images + run: | + IMAGE_VERSION=${IMAGE_VERSION//./-} + gcloud compute images add-iam-policy-binding ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-$IMAGE_VERSION --member='allAuthenticatedUsers' --role='roles/compute.imageUser' + env: + IMAGE_VERSION: ${{ inputs.image_version }} + + build-oci-images: + if: inputs.build_oci + runs-on: ubuntu-latest + env: + OCI_COMPARTMENT: ocid1.compartment.oc1..aaaaaaaaxu2uq64unfa2imwkp37icxqv6f7gwp2mczdt2mukuqbkauwqmbtq + OCI_SUBNET: ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaaewxkaqsmbi2tig5sfw4eexzo3mkb4zrpm4gwvfhqdddnxicxe4fa + OCI_AVAILABILITY_DOMAIN: kZql:EU-FRANKFURT-1-AD-3 + OCI_REGION: eu-frankfurt-1 + strategy: + matrix: + variant: ["", "-cuda"] + steps: + - uses: actions/checkout@v4 + - name: Setup OCI config + run: | + mkdir ~/.oci + echo "$OCI_KEY_CONTENT" > ~/.oci/key.pem + echo [DEFAULT] > ~/.oci/config + echo region=$OCI_REGION >> ~/.oci/config + echo tenancy=$OCI_TENANCY >> ~/.oci/config + echo user=$OCI_USER >> ~/.oci/config + echo fingerprint=$OCI_FINGERPRINT >> ~/.oci/config + echo key_file=~/.oci/key.pem >> ~/.oci/config + env: + OCI_TENANCY: ${{ secrets.OCI_CLI_TENANCY }} + OCI_USER: ${{ secrets.OCI_CLI_USER }} + OCI_FINGERPRINT: ${{ secrets.OCI_CLI_FINGERPRINT }} + OCI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + - name: Install packer + working-directory: scripts/packer + run: | + wget https://releases.hashicorp.com/packer/${{ env.PACKER_VERSION }}/packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + unzip packer_${{ env.PACKER_VERSION }}_linux_amd64.zip + chmod +x packer + ./packer plugins install github.com/hashicorp/oracle + - name: Run packer + working-directory: scripts/packer + run: | + ./packer build \ + -var-file=versions.json \ + -var image_version=${{ inputs.image_version }} \ + -var build_prefix=$VM_IMAGE_BUILD_PREFIX \ + -var oci_compartment_ocid=$OCI_COMPARTMENT \ + -var oci_subnet_ocid=$OCI_SUBNET \ + -var oci_availability_domain=$OCI_AVAILABILITY_DOMAIN \ + oci-image${{ matrix.variant }}.json + - uses: astral-sh/setup-uv@v5 + with: + python-version: "3.12" + - name: Install dependencies for publishing + run: | + uv sync --extra oci + - name: Copy image to target regions + if: ${{ !inputs.staging }} + run: | + uv run scripts/oci_image_tools.py copy \ + --image ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} \ + --from $OCI_REGION \ + --compartment $OCI_COMPARTMENT + - name: Publish image in OCI Marketplace + if: ${{ !inputs.staging }} + run: | + uv run scripts/oci_image_tools.py publish \ + --image ${VM_IMAGE_BUILD_PREFIX}dstack${{ matrix.variant }}-${{ inputs.image_version }} \ + --compartment $OCI_COMPARTMENT \ + --version ${{ inputs.image_version }} \ + --description "Image for running workloads with dstack - https://dstack.ai/" \ + --os "Ubuntu 22.04" \ + --contact-name dstack \ + --contact-email hello@dstack.ai diff --git a/.gitignore b/.gitignore index 0d2163398d..927093831a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ dist/ build/ venv/ +.venv/ +.tox/ +.python-version /site/ /.cache/ .pytest_cache/ @@ -26,3 +29,7 @@ uv.lock /src/dstack/_internal/server/statics profiling_results.html + +mkdocs/docs/reference/http/openapi.json +mkdocs/docs/reference/api/rest/openapi.json +mkdocs/docs/reference/plugins/rest/rest_plugin_openapi.json diff --git a/.justfile b/.justfile index a4fdb716cc..7bdb355552 100644 --- a/.justfile +++ b/.justfile @@ -5,13 +5,29 @@ # Run `just` to see all available commands. # # Components: -# * runner/justfile – Building and uploading dstack runner and shim +# * runner/.justfile – Building and uploading dstack runner and shim +# * frontend/.justfile – Building and running the frontend +# * mkdocs/.justfile – Building and previewing the docs site +# * website/.justfile – Building and previewing the React landing page +# * .tox.justfile – Running Python tests via tox -default: - @just --list +set minimum-version := '1.55.0' -set allow-duplicate-recipes +[doc("Building and uploading dstack runner and shim")] +mod runner "runner/.justfile" -import "runner/.justfile" +[doc("Running Python tests via tox")] +mod tox ".tox.justfile" + +set allow-duplicate-recipes import "frontend/.justfile" + +import "mkdocs/.justfile" + +import "website/.justfile" + +[default] +[private] +default: + @just --list --list-submodules --unsorted diff --git a/.tox.justfile b/.tox.justfile new file mode 100644 index 0000000000..f7db5d8231 --- /dev/null +++ b/.tox.justfile @@ -0,0 +1,44 @@ +# Keep in sync with pyproject.toml and .github/workflows/build-artifacts.yml +supported_python_versions := '3.10,3.11,3.12,3.13,3.14' + +install_tox_msg := ''' + tox is not installed; to install it using uv: + uv tool install tox --with tox-uv +''' + +python_version_msg := ''' + .python-version does not exist; to create (replace 3.14 with the desired version): + * using uv: + uv python pin 3.14 + * if uv is not used: + echo 3.14 > .python-version +''' + +[doc(''' + Run tests using the default environment + The default Python version is set via .python-version file +''')] +[positional-arguments] +test-default *args: _check_tox_installed + @if [ ! -f .python-version ]; then echo '{{python_version_msg}}' >&2; exit 1; fi + tox run -e default "${@}" + +[doc('Run tests against all supported Python versions')] +[positional-arguments] +test-supported *args: _check_tox_installed + tox run -e {{supported_python_versions}} "${@}" + +[doc(''' + Run tests using the current environment (no venv isolation) + All dependencies must be already installed; a dummy environment is still created +''')] +[positional-arguments] +test-current *args: _check_tox_installed + tox run -e dummy --skip-env-install "${@}" + +@_check_tox_installed: + if ! command -v 'tox' >/dev/null 2>&1; then echo '{{install_tox_msg}}' >&2; exit 1; fi + +[default] +_list: + @just --list {{module_path()}} --unsorted diff --git a/AGENTS.md b/AGENTS.md index cdf8497e1c..234464fbd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,15 @@ # Repository Guidelines +Before touching a subsystem, read the relevant notes in `contributing/`: `ARCHITECTURE.md`, +`PIPELINES.md`, `LOCKING.md`, `DATABASE.md`, `MIGRATIONS.md`, `RUNS-AND-JOBS.md`, `AUTOSCALING.md`, +`BACKENDS.md`, `GPUHUNT.md`, `PROXY.md`, `RUNNER-AND-SHIM.md`, `FRONTEND.md`, `DOCS.md`, +`DEVELOPMENT.md`, `RELEASE.md`. + ## Project Structure & Module Organization - Core Python package lives in `src/dstack`; internal modules (including server) sit under `_internal`, API surfaces under `api`, and plugin integrations under `plugins`. - Tests reside in `src/tests` and mirror package paths; add new suites alongside the code they cover. - Frontend lives in `frontend` (React/webpack) and is built into `src/dstack/_internal/server/statics`. -- Docs sources are in `docs` with extra contributor notes in `contributing/*.md`; examples for users sit in `examples/`. +- Docs sources are in `mkdocs/docs/` with extra contributor notes in `contributing/*.md`. ## Build, Test, and Development Commands - Install deps (editable package with extras): `uv sync --all-extras` (uses `.venv` in repo). @@ -15,18 +20,33 @@ - Frontend: from `frontend/` run `npm install`, `npm run build`, then copy `frontend/build` into `src/dstack/_internal/server/statics/`; for dev, `npm run start` with API on port 8000. ## Coding Style & Naming Conventions -- Python targets 3.9+ with 4-space indentation and max line length of 99 (see `ruff.toml`; `E501` is ignored but keep lines readable). +- Python targets 3.10+ with 4-space indentation and max line length of 99 (see `pyproject.toml`; `E501` is ignored but keep lines readable). - Imports are sorted via Ruff’s isort settings (`dstack` treated as first-party). +- Keep primary/public functions before local helper functions in a module section. +- Roughly keep function definitions in the order they are referenced within a file so call flow stays easy to follow. +- Prefer early returns over nested `if`/`else` blocks when they make the control flow simpler. +- Keep private classes, exceptions, and similar implementation-specific types close to the private functions that use them unless they are shared more broadly in the module. - Prefer pydantic-style models in `core/models`. +- Document attributes when the note adds behavior, compatibility, or semantic context that is not obvious from the name and type. Use attribute docstrings without leading newline. - Tests use `test_*.py` modules and `test_*` functions; fixtures live near usage. +- Never make network calls inside a DB session or transaction. Fetch what you need before opening the session, or commit and close it before the call. See `contributing/DATABASE.md`. +- Don't use function-level (inner) imports to break circular imports. Inject the dependency or move the shared code to a lower-level module instead. +- Never edit a migration that has already been applied or released; add a new migration instead. +- Derive paths under `SERVER_DIR_PATH` on access (a `get_*` function), not as module-level constants, so that patching `settings.SERVER_DIR_PATH` redirects all of them. Tests rely on this to keep server state out of the real `~/.dstack`. +- Preserve client/server backward compatibility when updating Pydantic models. New fields must be excluded from client requests when unset, for compatibility with older servers (see `core/compatibility/*.py`). If the allowed domain/type of an existing field is extended, server responses may need to be patched for older clients (see `server/compatibility/*.py`). No need to exclude new fields from server responses, since clients rely on `validate_extra_ignore`. ## Testing Guidelines -- Default to `uv run pytest`. Use markers from `tests/conftest.py` like `--runpostgres` if need to include specific tests. +- Default to `uv run pytest`. Use markers from `src/tests/conftest.py` like `--runpostgres` if need to include specific tests. +- Scope the run to the change: for trivial or localized edits, run only the affected test modules, `Test*` classes, or `-k` selection instead of the whole suite. Reserve the full suite for broad or cross-cutting changes. +- Speed up large runs with `-n auto` (pytest-xdist), e.g. `uv run pytest -n auto`. - Group tests for the same unit (function/class) using `Test*` classes that mirror unit's name. -- Keep tests hermetic (network disabled except localhost per `pytest.ini`); stub cloud calls with mocks. +- Keep tests hermetic (network disabled except localhost per `[tool.pytest.ini_options]` in `pyproject.toml`); stub cloud calls with mocks. +- Keep the suite fast. Never let a test wait on real time. +- Machinery that costs hundreds of milliseconds per test (spawning a subprocess, starting a container, generating a key, real HTTP) has to earn its place by covering something cheaper tests cannot. Say so in the test when it does. On the other hand, losing black-box coverage or making the test hard to follow is worse than the milliseconds it costs. ## Commit & Pull Request Guidelines +- Name branches `issue_{issue_num}_{title}` when the work tracks an issue (e.g. `issue_3959_replicated_alb_gateways`), and `pr_{title}` otherwise. +- Never create git tags. Tags are reserved for releases and are created only by the release process described in `contributing/RELEASE.md`. To mark or share a commit, push an appropriately named branch instead. - Commit messages follow the existing style: short, imperative summaries (e.g., “Fix exclude_not_available ignored”); include rationale in the body if needed. - For PRs, describe behavior changes and link related issues. - Include screenshots or terminal output when touching UX/CLI messages or frontend flows. -- Always disclose AI Assistance in PRs. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1782025bb6..cf398ba0cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,18 +2,24 @@ We appreciate your interest in contributing to `dstack`! This document will help you get up to speed with `dstack` codebase and guide you through the contribution process. -## AI Assistance Notice +## Who can contribute -If you are using any kind of AI assistance while contributing to `dstack`, -**this must be disclosed in the pull request**, along with the extent to -which AI assistance was used. -As an exception, tab-completions and trivial PRs don't need to be disclosed. +We'd be happy to see your contribution if you are a `dstack` user, a partner integrating with `dstack`, or any other person genuinely interested in `dstack`. -An example disclosure: +## Using AI assistance -> This PR was written primarily by Claude Code. +You may use AI assistance when contributing to `dstack`. However, we expect you, the human contributor, to initiate and validate the contribution, ensuring it meets the same standard as something you'd write by hand. -Failure to disclose this, makes it difficult to determine how much scrutiny to apply to the contribution. Please be respectful to maintainers and disclose AI assistance. +Contributions made entirely without a human in the loop (e.g., PRs filed by an autonomous agent against open issues) are discouraged and will be declined. + +## Accepted changes + +* Bug fixes that address a bug you've hit while using `dstack`. Include steps to reproduce in the linked issue or the PR. +* New features that improve `dstack` for you. Before submitting a feature PR, please describe your use case and proposal in a GitHub issue to discuss it with the core team. +* Minor fixes such as typos. +* [Examples](examples). + +Existing open issues are typically picked up by the core team — please only contribute to one if it actually affects how you use `dstack`. ## Set up your development environment @@ -30,13 +36,6 @@ If you make a non-trivial change to `dstack`, we recommend you learn about `dsta 3. Commit your changes. 4. Open a PR. [Link the PR to the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) (if you are solving one). -### Accepted changes - -* Bug fixes that address a clearly defined bug. Include steps to reproduce in the linked issue or the PR. -* New features. Before submitting a feature PR, create an issue with a proposal to discuss it with the core team and other interested parties. -* Minor fixes such as typos. -* [Examples](examples). - ### Before pushing your changes We use [`ruff`](https://docs.astral.sh/ruff/) to format Python code and to sort Python imports. Before committing your changes, run: @@ -63,6 +62,36 @@ Use the `--runpostgres` flag to run the tests against Postgres as well: uv run pytest src/tests --runpostgres ``` +Alternatively, you can run tests via [tox](https://tox.wiki/) inside an isolated environment ensuring that the `dstack` package itself is built correctly and all requirements are specified and correct. tox and [just](https://just.systems/) must be already installed. + +* Run tests in the default environment: + + ```shell + just tox::test-default + ``` + + The Python version of the default environment is configured via the `.python-version` file in the repo root directory. Note, the same file [is used by uv](https://docs.astral.sh/uv/concepts/python-versions/#python-version-files). + +* Run tests against all currently supported Python versions: + + ```shell + just tox::test-supported + ``` + +* Run tests in the _current_ environment: + + ```shell + just tox::test-current + ``` + + It saves about 30-60 seconds at the cost of Python environment isolation (defeating the core purpose of tox) — no packages are built or installed, and, as a consequence, all dependencies must be already installed, but, unlike the plain pytest command, the process environment variables are still isolated. + +It's possible to pass pytest arguments after the `--` separator (the arguments _before_ the separator are `tox run` arguments): + +```shell +just tox::test-default -- -vvv --last-failed +``` + ## Add a new backend If you'd like to integrate a new cloud provider to `dstack`, follow [contributing/BACKENDS.md](contributing/BACKENDS.md). diff --git a/README.md b/README.md index 71d8d1a8b7..3a9f91bae6 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

- - dstack + + dstack

@@ -18,63 +18,42 @@ It streamlines development, training, and inference, and is compatible with any hardware, open-source tools, and frameworks. -#### Hardware +#### Accelerators -`dstack` supports `NVIDIA`, `AMD`, `Google TPU`, `Intel Gaudi`, and `Tenstorrent` accelerators out of the box. +`dstack` supports `NVIDIA`, `AMD`, `Google TPU`, and `Tenstorrent` accelerators out of the box. ## Latest news ✨ -- [2025/12] [dstack 0.20.0: Fleet-first UX, Events, and more](https://github.com/dstackai/dstack/releases/tag/0.20.0) -- [2025/11] [dstack 0.19.38: Routers, SGLang Model Gateway integration](https://github.com/dstackai/dstack/releases/tag/0.19.38) -- [2025/10] [dstack 0.19.31: Kubernetes, GCP A4 spot](https://github.com/dstackai/dstack/releases/tag/0.19.31) -- [2025/08] [dstack 0.19.26: Repos](https://github.com/dstackai/dstack/releases/tag/0.19.26) -- [2025/08] [dstack 0.19.22: Service probes, GPU health-checks, Tenstorrent Galaxy](https://github.com/dstackai/dstack/releases/tag/0.19.22) -- [2025/07] [dstack 0.19.21: Scheduled tasks](https://github.com/dstackai/dstack/releases/tag/0.19.21) -- [2025/07] [dstack 0.19.17: Secrets, Files, Rolling deployment](https://github.com/dstackai/dstack/releases/tag/0.19.17) +- [2026/08] [dstack 0.21.0: Pydantic v2, Gateway replicas](https://github.com/dstackai/dstack/releases/tag/0.21.0) +- [2026/07] [dstack 0.20.29: Presets — agent-driven inference optimization (experimental)](https://dstack.ai/docs/concepts/presets/) +- [2026/07] [dstack 0.20.27: Slurm backend](https://github.com/dstackai/dstack/releases/tag/0.20.27) +- [2026/05] [dstack 0.20.21: Kubernetes multiple clusters](https://github.com/dstackai/dstack/releases/tag/0.20.21) +- [2026/05] [dstack 0.20.20: NVIDIA Dynamo integration](https://github.com/dstackai/dstack/releases/tag/0.20.20) +- [2026/04] [dstack 0.20.17: Kubernetes volumes](https://github.com/dstackai/dstack/releases/tag/0.20.17) +- [2026/02] [dstack 0.20.10: PD disaggregation support](https://github.com/dstackai/dstack/releases/tag/0.20.10) +- [2026/01] [dstack 0.20.7: Replica groups](https://github.com/dstackai/dstack/releases/tag/0.20.7) ## How does it work? - - + + -### Installation +### Launch the server -> Before using `dstack` through CLI or API, set up a `dstack` server. If you already have a running `dstack` server, you only need to [set up the CLI](#set-up-the-cli). +> Before using `dstack` through CLI or API, set up a `dstack` server. If you already have a running `dstack` server, you only need to [install the CLI](#install-the-cli). -#### Set up the server - -##### Configure backends - -To orchestrate compute across cloud providers or existing Kubernetes clusters, you need to configure backends. - -Backends can be set up in `~/.dstack/server/config.yml` or through the [project settings page](https://dstack.ai/docs/concepts/projects#backends) in the UI. - -For more details, see [Backends](https://dstack.ai/docs/concepts/backends). +To orchestrate compute across GPU clouds or Kubernetes clusters, you need to [configure backends](https://dstack.ai/docs/concepts/backends). > When using `dstack` with on-prem servers, backend configuration isn’t required. Simply create [SSH fleets](https://dstack.ai/docs/concepts/fleets#ssh-fleets) once the server is up. -##### Start the server - -You can install the server on Linux, macOS, and Windows (via WSL 2). It requires Git and +The server can be installed on Linux, macOS, and Windows (via WSL 2). It requires Git and OpenSSH. -##### uv - ```shell $ uv tool install "dstack[all]" -U -``` - -##### pip - -```shell -$ pip install "dstack[all]" -U -``` - -Once it's installed, go ahead and start the server. - -```shell $ dstack server + Applying ~/.dstack/server/config.yml... The admin token is "bbae0f28-d3dd-4820-bf61-8f4bb40815da" @@ -84,27 +63,18 @@ The server is running at http://127.0.0.1:3000/ > For more details on server configuration options, see the [Server deployment](https://dstack.ai/docs/guides/server-deployment) guide. +### Install the CLI -
Set up the CLI - -#### Set up the CLI +
If the CLI is not installed with the server -Once the server is up, you can access it via the `dstack` CLI. +Once the server is up, you can access it via the `dstack` CLI. The CLI can be installed on Linux, macOS, and Windows. It requires Git and OpenSSH. -##### uv - ```shell $ uv tool install dstack -U ``` -##### pip - -```shell -$ pip install dstack -U -``` - To point the CLI to the `dstack` server, configure it with the server address, user token, and project name: @@ -113,28 +83,38 @@ $ dstack project add \ --name main \ --url http://127.0.0.1:3000 \ --token bbae0f28-d3dd-4820-bf61-8f4bb40815da - + Configuration is updated at ~/.dstack/config.yml ```
+### Install agent skills + +Install [`dstack` skills](https://skills.sh/dstackai/dstack/dstack) to help AI agents use the CLI and edit configuration files. + +```shell +$ npx skills add dstackai/dstack +``` + +AI agents like Claude, Codex, and Cursor can now create and manage fleets and submit workloads on your behalf. + ### Define configurations `dstack` supports the following configurations: -* [Dev environments](https://dstack.ai/docs/dev-environments) — for interactive development using a desktop IDE -* [Tasks](https://dstack.ai/docs/tasks) — for scheduling jobs (incl. distributed jobs) or running web apps -* [Services](https://dstack.ai/docs/services) — for deployment of models and web apps (with auto-scaling and authorization) -* [Fleets](https://dstack.ai/docs/fleets) — for managing cloud and on-prem clusters -* [Volumes](https://dstack.ai/docs/concepts/volumes) — for managing persisted volumes -* [Gateways](https://dstack.ai/docs/concepts/gateways) — for configuring the ingress traffic and public endpoints +* [Fleets](https://dstack.ai/docs/concepts/fleets) — Provision and manage clusters across clouds, Kubernetes, and on-prem +* [Dev environments](https://dstack.ai/docs/concepts/dev-environments) — Launch dev environments to be accessed by agents or from your IDE +* [Tasks](https://dstack.ai/docs/concepts/tasks) — Run training, batch or other jobs across a single node or clusters +* [Services](https://dstack.ai/docs/concepts/services) — Deploy model inference as secure and scalable endpoints +* [Presets](https://dstack.ai/docs/concepts/presets) — Agent-driven inference optimization (experimental) +* [Volumes](https://dstack.ai/docs/concepts/volumes) — Managing instance and network volumes for persisting data Configuration can be defined as YAML files within your repo. ### Apply configurations -Apply the configuration either via the `dstack apply` CLI command or through a programmatic API. +Apply the configuration via the `dstack apply` CLI command, a programmatic API, or through [AI agent skills](#install-ai-agent-skills). `dstack` automatically manages provisioning, job queuing, auto-scaling, networking, volumes, run failures, out-of-capacity errors, port-forwarding, and more — across clouds and on-prem clusters. diff --git a/contributing/AUTOSCALING.md b/contributing/AUTOSCALING.md index 7fa987aaf9..f977e4d60f 100644 --- a/contributing/AUTOSCALING.md +++ b/contributing/AUTOSCALING.md @@ -2,18 +2,21 @@ `dstack` features auto-scaling for services published via the gateway. The general flow is: -- STEP 1: `dstack-gateway` parses nginx `access.log` to collect per-second statistics about requests to the service and request times. -- STEP 2: `dstack-gateway` aggregates statistics over a 1-minute window. -- STEP 3: The dstack server pulls all service statistics in the `process_gateways` background task. -- STEP 4: The `process_runs` background task passes statistics and current replicas to the autoscaler. -- STEP 5: The autoscaler (configured via the `dstack.yml` file) returns the replica change as an int. -- STEP 6: `process_runs` calls `scale_run_replicas` to add or remove replicas. -- STEP 7: `scale_run_replicas` terminates or starts replicas. - - `SUBMITTED` and `PROVISIONING` replicas get terminated before `RUNNING`. - - Replicas are terminated by descending `replica_num` and launched by ascending `replica_num`. +- STEP 1: The gateway parses nginx `access.log` to collect per-second statistics about requests to the service and request times. +- STEP 2: The gateway aggregates statistics over several predefined windows. +- STEP 3: The server keeps gateway connections alive in the scheduled `process_gateways_connections` task and continuously collects stats from active gateways. This is separate from `GatewayPipeline`, which handles gateway provisioning and deletion. +- STEP 4: When `RunPipeline` processes a service run, it loads the latest collected gateway stats for that service. +- STEP 5: The autoscaler (configured via `dstack.yml`) computes the desired replica count for each replica group. +- STEP 6: `RunPipeline` applies that desired state. + - For scale-up, it creates new `SUBMITTED` jobs. `JobSubmittedPipeline` then assigns existing capacity or provisions new capacity for them. + - For scale-down, it marks the least-important active replicas as `TERMINATING` with `SCALED_DOWN`. `JobTerminatingPipeline` unregisters and cleans them up. +- STEP 7: If the service is in rolling deployment, `RunPipeline` handles that in the same active-run processing path. + - It allows only a limited surge of replacement replicas. + - It delays teardown of old replicas until replacement capacity is available. + - It also cleans up replicas that belong to replica groups removed from the configuration. ## RPSAutoscaler -`RPSAutoscaler` implements simple target tracking scaling. The target value represents requests per second per replica (in a 1-minute window). +`RPSAutoscaler` implements simple target tracking scaling. The target value represents requests per second per replica (in a configurable window). `scale_up_delay` tells how much time has to pass since the last upscale or downscale event before the next upscaling. `scale_down_delay` tells how much time has to pass since the last upscale or downscale event before the next downscaling. diff --git a/contributing/BACKENDS.md b/contributing/BACKENDS.md index f53750791a..90b56a55ab 100644 --- a/contributing/BACKENDS.md +++ b/contributing/BACKENDS.md @@ -31,14 +31,23 @@ git clone https://github.com/dstackai/gpuhunt.git - **Online providers** offer dynamic machine configurations that are available at the very moment when you fetch configurations (e.g., GPU marketplaces). `gpuhunt` collects online providers' instance offers each time a `dstack` user provisions a new instance. - Examples: `tensordock`, `vastai`, etc. + Examples: `vastai`, `hotaisle`, etc. ### 1.3. Create the provider class Create the provider class file under `src/gpuhunt/providers`. -Make sure your class extends the [`AbstractProvider`](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/__init__.py) -base class. See its docstrings for descriptions of the methods that your class should implement. +Make sure your class extends either `OnlineProvider` or `OfflineProvider` from +[base.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/base.py), +matching the choice you made above. + +Both kinds implement `get`, returning `CatalogItem`s with `provider` set to your provider's `NAME`. +Additionally: + +- Online providers implement the `from_env` classmethod, which reads credentials from the + environment with `get_creds_env` and raises `MissingCredsError` if one is missing. Providers that + raise it are skipped by `default_catalog()` rather than failing the whole catalog. +- Offline providers may override `filter` to omit some offers from the published catalog. Refer to examples: - Offline providers: @@ -47,8 +56,8 @@ Refer to examples: [azure.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/azure.py), [lambdalabs.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/lambdalabs.py). - Online providers: - [vultr.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/vultr.py) - [tensordock.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/tensordock.py), + [vultr.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/vultr.py), + [hotaisle.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/hotaisle.py), [vastai.py](https://github.com/dstackai/gpuhunt/blob/main/src/gpuhunt/providers/vastai.py). ### 1.4. Register the provider with the catalog @@ -57,7 +66,8 @@ Add your provider in the following places: - Either `OFFLINE_PROVIDERS` or `ONLINE_PROVIDERS` in `src/gpuhunt/_internal/catalog.py`. - The `python -m gpuhunt` command in `src/gpuhunt/__main__.py`. - (offline providers) The CI workflow in `.github/workflows/catalogs.yml`. -- (online providers) The default catalog in `src/gpuhunt/_internal/default.py`. +- (online providers) `ONLINE_PROVIDER_MODULES` in `src/gpuhunt/_internal/default.py`, which is what + `default_catalog()` loads. ### 1.5. Add data quality tests @@ -160,8 +170,8 @@ If instances in the backend take more than 10 minutes to start, override the def ### 2.10. Document the backend -Add the backend to the [Concepts->Backends](https://github.com/dstackai/dstack/blob/master/docs/docs/concepts/backends.md -) page and the [server/comfig.yml](https://github.com/dstackai/dstack/blob/master/docs/docs/reference/server/config.yml.md) reference. +Add the backend to the [Concepts->Backends](https://github.com/dstackai/dstack/blob/master/mkdocs/docs/concepts/backends.md +) page and the [server/comfig.yml](https://github.com/dstackai/dstack/blob/master/mkdocs/docs/reference/server/config.yml.md) reference. ## 3. Appendix @@ -181,8 +191,9 @@ The agent controls the VM and starts Docker containers for users' jobs. Since `dstack` controls the entire VM, VM-based backends can support more features, such as blocks, instance volumes, privileged containers, and reusable instances. -Note, all VM-based backend `Compute`s should sublass the `ComputeWithPrivilegedSupport` mixin, -as the `dstack-shim` agent provides this functionality OOTB. +Note, all VM-based backend `Compute`s should sublass the `ComputeWithPrivilegedSupport` +and `ComputeWithInstanceVolumesSupport` mixins, as the `dstack-shim` agent +provides these functionalities OOTB. To support a VM-based backend, `dstack` expects the following: diff --git a/contributing/DATABASE.md b/contributing/DATABASE.md new file mode 100644 index 0000000000..f70dffa084 --- /dev/null +++ b/contributing/DATABASE.md @@ -0,0 +1,86 @@ +# Database + +This document describes the `dstack` server rules of working with the database. Separately covered: + +* DB migrations in `MIGRATIONS.md` +* Resource locking in `LOCKING.md` +* Pipeline-based background processing in `PIPELINES.md`. + +## Connections + +DB connections are a scarce resource. For example, a minimal RDS Postgres instance comes with ~80 connections. Increasing the number of connections requires +scaling the DB instance and is expensive. This section describes how the `dstack` server should use DB connections to avoid running out of them. + +### Connection budget + +The server creates one SQLAlchemy async engine per process with an `AsyncAdaptedQueuePool` of `DSTACK_DB_POOL_SIZE` (default 20) persistent connections plus `DSTACK_DB_MAX_OVERFLOW` (default 20) overflow connections, i.e. up to 40 connections per server process. When all of them are checked out, the next checkout waits `pool_timeout` (30 seconds) and then fails with: + +``` +sqlalchemy.exc.TimeoutError: QueuePool limit of size 20 overflow 20 reached, connection timed out, timeout 30.00 +``` + +Far more coroutines compete for the slots than there are slots. This works because each of them holds a connection for a short time. + +A session does not hold a connection until it executes its first statement. From that moment until the session commits, rolls back, or closes, the connection is checked out and unavailable to anyone else. The invariant that follows: + +**Hold a connection only while doing DB work. Do not keep a session open across when doing heavy work.** + +On SQLite the same rule applies for a different reason: a long write transaction blocks every other writer for up to `busy_timeout` (30 seconds). + +### Awaits that must not run inside a session + +Anything whose completion depends on something other than the database: + +* SSH: opening tunnels, `aexec()`, `acheck()`, any subprocess. +* HTTP to runners, shims, gateways, or external services. +* Cloud SDK calls, including those wrapped in `run_async()`. The thread pool executor is shared, so a saturated executor makes even a fast call wait. +* Retry loops with sleeps, and waiting on asyncio locks or events that other tasks control. +* Streaming a response to a client. + +Fetch what you need before opening the session, or commit and close the session before the call and open a new one to apply the results: + +```python +async with get_session_ctx() as session: + # Load and lock what processing needs. + context = await _load_context(session, item) + +# No connection is held here. +result = await _do_network_work(context) + +async with get_session_ctx() as session: + # Apply results with a guarded update. + await _apply_result(session, item, result) +``` + +### Session lifetime by context + +**Pipelines** + +Pipeline workers follow load, process, apply with a short session for the load and apply phases and no session during processing. See `PIPELINES.md`. + +**API handlers** + +The request session is created before the handler runs. The authentication dependency executes a SELECT on it, so a connection is checked out before the handler starts and stays checked out until the handler returns and the session commits. + +This is acceptable because API handlers are assumed to be quick: read or write a few rows and return. A handler that needs to do slow work must not do it while holding the request session's connection. Either: + +* Commit the session before the slow work. Committing returns the connection to the pool. The next statement on the session checks out a new connection and starts a new transaction, so any locks taken earlier are released, and the handler must not rely on them across the gap. +* Make the API async and move the slow work out of the handler. + +The rule is not applied currently applied to API handlers strictly: a handler may make one or a few network calls while holding the connection. + +**Startup** + +Migrations and server initialization may hold a connection and a session-level advisory lock for long: they run once per process, before anything else competes for connections. Recurring cross-replica coordination should use the pipeline lock columns instead. + +### Lock waits + +A coroutine blocked on a DB-level lock holds its connection while it waits. This applies to `SELECT ... FOR UPDATE` and to `pg_advisory_lock` alike: the waiter occupies a pool slot on its replica until the holder releases the lock. A lock held for a long time thus costs one slot on the holder plus one per waiter, and a hung holder pins all of them. + +Keep locked regions short and bounded. See `LOCKING.md` for how to take advisory locks correctly. + +### Settings + +`DSTACK_DB_COMMAND_TIMEOUT` (default 300 seconds) bounds every operation on a Postgres connection, so a coroutine waiting on a connection whose socket died silently gets an error and releases the slot instead of holding it forever. It applies to migrations too, since they run on the same engine, so it must leave room for index builds and backfills. + +`DSTACK_DB_POOL_SIZE` and `DSTACK_DB_MAX_OVERFLOW` size the pool per process. Raising them lets more coroutines hold connections at once, which hides a long hold rather than fixing it, and each replica needs `pool_size + max_overflow` connections from the Postgres `max_connections` budget. diff --git a/contributing/DOCS.md b/contributing/DOCS.md index c20076466e..aa07d9673f 100644 --- a/contributing/DOCS.md +++ b/contributing/DOCS.md @@ -1,5 +1,11 @@ # Documentation setup +> **The dstack.ai site has three parts on one origin:** the **landing** page (`/`) is a React +> app in [`website/`](../website); the **docs** (`/docs`) and **blog** (`/blog`) are built with +> MkDocs from `mkdocs/`. This guide covers the **docs and blog** (MkDocs). For the landing and +> for building everything together, see [The landing page](#the-landing-page-website) and +> [Building the whole site](#building-the-whole-site) below. + ## 1. Clone the repo: ```shell @@ -36,14 +42,183 @@ uv run pre-commit install ## 5. Preview documentation -To preview the documentation, run the follow command: +To preview the **docs and blog** (MkDocs), run the follow command: ```shell -uv run mkdocs serve -w examples -s +uv run mkdocs serve --livereload -s ``` +The `--livereload` flag is required to work around live-reload bugs in recent `mkdocs` versions. + +This serves the docs and blog only. The landing page (`/`) is a separate React app — when you +run `mkdocs serve` on its own, `/` simply redirects to `/docs/`. To work on the landing, see +[The landing page](#the-landing-page-website) below. + If you want to build static files, you can use the following command: ```shell uv run mkdocs build -s ``` + +## The landing page (website/) + +The landing page at `/` is a React (Vite) app in [`website/`](../website), not MkDocs. It has +its own `package.json`/`node_modules`. Preview it on its own (requires Node 20+): + +```shell +just website-dev # Vite dev server on http://127.0.0.1:5173 +``` + +Docs/blog links on the landing resolve same-origin (`/docs`, `/blog`), which 404 in standalone +dev. Point them at a live site while iterating: `just website-dev https://dstack.ai`. + +Keep page metadata in the HTML entries and `website/src/App/index.tsx` in sync. +When adding or moving product pages, update the [sitemap template](../mkdocs/overrides/sitemap.xml). + +## Building the whole site + +CI builds the landing and the MkDocs docs/blog and overlays them into a single `site/`: + +```shell +just site-build # website/dist + `mkdocs build` -> ./site (scripts/docs/build_site.sh) +just site-serve # preview the combined site on http://127.0.0.1:8001 +``` + +In the combined build the React `index.html` owns `/`, while MkDocs serves `/docs`, `/blog`, and +the shared `/assets`. This is what the `Build & Deploy Site` workflow deploys. + +## Documentation build system + +The documentation uses a custom build system with MkDocs hooks to generate various files dynamically. + +### Disable flags + +Use these in `.envrc` to disable expensive docs regeneration, especially during `mkdocs serve` auto-reload. Set any of them to disable the corresponding artifact. + +```shell +export DSTACK_DOCS_DISABLE_LLM_TXT=1 +export DSTACK_DOCS_DISABLE_CLI_REFERENCE=1 +export DSTACK_DOCS_DISABLE_YAML_SCHEMAS=1 +export DSTACK_DOCS_DISABLE_OPENAPI_REFERENCE=1 +export DSTACK_DOCS_DISABLE_REST_PLUGIN_SPEC_REFERENCE=1 +``` + +### Build hooks + +The build process is customized via hooks in `scripts/docs/hooks.py`: + +#### 1. Schema reference expansion + +Files in `docs/reference/**/*.md` can use `#SCHEMA#` placeholders that are expanded with generated schema documentation during the build. + +#### 2. llms.txt generation + +Two files are generated for LLM consumption: + +- **llms.txt**: Structured overview of documentation with titles and descriptions + - Generated from mkdocs nav structure + - Includes sections: Getting started, Concepts, Guides, Examples + - Excludes: Reference section + - Configuration: `scripts/docs/gen_llms_files.py` (INCLUDE_SECTIONS, EXCLUDE_SECTIONS) + +- **llms-full.txt**: Full concatenation of all pages from llms.txt + - Contains complete markdown content of all included pages + +The generation logic is in `scripts/docs/gen_llms_files.py` and uses: +- `site_name`, `site_description`, `site_url` from `mkdocs.yml` +- Page titles from mkdocs nav structure +- Page descriptions from markdown frontmatter + +**Adding descriptions**: To add descriptions to pages, add YAML frontmatter: + +```yaml +--- +title: Page Title +description: Short description of what this page covers +--- +``` + +For examples, add frontmatter to the page files (e.g., `mkdocs/docs/examples/training/trl.md`). + +#### 3. Skills discovery + +The build creates `.well-known/skills/` directory structure for skills discovery: +- Reads `skills/dstack/SKILL.md` +- Parses name and description from frontmatter +- Generates `.well-known/skills/index.json` +- Copies SKILL.md to both `.well-known/skills/dstack/` and site root + +#### 4. HTTP API reference + +The HTTP API reference is generated from the FastAPI OpenAPI schema: + +- `scripts/docs/gen_openapi_reference.py` writes `mkdocs/docs/reference/http/openapi.json`, + keeps the per-tag Markdown pages in sync, and updates the generated tag list in the HTTP API + index page. +- Tag pages use `!!swagger openapi.json tag=""!!`. Keep tag names exactly as they appear + in the OpenAPI schema. +- `scripts/docs/hooks.py` expands the `!!swagger` directive into the Swagger UI container and + the hidden operation headings that MkDocs uses for the page table of contents. +- `mkdocs/assets/javascripts/swagger.js` loads the shared `openapi.json`, filters it by tag on + the client, and adapts Swagger UI markup to the docs layout. +- `mkdocs/assets/stylesheets/swagger.css` contains Swagger-specific styling and should stay + scoped under `.dstack-swagger-ui`. + +Keep hook logic limited to build-time Markdown/page structure, generated assets, and data +attributes needed by the client. Small presentation changes belong in `swagger.css`; small +behavior changes belong in `swagger.js`. + +If the HTTP API reference needs deeper structural customization, such as replacing major Swagger +UI panels, request/response rendering, model rendering, or "try it out" behavior, prefer moving +toward a dedicated local bundle or custom Swagger UI layout instead of adding more DOM patching. +That bundle can still use the single generated `openapi.json` and filter by tag on the client, so +we should not reintroduce per-tag OpenAPI files unless there is a concrete reason. + +### File structure + +``` +mkdocs/ # docs_dir for the mkdocs site +├── index.md # Redirects to /docs/ (the landing "/" is the React app in website/) +├── docs/ # /docs/ URL section +│ ├── index.md # Getting started +│ ├── installation.md +│ ├── quickstart.md +│ ├── concepts/ # Concept pages +│ ├── guides/ # How-to guides +│ ├── reference/ # API reference (schema expansion) +│ └── examples/ # Example pages (inline source code) +│ └── training/ +│ └── trl.md # Page content with frontmatter +├── blog/ # Blog posts +├── overrides/ # Theme customization +├── layouts/ # Social card layouts +└── assets/ # Stylesheets, images, fonts + +website/ # React (Vite) landing page — served at "/" +├── index.html # Entry; title, OG/meta, Google Analytics +├── src/ # App, pages (Home, Sky, Factory), components, routes +└── public/static/ # Landing assets (namespaced to avoid clashing with /assets) + +scripts/docs/ +├── build_site.sh # Build landing + docs/blog and overlay into ./site +├── hooks.py # MkDocs build hooks +├── gen_llms_files.py # llms.txt generation +├── gen_schema_reference.py # Schema expansion +└── gen_cli_reference.py # CLI reference generation + +skills/ +└── dstack/ + └── SKILL.md # Skills discovery content +``` + +### Testing changes + +When modifying the build system: + +1. Test local build: `uv run mkdocs build -s` +2. Check generated files in `site/`: + - `site/llms.txt` + - `site/llms-full.txt` + - `site/.well-known/skills/index.json` +3. Verify example pages render correctly +4. Check that descriptions appear in llms.txt diff --git a/contributing/FRONTEND.md b/contributing/FRONTEND.md index 19b623b1d4..afecdaa29d 100644 --- a/contributing/FRONTEND.md +++ b/contributing/FRONTEND.md @@ -54,3 +54,18 @@ The `webpack` dev server expects the API to be running on `http://127.0.0.1:8000 ```shell dstack server --port 8000 ``` + +## Product flavors + +The shared frontend supports OSS, Enterprise, Factory, and Sky. From `frontend/`, use +`npm run build`, `npm run build-enterprise`, `npm run build-factory`, or +`npm run build-sky`; the corresponding development commands are `start`, +`start-enterprise`, `start-factory`, and `start-sky`. + +`frontend/product.config.cjs` defines product names and inherited UI features for both +webpack and the application. Components use `product` from `src/product.ts` instead of +checking `UI_VERSION` directly. Factory and Sky share billing and presets; hosted-service +content such as Sky terms and onboarding uses `product.isSky`. + +All flavors use the same login component. It displays supported providers enabled by +`/api/auth/list_providers`, independently of the UI flavor, and always offers token login. diff --git a/contributing/GPUHUNT.md b/contributing/GPUHUNT.md index 33e3150fa1..360e97366d 100644 --- a/contributing/GPUHUNT.md +++ b/contributing/GPUHUNT.md @@ -4,10 +4,12 @@ An offer is a possible configuration. It consists of: - Provider (or backend in dstack) +- CPU architecture - CPU count - RAM size - Disk size - GPU count +- GPU vendor (if any) - GPU model name (if any) - GPU VRAM size (if any) - Is interruptible (or spot) @@ -15,11 +17,14 @@ An offer is a possible configuration. It consists of: - Instance name or ID (provider-specific) - Price per hour +Offers are represented by `CatalogItem`. Providers construct them directly, setting `provider` to +their own `NAME`, and `gpu_vendor` whenever `gpu_count` is non-zero. + ## Catalog Some providers don't have a suitable API for querying all offers in real-time. That's why gpuhunt has two types of providers: -- Online — offers can be queried in real-time +- Online — offers can be queried quickly in real-time - Offline — offers must be loaded from a precomputed catalog file The `Catalog` class hides those details from the user, reading offers from the file for offline providers or querying online providers. @@ -28,7 +33,16 @@ The `Catalog` class pulls the latest catalog from the S3 bucket and caches it fo ## Provider implementation -Providers must implement a single method `get`. It has the same name for both online and offline providers but works differently. +Providers subclass either `OnlineProvider` or `OfflineProvider` from `src/gpuhunt/providers/base.py`. + +Both implement `get`. It has the same name for both online and offline providers but works differently. + +In addition: + +- Online providers implement the `from_env` classmethod, since `default_catalog()` constructs them + in the user's process. +- Offline providers may override `filter` to omit some offers from the published catalog. + Credentials are passed in by the caller, so a missing one is an error rather than a skip. ### Offers sorting @@ -100,7 +114,7 @@ These mechanisms are used to preserve backward compatibility: - **`gpuhunt` version**: The interfaces in the `gpuhunt` package preserve backward compatibility within a minor version (`X` in `0.X.Y`). -- **Offer flags**: If an offer breaks older `dstack` versions, it is marked with a flag in `RawCatalogItem.flags` +- **Offer flags**: If an offer breaks older `dstack` versions, it is marked with a flag in `CatalogItem.flags` and the flag is added to the list of supported flags in `dstack`. Older `dstack` versions that don't support this flag will not see the respective offers. - **Offline catalog versions**: If a breaking change in the structure or content of an offline catalog is unavoidable, diff --git a/contributing/LOCKING.md b/contributing/LOCKING.md index e23fb41f9e..420c493c01 100644 --- a/contributing/LOCKING.md +++ b/contributing/LOCKING.md @@ -1,7 +1,6 @@ # Locking -The `dstack` server supports SQLite and Postgres databases -with two implementations of resource locking to handle concurrent access: +The `dstack` server supports SQLite and Postgres databases with two implementations of resource locking to handle concurrent access: * In-memory locking for SQLite. * DB-level locking for Postgres. @@ -30,15 +29,15 @@ Locksets are an optimization. One can think of them as per-resource-id locks tha Postgres resource locking is implemented via standard SELECT FOR UPDATE. SQLAlchemy provides `.with_for_update()` that has no effect if SELECT FOR UPDATE is not supported as in SQLite. -There are few places that rely on advisory locks as when generating unique resource names. +There are few places that rely on advisory locks as when generating unique resource names or serializing server initialization across replicas. See **Advisory locks** below. ## Working with locks -Concurrency is hard. Below you'll find common patterns and gotchas when working with locks to make it a bit more manageable. +Concurrency is hard. Concurrency with locking is especially hard. Below you'll find common patterns and gotchas when working with locks to make it a bit more manageable. **A task should acquire locks on resources it modifies** -This is a common sense approach. An alternative could be the inverse: job processing cannot run in parallel with run processing, so job processing takes run lock. This indirection complicates things and is discouraged. In this example, run processing should take job lock instead. +This is common sense. An alternative could be the inverse: job processing cannot run in parallel with run processing, so job processing takes run lock. This indirection complicates things and is discouraged. In this example, run processing should take job lock instead. **Start new transaction after acquiring a lock to see other transactions changes in SQLite.** @@ -75,15 +74,19 @@ unlock resources If a transaction releases a lock before committing changes, the changes may not be visible to another transaction that acquired the lock and relies upon seeing all committed changes. -**Don't use `joinedload` when selecting `.with_for_update()`** +**Using `joinedload` when selecting `.with_for_update()`** -In fact, using `joinedload` and `.with_for_update()` will trigger an error because `joinedload` produces OUTER LEFT JOIN that cannot be used with SELECT FOR UPDATE. A regular `.join()` can be used to lock related resources but it may lead to no rows if there is no row to join. Usually, you'd select with `selectinload` or first select with `.with_for_update()` without loading related attributes and then re-selecting with `joinedload` without `.with_for_update()`. +Using `joinedload` and `.with_for_update()` triggers an error in case of no related rows because `joinedload` produces OUTER LEFT JOIN and SELECT FOR UPDATE cannot be applied to the nullable side of an OUTER JOIN. Here's the options: + +* Use `.with_for_update(of=MainModel)`. +* Select with `selectinload` +* First select with `.with_for_update()` without loading related attributes and then re-select with `joinedload` without `.with_for_update()`. +* Use regular `.join()` to lock related resources, but you may get 0 rows if there is no related row to join. **Always use `.with_for_update(key_share=True)` unless you plan to delete rows or update a primary key column** If you `SELECT FOR UPDATE` from a table that is referenced in a child table via a foreign key, it can lead to deadlocks if the child table is updated because Postgres will issue a `FOR KEY SHARE` lock on the parent table rows to ensure valid foreign keys. For this reason, you should always do `SELECT FOR NO KEY UPDATE` (.`with_for_update(key_share=True)`) if primary key columns are not modified. `SELECT FOR NO KEY UPDATE` is not blocked by a `FOR KEY SHARE` lock, so no deadlock. - **Lock unique names** The following pattern can be used to lock a unique name of some resource type: @@ -109,6 +112,39 @@ Note that: * This pattern works assuming that Postgres is using default isolation level Read Committed. By the time a transaction acquires the advisory lock, all other transactions that can take the name have committed, so their changes can be seen and a unique name is taken. * SQLite needs a commit before selecting taken names due to Snapshot Isolation as noted above. +**Advisory locks** + +Postgres has two kinds of advisory locks: + +* `pg_advisory_xact_lock` is released when the transaction ends. The unique names pattern above uses it. +* `pg_advisory_lock` is bound to the connection. It survives commit and rollback and is released only by `pg_advisory_unlock` on the same connection or when the connection closes. `advisory_lock_ctx()` in `services/locking.py` wraps this kind for work that must span several transactions, such as migrations or server initialization. + +Within a single transaction, prefer `pg_advisory_xact_lock` for automatic release on commit or rollback. When using `advisory_lock_ctx()`, the requirement is to acquire and release the lock on the same physical connection: + +* An `AsyncSession` is a valid bind when the locked block stays within one transaction. Do not commit, roll back, or close the session before releasing the lock. +* If the block spans multiple transactions, hold an `AsyncConnection` from `engine.connect()` and bind the lock and any sessions to it. Otherwise, a session's next transaction may use a different pooled connection, and `pg_advisory_unlock` goes to a connection that never held the lock. Postgres only returns `false` with a warning in this case, so the failure is silent: the lock stays on an idle pooled connection until the process exits, and every replica blocks forever on its next acquire. See https://github.com/dstackai/dstack/issues/3881 for an example. +* Run protected DB work on the connection holding the lock. When binding a new session to an explicit connection, end the lock statement's transaction first so that the session controls its own transactions. If the connection is lost, abort the protected operation instead of reconnecting and continuing without the lock. +* Keep the locked block short and bounded. Every waiter is blocked inside `pg_advisory_lock` holding a DB connection of its own, so a long or hung holder pins one connection per waiter. See `DATABASE.md`. + +For a block that spans multiple transactions: + +```python +async with get_db().engine.connect() as connection: + async with advisory_lock_ctx( + bind=connection, + dialect_name=get_db().dialect_name, + resource="server_init", + ): + # End the lock statement's transaction. The session-level lock survives the commit. + await connection.commit() + async with get_db().get_session(bind=connection) as session: + # Session commits keep using the connection that holds the lock. + ... + await session.commit() +``` + +A released connection goes back to the pool, so a lock that failed to release stays there too. `_release_advisory_lock()` tolerates failures because the common one is an invalidated connection, in which case Postgres has already dropped the lock. A release that fails on a live connection strands the lock. + **Use `AsyncExitStack`** In-memory locking typically requires taking lock for long (until commit). diff --git a/contributing/MIGRATIONS.md b/contributing/MIGRATIONS.md index 8918d9e36b..f494d829d5 100644 --- a/contributing/MIGRATIONS.md +++ b/contributing/MIGRATIONS.md @@ -47,3 +47,29 @@ These steps apply to **renaming a column** or **changing the type of a column** ### Altering multiple tables Altering a table requires Postgres to [take an ACCESS EXCLUSIVE lock](https://www.postgresql.org/docs/current/sql-altertable.html). (This applies not only to statements that rewrite the tables but also to statements that modify tables metadata.) Altering multiple tables can cause deadlocks due to conflict with read operations since the `dstack` server does not define an order for read operations. Altering multiple tables should be done in separate transactions/migrations. + +### Adding indexes + +Use `CREATE INDEX CONCURRENTLY` to avoid tacking exclusive lock on the table for a long time. +For migrations that create multiple indexes, failures can leave the schema in a partial state +(some indexes already created, some missing). On Postgres, concurrent index creation can also fail +midway and leave an invalid index object with the same name. Retrying the migration then fails +with "already exists". + +For retry-safe migrations, pre-drop indexes with `if_exists=True` before creating them again: + +```python +with op.get_context().autocommit_block(): + op.drop_index( + "ix_table_col", + table_name="table", + if_exists=True, + postgresql_concurrently=True, + ) + op.create_index( + "ix_table_col", + "table", + ["col"], + postgresql_concurrently=True, + ) +``` diff --git a/contributing/PIPELINES.md b/contributing/PIPELINES.md new file mode 100644 index 0000000000..7e01c2465e --- /dev/null +++ b/contributing/PIPELINES.md @@ -0,0 +1,127 @@ +# Pipelines + +This document describes how the `dstack` server implements background processing via so-called "pipelines". + +*Historical context: `dstack` used to do all background processing via scheduled tasks. A scheduled task would process a specific resource type like volumes or runs by keeping DB transaction open for the entire processing duration and keeping the resource lock with SELECT FOR UPDATE (or in-memory lock on SQLite). This approach didn't scale well because the number of DB connections was a huge bottleneck. Pipelines replaced scheduled tasks: the do all the heavy processing outside of DB transactions and write locks to DB columns.* + +## Overview + +* Resources are continuously processed in the background by pipelines. A pipeline consists of a fetcher, workers, and a heartbeater. +* A fetcher selects rows to be processed from the DB, marks them as locked in the DB, and puts them into an in-memory queue. +* Workers consume rows from the in-memory queue, process the rows, and unlock them. +* The locking (unlocking) is done by setting (unsetting) `lock_expires_at`, `lock_token`, `lock_owner`. +* If the replica/pipeline dies, the rows stay locked in the db. Another replica picks up the rows after `lock_expires_at`. +* `lock_token` prevents stale replica/pipeline to update the rows already picked up by the new replica. +* `lock_owner` stores the pipeline that's locked the row so that only that pipeline can recover if it's stale. +* A heartbeater tracks all rows in the pipeline (in the queue or in processing), and updates the lock expiration. This allows setting small `lock_expires_at` and picking up stale rows quickly +* A fetcher performs the fetch when the queue size goes under a configured lower limit. It has exponential retry delays between empty fetches, thus reducing load on the DB. +* There is a fetch hint mechanism that services can use to notify the pipelines within the replica – in that case the fetcher stops sleeping and fetches immediately. +* Each pipeline locks one main resource but may lock related resources as well. It's not necessary to heartbeat related resources if the pipeline ensures no one else can re-lock them. This is typically done via setting and respecting `lock_owner`. + +Related notes: + +* All write APIs must respect DB-level locks. The endpoints can either try to acquire the lock with a timeout and error or provide an async API by storing the request in the DB. + +## Implementation checklist + +Brief checklist for implementing a new pipeline: + +1. Fetcher locks only rows that are ready for processing: +`status/time` filters, `lock_expires_at` is empty or expired, and `lock_owner` is empty or equal to the pipeline name. Keep the fetch order stable with `last_processed_at`. +2. Fetcher takes row locks with `skip_locked` and updates `lock_expires_at`, `lock_token`, `lock_owner` before enqueueing items. +3. Worker keeps heavy work outside DB sessions. DB sessions should be short and used only for refetch/locking and final apply. +4. Apply stage updates rows using update maps/update rows, not by relying on mutating detached ORM models. +5. Main apply update is guarded by `id + lock_token`. If the update affects `0` rows, the item is stale and processing results must not be applied. +6. Successful apply updates `last_processed_at` and unlocks resources that were locked by this item. +7. If related lock is unavailable, reset main lock for retry: keep `lock_owner`, clear `lock_token` and `lock_expires_at`, and set `last_processed_at` to now. +8. Register the pipeline in `PipelineManager` and hint fetch from services after commit via `pipeline_hinter.hint_fetch(Model.__name__)`. +9. Add minimum tests: fetch eligibility/order, successful unlock path, stale lock token path, and related lock contention retry path. + +## Typical worker structure + +Most workers are easiest to reason about when `process()` is split into three phases: + +1. Load/refetch: open a short DB session, refetch the locked main row by `id + lock_token`, lock any required related rows, and gather any extra data needed for processing. +2. Process: do the heavy work outside DB sessions and build result objects or update maps instead of mutating detached ORM models. +3. Apply: open a short DB session, guard the main update by `id + lock_token`, resolve time placeholders, apply related updates, emit events, and unlock rows. + +A dedicated context object is often useful for the load step when the worker needs multiple loaded models, related lock metadata, or derived values that should be passed cleanly into processing and apply. For very small pipelines, a direct load -> process -> apply flow may still be clearer. + +Workers can share one context type and one apply function across all states even if the processing logic differs by state: + +```python +async def process(item): + context = await _load_process_context(item) + if context is None: + return + result = await _process_item(context) + await _apply_process_result(item, context, result) +``` + +Sometimes state-specific helpers are still the cleanest option, but they can still share a common apply phase if all states write results in the same general shape: + +```python +async def process(item): + if item.status == Status.PENDING: + context = await _load_pending_context(item) + elif item.status == Status.RUNNING: + context = await _load_running_context(item) + else: + return + if context is None: + return + result = await _process_item(context) + await _apply_process_result(item, context, result) +``` + +If different states have materially different write-side behavior, different apply paths are fine as well. This commonly happens when one state does a normal guarded update while another does delete-or-cleanup work with different related updates: + +```python +async def process(item): + if item.to_be_deleted: + await _process_to_be_deleted_item(item) + elif item.status == Status.SUBMITTED: + await _process_submitted_item(item) +``` + +It's ok not to force all pipelines into one exact shape. + +## Implementation patterns + +**Guarded apply by lock token** + +When writing processing results, update the main row with a filter by both `id` and `lock_token`. This guarantees that only the worker that still owns the lock can apply its results. If the update affects no rows, treat the item as stale and skip applying other changes (status changes, related updates, events). A stale item means another worker or replica already continued processing. + +**Locking related resources before refetch** + +If you first refetch a main resource and only after lock the related resources, you need to ensure the worker doesn't get the stale view on related resources or works properly even in this case. It's often more robust to first lock related resources and then refetch the main resource with related resources already locked. + +**Locking many related resources** + +A pipeline may need to lock a potentially big set of related resource, e.g. fleet pipeline locking all fleet's instances. For this, do one SELECT FOR UPDATE of non-locked instances and one SELECT to see how many instances there are, and check if you managed to lock all of them. If fail to lock, release the main lock and try processing on another fetch iteration. You may keep `lock_owner` on the main resource or set `lock_owner` on locked related resource and make other pipelines respect that to guarantee the eventual locking of all related resources and avoid lock starvation. + +**Locking a shared related resource** + +Multiple main resources may need to lock the same related resource, e.g. multiple jobs may need to change the shared instance. In this case it's not sufficient to set `lock_owner` on the related resource to the pipeline name because workers processing different main resources can still race with each other. To avoid heartbeating the related resource, you may include main resource id in `lock_owner`, e.g. set `lock_owner = f"{Pipeline.__name__}:{item.id}"`. + +**Reset-and-retry when related lock is unavailable** + +If a worker cannot lock a required related resource, it should release only the main lock state needed for fast retry: unset `lock_token` and `lock_expires_at`, keep `lock_owner`, and set `last_processed_at` to now. This avoids long waiting and lets the same pipeline retry quickly on the next fetch iteration while other pipelines can still respect ownership intent. + +**Dealing with side effects** + +If processing has side effects and the apply phase fails due to a lock mismatch, there are several options: a) revert side effects b) make processing idempotent, i.e. next processing iteration detects side effects does not perform duplicating actions c) log side effects as errors and warn user about possible issues such as orphaned instances – as a temporary solution. + +**Bulk apply with one consistent current time** + +When apply needs to update multiple rows (main + related resources), build update maps/update rows first and resolve current-time placeholders once in the apply transaction using `NOW_PLACEHOLDER` + `resolve_now_placeholders()`. This keeps timestamps consistent across all rows and avoids subtle ordering bugs when the same processing pass writes several `*_at` fields. + +## Performance analysis + +* Pipeline throughput = workers_num / worker_processing_time. So quick tasks easily give high-throughput pipelines, e.g. 1s task with 20 workers is 1200 tasks/min. +A slow 30s task gives only 40 tasks/min with the same number of workers. We can increase the number of workers but the peak memory usage will grow proportionally. +In general, workers should be optimized to be as quick as possible to improve throughput. +* Processing latency (wait) is close to 0 due to fetch hints if the pipeline is not saturated. In general, latency = queue_size / throughput. +* In-memory queue maxsize provides a cap on memory usage and recovery time after crashes (number of locked items to retry). +* Fetcher's DB load is proportional to the number of pipelines and is expected to be negligible. Workers can put a considerable read/write DB load as it's proportional to the number of workers. This can be optimized by batching workers' writes. Workers do processing outside of transactions so DB connections won't be a bottleneck. +* There is a risk of lock starvation if a worker needs to lock all related resources. This is to be mitigated by 1) related pipelines checking `lock_owner` and skip locking to let the parent pipeline acquire all the locks eventually and 2) do the related resource locking only on paths that require it. diff --git a/contributing/PROXY.md b/contributing/PROXY.md index bc79c4fbbe..a8dc522cc8 100644 --- a/contributing/PROXY.md +++ b/contributing/PROXY.md @@ -127,7 +127,7 @@ There are also similar gateway-specific helper functions to obtain gateway-speci ## Gateway operations -Gateway instances are managed by the `dstack` server. A gateway is associated with a project and some backend in the project. In `dstack` Sky, there is also a global gateway associated with all projects. +Gateway instances are managed by the `dstack` server. A gateway is associated with a project and some backend in the project. Gateways can be shared between projects using Exports. ### Creation @@ -137,7 +137,7 @@ Provisioning happens as follows: 1. Launch a non-GPU instance (usually the smallest) with all ports exposed. 2. Install Nginx, Certbot, and patch configs. 3. Create blue-green virtual environments. -4. Install the latest `dstack-gateway` package from the S3 bucket. `dstack-gateway` is a thin package that depends on the `dstack` package, which contains the actual gateway implementation. +4. Install the `dstack[gateway]` Python package. 5. Run the systemd service `dstack.gateway.service`. ### Update @@ -152,56 +152,18 @@ The update process looks like this: ## Gateway development -The gateway app needs to interact with Nginx and certbot, so running it locally can be challenging. One way to test your code is to upload your development branch to an existing gateway and run the gateway app from source. +The gateway app needs to interact with Nginx and certbot, so running it locally can be challenging. You can test your development version by deploying it to a live gateway. -1. Run `dstack server` with `DSTACK_SKIP_GATEWAY_UPDATE=1` environment variable. This will prevent `dstack` from updating and starting the standard gateway version on each server restart. +For example, push your version to GitHub and run `dstack server` with the `DSTACK_GATEWAY_PACKAGE_URL` environment variable, which will instruct `dstack` to deploy that version to all gateways. -1. Provision a gateway through `dstack`: - - ```shell - dstack apply -f my-gateway.dstack.yml - ``` - -1. Save the gateway key to a file: - - ```shell - sqlite3 ~/.dstack/server/data/sqlite.db "SELECT ssh_private_key FROM gateway_computes WHERE deleted = 0 AND ip_address = ''" > /tmp/gateway.key - chmod 600 /tmp/gateway.key - ``` - -1. Deliver your code to the gateway. For example, clone it from a remote repo: - - ```shell - ssh -i /tmp/gateway.key ubuntu@gateway.example "git clone https://github.com/dstackai/dstack.git ~/dstack-repo" - ``` - - Or push it from your machine: - - ```shell - ssh -i /tmp/gateway.key ubuntu@gateway.example "git init ~/dstack-repo" - git remote add gateway ubuntu@gateway.example:~/dstack-repo - GIT_SSH_COMMAND='ssh -i /tmp/gateway.key' git push gateway branch_name - ``` - -1. Connect to the gateway: - - ```shell - ssh -i /tmp/gateway.key ubuntu@gateway.example - ``` - -1. Prepare an environment with your development branch on the gateway: - - ```shell - cd ~/dstack-repo - git checkout branch_name - curl -LsSf https://astral.sh/uv/install.sh | sh - source ~/.local/bin/env - uv sync --extra gateway - ``` +```shell +DSTACK_GATEWAY_PACKAGE_URL=https://github.com/dstackai/dstack/archive/refs/heads/my_development_branch.zip dstack server +``` -1. Stop the gateway service and start your development version from source: +To access logs or troubleshoot, extract the gateway's private key from the database and connect to it via SSH. Logs are available in `journalctl`. - ```shell - sudo systemctl stop dstack.gateway.service - uv run uvicorn dstack._internal.proxy.gateway.main:app - ``` +```shell +sqlite3 ~/.dstack/server/data/sqlite.db "SELECT ssh_private_key FROM gateway_computes WHERE deleted = 0 AND ip_address = '$GATEWAY_IP_ADDR'" > /tmp/gateway.key +chmod 600 /tmp/gateway.key +ssh -i /tmp/gateway.key ubuntu@$GATEWAY_IP_ADDR sudo journalctl -u dstack.gateway.service +``` diff --git a/contributing/RELEASE.md b/contributing/RELEASE.md index 56df59595e..f0798082fc 100644 --- a/contributing/RELEASE.md +++ b/contributing/RELEASE.md @@ -8,7 +8,10 @@ This is a `dstack` release guide and checklist for core maintainers. 1. Compare changes to the previous release, e.g. [`https://github.com/dstackai/dstack/compare/0.19.39...master`](https://github.com/dstackai/dstack/compare/0.19.39...master). 2. Test that `master` CLI works with the previous server release. PRs that add new model fields can potentially break client backward compatibility. 3. Test that `master` server works with the previous CLI release. - 4. Pay special attention to releases with DB migrations. Migrations should work with rolling deployments and avoid locking multiple tables. See [MIGRATIONS.md](MIGRATIONS.md). + 4. Pay special attention to releases with DB migrations. See [MIGRATIONS.md](MIGRATIONS.md). + * Ensure migrations work with rolling deployments and do not lock multiple tables. + * Test applying migrations while old replicas do active processing. + * Test migrations can be retried if they fail. For example, concurrent index may fail and stay in invalid state. 2. Create a tag, e.g. `git tag 0.19.40`. 3. Push the tag to trigger the Release `workflow`, i.e. `git push --tags`. 4. Generate GitHub release notes from the tag. Highlight major features, deprecations, breaking changes. diff --git a/contributing/RUNNER-AND-SHIM.md b/contributing/RUNNER-AND-SHIM.md index edccdc14aa..bbb09ed4ff 100644 --- a/contributing/RUNNER-AND-SHIM.md +++ b/contributing/RUNNER-AND-SHIM.md @@ -31,7 +31,7 @@ A container is started in either `host` or `bridge` network mode depending on th In `bridge` mode, container ports are mapped to ephemeral host ports. `dstack-shim` stores port mapping as a part of task's state. Currently, the default `bridge` network is used for all containers, but this could be changed in the future to improve container isolation. -All communication between the `dstack` server and `dstack-shim` happens via REST API through an SSH tunnel. `dstack-shim` doesn't collect logs. Usually, it is run from a `cloud-init` user-data script. +All communication between the `dstack` server and `dstack-shim` happens via HTTP API through an SSH tunnel. `dstack-shim` doesn't collect logs. Usually, it is run from a `cloud-init` user-data script. The entrypoint for the container: - Installs `openssh-server` @@ -52,10 +52,10 @@ The entrypoint for the container: - Wait for the signal to terminate the commands - STEP 5: Wait until all logs are read by the server and the CLI. Or exit after a timeout -All communication between the `dstack` server and `dstack-runner` happens via REST API through an SSH tunnel. `dstack-runner` collects the job logs and its own logs. Only the job logs are served via WebSocket. +All communication between the `dstack` server and `dstack-runner` happens via HTTP API through an SSH tunnel. `dstack-runner` collects the job logs and its own logs. Only the job logs are served via WebSocket. ## SSH tunnels -`dstack` expects a running SSH server right next to the `dstack-runner`. It provides a secure channel for communication with the runner API and forwarding any ports without listening for `0.0.0.0`. The `dstack-gateway` also uses this SSH server for forwarding requests from public endpoints. +`dstack` expects a running SSH server right next to the `dstack-runner`. It provides a secure channel for communication with the runner API and forwarding any ports without listening for `0.0.0.0`. The gateway also uses this SSH server for forwarding requests from public endpoints. `dstack-shim` must also be running next to the SSH server. The `dstack` server connects to this SSH server for interacting with both `dstack-shim` and `dstack-runner`. The CLI uses this SSH server as a jump host because the user wants to connect to the container. diff --git a/contributing/RUNS-AND-JOBS.md b/contributing/RUNS-AND-JOBS.md index 32725b7353..2f00e751be 100644 --- a/contributing/RUNS-AND-JOBS.md +++ b/contributing/RUNS-AND-JOBS.md @@ -17,31 +17,38 @@ A run can spawn one or multiple jobs, depending on the configuration. A task tha ## Run's Lifecycle -- STEP 1: The user submits the run. `services.runs.submit_run` creates jobs with status `SUBMITTED`. Now the run has status `SUBMITTED`. -- STEP 2: `background.tasks.process_runs` periodically pulls unfinished runs and processes them: - - If any job is `RUNNING`, the run becomes `RUNNING`. - - If any job is `PROVISIONING` or `PULLING`, the run becomes `PROVISIONING`. - - If any job fails and cannot be retried, the run becomes `TERMINATING`, and after processing, `FAILED`. - - If all jobs are `DONE`, the run becomes `TERMINATING`, and after processing, `DONE`. - - If any job fails, can be retried, and there is any other active job, the failed job will be resubmitted in-place. - - If any jobs in a replica fail and can be retried and there is other active replicas, the jobs of the failed replica are resubmitted in-place (without stopping other replicas). But if some jobs in a replica fail, then all the jobs in a replica are terminated and resubmitted. This include multi-node tasks that represent one replica with multiple jobs. - - If all jobs fail and can be resubmitted, the run becomes `PENDING`. -- STEP 3: If the run is `TERMINATING`, the server makes all jobs `TERMINATING`. `background.tasks.process_runs` sets their status to `TERMINATING`, assigns `JobTerminationReason`, and sends a graceful stop command to `dstack-runner`. `process_terminating_jobs` then ensures that jobs are terminated assigns a finished status. -- STEP 4: Once all jobs are finished, the run becomes `TERMINATED`, `DONE`, or `FAILED` based on `RunTerminationReason`. -- STEP 0: If the run is `PENDING`, `background.tasks.process_runs` will resubmit jobs. The run becomes `SUBMITTED` again. - -> Use `switch_run_status()` for all status transitions. Do not set `RunModel.status` directly. - -> No one must assign the finished status to the run, except `services.runs.process_terminating_run`. To terminate the run, assign `TERMINATING` status and `RunTerminationReason`. +- STEP 1: The user submits the run. `services.runs.submit_run` creates jobs with status `SUBMITTED`. The run starts in `SUBMITTED`. +- STEP 2: `RunPipeline` continuously processes unfinished runs. + - For active runs, it derives the run status from the latest job states in priority order: + 1. If any non-retryable failure is present, the run becomes `TERMINATING` with the relevant `RunTerminationReason`. + 2. If `stop_criteria == MASTER_DONE` and the master job is done, the run becomes `TERMINATING` with `ALL_JOBS_DONE`. + 3. Otherwise, if any job is `RUNNING`, the run becomes `RUNNING`. + 4. Otherwise, if any job is `PROVISIONING` or `PULLING`, the run becomes `PROVISIONING`. + 5. Otherwise, if jobs are still waiting for placement or provisioning, the run stays `SUBMITTED`. + 6. Otherwise, if all contributing jobs are `DONE`, the run becomes `TERMINATING` with `ALL_JOBS_DONE`. + 7. Otherwise, if no active replicas remain and the run should be retried, the run becomes `PENDING`. + - Retryable replica failures are handled before the final transition is applied: + - If a replica fails with a retryable reason while other replicas are still active, `RunPipeline` creates a new `SUBMITTED` submission for that replica and terminates the old jobs in that replica. + - If all remaining work is retryable, the run ends up in `PENDING`. +- STEP 3: If the run is `PENDING`, `RunPipeline` processes it in the pending phase. + - For retrying runs, it waits for an exponential backoff before resubmitting. + - For scheduled runs, it waits until `next_triggered_at`. + - For scaled-to-zero services, it can keep the run in `PENDING` until autoscaling wants replicas again. + - Once the run is ready to continue, `RunPipeline` creates new `SUBMITTED` jobs and moves the run back to `SUBMITTED`. +- STEP 4: If the run is `TERMINATING`, `RunPipeline` marks active jobs as `TERMINATING` and assigns the corresponding `JobTerminationReason`. +- STEP 5: Once all jobs are finished, the terminating phase of `RunPipeline` either: + - assigns the final run status (`TERMINATED`, `DONE`, or `FAILED`), or + - for scheduled runs that were not stopped or aborted by the user, returns the run to `PENDING` and computes a new `next_triggered_at`. ### Services -Services' lifecycle has some modifications: +Services' run lifecycle has some modifications: -- During STEP 1, the service is registered on the gateway. If the gateway is not accessible or the domain name is taken, the run submission fails. -- During STEP 2, downscaled jobs are ignored. -- During STEP 4, the service is unregistered on the gateway. -- During STEP 0, the service can stay in `PENDING` status if it was downscaled to zero (WIP). +- During STEP 1, the service itself is registered on the gateway or the in-server proxy. If the gateway is not accessible or the domain name is taken, submission fails. +- During STEP 2, active run processing also computes desired replica counts from gateway stats and handles scale-up, scale-down, rolling deployment, and cleanup of removed replica groups. +- During STEP 2, jobs already marked `SCALED_DOWN` do not contribute to the run status. +- During STEP 3, a service can stay in `PENDING` when autoscaling currently wants zero replicas. +- During STEP 5, the terminating phase of `RunPipeline` unregisters the service from the gateway. ### When can the job be retried? @@ -54,29 +61,27 @@ Services' lifecycle has some modifications: ## Job's Lifecycle - STEP 1: A newly submitted job has status `SUBMITTED`. It is not assigned to any instance yet. -- STEP 2: `background.tasks.process_submitted_jobs` tries to assign an existing instance or provision a new one. - - On success, the job becomes `PROVISIONING`. - - On failure, the job becomes `TERMINATING`, and after processing, `FAILED` because of `FAILED_TO_START_DUE_TO_NO_CAPACITY`. -- STEP 3: `background.tasks.process_running_jobs` periodically pulls unfinished jobs and processes them. - - While `dstack-shim`/`dstack-runner` is not responding, the job stays `PROVISIONING`. - - Once `dstack-shim` (for VM-featured backends) becomes available, it submits the docker image name, and the job becomes `PULLING`. - - Once `dstack-runner` inside a docker container becomes available, it submits the code and the job spec, and the job becomes `RUNNING`. - - If `dstack-shim` or `dstack-runner` don't respond for a long time or fail to respond after successful connection and multiple retries, the job becomes `TERMINATING`, and after processing, `FAILED`. -- STEP 4: `background.tasks.process_running_jobs` processes `RUNNING` jobs, pulling job logs, runner logs, and job status. - - If the pulled status is `DONE`, the job becomes `TERMINATING`, and after processing, `DONE`. - - Otherwise, the job becomes `TERMINATING`, and after processing, `FAILED`. -- STEP 5: `background.tasks.process_terminating_jobs` processes `TERMINATING` jobs. - - If the job has `remove_at` in the future, nothing happens. This is to give the job some time for a graceful stop. - - Once `remove_at` is in the past, it stops the container via `dstack-shim`, detaches instance volumes, and releases the instance. The job becomes `TERMINATED`, `DONE`, `FAILED`, or `ABORTED` based on `JobTerminationReason`. - - If some volumes fail to detach, it keeps the job `TERMINATING` and checks volumes attachment status. - -> Use `switch_job_status()` for all status transitions. Do not set `JobModel.status` directly. - -> No one must assign the finished status to the job, except `services.jobs.process_terminating_job`. To terminate the job, assign `TERMINATING` status and `JobTerminationReason`. +- STEP 2: `JobSubmittedPipeline` assigns the job in two phases: + - Assignment: claim an existing instance or reserve a *placeholder* `InstanceModel`. Placeholders are `PENDING` instances that reserve an `instance_num` and a `nodes.max` slot. `InstancePipeline` ignores them. + - Provisioning: reuse the existing instance, or cloud-provision and promote the placeholder to `PROVISIONING`. + - On success, the job becomes `PROVISIONING`. + - On failure, the job becomes `TERMINATING`. `JobTerminatingPipeline` later assigns the final failed status. +- STEP 3: `JobRunningPipeline` processes `PROVISIONING`, `PULLING`, and `RUNNING` jobs. + - While `dstack-shim` / `dstack-runner` is not responding, the job stays `PROVISIONING`. + - Once `dstack-shim` (for VM-featured backends) becomes available, the pipeline submits the image and the job becomes `PULLING`. + - Once `dstack-runner` inside the container becomes available, the pipeline uploads the code and job spec, and the job becomes `RUNNING`. + - While the job is `RUNNING`, the pipeline keeps collecting logs and runner status. + - If startup, runner communication, or replica registration fails, the job becomes `TERMINATING`. +- STEP 4: Once the job is actually ready, `JobRunningPipeline` initializes probes. +- STEP 5: `JobTerminatingPipeline` processes `TERMINATING` jobs. + - If the job has `remove_at` in the future, it waits. This gives the job time for a graceful stop. + - Once `remove_at` is in the past, it stops the container, detaches volumes, unregisters service replicas if needed, and releases the instance assignment. + - If some volumes are not detached yet, the job stays `TERMINATING` and is retried. + - When cleanup is complete, the job becomes `TERMINATED`, `DONE`, `FAILED`, or `ABORTED` based on `JobTerminationReason`. ### Services' Jobs Services' jobs lifecycle has some modifications: -- During STEP 3, once the job becomes `RUNNING`, it is registered on the gateway as a replica. If the gateway is not accessible, the job fails. -- During STEP 5, the job is unregistered on the gateway (WIP). +- During STEP 3, once the primary job of a replica is `RUNNING` and ready to receive traffic, `JobRunningPipeline` registers that replica on the gateway. If the gateway is not accessible, the job fails with a gateway-related termination reason. +- During STEP 5, `JobTerminatingPipeline` unregisters the replica from receiving requests before the job is fully cleaned up. diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index 29e92f45ba..36da212b75 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -1,17 +1,72 @@ -# syntax = edrevo/dockerfile-plus ARG UBUNTU_VERSION -# Build stage -FROM nvidia/cuda:12.8.1-base-ubuntu${UBUNTU_VERSION}.04 AS builder +# ============================================================================ +# common: shared base for all flavors. Select a flavor with `--target ` +# (base / devel / devel-efa). +# ============================================================================ + +FROM nvidia/cuda:13.0.3-base-ubuntu${UBUNTU_VERSION}.04 AS common + +# ARGs before FROM must be redeclared to be used after FROM +ARG UBUNTU_VERSION + +ARG _UV_HOME="/opt/uv" + +ENV UV_INSTALL_DIR="${_UV_HOME}/bin" +ENV UV_MANAGED_PYTHON=1 +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 + +ENV PATH="${UV_INSTALL_DIR}:${PATH}" + +ENV OMPI_MCA_pml=^cm,ucx +ENV OMPI_MCA_btl=tcp,self +ENV OMPI_MCA_btl_tcp_if_exclude=lo,docker0 +ENV NCCL_SOCKET_IFNAME=^docker,lo + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update --fix-missing \ + && apt-get upgrade -y \ + && ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime \ + && apt-get install -y tzdata \ + && dpkg-reconfigure --frontend noninteractive tzdata \ + && apt-get install -y bzip2 ca-certificates curl build-essential git libglib2.0-0 libsm6 libxext6 libxrender1 openssh-server wget \ + libibverbs1 ibverbs-providers ibverbs-utils libibverbs-dev infiniband-diags \ + # nvidia/cuda ships nvidia's apt singing key in legacy format (/etc/apt/trusted.gpg). + # This lead to warnings, so we install cuda-keyring. + && wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}04/x86_64/cuda-keyring_1.1-1_all.deb \ + && dpkg -i cuda-keyring_1.1-1_all.deb \ + && rm cuda-keyring_1.1-1_all.deb \ + && rm -f /etc/apt/sources.list.d/cuda.list \ + && sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication no/g" /etc/ssh/sshd_config \ + && mkdir /run/sshd \ + && mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys \ + && chmod 600 ~/.ssh/authorized_keys \ + && rm /etc/ssh/ssh_host_* \ + # User: UID/GID 1001 because Ubuntu 24.04 ships a default 'ubuntu' user at 1000. + && apt-get install -y sudo \ + && groupadd -g 1001 dstack \ + && useradd -u 1001 -g 1001 -G sudo -s /bin/bash -m dstack \ + && echo 'dstack ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/dstack \ + # Default working dir + && mkdir -p /dstack/run \ + && chmod a+rwx /dstack/run \ + # Cleanup + && rm -rf /var/lib/apt/lists/* + +RUN curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 sh \ + && uv python install --preview --default + +# ============================================================================ +# builder: builds NCCL and nccl-tests from source for the base/devel flavors. +# ============================================================================ + +FROM nvidia/cuda:13.0.3-base-ubuntu${UBUNTU_VERSION}.04 AS builder ENV NCCL_HOME=/opt/nccl ENV CUDA_HOME=/usr/local/cuda ENV OPEN_MPI_PATH=/usr/lib/x86_64-linux-gnu/openmpi -# Prerequisites - RUN export DEBIAN_FRONTEND=noninteractive \ - && apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}04/x86_64/3bf863cc.pub \ && apt-get update --fix-missing \ && apt-get upgrade -y \ && ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime \ @@ -31,17 +86,13 @@ RUN export DEBIAN_FRONTEND=noninteractive \ python3 \ build-essential -# NCCL - -ARG NCCL_VERSION=2.26.2-1 +ARG NCCL_VERSION=2.28.3-1 RUN cd /tmp \ && git clone https://github.com/NVIDIA/nccl.git -b v${NCCL_VERSION} \ && cd nccl \ && make -j$(nproc) src.build BUILDDIR=${NCCL_HOME} -# NCCL tests - RUN cd /opt \ && git clone https://github.com/NVIDIA/nccl-tests \ && cd nccl-tests \ @@ -51,31 +102,86 @@ RUN cd /opt \ CUDA_HOME=${CUDA_HOME} \ NCCL_HOME=${NCCL_HOME} -# Final stage +# ============================================================================ +# base: common + NCCL (from builder) + OpenMPI. +# ============================================================================ -INCLUDE+ base/Dockerfile.common +FROM common AS base ENV NCCL_HOME=/opt/nccl COPY --from=builder ${NCCL_HOME} ${NCCL_HOME} COPY --from=builder /opt/nccl-tests/build /opt/nccl-tests/build -ARG FLAVOR - -# MPI, NVCC, and /etc/ld.so.conf.d - RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - openmpi-bin \ - && if [ "$FLAVOR" = "devel" ]; then \ - cuda_version=$(echo ${CUDA_VERSION} | awk -F . '{ print $1"-"$2 }') \ - && apt-get install -y --no-install-recommends \ - cuda-libraries-dev-${cuda_version} \ - cuda-nvcc-${cuda_version} \ - libhwloc-dev; \ - fi \ + && apt-get install -y --no-install-recommends openmpi-bin \ && rm -rf /var/lib/apt/lists/* \ && echo "${NCCL_HOME}/lib" >> /etc/ld.so.conf.d/nccl.conf \ && ldconfig WORKDIR /dstack/run + +# ============================================================================ +# devel: base + CUDA development libraries and NVCC. +# ============================================================================ + +FROM base AS devel + +RUN cuda_version=$(echo ${CUDA_VERSION} | awk -F . '{ print $1"-"$2 }') \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + cuda-libraries-dev-${cuda_version} \ + cuda-nvcc-${cuda_version} \ + libhwloc-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /dstack/run + +# ============================================================================ +# devel-efa: common + CUDA dev libraries + AWS EFA + EFA-aware NCCL. +# ============================================================================ + +FROM common AS devel-efa + +ENV NCCL_HOME=/usr/local +ENV CUDA_HOME=/usr/local/cuda +ENV LIBFABRIC_PATH=/opt/amazon/efa +ENV OPEN_MPI_PATH=/opt/amazon/openmpi +ENV PATH="${LIBFABRIC_PATH}/bin:${OPEN_MPI_PATH}/bin:${PATH}" +ENV LD_LIBRARY_PATH="${OPEN_MPI_PATH}/lib:${LD_LIBRARY_PATH}" + +RUN cuda_version=$(echo ${CUDA_VERSION} | awk -F . '{ print $1"-"$2 }') \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + cuda-libraries-dev-${cuda_version} \ + cuda-nvcc-${cuda_version} \ + && rm -rf /var/lib/apt/lists/* + +ARG EFA_VERSION=1.48.0 + +RUN cd /tmp \ + && apt-get update \ + && curl -O https://s3-us-west-2.amazonaws.com/aws-efa-installer/aws-efa-installer-${EFA_VERSION}.tar.gz \ + && tar -xf aws-efa-installer-${EFA_VERSION}.tar.gz \ + && cd aws-efa-installer \ + && ./efa_installer.sh -y --skip-kmod -g \ + && rm -rf /tmp/aws-efa-installer /var/lib/apt/lists/* + +ARG NCCL_VERSION=2.28.3-1 + +RUN cd /tmp \ + && git clone https://github.com/NVIDIA/nccl.git -b v${NCCL_VERSION} \ + && cd nccl \ + && make -j$(nproc) src.build BUILDDIR=${NCCL_HOME} \ + && rm -rf /tmp/nccl + +RUN cd /opt \ + && git clone https://github.com/NVIDIA/nccl-tests \ + && cd nccl-tests \ + && make -j$(nproc) \ + MPI=1 \ + MPI_HOME=${OPEN_MPI_PATH} \ + CUDA_HOME=${CUDA_HOME} \ + NCCL_HOME=${NCCL_HOME} + +WORKDIR /dstack/run diff --git a/docker/base/Dockerfile.common b/docker/base/Dockerfile.common deleted file mode 100644 index b486c16800..0000000000 --- a/docker/base/Dockerfile.common +++ /dev/null @@ -1,44 +0,0 @@ -ARG UBUNTU_VERSION - -FROM nvidia/cuda:12.8.1-base-ubuntu${UBUNTU_VERSION}.04 - -ARG _UV_HOME="/opt/uv" - -ENV UV_INSTALL_DIR="${_UV_HOME}/bin" -ENV UV_MANAGED_PYTHON=1 -ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 - -ENV PATH="${UV_INSTALL_DIR}:${PATH}" - -ENV OMPI_MCA_pml=^cm,ucx -ENV OMPI_MCA_btl=tcp,self -ENV OMPI_MCA_btl_tcp_if_exclude=lo,docker0 -ENV NCCL_SOCKET_IFNAME=^docker,lo - -RUN export DEBIAN_FRONTEND=noninteractive \ - && apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${UBUNTU_VERSION}04/x86_64/3bf863cc.pub \ - && apt-get update --fix-missing \ - && apt-get upgrade -y \ - && ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime \ - && apt-get install -y tzdata \ - && dpkg-reconfigure --frontend noninteractive tzdata \ - && apt-get install -y bzip2 ca-certificates curl build-essential git libglib2.0-0 libsm6 libxext6 libxrender1 mercurial openssh-server subversion wget \ - libibverbs1 ibverbs-providers ibverbs-utils libibverbs-dev infiniband-diags \ - && sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication no/g" /etc/ssh/sshd_config \ - && mkdir /run/sshd \ - && mkdir ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys \ - && chmod 600 ~/.ssh/authorized_keys \ - && rm /etc/ssh/ssh_host_* \ - # User - && apt-get install -y sudo \ - && groupadd -g 1000 dstack \ - && useradd -u 1000 -g 1000 -G sudo -s /bin/bash -m dstack \ - && echo 'dstack ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/dstack \ - # Default working dir - && mkdir -p /dstack/run \ - && chmod a+rwx /dstack/run \ - # Cleanup - && rm -rf /var/lib/apt/lists/* - -RUN curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 sh \ - && uv python install --preview --default diff --git a/docker/base/README.md b/docker/base/README.md index 3bbbafa732..5246cbc40b 100644 --- a/docker/base/README.md +++ b/docker/base/README.md @@ -1 +1,19 @@ -Image for `dstack` runner instances. +# dstack base images + +Base images for `dstack` runner instances. A single multi-stage `Dockerfile` +produces all flavors; select one with `docker build --target `: + +- **base** — CUDA 12.8, Python (uv-managed), NCCL 2.26.2-1 + NCCL Tests, Open MPI. +- **devel** — `base` plus the CUDA development libraries and NVCC. +- **devel-efa** — `base` plus CUDA dev libraries, AWS EFA Installer 1.48.0 + (Libfabric + Open MPI + AWS OFI NCCL 1.19.0), and an EFA-aware NCCL 2.27.7-1 + build + NCCL Tests. + +Build args: `UBUNTU_VERSION` (e.g. `24`). + +Example: + +```bash +docker build --target devel-efa --build-arg UBUNTU_VERSION=24 \ + -t dstackai/base:local-devel-efa-ubuntu24.04 -f base/Dockerfile . +``` diff --git a/docker/base/efa/Dockerfile b/docker/base/efa/Dockerfile deleted file mode 100644 index 105650a836..0000000000 --- a/docker/base/efa/Dockerfile +++ /dev/null @@ -1,78 +0,0 @@ -# syntax = edrevo/dockerfile-plus - -INCLUDE+ base/Dockerfile.common - -ENV NCCL_HOME=/usr/local -ENV CUDA_HOME=/usr/local/cuda -ENV LIBFABRIC_PATH=/opt/amazon/efa -ENV OPEN_MPI_PATH=/opt/amazon/openmpi -ENV PATH="${LIBFABRIC_PATH}/bin:${OPEN_MPI_PATH}/bin:${PATH}" -ENV LD_LIBRARY_PATH="${OPEN_MPI_PATH}/lib:${LD_LIBRARY_PATH}" - -# Prerequisites - -RUN cuda_version=$(echo ${CUDA_VERSION} | awk -F . '{ print $1"-"$2 }') \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - cuda-libraries-dev-${cuda_version} \ - cuda-nvcc-${cuda_version} \ - libhwloc-dev \ - autoconf \ - automake \ - libtool \ - && rm -rf /var/lib/apt/lists/* - -# EFA - -ARG EFA_VERSION=1.38.1 - -RUN cd /tmp \ - && apt-get update \ - && curl -O https://s3-us-west-2.amazonaws.com/aws-efa-installer/aws-efa-installer-${EFA_VERSION}.tar.gz \ - && tar -xf aws-efa-installer-${EFA_VERSION}.tar.gz \ - && cd aws-efa-installer \ - && ./efa_installer.sh -y --skip-kmod -g \ - && rm -rf /tmp/aws-efa-installer /var/lib/apt/lists/* - -# NCCL - -ARG NCCL_VERSION=2.26.2-1 - -RUN cd /tmp \ - && git clone https://github.com/NVIDIA/nccl.git -b v${NCCL_VERSION} \ - && cd nccl \ - && make -j$(nproc) src.build BUILDDIR=${NCCL_HOME} \ - && rm -rf /tmp/nccl - -# AWS OFI NCCL - -ARG OFI_VERSION=1.14.0 - -RUN cd /tmp \ - && git clone https://github.com/aws/aws-ofi-nccl.git -b v${OFI_VERSION} \ - && cd aws-ofi-nccl \ - && ./autogen.sh \ - && ./configure \ - --with-cuda=${CUDA_HOME} \ - --with-libfabric=${LIBFABRIC_PATH} \ - --with-mpi=${OPEN_MPI_PATH} \ - --with-cuda=${CUDA_HOME} \ - --with-nccl=${NCCL_HOME} \ - --disable-tests \ - --prefix=${NCCL_HOME} \ - && make -j$(nproc) \ - && make install \ - && rm -rf /tmp/aws-ofi-nccl /var/lib/apt/lists/* - -# NCCL Tests - -RUN cd /opt \ - && git clone https://github.com/NVIDIA/nccl-tests \ - && cd nccl-tests \ - && make -j$(nproc) \ - MPI=1 \ - MPI_HOME=${OPEN_MPI_PATH} \ - CUDA_HOME=${CUDA_HOME} \ - NCCL_HOME=${NCCL_HOME} - -WORKDIR /dstack/run diff --git a/docker/base/efa/README.md b/docker/base/efa/README.md deleted file mode 100644 index 29ed748213..0000000000 --- a/docker/base/efa/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# dstack AWS EFA - -This image has the following installed: - -* CUDA 12.1 -* AWS EFA Installer 1.38.1 (Libfabric + Open MPI 4 + Open MPI 5) -* NCCL 2.26.2-1 -* AWS OFI NCCL 1.14.0 -* NCCL Tests diff --git a/docker/server/README.md b/docker/server/README.md index 611dbe6cac..2c91a4e152 100644 --- a/docker/server/README.md +++ b/docker/server/README.md @@ -22,6 +22,27 @@ The admin user token is 'bbae0f28-d3dd-4820-bf61-8f4bb40815da' For more details on server configuration options, see the [server deployment](https://dstack.ai/docs/guides/server-deployment.md) guide. +### Run with PostgreSQL and the SSH proxy + +In production, the `dstack` server is usually run with +[PostgreSQL](https://dstack.ai/docs/guides/server-deployment#postgresql) instead of the default +SQLite, and with the [SSH proxy](https://dstack.ai/docs/guides/server-deployment#ssh-proxy). The +[`docker-compose.yml`](https://github.com/dstackai/dstack/blob/master/docker/server/docker-compose.yml) +runs that combination locally, so you can try or test a production-like server on your own machine. +A full production deployment would also configure external +[logs storage](https://dstack.ai/docs/guides/server-deployment#logs-storage) and +[file storage](https://dstack.ai/docs/guides/server-deployment#file-storage). + +```shell +docker compose -f docker/server/docker-compose.yml up +``` + +This starts PostgreSQL, the `dstack` server at `http://localhost:3000`, and the SSH proxy at +`localhost:30022`. The admin token is printed to the logs (`docker compose logs server`). + +To access the server from the CLI, add it as a project with `dstack project add`, using the +admin token from the logs (see [Set up the CLI](#set-up-the-cli) below). + ## Set up the CLI To point the CLI to the `dstack` server, configure it diff --git a/docker/server/docker-compose.yml b/docker/server/docker-compose.yml new file mode 100644 index 0000000000..529271059c --- /dev/null +++ b/docker/server/docker-compose.yml @@ -0,0 +1,73 @@ +name: dstack + +services: + postgres: + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_USER: ${DSTACK_POSTGRES_USER:-dstack} + POSTGRES_PASSWORD: ${DSTACK_POSTGRES_PASSWORD:-dstack} + POSTGRES_DB: ${DSTACK_POSTGRES_DB:-dstack} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 10 + + server: + image: dstackai/dstack:latest # multi-arch — no platform override needed + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + DSTACK_DATABASE_URL: postgresql+asyncpg://${DSTACK_POSTGRES_USER:-dstack}:${DSTACK_POSTGRES_PASSWORD:-dstack}@postgres:5432/${DSTACK_POSTGRES_DB:-dstack} + DSTACK_SERVER_LOG_FORMAT: rich # human-readable console logs (image defaults to json) + # Shared secret between the server and the SSH proxy. The default works locally; + # override with a real secret for non-local deployments. + DSTACK_SSHPROXY_API_TOKEN: ${DSTACK_SSHPROXY_API_TOKEN:-dstack-sshproxy-token} + # Address clients use to reach the SSH proxy. `localhost:30022` is correct for a + # client on the same host; use a publicly reachable address for multi-host setups. + DSTACK_SERVER_SSHPROXY_ADDRESS: localhost:30022 + volumes: + - server-data:/root/.dstack/server # config.yml + run logs (Postgres holds the rest) + ports: + - "3000:3000" + + # One-shot init: generates the SSH proxy's host key into a shared volume so no manual + # ssh-keygen is needed. The proxy refuses to start without a host key. + sshproxy-keygen: + image: alpine:3 + command: + - sh + - -c + - | + apk add --no-cache openssh-keygen >/dev/null + [ -f /keys/host_key ] || ssh-keygen -t ed25519 -f /keys/host_key -N "" -C dstack-sshproxy + volumes: + - sshproxy-keys:/keys + + sshproxy: + image: dstackai/sshproxy:latest + platform: linux/amd64 # image is amd64-only; emulated on Apple Silicon + restart: unless-stopped + depends_on: + sshproxy-keygen: + condition: service_completed_successfully + server: + condition: service_started + environment: + DSTACK_SSHPROXY_API_URL: http://server:3000 + DSTACK_SSHPROXY_API_TOKEN: ${DSTACK_SSHPROXY_API_TOKEN:-dstack-sshproxy-token} + command: ["--host-key", "/keys/host_key"] + volumes: + - sshproxy-keys:/keys + ports: + - "30022:30022" + +volumes: + postgres-data: + server-data: + sshproxy-keys: diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index 6a87468490..8cd67fd260 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -13,15 +13,9 @@ if [[ -z "${LITESTREAM_REPLICA_URL}" ]]; then exec dstack server --host 0.0.0.0 else if [[ ! -f "$DB_PATH" ]]; then - echo "Attempting Litestream restore..." - if ! output=$(litestream restore -o "$DB_PATH" "$LITESTREAM_REPLICA_URL" 2>&1); then - if echo "$output" | grep -qiE "cannot calc restore plan"; then - echo "No replica snapshots found; starting with empty database." - else - echo "$output" >&2 - exit 1 - fi - fi + echo "Starting db restore" + litestream restore -if-replica-exists -o "$DB_PATH" "$LITESTREAM_REPLICA_URL" + echo "Finished db restore" fi exec litestream replicate -exec "dstack server --host 0.0.0.0" "$DB_PATH" "$LITESTREAM_REPLICA_URL" fi diff --git a/docker/server/release/Dockerfile b/docker/server/release/Dockerfile index ca94a26f2e..a88439d61e 100644 --- a/docker/server/release/Dockerfile +++ b/docker/server/release/Dockerfile @@ -15,8 +15,8 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* RUN if [ $(uname -m) = "aarch64" ]; then ARCH="arm64"; else ARCH="x86_64"; fi && \ - curl https://github.com/benbjohnson/litestream/releases/download/v0.5.0/litestream-0.5.0-linux-$ARCH.deb -O -L && \ - dpkg -i litestream-0.5.0-linux-$ARCH.deb + curl https://github.com/benbjohnson/litestream/releases/download/v0.5.9/litestream-0.5.9-linux-$ARCH.deb -O -L && \ + dpkg -i litestream-0.5.9-linux-$ARCH.deb ADD https://astral.sh/uv/install.sh /uv-installer.sh RUN sh /uv-installer.sh && rm /uv-installer.sh diff --git a/docker/server/stgn/Dockerfile b/docker/server/stgn/Dockerfile index 1933ed636c..4fce817787 100644 --- a/docker/server/stgn/Dockerfile +++ b/docker/server/stgn/Dockerfile @@ -23,8 +23,8 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* RUN if [ $(uname -m) = "aarch64" ]; then ARCH="arm64"; else ARCH="x86_64"; fi && \ - curl https://github.com/benbjohnson/litestream/releases/download/v0.5.0/litestream-0.5.0-linux-$ARCH.deb -O -L && \ - dpkg -i litestream-0.5.0-linux-$ARCH.deb + curl https://github.com/benbjohnson/litestream/releases/download/v0.5.9/litestream-0.5.9-linux-$ARCH.deb -O -L && \ + dpkg -i litestream-0.5.9-linux-$ARCH.deb ADD https://astral.sh/uv/install.sh /uv-installer.sh RUN sh /uv-installer.sh && rm /uv-installer.sh @@ -33,6 +33,8 @@ ENV PATH="/root/.local/bin/:$PATH" COPY pyproject.toml uv.lock README.md ./ COPY src src COPY examples examples +# pyproject force-includes skills/*/SKILL.md into the wheel, so uv sync needs them. +COPY skills skills # Copy built frontend to statics directory COPY --from=frontend-builder /frontend/build src/dstack/_internal/server/statics diff --git a/docker/tt-smi/Dockerfile b/docker/tt-smi/Dockerfile index b8417e4259..da190db313 100644 --- a/docker/tt-smi/Dockerfile +++ b/docker/tt-smi/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 ARG IMAGE_NAME ARG TT_SMI_VERSION @@ -10,7 +10,8 @@ RUN \ apt-get update && \ apt-get install -y curl git python3 python3-pip && \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - pip3 install --no-cache-dir git+https://github.com/tenstorrent/tt-smi@v${TT_SMI_VERSION} && \ + pip3 install --no-cache-dir --upgrade pip setuptools wheel tomli && \ + pip3 install --no-cache-dir --no-build-isolation git+https://github.com/tenstorrent/tt-smi@v${TT_SMI_VERSION} && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/docs/assets/javascripts/extra.js b/docs/assets/javascripts/extra.js deleted file mode 100644 index 30bdcd5fd6..0000000000 --- a/docs/assets/javascripts/extra.js +++ /dev/null @@ -1,165 +0,0 @@ -controller = (function () { - let setupEventListeners = function () { - document.addEventListener('keydown', function (event) { - if (event.keyCode === 75 && (event.metaKey || event.ctrlKey)) { - document.querySelector('.md-search__input').focus() - } - }); - }; - return { - init: function () { - setupEventListeners(); - } - }; -})(); - -controller.init(); - -function setupTermynal() { - document.querySelectorAll(".use-termynal").forEach(node => { - node.style.display = "block"; - new Termynal(node, { - lineDelay: 500 - }); - }); - const progressLiteralStart = "---> 100%"; - const promptLiteralStart = "$ "; - const customPromptLiteralStart = "# "; - const termynalActivateClass = "termy"; - let termynals = []; - - function createTermynals() { - document - .querySelectorAll(`.${termynalActivateClass} .highlight`) - .forEach(node => { - const text = node.textContent; - const lines = text.split(/(? { - if (line) { - isBlankSpace = false; - } - }); - dataValue = {}; - if (isBlankSpace) { - dataValue["delay"] = 0; - } - if (buffer[buffer.length - 1] === "") { - // A last single
won't have effect - // so put an additional one - buffer.push(""); - } - const bufferValue = buffer.join("
"); - dataValue["value"] = bufferValue; - useLines.push(dataValue); - buffer = []; - } - } - for (let line of lines) { - if (line === progressLiteralStart) { - saveBuffer(); - useLines.push({ - type: "progress" - }); - } else if (line.startsWith(promptLiteralStart)) { - saveBuffer(); - const value = line.replace(promptLiteralStart, "").trimEnd(); - useLines.push({ - type: "input", - value: value - }); - } else if (line.startsWith("// ")) { - saveBuffer(); - const value = "💬 " + line.replace("// ", "").trimEnd(); - useLines.push({ - value: value, - class: "termynal-comment", - delay: 0 - }); - } else if (line.startsWith(customPromptLiteralStart)) { - saveBuffer(); - const promptStart = line.indexOf(promptLiteralStart); - if (promptStart === -1) { - console.error("Custom prompt found but no end delimiter", line) - } - const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") - let value = line.slice(promptStart + promptLiteralStart.length); - useLines.push({ - type: "input", - value: value, - prompt: prompt - }); - } else { - buffer.push(line); - } - } - saveBuffer(); - const div = document.createElement("div"); - node.replaceWith(div); - const termynal = new Termynal(div, { - lineData: useLines, - noInit: true, - lineDelay: 500, - typeDelay: 20 - }); - termynals.push(termynal); - }); - } - - function loadVisibleTermynals() { - termynals = termynals.filter(termynal => { - if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { - termynal.init(); - return false; - } - return true; - }); - } - window.addEventListener("scroll", loadVisibleTermynals); - createTermynals(); - loadVisibleTermynals(); -} - -function setupCustomCodeTitles() { - document.querySelectorAll("div[editor-title]").forEach(div => { - let code = div.getElementsByTagName('code')[0]; - // code.setAttribute("editor-title", div.getAttribute("editor-title")) - let editorTitle = document.createElement("span") - editorTitle.className = "editor-title" - editorTitle.innerHTML = div.getAttribute("editor-title") - code.appendChild(editorTitle) - }); -} - -window.addEventListener("DOMContentLoaded", function() { - let tabs = document.querySelector(".md-tabs") - let header = document.querySelector(".md-header") - let search = document.querySelector(".md-search") - search.parentNode.insertBefore(tabs, search) - header.classList.add("ready") - setupTermynal() - setupCustomCodeTitles() -}); - -(function () { - document.querySelectorAll('.tx-faq__item').forEach(function (faqItem) { - faqItem.querySelector('.tx-faq__item-title').addEventListener('click', function () { - if (faqItem.classList.contains('_open')) { - faqItem.classList.remove('_open') - } else { - faqItem.classList.add('_open') - } - }); - }) - - document.querySelectorAll('a[href^="http"]').forEach(link => { - if (!link.href.includes(location.hostname)) { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); - } - }); -})() diff --git a/docs/assets/stylesheets/termynal.css b/docs/assets/stylesheets/termynal.css deleted file mode 100644 index 436db2b65b..0000000000 --- a/docs/assets/stylesheets/termynal.css +++ /dev/null @@ -1,125 +0,0 @@ -/** - * termynal.js - * - * @author Ines Montani - * @version 0.0.1 - * @license MIT - */ - -:root { - --color-bg: rgb(21, 22, 29); - --color-text: #eee; - --color-text-subtle: #a2a2a2; -} - -[data-termynal] span { - white-space: pre; -} - -.small > [data-termynal] { - font-size: 14px; - line-height: 1.4; -} - -[data-termynal] { - margin-block-start: 1em; - margin-block-end: 1em; - overflow-x: scroll; - /*white-space: pre;*/ - /*width: 750px;*/ - max-width: 100%; - background: var(--color-bg); - color: var(--color-text); - /* font-size: 18px; */ - font-size: 14px; - /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: var(--md-code-font-family) !important; - border-radius: 3px; - padding: 45px 25px 25px; - /*padding: 75px 45px 35px;*/ - position: relative; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -[data-termynal]:before { - content: ''; - position: absolute; - top: 15px; - left: 15px; - display: inline-block; - width: 12px; - height: 12px; - border-radius: 50%; - /* A little hack to display the window buttons in one pseudo element. */ - background: #d9515d; - /*-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;*/ - /* box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;*/ - -webkit-box-shadow: 20px 0 0 #f4c025, 40px 0 0 #3ec930; - box-shadow: 20px 0 0 #f4c025, 40px 0 0 #3ec930; -} - -[data-termynal]:after { - content: ''; - position: absolute; - color: var(--color-text-subtle); - top: 7px; - left: 0; - width: 100%; - text-align: center; -} - -a[data-terminal-control] { - text-align: right; - display: block; - color: #aebbff; -} - -[data-ty] { - display: block; - line-height: 2; -} - -[data-ty]:before { - /* Set up defaults and ensure empty lines are displayed. */ - content: ''; - display: inline-block; - vertical-align: middle; -} - -[data-ty="input"]:before, -[data-ty-prompt]:before { - margin-right: 0.75em; - color: var(--color-text-subtle); -} - -[data-ty="input"]:before { - content: '$'; -} - -[data-ty][data-ty-prompt]:before { - content: attr(data-ty-prompt); -} - -[data-ty-cursor]:after { - content: attr(data-ty-cursor); - font-family: monospace; - margin-left: 0.5em; - -webkit-animation: blink 1s infinite; - animation: blink 1s infinite; -} - - -/* Cursor animation */ - -@-webkit-keyframes blink { - 50% { - opacity: 0; - } -} - -@keyframes blink { - 50% { - opacity: 0; - } -} diff --git a/docs/docs/concepts/fleets.md b/docs/docs/concepts/fleets.md deleted file mode 100644 index 99912cd75b..0000000000 --- a/docs/docs/concepts/fleets.md +++ /dev/null @@ -1,516 +0,0 @@ -# Fleets - -Fleets act both as pools of instances and as templates for how those instances are provisioned. - -`dstack` supports two kinds of fleets: - -* [Backend fleets](#backend-fleets) – dynamically provisioned through configured backends; they are supported with any type of backends: [VM-based](backends.md#vm-based) and [container-based](backends.md#container-based) (incl. [Kubernetes](backends.md#kubernetes)) -* [SSH fleets](#ssh-fleets) – created using on-prem servers; do not require backends - -When you submit a dev environment, task, or service, `dstack` reuses idle instances or provisions new ones based on the fleet configuration. - -> You must create a fleet before submitting runs. - -## Backend fleets - -Backend fleets allow provisioning compute across cloud providers or Kubernetes clusters. - -??? info "Prerequisites" - Before creating a backend fleet, make sure to configure the corresponding [backends](backends.md). - -### Apply the configuration - -To create a backend fleet, define a configuration as a YAML file. The file must have a -`.dstack.yml` extension (e.g. `.dstack.yml` or `fleet.dstack.yml`). - -
- - ```yaml - type: fleet - # The name is optional, if not specified, generated randomly - name: default - - # Can be a range or a fixed number - # Allow to provision of up to 2 instances - nodes: 0..2 - - # Uncomment to ensure instances are inter-connected - #placement: cluster - - # Deprovision instances above the minimum if they remain idle - idle_duration: 1h - - resources: - # Allow to provision up to 8 GPUs - gpu: 0..8 - ``` - -
- -To create or update the fleet, pass the fleet configuration to [`dstack apply`](../reference/cli/dstack/apply.md): - -
- -```shell -$ dstack apply -f fleet.dstack.yml - -Provisioning... ----> 100% - - FLEET INSTANCE BACKEND GPU PRICE STATUS CREATED - my-fleet - - - - - - -``` - -
- -If `nodes` is a range that starts above `0`, `dstack` pre-creates the initial number of instances up front, while any additional ones are created on demand. - -> Setting the `nodes` range to start above `0` is supported only for [VM-based backends](backends.md#vm-based). - -??? info "Target number of nodes" - - If `nodes` is defined as a range, you can start with more than the minimum number of instances by using the `target` parameter when creating the fleet. - -
- - ```yaml - type: fleet - - name: my-fleet - - nodes: - min: 0 - max: 2 - - # Provision 2 instances initially - target: 2 - - # Deprovision instances above the minimum if they remain idle - idle_duration: 1h - ``` - -
- -By default, when you submit a [dev environment](dev-environments.md), [task](tasks.md), or [service](services.md), `dstack` tries all available fleets. However, you can explicitly specify the [`fleets`](../reference/dstack.yml/dev-environment.md#fleets) in your run configuration -or via [`--fleet`](../reference/cli/dstack/apply.md#fleet) with `dstack apply`. - -### Configuration options - -#### Placement { #backend-placement } - -To ensure instances are interconnected (e.g., for -[distributed tasks](tasks.md#distributed-tasks)), set `placement` to `cluster`. -This ensures all instances are provisioned with optimal inter-node connectivity. - -??? info "AWS" - When you create a fleet with AWS, [Elastic Fabric Adapter networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html) is automatically configured if it’s supported for the corresponding instance type. - Note, EFA requires the `public_ips` to be set to `false` in the `aws` backend configuration. - Otherwise, instances are only connected by the default VPC subnet. - - Refer to the [AWS](../../examples/clusters/aws/index.md) example for more details. - -??? info "GCP" - When you create a fleet with GCP, `dstack` automatically configures [GPUDirect-TCPXO and GPUDirect-TCPX](https://cloud.google.com/kubernetes-engine/docs/how-to/gpu-bandwidth-gpudirect-tcpx-autopilot) networking for the A3 Mega and A3 High instance types, as well as RoCE networking for the A4 instance type. - - !!! info "Backend configuration" - You may need to configure `extra_vpcs` and `roce_vpcs` in the `gcp` backend configuration. - Refer to the [GCP](../../examples/clusters/gcp/index.md) examples for more details. - -??? info "Nebius" - When you create a fleet with Nebius, [InfiniBand networking](https://docs.nebius.com/compute/clusters/gpu) is automatically configured if it’s supported for the corresponding instance type. - Otherwise, instances are only connected by the default VPC subnet. - - An InfiniBand fabric for the cluster is selected automatically. If you prefer to use some specific fabrics, configure them in the - [backend settings](../reference/server/config.yml.md#nebius). - -The `cluster` placement is supported for `aws`, `azure`, `gcp`, `nebius`, `oci`, and `vultr` -backends. - -> For more details on optimal inter-node connectivity, read the [Clusters](../guides/clusters.md) guide. - - - -#### Resources - -When you specify a resource value like `cpu` or `memory`, -you can either use an exact value (e.g. `24GB`) or a -range (e.g. `24GB..`, or `24GB..80GB`, or `..80GB`). - -
- -```yaml -type: fleet -# The name is optional, if not specified, generated randomly -name: my-fleet - -nodes: 2 - -resources: - # 200GB or more RAM - memory: 200GB.. - # 4 GPUs from 40GB to 80GB - gpu: 40GB..80GB:4 - # Disk size - disk: 500GB -``` - -
- -The `gpu` property allows specifying not only memory size but also GPU vendor, names -and their quantity. Examples: `nvidia` (one NVIDIA GPU), `A100` (one A100), `A10G,A100` (either A10G or A100), -`A100:80GB` (one A100 of 80GB), `A100:2` (two A100), `24GB..40GB:2` (two GPUs between 24GB and 40GB), -`A100:40GB:2` (two A100 GPUs of 40GB). - -??? info "Google Cloud TPU" - To use TPUs, specify its architecture via the `gpu` property. - - ```yaml - type: fleet - # The name is optional, if not specified, generated randomly - name: my-fleet - - nodes: 2 - - resources: - gpu: v2-8 - ``` - - Currently, only 8 TPU cores can be specified, supporting single TPU device workloads. Multi-TPU support is coming soon. - -> If you’re unsure which offers (hardware configurations) are available from the configured backends, use the -> [`dstack offer`](../reference/cli/dstack/offer.md#list-gpu-offers) command to list them. - -#### Blocks { #backend-blocks } - -For backend fleets, `blocks` function the same way as in SSH fleets. -See the [`Blocks`](#ssh-blocks) section under SSH fleets for details on the blocks concept. - -
- -```yaml -type: fleet - -name: my-fleet - -resources: - gpu: NVIDIA:80GB:8 - -# Split into 4 blocks, each with 2 GPUs -blocks: 4 -``` - -
- -#### Idle duration - -By default, fleet instances stay `idle` for 3 days and can be reused within that time. -If an instance is not reused within this period, it is automatically terminated. - -To change the default idle duration, set -[`idle_duration`](../reference/dstack.yml/fleet.md#idle_duration) in the fleet configuration (e.g., `0s`, `1m`, or `off` for -unlimited). - -
- - ```yaml - type: fleet - # The name is optional, if not specified, generated randomly - name: my-fleet - - nodes: 2 - - # Terminate instances idle for more than 1 hour - idle_duration: 1h - - resources: - gpu: 24GB - ``` - -
- -#### Spot policy - -By default, `dstack` uses on-demand instances. However, you can change that -via the [`spot_policy`](../reference/dstack.yml/fleet.md#spot_policy) property. It accepts `spot`, `on-demand`, and `auto`. - -#### Retry policy - -By default, if `dstack` fails to provision an instance or an instance is interrupted, no retry is attempted. - -If you'd like `dstack` to do it, configure the -[retry](../reference/dstack.yml/fleet.md#retry) property accordingly: - -
- -```yaml -type: fleet -# The name is optional, if not specified, generated randomly -name: my-fleet - -nodes: 1 - -resources: - gpu: 24GB - -retry: - # Retry on specific events - on_events: [no-capacity, interruption] - # Retry for up to 1 hour - duration: 1h -``` - -
- -!!! info "Reference" - Backend fleets support many more configuration options, - incl. [`backends`](../reference/dstack.yml/fleet.md#backends), - [`regions`](../reference/dstack.yml/fleet.md#regions), - [`max_price`](../reference/dstack.yml/fleet.md#max_price), and - among [others](../reference/dstack.yml/fleet.md). - -## SSH fleets - -If you have a group of on-prem servers accessible via SSH, you can create an SSH fleet. - -### Apply a configuration - -Define a fleet configuration as a YAML file in your project directory. The file must have a -`.dstack.yml` extension (e.g. `.dstack.yml` or `fleet.dstack.yml`). - -
- - ```yaml - type: fleet - # The name is optional, if not specified, generated randomly - name: my-fleet - - # Uncomment if instances are interconnected - #placement: cluster - - # SSH credentials for the on-prem servers - ssh_config: - user: ubuntu - identity_file: ~/.ssh/id_rsa - hosts: - - 3.255.177.51 - - 3.255.177.52 - ``` - -
- -??? info "Requirements" - 1. Hosts must be pre-installed with Docker. - - === "NVIDIA" - 2. Hosts with NVIDIA GPUs must also be pre-installed with CUDA 12.1 and - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). - - === "AMD" - 2. Hosts with AMD GPUs must also be pre-installed with AMDGPU-DKMS kernel driver (e.g. via - [native package manager](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/install/native-install/index.html) - or [AMDGPU installer](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/install/amdgpu-install.html).) - - === "Intel Gaudi" - 2. Hosts with Intel Gaudi accelerators must be pre-installed with [Gaudi software and drivers](https://docs.habana.ai/en/latest/Installation_Guide/Driver_Installation.html#driver-installation). - This must include the drivers, `hl-smi`, and Habana Container Runtime. - - === "Tenstorrent" - 2. Hosts with Tenstorrent accelerators must be pre-installed with [Tenstorrent software](https://docs.tenstorrent.com/getting-started/README.html#software-installation). - This must include the drivers, `tt-smi`, and HugePages. - - 3. The user specified must have passwordless `sudo` access. - - 4. The SSH server must be running and configured with `AllowTcpForwarding yes` in `/etc/ssh/sshd_config`. - - 5. The firewall must allow SSH and should forbid any other connections from external networks. For `placement: cluster` fleets, it should also allow any communication between fleet nodes. - -To create or update the fleet, pass the fleet configuration to [`dstack apply`](../reference/cli/dstack/apply.md): - -
- -```shell -$ dstack apply -f examples/misc/fleets/.dstack.yml - -Provisioning... ----> 100% - - FLEET INSTANCE GPU PRICE STATUS CREATED - my-fleet 0 L4:24GB (spot) $0 idle 3 mins ago - 1 L4:24GB (spot) $0 idle 3 mins ago -``` - -
- -When you apply, `dstack` connects to the specified hosts using the provided SSH credentials, -installs the dependencies, and configures these hosts as a fleet. - -Once the status of instances changes to `idle`, they can be used by dev environments, tasks, and services. - -### Configuration options - -#### Placement { #ssh-placement } - -If the hosts are interconnected (i.e. share the same network), set `placement` to `cluster`. -This is required if you'd like to use the fleet for [distributed tasks](tasks.md#distributed-tasks). - -??? info "Network" - By default, `dstack` automatically detects the network shared by the hosts. - However, it's possible to configure it explicitly via - the [`network`](../reference/dstack.yml/fleet.md#network) property. - - [//]: # (TODO: Provide an example and more detail) - -> For more details on optimal inter-node connectivity, read the [Clusters](../guides/clusters.md) guide. - -#### Blocks { #ssh-blocks } - -By default, a job uses the entire instance—e.g., all 8 GPUs. To allow multiple jobs on the same instance, set the `blocks` property to divide the instance. Each job can then use one or more blocks, up to the full instance. - -
- - ```yaml - type: fleet - name: my-fleet - - ssh_config: - user: ubuntu - identity_file: ~/.ssh/id_rsa - hosts: - - hostname: 3.255.177.51 - blocks: 4 - - hostname: 3.255.177.52 - # As many as possible, according to numbers of GPUs and CPUs - blocks: auto - - hostname: 3.255.177.53 - # Do not slice. This is the default value, may be omitted - blocks: 1 - ``` - -
- -All resources (GPU, CPU, memory) are split evenly across blocks, while disk is shared. - -For example, with 8 GPUs, 128 CPUs, and 2TB RAM, setting `blocks` to `8` gives each block 1 GPU, 16 CPUs, and 256 GB RAM. - -Set `blocks` to `auto` to match the number of blocks to the number of GPUs. - -!!! info "Distributed tasks" - Distributed tasks require exclusive access to all host resources and therefore must use all blocks on each node. - -#### Environment variables - -If needed, you can specify environment variables that will be used by `dstack-shim` and passed to containers. - -[//]: # (TODO: Explain what dstack-shim is) - -For example, these variables can be used to configure a proxy: - -```yaml -type: fleet -name: my-fleet - -env: - - HTTP_PROXY=http://proxy.example.com:80 - - HTTPS_PROXY=http://proxy.example.com:80 - - NO_PROXY=localhost,127.0.0.1 - -ssh_config: - user: ubuntu - identity_file: ~/.ssh/id_rsa - hosts: - - 3.255.177.51 - - 3.255.177.52 -``` - -#### Proxy jump - -If fleet hosts are behind a head node (aka "login node"), configure [`proxy_jump`](../reference/dstack.yml/fleet.md#proxy_jump): - -
- - ```yaml - type: fleet - name: my-fleet - - ssh_config: - user: ubuntu - identity_file: ~/.ssh/worker_node_key - hosts: - - 3.255.177.51 - - 3.255.177.52 - proxy_jump: - hostname: 3.255.177.50 - user: ubuntu - identity_file: ~/.ssh/head_node_key - ``` - -
- -To be able to attach to runs, both explicitly with `dstack attach` and implicitly with `dstack apply`, you must either -add a front node key (`~/.ssh/head_node_key`) to an SSH agent or configure a key path in `~/.ssh/config`: - -
- - ``` - Host 3.255.177.50 - IdentityFile ~/.ssh/head_node_key - ``` - -
- -where `Host` must match `ssh_config.proxy_jump.hostname` or `ssh_config.hosts[n].proxy_jump.hostname` if you configure head nodes -on a per-worker basis. - -!!! info "Reference" - For all SSH fleet configuration options, refer to the [reference](../reference/dstack.yml/fleet.md). - -#### Troubleshooting - -!!! info "Resources" - Once the fleet is created, double-check that the GPU, memory, and disk are detected correctly. - -If the status does not change to `idle` after a few minutes or the resources are not displayed correctly, ensure that -all host requirements are satisfied. - -If the requirements are met but the fleet still fails to be created correctly, check the logs at -`/root/.dstack/shim.log` on the hosts for error details. - -## Manage fleets - -### List fleets - -The [`dstack fleet`](../reference/cli/dstack/fleet.md#dstack-fleet-list) command lists fleet instances and their status: - -
- -```shell -$ dstack fleet - - FLEET INSTANCE BACKEND GPU PRICE STATUS CREATED - my-fleet 0 gcp (europe-west-1) L4:24GB (spot) $0.1624 idle 3 mins ago - 1 gcp (europe-west-1) L4:24GB (spot) $0.1624 idle 3 mins ago -``` - -
- -### Delete fleets - -When a fleet isn't used by a run, you can delete it by passing the fleet configuration to `dstack delete`: - -
- -```shell -$ dstack delete -f cluster.dstack.yaml -Delete the fleet my-gcp-fleet? [y/n]: y -Fleet my-gcp-fleet deleted -``` - -
- -Alternatively, you can delete a fleet by passing the fleet name to `dstack fleet delete`. -To terminate and delete specific instances from a fleet, pass `-i INSTANCE_NUM`. - -!!! info "What's next?" - 1. Check [dev environments](dev-environments.md), [tasks](tasks.md), and - [services](services.md) - 2. Read the [Clusters](../guides/clusters.md) guide diff --git a/docs/docs/concepts/gateways.md b/docs/docs/concepts/gateways.md deleted file mode 100644 index 7402bc5762..0000000000 --- a/docs/docs/concepts/gateways.md +++ /dev/null @@ -1,162 +0,0 @@ -# Gateways - -Gateways manage ingress traffic for running [services](services.md), handle auto-scaling and rate limits, enable HTTPS, and allow you to configure a custom domain. They also support custom routers, such as the [SGLang Model Gateway](https://docs.sglang.ai/advanced_features/router.html#). - - - -## Apply a configuration - -First, define a gateway configuration as a YAML file in your project folder. -The filename must end with `.dstack.yml` (e.g. `.dstack.yml` or `gateway.dstack.yml` are both acceptable). - -
- -```yaml -type: gateway -# A name of the gateway -name: example-gateway - -# Gateways are bound to a specific backend and region -backend: aws -region: eu-west-1 - -# This domain will be used to access the endpoint -domain: example.com -``` - -
- -A domain name is required to create a gateway. - -To create or update the gateway, simply call the [`dstack apply`](../reference/cli/dstack/apply.md) command: - -
- -```shell -$ dstack apply -f gateway.dstack.yml -The example-gateway doesn't exist. Create it? [y/n]: y - -Provisioning... ----> 100% - - BACKEND REGION NAME HOSTNAME DOMAIN DEFAULT STATUS - aws eu-west-1 example-gateway example.com ✓ submitted -``` - -
- -## Configuration options - -### Backend - -You can create gateways with the `aws`, `azure`, `gcp`, or `kubernetes` backends, but that does not limit where services run. A gateway can use one backend while services run on any other backend supported by dstack, including backends where gateways themselves cannot be created. - -??? info "Kubernetes" - Gateways in `kubernetes` backend require an external load balancer. Managed Kubernetes solutions usually include a load balancer. - For self-hosted Kubernetes, you must provide a load balancer by yourself. - -### Instance type - -By default, `dstack` provisions a small, low-cost instance for the gateway. If you expect to run high-traffic services, you can configure a larger instance type using the `instance_type` property. - -
- -```yaml -type: gateway -name: example-gateway - -backend: aws -region: eu-west-1 - -# (Optional) Override the gateway instance type -instance_type: t3.large - -domain: example.com -``` - -
- -### Router - -By default, the gateway uses its own load balancer to route traffic between replicas. However, you can delegate this responsibility to a specific router by setting the `router` property. Currently, the only supported external router is `sglang`. - -#### SGLang - -The `sglang` router delegates routing logic to the [SGLang Model Gateway](https://docs.sglang.ai/advanced_features/router.html#). - -To enable it, set `type` field under `router` to `sglang`: - -
- -```yaml -type: gateway -name: sglang-gateway - -backend: aws -region: eu-west-1 - -domain: example.com - -router: - type: sglang - policy: cache_aware -``` - -
- -!!! info "Policy" - The `policy` property allows you to configure the routing policy: - - * `cache_aware` — Default policy; combines cache locality with load balancing, falling back to shortest queue. - * `power_of_two` — Samples two workers and picks the lighter one. - * `random` — Uniform random selection. - * `round_robin` — Cycles through workers in order. - - -> Currently, services using this type of gateway must run standard SGLang workers. See the [example](../../examples/inference/sglang/index.md). -> -> Support for prefill/decode disaggregation and auto-scaling based on inter-token latency is coming soon. - -### Public IP - -If you don't need/want a public IP for the gateway, you can set the `public_ip` to `false` (the default value is `true`), making the gateway private. -Private gateways are currently supported in `aws` and `gcp` backends. - -!!! info "Reference" - For all gateway configuration options, refer to the [reference](../reference/dstack.yml/gateway.md). - -## Update DNS records - -Once the gateway is assigned a hostname, go to your domain's DNS settings -and add a DNS record for `*.`, e.g. `*.example.com`. -The record should point to the gateway's hostname shown in `dstack` -and should be of type `A` if the hostname is an IP address (most cases), -or of type `CNAME` if the hostname is another domain (some private gateways and Kubernetes). - -## Manage gateways - -### List gateways - -The [`dstack gateway list`](../reference/cli/dstack/gateway.md#dstack-gateway-list) command lists existing gateways and their status. - -### Delete a gateway - -To delete a gateway, pass the gateway configuration to [`dstack delete`](../reference/cli/dstack/delete.md): - -
- -```shell -$ dstack delete -f examples/inference/gateway.dstack.yml -``` - -
- -Alternatively, you can delete a gateway by passing the gateway name to `dstack gateway delete`. - -[//]: # (TODO: Elaborate on default) - -[//]: # (TODO: ## Accessing endpoints) - -!!! info "What's next?" - 1. See [services](services.md) on how to run services diff --git a/docs/docs/concepts/snippets/manage-fleets.ext b/docs/docs/concepts/snippets/manage-fleets.ext deleted file mode 100644 index 414aab91ab..0000000000 --- a/docs/docs/concepts/snippets/manage-fleets.ext +++ /dev/null @@ -1,25 +0,0 @@ -### Creation policy - -By default, when you run `dstack apply` with a dev environment, task, or service, -if no `idle` instances from the available fleets meet the requirements, `dstack` provisions a new instance -using configured backends. - -To ensure `dstack apply` doesn't provision a new instance but reuses an existing one, -pass `-R` (or `--reuse`) to `dstack apply`. - -
- -```shell -$ dstack apply -R -f examples/.dstack.yml -``` - -
- -Or, set [`creation_policy`](../reference/dstack.yml/dev-environment.md#creation_policy) to `reuse` in the run configuration. - -### Idle duration - -If a run provisions a new instance, the instance stays `idle` for 5 minutes by default and can be reused within that time. -If the instance is not reused within this period, it is automatically terminated. -To change the default idle duration, set -[`idle_duration`](../reference/dstack.yml/fleet.md#idle_duration) in the run configuration (e.g., `0s`, `1m`, or `off` for unlimited). diff --git a/docs/docs/guides/clusters.md b/docs/docs/guides/clusters.md deleted file mode 100644 index 30bfbee6ea..0000000000 --- a/docs/docs/guides/clusters.md +++ /dev/null @@ -1,82 +0,0 @@ -# Clusters - -A cluster is a [fleet](../concepts/fleets.md) with its `placement` set to `cluster`. This configuration ensures that the instances within the fleet are interconnected, enabling fast inter-node communication—crucial for tasks such as efficient distributed training. - -## Fleets - -Ensure a fleet is created before you run any distributed task. This can be either an SSH fleet or a cloud fleet. - -### SSH fleets - -[SSH fleets](../concepts/fleets.md#ssh-fleets) can be used to create a fleet out of existing baremetals or VMs, e.g. if they are already pre-provisioned, or set up on-premises. - -> For SSH fleets, fast interconnect is supported provided that the hosts are pre-configured with the appropriate interconnect drivers. - -### Cloud fleets - -[Cloud fleets](../concepts/fleets.md#backend-fleets) allow to provision interconnected clusters across supported backends. -For cloud fleets, fast interconnect is currently supported only on the `aws`, `gcp`, `nebius`, and `runpod` backends. - -=== "AWS" - When you create a cloud fleet with AWS, [Elastic Fabric Adapter](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html) networking is automatically configured if it’s supported for the corresponding instance type. - - !!! info "Backend configuration" - Note, EFA requires the `public_ips` to be set to `false` in the `aws` backend configuration. - Refer to the [AWS](../../examples/clusters/aws/index.md) example for more details. - -=== "GCP" - When you create a cloud fleet with GCP, `dstack` automatically configures [GPUDirect-TCPXO and GPUDirect-TCPX](https://cloud.google.com/kubernetes-engine/docs/how-to/gpu-bandwidth-gpudirect-tcpx-autopilot) networking for the A3 Mega and A3 High instance types, as well as RoCE networking for the A4 instance type. - - !!! info "Backend configuration" - You may need to configure `extra_vpcs` and `roce_vpcs` in the `gcp` backend configuration. - Refer to the [GCP](../../examples/clusters/gcp/index.md) examples for more details. - -=== "Nebius" - When you create a cloud fleet with Nebius, [InfiniBand](https://docs.nebius.com/compute/clusters/gpu) networking is automatically configured if it’s supported for the corresponding instance type. - -=== "Runpod" - When you run multinode tasks in a cluster cloud fleet with Runpod, `dstack` provisions [Runpod Instant Clusters](https://docs.runpod.io/instant-clusters) with InfiniBand networking configured. - -> To request fast interconnect support for other backends, -file an [issue](https://github.com/dstackai/dstack/issues){:target="_ blank"}. - -## Distributed tasks - -A distributed task is a task with `nodes` set to a value greater than `2`. In this case, `dstack` first ensures a -suitable fleet is available, then selects the master node (to obtain its IP) and finally runs jobs on each node. - -Within the task's `commands`, it's possible to use `DSTACK_MASTER_NODE_IP`, `DSTACK_NODES_IPS`, `DSTACK_NODE_RANK`, and other -[system environment variables](../concepts/tasks.md#system-environment-variables) for inter-node communication. - -??? info "MPI" - If want to use MPI, you can set `startup_order` to `workers-first` and `stop_criteria` to `master-done`, and use `DSTACK_MPI_HOSTFILE`. - See the [NCCL/RCCL tests](../../examples/clusters/nccl-rccl-tests/index.md) examples. - -!!! info "Retry policy" - By default, if any of the nodes fails, `dstack` terminates the entire run. Configure a [retry policy](../concepts/tasks.md#retry-policy) to restart the run if any node fails. - -Refer to [distributed tasks](../concepts/tasks.md#distributed-tasks) for an example. - -## NCCL/RCCL tests - -To test the interconnect of a created fleet, ensure you run [NCCL/RCCL tests](../../examples/clusters/nccl-rccl-tests/index.md) tests using MPI. - -## Volumes - -### Instance volumes - -[Instance volumes](../concepts/volumes.md#instance-volumes) enable mounting any folder from the host into the container, allowing data persistence during distributed tasks. - -Instance volumes can be used to mount: - -* Regular folders (data persists only while the fleet exists) -* Folders that are mounts of shared filesystems (e.g., manually mounted shared filesystems). - -### Network volumes - -Currently, no backend supports multi-attach [network volumes](../concepts/volumes.md#network-volumes) for distributed tasks. However, single-attach volumes can be used by leveraging volume name [interpolation syntax](../concepts/volumes.md#distributed-tasks). This approach mounts a separate single-attach volume to each node. - -!!! info "What's next?" - 1. Read about [distributed tasks](../concepts/tasks.md#distributed-tasks), [fleets](../concepts/fleets.md), and [volumes](../concepts/volumes.md) - 2. Browse the [Clusters](../../examples.md#clusters) and [Distributed training](../../examples.md#distributed-training) examples - diff --git a/docs/docs/guides/dstack-sky.md b/docs/docs/guides/dstack-sky.md deleted file mode 100644 index e12f1ccef3..0000000000 --- a/docs/docs/guides/dstack-sky.md +++ /dev/null @@ -1,44 +0,0 @@ -# dstack Sky - -If you don't want to host the `dstack` server or would like to access GPU from the `dstack` marketplace, -sign up with [dstack Sky](../guides/dstack-sky.md). - -### Set up the CLI - -If you've signed up, open your project settings, and copy the `dstack project add` command to point the CLI to the project. - -![](https://raw.githubusercontent.com/dstackai/static-assets/main/static-assets/images/dstack-sky-project-config.png){ width=800 } - -Then, install the CLI on your machine and use the copied command. - -
- -```shell -$ pip install dstack -$ dstack project add --name peterschmidt85 \ - --url https://sky.dstack.ai \ - --token bbae0f28-d3dd-4820-bf61-8f4bb40815da - -Configuration is updated at ~/.dstack/config.yml -``` - -
- -### Configure clouds - -By default, [dstack Sky](https://sky.dstack.ai) -uses the GPU from its marketplace, which requires a credit card to be attached in your account -settings. - -To use your own cloud accounts, click the settings icon of the corresponding backend and specify credentials: - -![](https://raw.githubusercontent.com/dstackai/static-assets/main/static-assets/images/dstack-sky-edit-backend-config.png){ width=800 } - -For more details on how to configure your own cloud accounts, check -the [server/config.yml reference](../reference/server/config.yml.md). - -## What's next? - -1. Follow [quickstart](../quickstart.md) -2. Browse [examples](https://dstack.ai/examples) -3. Join the community via [Discord](https://discord.gg/u8SmfwPpMd) diff --git a/docs/docs/guides/kubernetes.md b/docs/docs/guides/kubernetes.md deleted file mode 100644 index 85dc22a80f..0000000000 --- a/docs/docs/guides/kubernetes.md +++ /dev/null @@ -1,114 +0,0 @@ -# Kubernetes - -The [kubernetes](../concepts/backends.md#kubernetes) backend enables `dstack` to run [dev environments](/docs/concepts/dev-environments), [tasks](/docs/concepts/tasks), and [services](/docs/concepts/services) directly on existing Kubernetes clusters. - -If your GPUs are already deployed on Kubernetes and your team relies on its ecosystem and tooling, use this backend to integrate `dstack` with your clusters. - -> If Kubernetes is not required, you can run `dstack` on clouds or on-prem clusters without Kubernetes by using [VM-based](../concepts/backends.md#vm-based), [container-based](../concepts/backends.md#container-based), or [on-prem](../concepts/backends.md#on-prem) backends. - -## Setting up the backend - -To use the `kubernetes` backend with `dstack`, you need to configure it with the path to the kubeconfig file, the IP address of any node in the cluster, and the port that `dstack` will use for proxying SSH traffic. -This configuration is defined in the `~/.dstack/server/config.yml` file: - -
- -```yaml -projects: -- name: main - backends: - - type: kubernetes - kubeconfig: - filename: ~/.kube/config - proxy_jump: - hostname: 204.12.171.137 - port: 32000 -``` - -
- -### Proxy jump - -To allow the `dstack` server and CLI to access runs via SSH, `dstack` requires a node that acts as a jump host to proxy SSH traffic into containers. - -To configure this node, specify `hostname` and `port` under the `proxy_jump` property: - -- `hostname` — the IP address of any cluster node selected as the jump host. Both the `dstack` server and CLI must be able to reach it. This node can be either a GPU node or a CPU-only node — it makes no difference. -- `port` — any accessible port on that node, which `dstack` uses to forward SSH traffic. - -No additional setup is required — `dstack` configures and manages the proxy automatically. - -### NVIDIA GPU Operator - -> For `dstack` to correctly detect GPUs in your Kubernetes cluster, the cluster must have the -[NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html) pre-installed. - -After the backend is set up, you interact with `dstack` just as you would with other backends or SSH fleets. You can run dev environments, tasks, and services. - -## Fleets - -### Clusters - -If you’d like to run [distributed tasks](../concepts/tasks.md#distributed-tasks) with the `kubernetes` backend, you first need to create a fleet with `placement` set to `cluster`: - -
- - ```yaml - type: fleet - # The name is optional; if not specified, one is generated automatically - name: my-k8s-fleet - - # For `kubernetes`, `min` should be set to `0` since it can't pre-provision VMs. - # Optionally, you can set the maximum number of nodes to limit scaling. - nodes: 0.. - - placement: cluster - - backends: [kubernetes] - - resources: - # Specify requirements to filter nodes - gpu: 1..8 - ``` - -
- -Then, create the fleet using the `dstack apply` command: - -
- -```shell -$ dstack apply -f examples/misc/fleets/.dstack.yml - -Provisioning... ----> 100% - - FLEET INSTANCE BACKEND GPU PRICE STATUS CREATED -``` - -
- -Once the fleet is created, you can run [distributed tasks](../concepts/tasks.md#distributed-tasks). `dstack` takes care of orchestration automatically. - -For more details on clusters, see the [corresponding guide](clusters.md). - -> Fleets with `placement` set to `cluster` can be used not only for distributed tasks, but also for dev environments, single-node tasks, and services. -> Since Kubernetes clusters are interconnected by default, you can always set `placement` to `cluster`. - -!!! info "Fleets" - It’s generally recommended to create [fleets](../concepts/fleets.md) even if you don’t plan to run distributed tasks. - -## FAQ - -??? info "Is managed Kubernetes with auto-scaling supported?" - Managed Kubernetes is supported. However, the `kubernetes` backend can only run on pre-provisioned nodes. - Support for auto-scalable Kubernetes clusters is coming soon—you can track progress in the corresponding [issue](https://github.com/dstackai/dstack/issues/3126). - - If on-demand provisioning is important, we recommend using [VM-based](../concepts/backends.md#vm-based) backends as they already support auto-scaling. - -??? info "When should I use the Kubernetes backend?" - Choose the `kubernetes` backend if your GPUs already run on Kubernetes and your team depends on its ecosystem and tooling. - - If your priority is orchestrating cloud GPUs and Kubernetes isn’t a must, [VM-based](../concepts/backends.md#vm-based) backends are a better fit thanks to their native cloud integration. - - For on-prem GPUs where Kubernetes is optional, [SSH fleets](../concepts/fleets.md#ssh-fleets) provide a simpler and more lightweight alternative. diff --git a/docs/docs/guides/upgrade.md b/docs/docs/guides/upgrade.md deleted file mode 100644 index aacf473fd8..0000000000 --- a/docs/docs/guides/upgrade.md +++ /dev/null @@ -1,49 +0,0 @@ -# Upgrade guide - - - -## 0.20.* { #0_20 } - -### CLI compatibility - -- CLI versions `0.19.*` and earlier remain backward compatible with the `0.20.*` `dstack` server. -- CLI versions `0.20.` are not compatible with server versions prior to `0.20.*`. - -> Do not upgrade the CLI to `0.20.*` until the server has been upgraded. - -### Fleets - -* Prior to `0.20`, `dstack` automatically provisioned a fleet if one did not exist at run time. - Beginning with `0.20`, `dstack` will only use existing fleets. - -> Create fleets before submitting runs. To enable on-demand instance provisioning, configure `nodes` as a range in the [backend fleet](../concepts/fleets.md#backend-fleets) configuration. - -### Working directory - -- Previously, when `working_dir` was not specified, `dstack` defaulted to `/workflow`. As of `0.20`, `dstack` uses the working directory defined in the Docker image. If the image does not define a working directory, `dstack` falls back to `/`. -- The default image introduced in `0.20` uses `/dstack/run` as its default working directory. - -> To override the directory defined in the Docker image, specify [`working_dir`](../concepts/dev-environments.md#working-directory) explicitly. - -### Repo directory - -- Previously, if no [repo directory](../concepts/dev-environments.md#repos) was specified, `dstack` cloned the repository into `/workflow`. With `0.20`, the working directory becomes the default repo directory. -- In earlier versions, cloning was skipped if the repo directory was non-empty. Starting with `0.20`, this results in a `runner error` unless `if_exists` is set to `skip` in the repo configuration. - -> Ensure repo directories are empty, or explicitly set `if_exists` to `skip`. - -### Deprecated feature removal - -The following deprecated commands have been removed in **0.20**: - -- `dstack config` -- `dstack stats` -- `dstack gateway create` - -Use the corresponding replacements: - -- `dstack project` -- `dstack metrics` -- `dstack apply` - -> For more details on the changes, see the [release notes](https://github.com/dstackai/dstack/releases). diff --git a/docs/docs/index.md b/docs/docs/index.md deleted file mode 100644 index b0228fb2c9..0000000000 --- a/docs/docs/index.md +++ /dev/null @@ -1,43 +0,0 @@ -# What is dstack? - -`dstack` is a unified control plane for GPU provisioning and orchestration that works with any GPU cloud, Kubernetes, or on-prem clusters. - -It streamlines development, training, and inference, and is compatible with any hardware, open-source tools, and frameworks. - -#### Hardware - -`dstack` supports `NVIDIA`, `AMD`, `TPU`, `Intel Gaudi`, and `Tenstorrent` accelerators out of the box. - -## How does it work? - - - -#### 1. Set up the server - -> Before using `dstack`, ensure you've [installed](installation/index.md) the server, or signed up for [dstack Sky](https://sky.dstack.ai). - -#### 2. Define configurations - -`dstack` supports the following configurations: - -* [Dev environments](concepts/dev-environments.md) — for interactive development using a desktop IDE -* [Tasks](concepts/tasks.md) — for scheduling jobs, incl. distributed ones (or running web apps) -* [Services](concepts/services.md) — for deploying models (or web apps) -* [Fleets](concepts/fleets.md) — for managing cloud and on-prem clusters -* [Volumes](concepts/volumes.md) — for managing network volumes (to persist data) -* [Gateways](concepts/gateways.md) — for publishing services with a custom domain and HTTPS - -Configuration can be defined as YAML files within your repo. - -#### 3. Apply configurations - -Apply the configuration either via the `dstack apply` CLI command (or through a programmatic API.) - -`dstack` automatically manages infrastructure provisioning and job scheduling, while also handling auto-scaling, -port-forwarding, ingress, and more. - -!!! info "Where do I start?" - 1. Proceed to [installation](installation/index.md) - 2. See [quickstart](quickstart.md) - 3. Browse [examples](/examples) - 4. Join [Discord](https://discord.gg/u8SmfwPpMd) diff --git a/docs/docs/reference/dstack.yml/fleet.md b/docs/docs/reference/dstack.yml/fleet.md deleted file mode 100644 index b6d0553853..0000000000 --- a/docs/docs/reference/dstack.yml/fleet.md +++ /dev/null @@ -1,77 +0,0 @@ -# `fleet` - -The `fleet` configuration type allows creating and updating fleets. - -## Root reference - -#SCHEMA# dstack._internal.core.models.fleets.FleetConfiguration - overrides: - show_root_heading: false - type: - required: true - -### `ssh_config` { data-toc-label="ssh_config" } - -#SCHEMA# dstack._internal.core.models.fleets.SSHParams - overrides: - show_root_heading: false - item_id_prefix: ssh_config- - -#### `ssh_config.proxy_jump` { #ssh_config-proxy_jump data-toc-label="proxy_jump" } - -#SCHEMA# dstack._internal.core.models.fleets.SSHProxyParams - overrides: - show_root_heading: false - item_id_prefix: proxy_jump- - -#### `ssh_config.hosts[n]` { #ssh_config-hosts data-toc-label="hosts" } - -#SCHEMA# dstack._internal.core.models.fleets.SSHHostParams - overrides: - show_root_heading: false - -##### `ssh_config.hosts[n].proxy_jump` { #proxy_jump data-toc-label="hosts[n].proxy_jump" } - -#SCHEMA# dstack._internal.core.models.fleets.SSHProxyParams - overrides: - show_root_heading: false - item_id_prefix: hosts-proxy_jump- - -### `resources` - -#SCHEMA# dstack._internal.core.models.resources.ResourcesSpec - overrides: - show_root_heading: false - type: - required: true - item_id_prefix: resources- - -#### `resources.cpu` { #resources-cpu data-toc-label="cpu" } - -#SCHEMA# dstack._internal.core.models.resources.CPUSpec - overrides: - show_root_heading: false - type: - required: true - -#### `resources.gpu` { #resources-gpu data-toc-label="gpu" } - -#SCHEMA# dstack._internal.core.models.resources.GPUSpec - overrides: - show_root_heading: false - type: - required: true - -#### `resources.disk` { #resources-disk data-toc-label="disk" } - -#SCHEMA# dstack._internal.core.models.resources.DiskSpec - overrides: - show_root_heading: false - type: - required: true - -### `retry` - -#SCHEMA# dstack._internal.core.models.profiles.ProfileRetry - overrides: - show_root_heading: false diff --git a/docs/docs/reference/dstack.yml/volume.md b/docs/docs/reference/dstack.yml/volume.md deleted file mode 100644 index af34a166a1..0000000000 --- a/docs/docs/reference/dstack.yml/volume.md +++ /dev/null @@ -1,11 +0,0 @@ -# `volume` - -The `volume` configuration type allows creating, registering, and updating [volumes](../../concepts/volumes.md). - -## Root reference - -#SCHEMA# dstack._internal.core.models.volumes.VolumeConfiguration - overrides: - show_root_heading: false - type: - required: true diff --git a/docs/docs/reference/environment-variables.md b/docs/docs/reference/environment-variables.md deleted file mode 100644 index 4575f1b8f8..0000000000 --- a/docs/docs/reference/environment-variables.md +++ /dev/null @@ -1,181 +0,0 @@ -# Environment variables - -## .dstack.yml - -The following read-only environment variables are automatically propagated to configurations for dev environments, -tasks, and services: - -- `DSTACK_RUN_NAME`{ #DSTACK_RUN_NAME } – The name of the run. - - The example below simply prints `vscode` to the output. - - ```yaml - type: task - name: vscode - - commands: - - echo $DSTACK_RUN_NAME - ``` - - If `name` is not set in the configuration, it is assigned a random name (e.g. `wet-mangust-1`). - -- `DSTACK_RUN_ID`{ #DSTACK_RUN_ID } – The UUID of the run. -- `DSTACK_JOB_ID`{ #DSTACK_JOB_ID } – The UUID of the job submission. -- `DSTACK_REPO_ID`{ #DSTACK_REPO_ID } – The ID of the repo. -- `DSTACK_GPUS_NUM`{ #DSTACK_GPUS_NUM } – The total number of GPUs in the run. - - Example: - - ```yaml - type: service - name: llama31 - - env: - - HF_TOKEN - commands: - - pip install vllm - - vllm serve meta-llama/Meta-Llama-3.1-8B-Instruct - --max-model-len 4096 - --tensor-parallel-size $DSTACK_GPUS_NUM - port: 8000 - model: meta-llama/Meta-Llama-3.1-8B-Instruct - - resources: - gpu: 24GB - ``` - -- `DSTACK_NODES_NUM`{ #DSTACK_NODES_NUM } – The number of nodes in the run -- `DSTACK_GPUS_PER_NODE`{ #DSTACK_GPUS_PER_NODE } – The number of GPUs per node -- `DSTACK_NODE_RANK`{ #DSTACK_NODE_RANK } – The rank of the node -- `DSTACK_MASTER_NODE_IP`{ #DSTACK_NODE_RANK } – The internal IP address of the master node. - - Below is an example of using `DSTACK_NODES_NUM`, `DSTACK_GPUS_PER_NODE`, `DSTACK_NODE_RANK`, and `DSTACK_MASTER_NODE_IP` - for distributed training: - - ```yaml - type: task - name: train-distrib - - # The size of the cluster - nodes: 2 - - python: 3.12 - env: - - NCCL_DEBUG=INFO - commands: - - git clone https://github.com/pytorch/examples.git pytorch-examples - - cd pytorch-examples/distributed/ddp-tutorial-series - - uv pip install -r requirements.txt - - | - torchrun \ - --nproc-per-node=$DSTACK_GPUS_PER_NODE \ - --node-rank=$DSTACK_NODE_RANK \ - --nnodes=$DSTACK_NODES_NUM \ - --master-addr=$DSTACK_MASTER_NODE_IP \ - --master-port=12345 \ - multinode.py 50 10 - - resources: - gpu: 24GB:1..2 - # Uncomment if using multiple GPUs - #shm_size: 24GB - ``` - -- `DSTACK_NODES_IPS`{ #DSTACK_NODES_IPS } – The list of internal IP addresses of all nodes delimited by `"\n"`. -- `DSTACK_MPI_HOSTFILE`{ #DSTACK_MPI_HOSTFILE } – The path to a pre-populated MPI hostfile that can be used directly as `mpirun --hostfile $DSTACK_MPI_HOSTFILE`. - -## Server - -The following environment variables are supported by the `dstack` server and can be specified whether the server is run -via `dstack server` or deployed using Docker. - -For more details on the options below, refer to the [server deployment](../guides/server-deployment.md) guide. - -- `DSTACK_SERVER_LOG_LEVEL`{ #DSTACK_SERVER_LOG_LEVEL } – Has the same effect as `--log-level`. Defaults to `INFO`. - - Example: - -
- - ```shell - $ DSTACK_SERVER_LOG_LEVEL=debug dstack server - ``` - -
- -- `DSTACK_SERVER_LOG_FORMAT`{ #DSTACK_SERVER_LOG_FORMAT } – Sets format of log output. Can be `rich`, `standard`, `json`. Defaults to `rich`. -- `DSTACK_SERVER_HOST`{ #DSTACK_SERVER_HOST } – Has the same effect as `--host`. Defaults to `127.0.0.1`. -- `DSTACK_SERVER_PORT`{ #DSTACK_SERVER_PORT } – Has the same effect as `--port`. Defaults to `3000`. -- `DSTACK_SERVER_URL`{ #DSTACK_SERVER_URL } – The URL that the server is running on, e.g. `https://my-server.dstack.ai` Defaults to `http://{DSTACK_SERVER_HOST}:{DSTACK_SERVER_PORT}`. -- `DSTACK_SERVER_ADMIN_TOKEN`{ #DSTACK_SERVER_ADMIN_TOKEN } – Has the same effect as `--token`. Defaults to `None`. -- `DSTACK_SERVER_DIR`{ #DSTACK_SERVER_DIR } – Sets path to store data and server configs. Defaults to `~/.dstack/server`. -- `DSTACK_DATABASE_URL`{ #DSTACK_DATABASE_URL } – The database URL to use instead of default SQLite. Currently `dstack` supports Postgres. Example: `postgresql+asyncpg://myuser:mypassword@localhost:5432/mydatabase`. Defaults to `None`. -- `DSTACK_SERVER_CLOUDWATCH_LOG_GROUP`{ #DSTACK_SERVER_CLOUDWATCH_LOG_GROUP } – The CloudWatch Logs group for storing workloads logs. If not set, the default file-based log storage is used. -- `DSTACK_SERVER_CLOUDWATCH_LOG_REGION`{ #DSTACK_SERVER_CLOUDWATCH_LOG_REGION } – The CloudWatch Logs region. Defaults to `None`. -- `DSTACK_SERVER_GCP_LOGGING_PROJECT`{ #DSTACK_SERVER_GCP_LOGGING_PROJECT } – The GCP Logging project for storing workloads logs. If not set, the default file-based log storage is used. -- `DSTACK_ENABLE_PROMETHEUS_METRICS`{ #DSTACK_ENABLE_PROMETHEUS_METRICS } — Enables Prometheus metrics collection and export. -- `DSTACK_DEFAULT_SERVICE_CLIENT_MAX_BODY_SIZE`{ #DSTACK_DEFAULT_SERVICE_CLIENT_MAX_BODY_SIZE } – Request body size limit for services running with a gateway, in bytes. Defaults to 64 MiB. -- `DSTACK_SERVICE_CLIENT_TIMEOUT`{ #DSTACK_SERVICE_CLIENT_TIMEOUT } – Timeout in seconds for HTTP requests sent from the in-server proxy and gateways to service replicas. Defaults to 60. -- `DSTACK_FORBID_SERVICES_WITHOUT_GATEWAY`{ #DSTACK_FORBID_SERVICES_WITHOUT_GATEWAY } – Forbids registering new services without a gateway if set to any value. -- `DSTACK_SERVER_CODE_UPLOAD_LIMIT`{ #DSTACK_SERVER_CODE_UPLOAD_LIMIT } - The repo size limit when uploading diffs or local repos, in bytes. Set to `0` to disable size limits. Defaults to `2MiB`. -- `DSTACK_SERVER_S3_BUCKET`{ #DSTACK_SERVER_S3_BUCKET } - The bucket that repo diffs will be uploaded to if set. If unset, diffs are uploaded to the database. -- `DSTACK_SERVER_S3_BUCKET_REGION`{ #DSTACK_SERVER_S3_BUCKET_REGION } - The region of the S3 Bucket. -- `DSTACK_SERVER_GCS_BUCKET`{ #DSTACK_SERVER_GCS_BUCKET } - The bucket that repo diffs will be uploaded to if set. If unset, diffs are uploaded to the database. -- `DSTACK_DB_POOL_SIZE`{ #DSTACK_DB_POOL_SIZE } - The client DB connections pool size. Defaults to `20`, -- `DSTACK_DB_MAX_OVERFLOW`{ #DSTACK_DB_MAX_OVERFLOW } - The client DB connections pool allowed overflow. Defaults to `20`. -- `DSTACK_SERVER_BACKGROUND_PROCESSING_FACTOR`{ #DSTACK_SERVER_BACKGROUND_PROCESSING_FACTOR } - The number of background jobs for processing server resources. Increase if you need to process more resources per server replica quickly. Defaults to `1`. -- `DSTACK_SERVER_BACKGROUND_PROCESSING_DISABLED`{ #DSTACK_SERVER_BACKGROUND_PROCESSING_DISABLED } - Disables background processing if set to any value. Useful to run only web frontend and API server. -- `DSTACK_SERVER_MAX_PROBES_PER_JOB`{ #DSTACK_SERVER_MAX_PROBES_PER_JOB } - Maximum number of probes allowed in a run configuration. Validated at apply time. -- `DSTACK_SERVER_MAX_PROBE_TIMEOUT`{ #DSTACK_SERVER_MAX_PROBE_TIMEOUT } - Maximum allowed timeout for a probe. Validated at apply time. -- `DSTACK_SERVER_METRICS_RUNNING_TTL_SECONDS`{ #DSTACK_SERVER_METRICS_RUNNING_TTL_SECONDS } – Maximum age of metrics samples for running jobs. -- `DSTACK_SERVER_METRICS_FINISHED_TTL_SECONDS`{ #DSTACK_SERVER_METRICS_FINISHED_TTL_SECONDS } – Maximum age of metrics samples for finished jobs. -- `DSTACK_SERVER_INSTANCE_HEALTH_TTL_SECONDS`{ #DSTACK_SERVER_INSTANCE_HEALTH_TTL_SECONDS } – Maximum age of instance health checks. -- `DSTACK_SERVER_INSTANCE_HEALTH_MIN_COLLECT_INTERVAL_SECONDS`{ #DSTACK_SERVER_INSTANCE_HEALTH_MIN_COLLECT_INTERVAL_SECONDS } – Minimum time interval between consecutive health checks of the same instance. -- `DSTACK_SERVER_EVENTS_TTL_SECONDS`{ #DSTACK_SERVER_EVENTS_TTL_SECONDS } - Maximum age of event records. Set to `0` to disable event storage. Defaults to 30 days. - -??? info "Internal environment variables" - The following environment variables are intended for development purposes: - - * `DSTACK_SERVER_ROOT_LOG_LEVEL` – Sets root logger log level. Defaults to `ERROR`. - * `DSTACK_SERVER_UVICORN_LOG_LEVEL` – Sets uvicorn logger log level. Defaults to `ERROR`. - * `DSTACK_SERVER_MAX_OFFERS_TRIED` - Sets how many instance offers to try when starting a job. - Setting a high value can degrade server performance. - * `DSTACK_RUNNER_VERSION` – Sets exact runner version for debug. Defaults to `latest`. Ignored if `DSTACK_RUNNER_DOWNLOAD_URL` is set. - * `DSTACK_RUNNER_DOWNLOAD_URL` – Overrides `dstack-runner` binary download URL. The URL can contain `{version}` and/or `{arch}` placeholders, - where `{version}` is `dstack` version in the `X.Y.Z` format or `latest`, and `{arch}` is either `amd64` or `arm64`, for example, - `https://dstack.example.com/{arch}/{version}/dstack-runner`. - * `DSTACK_SHIM_DOWNLOAD_URL` – Overrides `dstack-shim` binary download URL. The URL can contain `{version}` and/or `{arch}` placeholders, - see `DSTACK_RUNNER_DOWNLOAD_URL` for the details. - * `DSTACK_DEFAULT_CREDS_DISABLED` – Disables default credentials detection if set. Defaults to `None`. - * `DSTACK_LOCAL_BACKEND_ENABLED` – Enables local backend for debug if set. Defaults to `None`. - -## CLI - -The following environment variables are supported by the CLI. - -- `DSTACK_CLI_LOG_LEVEL`{ #DSTACK_CLI_LOG_LEVEL } – Sets the logging level for CLI output to stdout. Defaults to `INFO`. - -Example: - -
- -```shell -$ DSTACK_CLI_LOG_LEVEL=debug dstack apply -f .dstack.yml -``` - -
- -- `DSTACK_CLI_FILE_LOG_LEVEL`{ #DSTACK_CLI_FILE_LOG_LEVEL } – Sets the logging level for CLI log files. Defaults to `DEBUG`. - -
- -```shell -$ find ~/.dstack/logs/cli/ - - ~/.dstack/logs/cli/latest.log - ~/.dstack/logs/cli/2025-07-31.log -``` - -
- -- `DSTACK_PROJECT`{ #DSTACK_PROJECT } – Has the same effect as `--project`. Defaults to `None`. diff --git a/docs/examples/single-node-training/axolotl/index.md b/docs/examples/single-node-training/axolotl/index.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/examples/single-node-training/trl/index.md b/docs/examples/single-node-training/trl/index.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 53b5aaa461..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -template: home.html -title: AI container orchestration for AI teams -hide: - - navigation - - toc - - footer ---- diff --git a/docs/overrides/assets/images/quotes/cudopete.png b/docs/overrides/assets/images/quotes/cudopete.png deleted file mode 100644 index dd447d0f3d..0000000000 Binary files a/docs/overrides/assets/images/quotes/cudopete.png and /dev/null differ diff --git a/docs/overrides/assets/images/quotes/spott.jpg b/docs/overrides/assets/images/quotes/spott.jpg deleted file mode 100644 index cc9e0ae625..0000000000 Binary files a/docs/overrides/assets/images/quotes/spott.jpg and /dev/null differ diff --git a/docs/overrides/header-2.html b/docs/overrides/header-2.html deleted file mode 100644 index dbcb7ad9ae..0000000000 --- a/docs/overrides/header-2.html +++ /dev/null @@ -1,73 +0,0 @@ -{#- - This file was automatically generated - do not edit --#} -{% set class = "md-header" %} -{% if "navigation.tabs.sticky" in features %} - {% set class = class ~ " md-header--shadow md-header--lifted" %} -{% elif "navigation.tabs" not in features %} - {% set class = class ~ " md-header--shadow" %} -{% endif %} -
- - {% if "navigation.tabs.sticky" in features %} - {% if "navigation.tabs" in features %} - {% include "partials/tabs.html" %} - {% endif %} - {% endif %} -
diff --git a/docs/overrides/home.html b/docs/overrides/home.html deleted file mode 100644 index ced53fb1e8..0000000000 --- a/docs/overrides/home.html +++ /dev/null @@ -1,664 +0,0 @@ -{% extends "landing.html" %} - -{% block scripts %} -{{ super() }} - - - - -{% endblock %} - -{% block content %} -
-
-
-
-

The orchestration layer for modern ML teams

- -

- dstack provides ML teams with a unified control plane for GPU provisioning and orchestration - across cloud, Kubernetes, and on-prem. It streamlines development, training, and inference — reducing costs 3–7x and - preventing lock-in. -

-
- - -
- One control plane for GPUs across cloud, Kubernetes, and on-prem. -
-
- -
-
-
-

An open platform for GPU orchestration

-

- Managing AI infrastructure requires efficient GPU orchestration, whether workloads run - on a single GPU cloud, across multiple GPU providers, or on-prem clusters. -

- -

- dstack provides an open stack for GPU orchestration that streamlines development, training, - and inference, and can be used with any hardware, open-source tools, and frameworks. -

- - - -
- -
- -
-
-
- -
-
-
- -
- -
-

Native integration with GPU clouds

-

- dstack natively integrates with top GPU clouds—automating cluster provisioning and - workload orchestration to maximize efficiency and minimize overhead. -

- -

- It can provision and manage VM clusters through native integrations or via Kubernetes. -

- -

- - Backends - - - - - - - Kubernetes - - - - -

-
-
-
- -
-
-
-

Easy to use with on-prem clusters

- -

- For provisioned Kubernetes clusters, connect them to dstack using the Kubernetes backend. - If you run vanilla bare-metal servers or VMs without Kubernetes, use SSH fleets - instead. -

- -

- - Kubernetes - - - - - - - SSH fleets - - - - -

- -

- Either way, connecting existing on-prem clusters to dstack takes just minutes. -

-
- -
- -
-
-
- - -
-
-
- -
- -
-

Dev environments

-

- Before training or deployment, ML engineers explore and debug their code. -

- -

dstack's dev environments make it easy to connect your - desktop IDE to powerful cloud or on-prem GPUs—streamlining the entire development loop. -

- -

- - Dev environments - - -

-
-
-
- -
-
-
-

Single-node & distributed tasks

- -

- Move from single-instance experiments to multi-node distributed training without friction. dstack lets you define complex jobs with a simple configuration, - handling the scheduling and orchestration for you. -

- -

This allows your team to focus on research while ensuring that expensive cluster resources are utilized efficiently.

- -

- - Tasks - - - - - Clusters - - -

-
- -
- -
-
-
- -
-
-
- -
- -
-

Scalable service endpoints

- -

- With dstack, you can easily deploy any model as a secure, - auto-scaling OpenAI-compatible endpoint, all while using your custom code, Docker image, and - serving framework. -

- -

- - Services - - -

-
-
-
- -
-
-

Loved by world-class ML teams

- -
-
-
- -
-

Wah Loon Keng

- -

Sr. AI Engineer @Electronic Arts

- -

- With dstack, AI researchers at EA can spin up and scale experiments without touching - infrastructure. It supports everything from quick prototyping to multi-node training on any cloud. -

-
- -
-
- -
-

Aleksandr Movchan

- -

ML Engineer @Mobius Labs

- -

- Thanks to dstack, my team can quickly tap into affordable - GPUs and streamline our workflows - from testing and development to full-scale application deployment. -

-
- -
-
- -
-

Alvaro Bartolome

- -

ML Engineer @Argilla

- -

- With dstack it's incredibly easy to define a configuration - within a - repository - and run it without worrying about GPU availability. It lets you focus on - data and your research. -

-
- -
-
- -
-

Park Chansung

- -

ML Researcher @ETRI

- -

- Thanks to dstack, I can effortlessly access the top GPU - options across - different clouds, - saving me time and money while pushing my AI work forward. -

-
- -
-
- -
-

Eckart Burgwedel

- -

CEO @Uberchord

- -

- With dstack, running LLMs on a cloud GPU is as - easy as running a local Docker container. - It combines the ease of Docker with the auto-scaling capabilities of K8S. -

-
- -
-
- -
-

Peter Hill

- -

Co-Founder @CUDO Compute

- -

- dstack simplifies infrastructure provisioning and AI - development. - If your team is on the lookout for an AI platform, I - wholeheartedly recommend dstack. -

-
-
-
-
- -
-
-

FAQ

-
- -
-
-
- How does dstack differ from Slurm? -
-
-
- -
-

- Slurm is a battle-tested system with decades of production use in HPC environments. - dstack by contrast, is built for modern ML/AI workloads with cloud-native provisioning and a container-first architecture. - While both support distributed training and batch jobs, dstack - also natively supports development and production-grade inference. -

- -

- See the migration guide for a detailed comparison. -

-
-
- -
-
- How does dstack compare to Kubernetes? -
-
-
- -
-

- Kubernetes is a general-purpose container orchestrator. dstack also - orchestrates containers, but it provides a lightweight and streamlined interface that is purpose - built for ML. -

- -

- You declare - dev environments, - tasks, - services, and - fleets - with simple configuration. dstack provisions GPUs, manages clusters via fleets with fine-grained - controls, and optimizes cost and utilization, while keeping a simple UI and CLI. -

- -

- If you already use Kubernetes, you can run dstack on it via the Kubernetes backend. -

-
-
- -
-
- Can I use dstack with Kubernetes? -
-
-
- -
-

- Yes. You can connect existing Kubernetes clusters using the Kubernetes backend and run - dev environments, - tasks, and - services on it. - Choose the Kubernetes backend if your GPUs already run on Kubernetes and your team depends on its - ecosystem and tooling. - See the - Kubernetes guide for setup and best practices. -

-

- If your priority is orchestrating cloud GPUs and Kubernetes isn’t a must, VM-based backends are a better fit - thanks to their native cloud integration. - For on-prem GPUs where Kubernetes is optional, SSH fleets provide a simpler and more lightweight alternative. -

-
-
- -
-
- When should I use dstack? -
-
-
- -
-

- dstack accelerates ML development with a simple, ML‑native interface. - Spin up dev environments, run - single‑node or distributed tasks, and deploy services without infrastructure overhead. -

- -

- It radically reduces GPU costs via smart orchestration and fine‑grained fleet controls, including efficient reuse, - right‑sizing, and support for spot, on‑demand, and reserved capacity. -

- -

- It is 100% interoperable with your stack and works with any open‑source frameworks and tools, as - well as your own Docker images and code, across cloud, Kubernetes, and on‑prem GPUs. -

-
-
-
-
- -
-

- Have questions, or need help? -
- - Discord - - - Talk to us - -

-
- - - -


- -
-

Get started in minutes

-
- -
-
-
-

Install dstack on your laptop via uv - and start it using the CLI, or deploy it anywhere with the dstackai/dstack Docker image.

- -

Set up backends or SSH fleets, then add your team.

- -

- - Quickstart - - - - -

-
- -
-
- -
-
-
-
- -
-
-

dstack Sky

- -
-
-

Don't want to host the dstack server or want to get the - cheapest GPUs - from the marketplace?

-
-
- - -
- Get $5 free credit. Already have an account? Sign in -
-
-
-
- -
-

dstack Enterprise

- -
-
-

Looking for a self-hosted dstack with SSO, governance - controls, and enterprise support?

-
-
- - -
- Talk to our experts and learn. -
-
-
-
-
-
-{% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index 6b2f47dbc0..0000000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,191 +0,0 @@ -{% extends "base.html" %} - -{% block container %} -
- {% if "navigation.path" in features %} - {% include "path.html" %} - {% endif %} -
- {% block content %} - {% include "partials/content.html" %} - - {% if page.previous_page or page.next_page %} - {% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "footer" in page.meta.hide %} - {% endif %} - - {% endif %} - {% endblock %} -
-
-{% endblock %} - -{% block header %} - {% include "header-2.html" %} -{% endblock %} - -{% block scripts %} - - -{{ super() }} - -{% endblock %} - -{% block announce %} -Support dstack with a ⭐ on -GitHub -{% endblock %} - -{% block footer %} - -{% endblock %} - -{% block site_nav %} - {% if nav %} - {% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "navigation" in page.meta.hide %} - {% endif %} - - {% endif %} - {% if "toc.integrate" not in features %} - {% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "toc" in page.meta.hide %} - {% endif %} - - {% endif %} -{% endblock %} diff --git a/docs/overrides/toc.html b/docs/overrides/toc.html deleted file mode 100644 index 577f4988ac..0000000000 --- a/docs/overrides/toc.html +++ /dev/null @@ -1,25 +0,0 @@ -{#- - This file was automatically generated - do not edit --#} -{% set title = lang.t("toc") %} -{% if config.mdx_configs.toc and config.mdx_configs.toc.title %} - {% set title = config.mdx_configs.toc.title %} -{% endif %} - diff --git a/docs/partners.md b/docs/partners.md deleted file mode 100644 index a863fd6a8e..0000000000 --- a/docs/partners.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -#template: backends.html -hide: - - navigation -# - toc - - footer ---- - - - -> Here are the cloud GPU providers that `dstack` integrates with and collaborates with. - -## NVIDIA - - - -## AMD - - - -[//]: # (## Google TPU) - - - -## Join the ecosystem - -Are you interested in integrating and collaborating with us to become part of the ecosystem? - - - Contribute -   - Talk to an expert - diff --git a/docs/privacy.md b/docs/privacy.md deleted file mode 100644 index 46a2da0efd..0000000000 --- a/docs/privacy.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -template: privacy.html -title: Privacy policy -hide: - - navigation - - toc - - footer ---- diff --git a/docs/terms.md b/docs/terms.md deleted file mode 100644 index 630079cd81..0000000000 --- a/docs/terms.md +++ /dev/null @@ -1,461 +0,0 @@ ---- -hide: - - navigation - - footer ---- - -# Terms of service - -## Agreement to terms - -We are dstack GmbH ("**Company**," "**we**," "**us**," "**our**"), a company registered in Germany at Franz-Joseph-Straße, 11, Munich, -Bayern 80801. - -These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf of an -entity ("**you**"), and dstack GmbH, concerning your access to and use of the Services. You agree that by accessing the -Services, you have read, understood, and agreed to be bound by all of these Legal Terms. IF YOU DO NOT AGREE WITH ALL OF -THESE LEGAL TERMS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE SERVICES AND YOU MUST DISCONTINUE USE IMMEDIATELY. - -Supplemental terms and conditions or documents that may be posted on the Services from time to time are hereby expressly -incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or modifications to -these Legal Terms from time to time. We will alert you about any changes by updating the "Last updated" date of these -Legal Terms, and you waive any right to receive specific notice of each such change. It is your responsibility to -periodically review these Legal Terms to stay informed of updates. You will be subject to, and will be deemed to have -been made aware of and to have accepted, the changes in any revised Legal Terms by your continued use of the Services -after the date such revised Legal Terms are posted. - -## 1. Our services - -The information provided when using the Services is not intended for distribution to or use by any person or entity in -any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject -us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to access -the Services from other locations do so on their own initiative and are solely responsible for compliance with local -laws, if and to the extent local laws are applicable. - -The Services are not tailored to comply with industry-specific regulations (Health Insurance Portability and -Accountability Act (HIPAA), Federal Information Security Management Act (FISMA), etc.), so if your interactions would be -subjected to such laws, you may not use the Services. You may not use the Services in a way that would violate the -Gramm-Leach-Bliley Act (GLBA). - -## 2. Intelliectual property rights - -**Our intellectual property** - -We are the owner or the licensee of all intellectual property rights in our Services, including all source code, -databases, functionality, software, website designs, audio, video, text, photographs, and graphics in the Services ( -collectively, the "Content"), as well as the trademarks, service marks, and logos contained therein (the "Marks"). - -Our Content and Marks are protected by copyright and trademark laws (and various other intellectual property rights and -unfair competition laws) and treaties in the United States and around the world. - -The Content and Marks are provided in or through the Services "AS IS" for your personal, non-commercial use or internal -business purpose only. - -**Your use of our Services** - -Subject to your compliance with these Legal Terms, including the "Prohibited activities" section below, we grant you a -non-exclusive, non-transferable, revocable license to: - - * access the Services; and - * download or print a copy of any portion of the Content to which you have properly gained access. -solely for your personal, non-commercial use or internal business purpose. - -Except as set out in this section or elsewhere in our Legal Terms, no part of the Services and no Content or Marks may -be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted, -distributed, sold, licensed, or otherwise exploited for any commercial purpose whatsoever, without our express prior -written permission. - -If you wish to make any use of the Services, Content, or Marks other than as set out in this section or elsewhere in our -Legal Terms, please address your request to: hello@dstack.ai. If we ever grant you the permission to post, reproduce, or -publicly display any part of our Services or Content, you must identify us as the owners or licensors of the Services, -Content, or Marks and ensure that any copyright or proprietary notice appears or is visible on posting, reproducing, or -displaying our Content. - -We reserve all rights not expressly granted to you in and to the Services, Content, and Marks. - -Any breach of these Intellectual Property Rights will constitute a material breach of our Legal Terms and your right to -use our Services will terminate immediately. - -**Your submissions** - -Please review this section and the "Prohibited activities" section carefully prior to using our Services to understand -the (a) rights you give us and (b) obligations you have when you post or upload any content through the Services. - -**Submissions**: By directly sending us any question, comment, suggestion, idea, feedback, or other information about the -Services ("Submissions"), you agree to assign to us all intellectual property rights in such Submission. You agree that -we shall own this Submission and be entitled to its unrestricted use and dissemination for any lawful purpose, -commercial or otherwise, without acknowledgment or compensation to you. - -**You are responsible for what you post or upload**: By sending us Submissions through any part of the Services you: -* confirm that you have read and agree with our "Prohibited activities" and will not post, send, publish, upload, or -* transmit through the Services any Submission that is illegal, harassing, hateful, harmful, defamatory, obscene, -* bullying, abusive, discriminatory, threatening to any person or group, sexually explicit, false, inaccurate, deceitful, - or misleading; -* to the extent permissible by applicable law, waive any and all moral rights to any such Submission; -* warrant that any such Submission are original to you or that you have the necessary rights and licenses to submit such - Submissions and that you have full authority to grant us the above-mentioned rights in relation to your Submissions; and -* warrant and represent that your Submissions do not constitute confidential information. - -You are solely responsible for your Submissions and you expressly agree to reimburse us for any and all losses that we -may suffer because of your breach of (a) this section, (b) any third party’s intellectual property rights, or (c) -applicable law. - -## 3. User representations - -By using the Services, you represent and warrant that: (1) all registration information you submit will be true, -accurate, current, and complete; (2) you will maintain the accuracy of such information and promptly update such -registration information as necessary; (3) you have the legal capacity and you agree to comply with these Legal Terms; ( -4) you are not a minor in the jurisdiction in which you reside; (5) you will not access the Services through automated -or non-human means, whether through a bot, script or otherwise; (6) you will not use the Services for any illegal or -unauthorized purpose; and (7) your use of the Services will not violate any applicable law or regulation. - -If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to suspend or -terminate your account and refuse any and all current or future use of the Services (or any portion thereof). - -## 4. User registration - -You may be required to register to use the Services. You agree to keep your password confidential and will be -responsible for all use of your account and password. We reserve the right to remove, reclaim, or change a username you -select if we determine, in our sole discretion, that such username is inappropriate, obscene, or otherwise -objectionable. - -## 5. Purchases and payment - -We accept the following forms of payment: - -* Visa -* Mastercard - -You agree to provide current, complete, and accurate purchase and account information for all purchases made via the -Services. You further agree to promptly update account and payment information, including email address, payment method, -and payment card expiration date, so that we can complete your transactions and contact you as needed. Sales tax will be -added to the price of purchases as deemed required by us. We may change prices at any time. All payments shall be in US -dollars. - -You agree to pay all charges at the prices then in effect for your purchases and any applicable shipping fees, and you -authorize us to charge your chosen payment provider for any such amounts upon placing your order. We reserve the right -to correct any errors or mistakes in pricing, even if we have already requested or received payment. - -We reserve the right to refuse any order placed through the Services. We may, in our sole discretion, limit or cancel -quantities purchased per person, per household, or per order. These restrictions may include orders placed by or under -the same customer account, the same payment method, and/or orders that use the same billing or shipping address. We -reserve the right to limit or prohibit orders that, in our sole judgment, appear to be placed by dealers, resellers, or -distributors. - -## 6. Subscriptions - -**Billing and Renewal** - -e.g. by topping up their balance manually using their credit card. - -**Cancellation** - -You can cancel your subscription at any time by contacting us using the contact information provided below. Your -cancellation will take effect at the end of the current paid term. If you have any questions or are unsatisfied with our -Services, please email us at hello@dstack.ai . - -**Fee Changes** - -We may, from time to time, make changes to the subscription fee and will communicate any price changes to you in -accordance with applicable law. - -## 7. Software - -We may include software for use in connection with our Services. If such software is accompanied by an end user license -agreement ("EULA"), the terms of the EULA will govern your use of the software. If such software is not accompanied by a -EULA, then we grant to you a non-exclusive, revocable, personal, and non-transferable license to use such software -solely in connection with our services and in accordance with these Legal Terms. Any software and any related -documentation is provided "AS IS" without warranty of any kind, either express or implied, including, without -limitation, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. You accept -any and all risk arising out of use or performance of any software. You may not reproduce or redistribute any software -except in accordance with the EULA or these Legal Terms. - -## 8. Prohibited activities - -You may not access or use the Services for any purpose other than that for which we make the Services available. The -Services may not be used in connection with any commercial endeavors except those that are specifically endorsed or -approved by us. - -As a user of the Services, you agree not to: - -* Systematically retrieve data or other content from the Services to create or compile, directly or indirectly, a - collection, compilation, database, or directory without written permission from us. -* Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such - as user passwords. -* Circumvent, disable, or otherwise interfere with security-related features of the Services, including features that - prevent or restrict the use or copying of any Content or enforce limitations on the use of the Services and/or the - Content contained therein. -* Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Services. -* Use any information obtained from the Services in order to harass, abuse, or harm another person. -* Make improper use of our support services or submit false reports of abuse or misconduct. -* Use the Services in a manner inconsistent with any applicable laws or regulations. -* Engage in unauthorized framing of or linking to the Services. -* Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including - excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any - party’s uninterrupted use and enjoyment of the Services or modifies, impairs, disrupts, alters, or interferes with the - use, features, functions, operation, or maintenance of the Services. -* Engage in any automated use of the system, such as using scripts to send comments or messages, or using any data - mining, robots, or similar data gathering and extraction tools. -* Delete the copyright or other proprietary rights notice from any Content. -* Attempt to impersonate another user or person or use the username of another user. -* Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information - collection or transmission mechanism, including without limitation, clear graphics interchange formats ("gifs"), 1×1 - pixels, web bugs, cookies, or other similar devices (sometimes referred to as "spyware" or "passive collection - mechanisms" or "pcms"). -* Interfere with, disrupt, or create an undue burden on the Services or the networks or services connected to the - Services. -* Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion of the Services - to you. -* Attempt to bypass any measures of the Services designed to prevent or restrict access to the Services, or any portion - of the Services. -* Copy or adapt the Services' software, including but not limited to Flash, PHP, HTML, JavaScript, or other code. -* Except as permitted by applicable law, decipher, decompile, disassemble, or reverse engineer any of the software - comprising or in any way making up a part of the Services. -* Except as may be the result of standard search engine or Internet browser usage, use, launch, develop, or distribute - any automated system, including without limitation, any spider, robot, cheat utility, scraper, or offline reader that - accesses the Services, or use or launch any unauthorized script or other software. -* Use a buying agent or purchasing agent to make purchases on the Services. -* Make any unauthorized use of the Services, including collecting usernames and/or email addresses of users by - electronic or other means for the purpose of sending unsolicited email, or creating user accounts by automated means - or under false pretenses. -* Use the Services as part of any effort to compete with us or otherwise use the Services and/or the Content for any - revenue-generating endeavor or commercial enterprise. - -## 9. User generated contributions - -The Services does not offer users to submit or post content. - -## 10. Contribution license - -You and Services agree that we may access, store, process, and use any information and personal data that you provide -following the terms of the Privacy Policy and your choices (including settings). - -By submitting suggestions or other feedback regarding the Services, you agree that we can use and share such feedback -for any purpose without compensation to you. - -## 11. Social media - -As part of the functionality of the Services, you may link your account with online accounts you have with third-party -service providers (each such account, a "Third-Party Account") by either: (1) providing your Third-Party Account login -information through the Services; or (2) allowing us to access your Third-Party Account, as is permitted under the -applicable terms and conditions that govern your use of each Third-Party Account. You represent and warrant that you are -entitled to disclose your Third-Party Account login information to us and/or grant us access to your Third-Party -Account, without breach by you of any of the terms and conditions that govern your use of the applicable Third-Party -Account, and without obligating us to pay any fees or making us subject to any usage limitations imposed by the -third-party service provider of the Third-Party Account. By granting us access to any Third-Party Accounts, you -understand that (1) we may access, make available, and store (if applicable) any content that you have provided to and -stored in your Third-Party Account (the "Social Network Content") so that it is available on and through the Services -via your account, including without limitation any friend lists and (2) we may submit to and receive from your -Third-Party Account additional information to the extent you are notified when you link your account with the -Third-Party Account. Depending on the Third-Party Accounts you choose and subject to the privacy settings that you have -set in such Third-Party Accounts, personally identifiable information that you post to your Third-Party Accounts may be -available on and through your account on the Services. Please note that if a Third-Party Account or associated service -becomes unavailable or our access to such Third-Party Account is terminated by the third-party service provider, then -Social Network Content may no longer be available on and through the Services. You will have the ability to disable the -connection between your account on the Services and your Third-Party Accounts at any time. PLEASE NOTE THAT YOUR -RELATIONSHIP WITH THE THIRD-PARTY SERVICE PROVIDERS ASSOCIATED WITH YOUR THIRD-PARTY ACCOUNTS IS GOVERNED SOLELY BY YOUR -AGREEMENT(S) WITH SUCH THIRD-PARTY SERVICE PROVIDERS. We make no effort to review any Social Network Content for any -purpose, including but not limited to, for accuracy, legality, or non-infringement, and we are not responsible for any -Social Network Content. You acknowledge and agree that we may access your email address book associated with a -Third-Party Account and your contacts list stored on your mobile device or tablet computer solely for purposes of -identifying and informing you of those contacts who have also registered to use the Services. You can deactivate the -connection between the Services and your Third-Party Account by contacting us using the contact information below or -through your account settings (if applicable). We will attempt to delete any information stored on our servers that was -obtained through such Third-Party Account, except the username and profile picture that become associated with your -account. - -## 12. Third-party websites and content - -The Services may contain (or you may be sent via the Site) links to other websites ("Third-Party Websites") as well as -articles, photographs, text, graphics, pictures, designs, music, sound, video, information, applications, software, and -other content or items belonging to or originating from third parties ("Third-Party Content"). Such Third-Party Websites -and Third-Party Content are not investigated, monitored, or checked for accuracy, appropriateness, or completeness by -us, and we are not responsible for any Third-Party Websites accessed through the Services or any Third-Party Content -posted on, available through, or installed from the Services, including the content, accuracy, offensiveness, opinions, -reliability, privacy practices, or other policies of or contained in the Third-Party Websites or the Third-Party -Content. Inclusion of, linking to, or permitting the use or installation of any Third-Party Websites or any Third-Party -Content does not imply approval or endorsement thereof by us. If you decide to leave the Services and access the -Third-Party Websites or to use or install any Third-Party Content, you do so at your own risk, and you should be aware -these Legal Terms no longer govern. You should review the applicable terms and policies, including privacy and data -gathering practices, of any website to which you navigate from the Services or relating to any applications you use or -install from the Services. Any purchases you make through Third-Party Websites will be through other websites and from -other companies, and we take no responsibility whatsoever in relation to such purchases which are exclusively between -you and the applicable third party. You agree and acknowledge that we do not endorse the products or services offered on -Third-Party Websites and you shall hold us blameless from any harm caused by your purchase of such products or services. -Additionally, you shall hold us blameless from any losses sustained by you or harm caused to you relating to or -resulting in any way from any Third-Party Content or any contact with Third-Party Websites. - -## 13. Services management - -We reserve the right, but not the obligation, to: (1) monitor the Services for violations of these Legal Terms; (2) take -appropriate legal action against anyone who, in our sole discretion, violates the law or these Legal Terms, including -without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without -limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) -any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or -liability, to remove from the Services or otherwise disable all files and content that are excessive in size or are in -any way burdensome to our systems; and (5) otherwise manage the Services in a manner designed to protect our rights and -property and to facilitate the proper functioning of the Services. - -## 14. Privacy policy - -We care about data privacy and security. Please review our [Privacy Policy](https://dstack.ai/privacy/). By using the -Services, you agree to be bound by our Privacy Policy, which is incorporated into these Legal Terms. Please be advised -the Services are hosted in Germany and United States. If you access the Services from any other region of the world with -laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in -Germany and United States, then through your continued use of the Services, you are transferring your data to Germany -and United States, and you expressly consent to have your data transferred to and processed in Germany and United -States. - -## 15. Term and termination - -These Legal Terms shall remain in full force and effect while you use the Services. WITHOUT LIMITING ANY OTHER PROVISION OF THESE LEGAL TERMS, WE RESERVE THE RIGHT TO, IN OUR SOLE DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SERVICES (INCLUDING BLOCKING CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE LEGAL TERMS OR OF ANY APPLICABLE LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SERVICES OR DELETE YOUR ACCOUNT AND ANY CONTENT OR INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION. - -If we terminate or suspend your account for any reason, you are prohibited from registering and creating a new account under your name, a fake or borrowed name, or the name of any third party, even if you may be acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the right to take appropriate legal action, including without limitation pursuing civil, criminal, and injunctive redress. - -## 16. Modifications and interruptions - -We reserve the right to change, modify, or remove the contents of the Services at any time or for any reason at our sole -discretion without notice. However, we have no obligation to update any information on our Services. We will not be -liable to you or any third party for any modification, price change, suspension, or discontinuance of the Services. - -We cannot guarantee the Services will be available at all times. We may experience hardware, software, or other problems -or need to perform maintenance related to the Services, resulting in interruptions, delays, or errors. We reserve the -right to change, revise, update, suspend, discontinue, or otherwise modify the Services at any time or for any reason -without notice to you. You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by -your inability to access or use the Services during any downtime or discontinuance of the Services. Nothing in these -Legal Terms will be construed to obligate us to maintain and support the Services or to supply any corrections, updates, -or releases in connection therewith. - -## 17. Governing law - -These Legal Terms are governed by and interpreted following the laws of Germany, and the use of the United Nations -Convention of Contracts for the International Sales of Goods is expressly excluded. If your habitual residence is in the -EU, and you are a consumer, you additionally possess the protection provided to you by obligatory provisions of the law -in your country to residence. dstack GmbH and yourself both agree to submit to the non-exclusive jurisdiction of the -courts of Bayern, which means that you may make a claim to defend your consumer protection rights in regards to these -Legal Terms in Germany, or in the EU country in which you reside. - -## 18. Dispute resolution - -**Informal Negotiations** - -To expedite resolution and control the cost of any dispute, controversy, or claim related to these Legal Terms (each a " -Dispute" and collectively, the "Disputes") brought by either you or us (individually, a "Party" and collectively, the " -Parties"), the Parties agree to first attempt to negotiate any Dispute (except those Disputes expressly provided below) -informally for at least thirty (30) days before initiating arbitration. Such informal negotiations commence upon written -notice from one Party to the other Party. - -**Binding Arbitration** - -Any dispute arising from the relationships between the Parties to these Legal Terms shall be determined by one -arbitrator who will be chosen in accordance with the Arbitration and Internal Rules of the European Court of Arbitration -being part of the European Centre of Arbitration having its seat in Strasbourg, and which are in force at the time the -application for arbitration is filed, and of which adoption of this clause constitutes acceptance. The seat of -arbitration shall be Munich , Germany . The language of the proceedings shall be German . Applicable rules of -substantive law shall be the law of Germany . - -**Restrictions** - -The Parties agree that any arbitration shall be limited to the Dispute between the Parties individually. To the full -extent permitted by law, (a) no arbitration shall be joined with any other proceeding; (b) there is no right or -authority for any Dispute to be arbitrated on a class-action basis or to utilize class action procedures; and (c) there -is no right or authority for any Dispute to be brought in a purported representative capacity on behalf of the general -public or any other persons. - -**Exceptions to Informal Negotiations and Arbitration** - -The Parties agree that the following Disputes are not subject to the above provisions concerning informal negotiations -binding arbitration: (a) any Disputes seeking to enforce or protect, or concerning the validity of, any of the -intellectual property rights of a Party; (b) any Dispute related to, or arising from, allegations of theft, piracy, -invasion of privacy, or unauthorized use; and (c) any claim for injunctive relief. If this provision is found to be -illegal or unenforceable, then neither Party will elect to arbitrate any Dispute falling within that portion of this -provision found to be illegal or unenforceable and such Dispute shall be decided by a court of competent jurisdiction -within the courts listed for jurisdiction above, and the Parties agree to submit to the personal jurisdiction of that -court. - -## 19. Corrections - -There may be information on the Services that contains typographical errors, inaccuracies, or omissions, including -descriptions, pricing, availability, and various other information. We reserve the right to correct any errors, -inaccuracies, or omissions and to change or update the information on the Services at any time, without prior notice. - -## 20. Disclaimer - -THE SERVICES ARE PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SERVICES WILL BE AT YOUR -SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN CONNECTION WITH -THE SERVICES AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR -A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS -OF THE SERVICES' CONTENT OR THE CONTENT OF ANY WEBSITES OR MOBILE APPLICATIONS LINKED TO THE SERVICES AND WE WILL ASSUME -NO LIABILITY OR RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL -INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SERVICES, (3) ANY -UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION -STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SERVICES, (5) ANY BUGS, VIRUSES, TROJAN -HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE SERVICES BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR -OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY -CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE SERVICES. WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR -ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH THE SERVICES, ANY -HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT -BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF -PRODUCTS OR SERVICES. AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD -USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE. - -## 21. Limitations of liability - -IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, -CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST REVENUE, LOSS OF DATA, -OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SERVICES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU FOR ANY CAUSE WHATSOEVER AND REGARDLESS -OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE LESSER OF THE AMOUNT PAID, IF ANY, BY YOU TO US DURING -THE zero (0) MONTH PERIOD PRIOR TO ANY CAUSE OF ACTION ARISING OR $0.00 USD. CERTAIN US STATE LAWS AND INTERNATIONAL -LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS -APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL -RIGHTS. - -## 22. Indemnification - -You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys’ fees and expenses, made by any third party due to or arising out of: (1) use of the Services; (2) breach of these Legal Terms; (3) any breach of your representations and warranties set forth in these Legal Terms; (4) your violation of the rights of a third party, including but not limited to intellectual property rights; or (5) any overt harmful act toward any other user of the Services with whom you connected via the Services. Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and control of any matter for which you are required to indemnify us, and you agree to cooperate, at your expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim, action, or proceeding which is subject to this indemnification upon becoming aware of it. - -## 23. User data - -We will maintain certain data that you transmit to the Services for the purpose of managing the performance of the -Services, as well as data relating to your use of the Services. Although we perform regular routine backups of data, you -are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the -Services. You agree that we shall have no liability to you for any loss or corruption of any such data, and you hereby -waive any right of action against us arising from any such loss or corruption of such data. - -## 24. Electronic communications, transactions, and signatures - -Visiting the Services, sending us emails, and completing online forms constitute electronic communications. You consent -to receive electronic communications, and you agree that all agreements, notices, disclosures, and other communications -we provide to you electronically, via email and on the Services, satisfy any legal requirement that such communication -be in writing. YOU HEREBY AGREE TO THE USE OF ELECTRONIC SIGNATURES, CONTRACTS, ORDERS, AND OTHER RECORDS, AND TO -ELECTRONIC DELIVERY OF NOTICES, POLICIES, AND RECORDS OF TRANSACTIONS INITIATED OR COMPLETED BY US OR VIA THE SERVICES. -You hereby waive any rights or requirements under any statutes, regulations, rules, ordinances, or other laws in any -jurisdiction which require an original signature or delivery or retention of non-electronic records, or to payments or -the granting of credits by any means other than electronic means. - -## 25. California users and residents - -If any complaint with us is not satisfactorily resolved, you can contact the Complaint Assistance Unit of the Division -of Consumer Services of the California Department of Consumer Affairs in writing at 1625 North Market Blvd., Suite N -112, Sacramento, California 95834 or by telephone at (800) 952-5210 or (916) 445-1254. - -## 26. Miscellaneous - -These Legal Terms and any policies or operating rules posted by us on the Services or in respect to the Services -constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or -provision of these Legal Terms shall not operate as a waiver of such right or provision. These Legal Terms operate to -the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time. We -shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our -reasonable control. If any provision or part of a provision of these Legal Terms is determined to be unlawful, void, or -unenforceable, that provision or part of the provision is deemed severable from these Legal Terms and does not affect -the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or -agency relationship created between you and us as a result of these Legal Terms or use of the Services. You agree that -these Legal Terms will not be construed against us by virtue of having drafted them. You hereby waive any and all -defenses you may have based on the electronic form of these Legal Terms and the lack of signing by the parties hereto to -execute these Legal Terms. - -## 27. Contact us - -In order to resolve a complaint regarding the Services or to receive further information regarding use of the Services, -please contact us at **hello@dstack.ai**. diff --git a/examples/accelerators/amd/README.md b/examples/accelerators/amd/README.md deleted file mode 100644 index 863a0462bf..0000000000 --- a/examples/accelerators/amd/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# AMD - -`dstack` supports running dev environments, tasks, and services on AMD GPUs. -You can do that by setting up an [SSH fleet](https://dstack.ai/docs/concepts/fleets#ssh-fleets) -with on-prem AMD GPUs or configuring a backend that offers AMD GPUs such as the `runpod` backend. - -## Deployment - -Most serving frameworks including vLLM and TGI have AMD support. Here's an example of a [service](https://dstack.ai/docs/services) that deploys -Llama 3.1 70B in FP16 using [TGI](https://huggingface.co/docs/text-generation-inference/en/installation_amd) and [vLLM](https://docs.vllm.ai/en/latest/getting_started/amd-installation.html). - -=== "TGI" - -
- - ```yaml - type: service - name: amd-service-tgi - - # Using the official TGI's ROCm Docker image - image: ghcr.io/huggingface/text-generation-inference:sha-a379d55-rocm - - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-70B-Instruct - - TRUST_REMOTE_CODE=true - - ROCM_USE_FLASH_ATTN_V2_TRITON=true - commands: - - text-generation-launcher --port 8000 - port: 8000 - # Register the model - model: meta-llama/Meta-Llama-3.1-70B-Instruct - - # Uncomment to leverage spot instances - #spot_policy: auto - - resources: - gpu: MI300X - disk: 150GB - ``` - -
- - -=== "vLLM" - -
- - ```yaml - type: service - name: llama31-service-vllm-amd - - # Using RunPod's ROCm Docker image - image: runpod/pytorch:2.4.0-py3.10-rocm6.1.0-ubuntu22.04 - # Required environment variables - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-70B-Instruct - - MAX_MODEL_LEN=126192 - # Commands of the task - commands: - - export PATH=/opt/conda/envs/py_3.10/bin:$PATH - - wget https://github.com/ROCm/hipBLAS/archive/refs/tags/rocm-6.1.0.zip - - unzip rocm-6.1.0.zip - - cd hipBLAS-rocm-6.1.0 - - python rmake.py - - cd .. - - git clone https://github.com/vllm-project/vllm.git - - cd vllm - - pip install triton - - pip uninstall torch -y - - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.1 - - pip install /opt/rocm/share/amd_smi - - pip install --upgrade numba scipy huggingface-hub[cli] - - pip install "numpy<2" - - pip install -r requirements-rocm.txt - - wget -N https://github.com/ROCm/vllm/raw/fa78403/rocm_patch/libamdhip64.so.6 -P /opt/rocm/lib - - rm -f "$(python3 -c 'import torch; print(torch.__path__[0])')"/lib/libamdhip64.so* - - export PYTORCH_ROCM_ARCH="gfx90a;gfx942" - - wget https://dstack-binaries.s3.amazonaws.com/vllm-0.6.0%2Brocm614-cp310-cp310-linux_x86_64.whl - - pip install vllm-0.6.0+rocm614-cp310-cp310-linux_x86_64.whl - - vllm serve $MODEL_ID --max-model-len $MAX_MODEL_LEN --port 8000 - # Service port - port: 8000 - # Register the model - model: meta-llama/Meta-Llama-3.1-70B-Instruct - - # Uncomment to leverage spot instances - #spot_policy: auto - - resources: - gpu: MI300X - disk: 200GB - ``` -
- - Note, maximum size of vLLM’s `KV cache` is 126192, consequently we must set `MAX_MODEL_LEN` to 126192. Adding `/opt/conda/envs/py_3.10/bin` to PATH ensures we use the Python 3.10 environment necessary for the pre-built binaries compiled specifically for this version. - - > To speed up the `vLLM-ROCm` installation, we use a pre-built binary from S3. - > You can find the task to build and upload the binary in - > [`examples/inference/vllm/amd/`](https://github.com/dstackai/dstack/blob/master/examples/inference/vllm/amd/). - -!!! info "Docker image" - If you want to use AMD, specifying `image` is currently required. This must be an image that includes - ROCm drivers. - -To request multiple GPUs, specify the quantity after the GPU name, separated by a colon, e.g., `MI300X:4`. - -## Fine-tuning - -=== "TRL" - - Below is an example of LoRA fine-tuning Llama 3.1 8B using [TRL](https://rocm.docs.amd.com/en/latest/how-to/llm-fine-tuning-optimization/single-gpu-fine-tuning-and-inference.html) - and the [`mlabonne/guanaco-llama2-1k`](https://huggingface.co/datasets/mlabonne/guanaco-llama2-1k) - dataset. - -
- - ```yaml - type: task - name: trl-amd-llama31-train - - # Using RunPod's ROCm Docker image - image: runpod/pytorch:2.1.2-py3.10-rocm6.1-ubuntu22.04 - - # Required environment variables - env: - - HF_TOKEN - # Mount files - files: - - train.py - # Commands of the task - commands: - - export PATH=/opt/conda/envs/py_3.10/bin:$PATH - - git clone https://github.com/ROCm/bitsandbytes - - cd bitsandbytes - - git checkout rocm_enabled - - pip install -r requirements-dev.txt - - cmake -DBNB_ROCM_ARCH="gfx942" -DCOMPUTE_BACKEND=hip -S . - - make - - pip install . - - pip install trl - - pip install peft - - pip install transformers datasets huggingface-hub scipy - - cd .. - - python train.py - - # Uncomment to leverage spot instances - #spot_policy: auto - - resources: - gpu: MI300X - disk: 150GB - ``` - -
- -=== "Axolotl" - Below is an example of fine-tuning Llama 3.1 8B using [Axolotl](https://rocm.blogs.amd.com/artificial-intelligence/axolotl/README.html) - and the [tatsu-lab/alpaca](https://huggingface.co/datasets/tatsu-lab/alpaca) - dataset. - -
- - ```yaml - type: task - # The name is optional, if not specified, generated randomly - name: axolotl-amd-llama31-train - - # Using RunPod's ROCm Docker image - image: runpod/pytorch:2.1.2-py3.10-rocm6.0.2-ubuntu22.04 - # Required environment variables - env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - WANDB_NAME=axolotl-amd-llama31-train - - HUB_MODEL_ID - # Commands of the task - commands: - - export PATH=/opt/conda/envs/py_3.10/bin:$PATH - - pip uninstall torch torchvision torchaudio -y - - python3 -m pip install --pre torch==2.3.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.0/ - - git clone https://github.com/OpenAccess-AI-Collective/axolotl - - cd axolotl - - git checkout d4f6c65 - - pip install -e . - # Latest pynvml is not compatible with axolotl commit d4f6c65, so we need to fall back to version 11.5.3 - - pip uninstall pynvml -y - - pip install pynvml==11.5.3 - - cd .. - - wget https://dstack-binaries.s3.amazonaws.com/flash_attn-2.0.4-cp310-cp310-linux_x86_64.whl - - pip install flash_attn-2.0.4-cp310-cp310-linux_x86_64.whl - - wget https://dstack-binaries.s3.amazonaws.com/xformers-0.0.26-cp310-cp310-linux_x86_64.whl - - pip install xformers-0.0.26-cp310-cp310-linux_x86_64.whl - - git clone --recurse https://github.com/ROCm/bitsandbytes - - cd bitsandbytes - - git checkout rocm_enabled - - pip install -r requirements-dev.txt - - cmake -DBNB_ROCM_ARCH="gfx942" -DCOMPUTE_BACKEND=hip -S . - - make - - pip install . - - cd .. - - accelerate launch -m axolotl.cli.train -- axolotl/examples/llama-3/fft-8b.yaml - --wandb-project "$WANDB_PROJECT" - --wandb-name "$WANDB_NAME" - --hub-model-id "$HUB_MODEL_ID" - - resources: - gpu: MI300X - disk: 150GB - ``` -
- - Note, to support ROCm, we need to checkout to commit `d4f6c65`. This commit eliminates the need to manually modify the Axolotl source code to make xformers compatible with ROCm, as described in the [xformers workaround](https://docs.axolotl.ai/docs/amd_hpc.html#apply-xformers-workaround). This installation approach is also followed for building Axolotl ROCm docker image. [(See Dockerfile)](https://github.com/ROCm/rocm-blogs/blob/release/blogs/artificial-intelligence/axolotl/src/Dockerfile.rocm). - - > To speed up installation of `flash-attention` and `xformers `, we use pre-built binaries uploaded to S3. - > You can find the tasks that build and upload the binaries - > in [`examples/single-node-training/axolotl/amd/`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/axolotl/amd/). - -## Running a configuration - -Once the configuration is ready, run `dstack apply -f `, and `dstack` will automatically provision the -cloud resources and run the configuration. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ WANDB_PROJECT=... -$ WANDB_NAME=axolotl-amd-llama31-train -$ HUB_MODEL_ID=... -$ dstack apply -f examples/inference/vllm/amd/.dstack.yml -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/inference/tgi/amd`](https://github.com/dstackai/dstack/blob/master/examples/inference/tgi/amd), -[`examples/inference/vllm/amd`](https://github.com/dstackai/dstack/blob/master/examples/inference/vllm/amd), -[`examples/single-node-training/axolotl/amd`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/axolotl/amd) and -[`examples/single-node-training/trl/amd`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/trl/amd) - -## What's next? - -1. Browse [TGI](https://rocm.docs.amd.com/en/latest/how-to/rocm-for-ai/deploy-your-model.html#serving-using-hugging-face-tgi), - [vLLM](https://docs.vllm.ai/en/latest/getting_started/amd-installation.html#build-from-source-rocm), - [Axolotl](https://github.com/ROCm/rocm-blogs/tree/release/blogs/artificial-intelligence/axolotl), - [TRL](https://rocm.docs.amd.com/en/latest/how-to/llm-fine-tuning-optimization/fine-tuning-and-inference.html) and - [ROCm Bitsandbytes](https://github.com/ROCm/bitsandbytes) -2. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), and - [services](https://dstack.ai/docs/services). diff --git a/examples/accelerators/intel/README.md b/examples/accelerators/intel/README.md deleted file mode 100644 index 0220a74ce8..0000000000 --- a/examples/accelerators/intel/README.md +++ /dev/null @@ -1,188 +0,0 @@ -# Intel Gaudi - -`dstack` supports running dev environments, tasks, and services on Intel Gaudi GPUs via -[SSH fleets](https://dstack.ai/docs/concepts/fleets#ssh-fleets). - -## Deployment - -Serving frameworks like vLLM and TGI have Intel Gaudi support. Here's an example of -a service that deploys -[`DeepSeek-R1-Distill-Llama-70B`](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-70B) -using [TGI on Gaudi](https://github.com/huggingface/tgi-gaudi) -and [vLLM](https://github.com/HabanaAI/vllm-fork). - -=== "TGI" -
- - ```yaml - type: service - name: tgi - - image: ghcr.io/huggingface/tgi-gaudi:2.3.1 - env: - - HF_TOKEN - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - PORT=8000 - - OMPI_MCA_btl_vader_single_copy_mechanism=none - - TEXT_GENERATION_SERVER_IGNORE_EOS_TOKEN=true - - PT_HPU_ENABLE_LAZY_COLLECTIVES=true - - MAX_TOTAL_TOKENS=2048 - - BATCH_BUCKET_SIZE=256 - - PREFILL_BATCH_BUCKET_SIZE=4 - - PAD_SEQUENCE_TO_MULTIPLE_OF=64 - - ENABLE_HPU_GRAPH=true - - LIMIT_HPU_GRAPH=true - - USE_FLASH_ATTENTION=true - - FLASH_ATTENTION_RECOMPUTE=true - commands: - - text-generation-launcher - --sharded true - --num-shard $DSTACK_GPUS_NUM - --max-input-length 1024 - --max-total-tokens 2048 - --max-batch-prefill-tokens 4096 - --max-batch-total-tokens 524288 - --max-waiting-tokens 7 - --waiting-served-ratio 1.2 - --max-concurrent-requests 512 - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - resources: - gpu: gaudi2:8 - - # Uncomment to cache downloaded models - #volumes: - # - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - ``` - -
- -=== "vLLM" - -
- - ```yaml - type: service - name: deepseek-r1-gaudi - - image: vault.habana.ai/gaudi-docker/1.19.0/ubuntu22.04/habanalabs/pytorch-installer-2.5.1:latest - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - HABANA_VISIBLE_DEVICES=all - - OMPI_MCA_btl_vader_single_copy_mechanism=none - commands: - - git clone https://github.com/HabanaAI/vllm-fork.git - - cd vllm-fork - - git checkout habana_main - - pip install -r requirements-hpu.txt - - python setup.py develop - - vllm serve $MODEL_ID - --tensor-parallel-size 8 - --trust-remote-code - --download-dir /data - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - - resources: - gpu: gaudi2:8 - - # Uncomment to cache downloaded models - #volumes: - # - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - ``` -
- - -## Fine-tuning - -Below is an example of LoRA fine-tuning of [`DeepSeek-R1-Distill-Qwen-7B`](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B) -using [Optimum for Intel Gaudi](https://github.com/huggingface/optimum-habana) -and [DeepSpeed](https://docs.habana.ai/en/latest/PyTorch/DeepSpeed/DeepSpeed_User_Guide/DeepSpeed_User_Guide.html#deepspeed-user-guide) with -the [`lvwerra/stack-exchange-paired`](https://huggingface.co/datasets/lvwerra/stack-exchange-paired) dataset. - -
- -```yaml -type: task -name: trl-train - -image: vault.habana.ai/gaudi-docker/1.18.0/ubuntu22.04/habanalabs/pytorch-installer-2.4.0 -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - - WANDB_API_KEY - - WANDB_PROJECT -commands: - - pip install --upgrade-strategy eager optimum[habana] - - pip install git+https://github.com/HabanaAI/DeepSpeed.git@1.19.0 - - git clone https://github.com/huggingface/optimum-habana.git - - cd optimum-habana/examples/trl - - pip install -r requirements.txt - - pip install wandb - - DEEPSPEED_HPU_ZERO3_SYNC_MARK_STEP_REQUIRED=1 python ../gaudi_spawn.py --world_size $DSTACK_GPUS_NUM --use_deepspeed sft.py - --model_name_or_path $MODEL_ID - --dataset_name "lvwerra/stack-exchange-paired" - --deepspeed ../language-modeling/llama2_ds_zero3_config.json - --output_dir="./sft" - --do_train - --max_steps=500 - --logging_steps=10 - --save_steps=100 - --per_device_train_batch_size=1 - --per_device_eval_batch_size=1 - --gradient_accumulation_steps=2 - --learning_rate=1e-4 - --lr_scheduler_type="cosine" - --warmup_steps=100 - --weight_decay=0.05 - --optim="paged_adamw_32bit" - --lora_target_modules "q_proj" "v_proj" - --bf16 - --remove_unused_columns=False - --run_name="sft_deepseek_70" - --report_to="wandb" - --use_habana - --use_lazy_mode - -resources: - gpu: gaudi2:8 -``` - -
- -To finetune `DeepSeek-R1-Distill-Llama-70B` with eight Gaudi 2, -you can partially offload parameters to CPU memory using the Deepspeed configuration file. -For more details, refer to [parameter offloading](https://deepspeed.readthedocs.io/en/latest/zero3.html#deepspeedzerooffloadparamconfig). - -## Applying a configuration - -Once the configuration is ready, run `dstack apply -f `. - -
- -```shell -$ dstack apply -f examples/inference/vllm/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 ssh remote 152xCPU,1007GB,8xGaudi2:96GB yes $0 idle - -Submit a new run? [y/n]: y - -Provisioning... ----> 100% -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/llms/deepseek/tgi/intel`](https://github.com/dstackai/dstack/blob/master/examples/llms/deepseek/tgi/intel), -[`examples/llms/deepseek/vllm/intel`](https://github.com/dstackai/dstack/blob/master/examples/llms/deepseek/vllm/intel) and -[`examples/llms/deepseek/trl/intel`](https://github.com/dstackai/dstack/blob/master/examples/llms/deepseek/trl/intel). - -!!! info "What's next?" - 1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), and [services](https://dstack.ai/docs/services). - 2. See also [Intel Gaudi Documentation](https://docs.habana.ai/en/latest/index.html), [vLLM Inference with Gaudi](https://docs.habana.ai/en/latest/PyTorch/Inference_on_PyTorch/vLLM_Inference.html) - and [Optimum for Gaudi examples](https://github.com/huggingface/optimum-habana/blob/main/examples/trl/README.md). diff --git a/examples/accelerators/tenstorrent/.dstack.yml b/examples/accelerators/tenstorrent/.dstack.yml deleted file mode 100644 index 6e3319a001..0000000000 --- a/examples/accelerators/tenstorrent/.dstack.yml +++ /dev/null @@ -1,9 +0,0 @@ -type: dev-environment -name: cursor - -image: dstackai/tt-smi:latest - -ide: cursor - -resources: - gpu: n150:1 diff --git a/examples/accelerators/tenstorrent/README.md b/examples/accelerators/tenstorrent/README.md deleted file mode 100644 index bbb0b2207e..0000000000 --- a/examples/accelerators/tenstorrent/README.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: "Tenstorrent" -description: "You can now use SSH fleets to run dev environments, tasks, and services on Tenstorrent." -image: https://dstack.ai/static-assets/static-assets/images/dstack-tenstorrent-min.png ---- - -# Tenstorrent - -`dstack` supports running dev environments, tasks, and services on Tenstorrent -[Wormwhole](https://tenstorrent.com/en/hardware/wormhole) accelerators via SSH fleets. - - -??? info "SSH fleets" -
- - ```yaml - type: fleet - name: wormwhole-fleet - - ssh_config: - user: root - identity_file: ~/.ssh/id_rsa - # Configure any number of hosts with n150 or n300 PCEe boards - hosts: - - 192.168.2.108 - ``` - -
- - > Hosts should be pre-installed with [Tenstorrent software](https://docs.tenstorrent.com/getting-started/README.html#software-installation). - This should include the drivers, `tt-smi`, and HugePages. - - To apply the fleet configuration, run: - -
- - ```bash - $ dstack apply -f examples/acceleators/tenstorrent/fleet.dstack.yml - - FLEET RESOURCES PRICE STATUS CREATED - wormwhole-fleet cpu=12 mem=32GB disk=243GB n150:12GB $0 idle 18 sec ago - ``` - -
- - For more details on fleet configuration, refer to [SSH fleets](https://dstack.ai/docs/concepts/fleets#ssh-fleets). - -## Services - -Here's an example of a service that deploys -[`Llama-3.2-1B-Instruct`](https://huggingface.co/meta-llama/Llama-3.2-1B) -using [Tenstorrent Inference Service](https://github.com/tenstorrent/tt-inference-server). - -
- -```yaml -type: service -name: tt-inference-server - -env: - - HF_TOKEN - - HF_MODEL_REPO_ID=meta-llama/Llama-3.2-1B-Instruct -image: ghcr.io/tenstorrent/tt-inference-server/vllm-tt-metal-src-release-ubuntu-20.04-amd64:0.0.4-v0.56.0-rc47-e2e0002ac7dc -commands: - - | - . ${PYTHON_ENV_DIR}/bin/activate - pip install "huggingface_hub[cli]" - export LLAMA_DIR="/data/models--$(echo "$HF_MODEL_REPO_ID" | sed 's/\//--/g')/" - huggingface-cli download $HF_MODEL_REPO_ID --local-dir $LLAMA_DIR - python /home/container_app_user/app/src/run_vllm_api_server.py -port: 7000 - -model: meta-llama/Llama-3.2-1B-Instruct - -# Cache downloaded model -volumes: - - /mnt/data/tt-inference-server/data:/data - -resources: - gpu: n150:1 -``` - -
- -Go ahead and run configuration using `dstack apply`: - -
- - ```bash - $ dstack apply -f examples/acceleators/tenstorrent/tt-inference-server.dstack.yml - ``` -
- -Once the service is up, it will be available via the service endpoint -at `/proxy/services///`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "meta-llama/Llama-3.2-1B-Instruct", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "stream": true, - "max_tokens": 512 - }' -``` - -
- -Additionally, the model is available via `dstack`'s control plane UI: - -![](https://dstack.ai/static-assets/static-assets/images/dstack-tenstorrent-model-ui.png){ width=800 } - -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the service endpoint -is available at `https://./`. - -> Services support many options, including authentication, auto-scaling policies, etc. To learn more, refer to [Services](https://dstack.ai/docs/concepts/services). - -## Tasks - -Below is a task that simply runs `tt-smi -s`. Tasks can be used for training, fine-tuning, batch inference, or antything else. - -
- -```yaml -type: task -# The name is optional, if not specified, generated randomly -name: tt-smi - -env: - - HF_TOKEN - -# (Required) Use any image with TT drivers -image: dstackai/tt-smi:latest - -# Use any commands -commands: - - tt-smi -s - -# Specify the number of accelerators, model, etc -resources: - gpu: n150:1 - -# Uncomment if you want to run on a cluster of nodes -#nodes: 2 -``` - -
- -> Tasks support many options, including multi-node configuration, max duration, etc. To learn more, refer to [Tasks](https://dstack.ai/docs/concepts/tasks). - -## Dev environments - -Below is an example of a dev environment configuration. It can be used to provision a dev environemnt that can be accessed via your desktop IDE. - -
- -```yaml -type: dev-environment -# The name is optional, if not specified, generated randomly -name: cursor - -# (Optional) List required env variables -env: - - HF_TOKEN - -image: dstackai/tt-smi:latest - -# Can be `vscode` or `cursor` -ide: cursor - -resources: - gpu: n150:1 -``` - -
- -If you run it via `dstack apply`, it will output the URL to access it via your desktop IDE. - -![](https://dstack.ai/static-assets/static-assets/images/dstack-tenstorrent-cursor.png){ width=800 } - -> Dev nevironments support many options, including inactivity and max duration, IDE configuration, etc. To learn more, refer to [Dev environments](https://dstack.ai/docs/concepts/tasks). - -??? info "Feedback" - Found a bug, or want to request a feature? File it in the [issue tracker](https://github.com/dstackai/dstack/issues), - or share via [Discord](https://discord.gg/u8SmfwPpMd). diff --git a/examples/accelerators/tenstorrent/tt-inference-server.dstack.yml b/examples/accelerators/tenstorrent/tt-inference-server.dstack.yml deleted file mode 100644 index 6f1815ead1..0000000000 --- a/examples/accelerators/tenstorrent/tt-inference-server.dstack.yml +++ /dev/null @@ -1,24 +0,0 @@ -type: service -name: tt-inference-server - -env: - - HF_TOKEN - - HF_MODEL_REPO_ID=meta-llama/Llama-3.2-1B-Instruct -image: ghcr.io/tenstorrent/tt-inference-server/vllm-tt-metal-src-release-ubuntu-20.04-amd64:0.0.4-v0.56.0-rc47-e2e0002ac7dc -commands: - - | - . ${PYTHON_ENV_DIR}/bin/activate - pip install "huggingface_hub[cli]" - export LLAMA_DIR="/data/models--$(echo "$HF_MODEL_REPO_ID" | sed 's/\//--/g')/" - huggingface-cli download $HF_MODEL_REPO_ID --local-dir $LLAMA_DIR - python /home/container_app_user/app/src/run_vllm_api_server.py -port: 7000 - -model: meta-llama/Llama-3.2-1B-Instruct - -# Cache downloaded model -volumes: - - /mnt/data/tt-inference-server/data:/data - -resources: - gpu: n150:1 diff --git a/examples/accelerators/tenstorrent/tt-smi.dstack.yml b/examples/accelerators/tenstorrent/tt-smi.dstack.yml deleted file mode 100644 index b9478cb166..0000000000 --- a/examples/accelerators/tenstorrent/tt-smi.dstack.yml +++ /dev/null @@ -1,10 +0,0 @@ -type: task -name: tt-smi - -image: dstackai/tt-smi:latest - -commands: - - tt-smi -s - -resources: - gpu: n150:1 diff --git a/examples/clusters/aws/fleet.dstack.yml b/examples/clusters/aws/fleet.dstack.yml deleted file mode 100644 index 9914c3df1f..0000000000 --- a/examples/clusters/aws/fleet.dstack.yml +++ /dev/null @@ -1,8 +0,0 @@ -type: fleet -name: my-efa-fleet - -nodes: 2 -placement: cluster - -resources: - gpu: H100:8 diff --git a/examples/clusters/crusoe/README.md b/examples/clusters/crusoe/README.md deleted file mode 100644 index fb6f4b1a7f..0000000000 --- a/examples/clusters/crusoe/README.md +++ /dev/null @@ -1,297 +0,0 @@ ---- -title: Distributed workload orchestration on Crusoe with dstack ---- - -# Crusoe - -Crusoe offers two ways to use clusters with fast interconnect: - -* [Crusoe Managed Kubernetes](#kubernetes) – Lets you interact with clusters through the Kubernetes API and includes support for NVIDIA and AMD GPU operators and related tools. -* [Virtual Machines (VMs)](#vms) – Gives you direct access to clusters in the form of virtual machines with NVIDIA and AMD GPUs. - -Both options use the same underlying networking infrastructure. This example walks you through how to set up Crusoe clusters to use with `dstack`. - -## Crusoe Managed Kubernetes { #kubernetes } - -!!! info "Prerequsisites" - 1. Go `Networking` → `Firewall Rules`, click `Create Firewall Rule`, and allow ingress traffic on port `30022`. This port will be used by the `dstack` server to access the jump host. - 2. Go to `Orchestration` and click `Create Cluster`. Make sure to enable the `NVIDIA GPU Operator` add-on. - 3. Go the the cluster, and click `Create Node Pool`. Select the right type of the instance. If you intend to auto-scale the cluster, make sure to set `Desired Number of Nodes` at least to `1`, since `dstack` doesn't currently support clusters that scale down to `0` nodes. - 4. Wait until at least one node is running. - -### Configure the backend - -Follow the standard instructions for setting up a [`kubernetes`](https://dstack.ai/docs/concepts/backends/#kubernetes) backend: - -
- -```yaml -projects: - - name: main - backends: - - type: kubernetes - kubeconfig: - filename: - proxy_jump: - port: 30022 -``` - -
- -### Create a fleet - -Once the Crusoe Managed Kubernetes cluster and the `dstack` server are running, you can create a fleet: - -
- -```yaml -type: fleet -name: crusoe-fleet - -placement: cluster -nodes: 0.. - -backends: [kubernetes] - -resources: - # Specify requirements to filter nodes - gpu: 1..8 -``` - -
- -Pass the fleet configuration to `dstack apply`: - -
- -```shell -$ dstack apply -f crusoe-fleet.dstack.yml -``` - -
- -Once the fleet is created, you can run [dev environments](https://dstack.ai/docs/concepts/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), and [services](https://dstack.ai/docs/concepts/services). - -## VMs - -Another way to work with Crusoe clusters is through VMs. While `dstack` typically supports VM-based compute providers via [dedicated backends](https://dstack.ai/docs/concepts/backends#vm-based) that automate provisioning, Crusoe does not yet have [such a backend](https://github.com/dstackai/dstack/issues/3378). As a result, to use a VM-based Crusoe cluster with `dstack`, you should use [SSH fleets](https://dstack.ai/docs/concepts/fleets). - -!!! info "Prerequsisites" - 1. Go to `Compute`, then `Instances`, and click `Create Instance`. Make sure to select the right instance type and VM image (that [support interconnect](https://docs.crusoecloud.com/networking/infiniband/managing-infiniband-networks/index.html)). Make sure to create as many instances as needed. - -### Create a fleet - -Follow the standard instructions for setting up an [SSH fleet](https://dstack.ai/docs/concepts/fleets/#ssh-fleets): - -
- -```yaml -type: fleet -name: crusoe-fleet - -placement: cluster - -# SSH credentials for the on-prem servers -ssh_config: - user: ubuntu - identity_file: ~/.ssh/id_rsa - hosts: - - 3.255.177.51 - - 3.255.177.52 -``` - -
- -Pass the fleet configuration to `dstack apply`: - -
- -```shell -$ dstack apply -f crusoe-fleet.dstack.yml -``` - -
- -Once the fleet is created, you can run [dev environments](https://dstack.ai/docs/concepts/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), and [services](https://dstack.ai/docs/concepts/services). - -## Run NCCL tests - -Use a [distributed task](https://dstack.ai/docs/concepts/tasks#distributed-task) that runs NCCL tests to validate cluster network bandwidth. - -=== "Crusoe Managed Kubernetes" - - If you’re running on Crusoe Managed Kubernetes, make sure to install HPC-X and provide an up-to-date topology file. - -
- - ```yaml - type: task - name: nccl-tests - - nodes: 2 - startup_order: workers-first - stop_criteria: master-done - - commands: - # Install NCCL topology files - - curl -sSL https://gist.github.com/un-def/48df8eea222fa9547ad4441986eb15af/archive/df51d56285c5396a0e82bb42f4f970e7bb0a9b65.tar.gz -o nccl_topo.tar.gz - - mkdir -p /etc/crusoe/nccl_topo - - tar -C /etc/crusoe/nccl_topo -xf nccl_topo.tar.gz --strip-components=1 - # Install and initialize HPC-X - - curl -sSL https://content.mellanox.com/hpc/hpc-x/v2.21.3/hpcx-v2.21.3-gcc-doca_ofed-ubuntu22.04-cuda12-x86_64.tbz -o hpcx.tar.bz - - mkdir -p /opt/hpcx - - tar -C /opt/hpcx -xf hpcx.tar.bz --strip-components=1 --checkpoint=10000 - - . /opt/hpcx/hpcx-init.sh - - hpcx_load - # Run NCCL Tests - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun \ - --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --bind-to none \ - -mca btl tcp,self \ - -mca coll_hcoll_enable 0 \ - -x PATH \ - -x LD_LIBRARY_PATH \ - -x CUDA_DEVICE_ORDER=PCI_BUS_ID \ - -x NCCL_SOCKET_NTHREADS=4 \ - -x NCCL_NSOCKS_PERTHREAD=8 \ - -x NCCL_TOPO_FILE=/etc/crusoe/nccl_topo/a100-80gb-sxm-ib-cloud-hypervisor.xml \ - -x NCCL_IB_MERGE_VFS=0 \ - -x NCCL_IB_AR_THRESHOLD=0 \ - -x NCCL_IB_PCI_RELAXED_ORDERING=1 \ - -x NCCL_IB_SPLIT_DATA_ON_QPS=0 \ - -x NCCL_IB_QPS_PER_CONNECTION=2 \ - -x NCCL_IB_HCA=mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1,mlx5_8:1 \ - -x UCX_NET_DEVICES=mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1,mlx5_8:1 \ - /opt/nccl-tests/build/all_reduce_perf -b 8 -e 2G -f 2 -t 1 -g 1 -c 1 -n 100 - else - sleep infinity - fi - - # Required for IB - privileged: true - - resources: - gpu: A100:8 - shm_size: 16GB - ``` - -
- - > The task above downloads an A100 topology file from a Gist. The most reliable way to obtain the latest topology is to copy it from a Crusoe-provisioned VM (see [VMs](#vms)). - - ??? info "Privileged" - When running on Crusoe Managed Kubernetes, set `privileged` to `true` to ensure access to InfiniBand. - -=== "VMs" - -With Crusoe VMs, HPC-X and up-to-date topology files are already available on the hosts. When using SSH fleets, simply mount them via [instance volumes](https://dstack.ai/docs/concepts/volumes#instance-volumes). - -```yaml -type: task -name: nccl-tests - -nodes: 2 -startup_order: workers-first -stop_criteria: master-done - -volumes: - - /opt/hpcx:/opt/hpcx - - /etc/crusoe/nccl_topo:/etc/crusoe/nccl_topo - -commands: - - . /opt/hpcx/hpcx-init.sh - - hpcx_load - # Run NCCL Tests - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun \ - --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --bind-to none \ - -mca btl tcp,self \ - -mca coll_hcoll_enable 0 \ - -x PATH \ - -x LD_LIBRARY_PATH \ - -x CUDA_DEVICE_ORDER=PCI_BUS_ID \ - -x NCCL_SOCKET_NTHREADS=4 \ - -x NCCL_NSOCKS_PERTHREAD=8 \ - -x NCCL_TOPO_FILE=/etc/crusoe/nccl_topo/a100-80gb-sxm-ib-cloud-hypervisor.xml \ - -x NCCL_IB_MERGE_VFS=0 \ - -x NCCL_IB_AR_THRESHOLD=0 \ - -x NCCL_IB_PCI_RELAXED_ORDERING=1 \ - -x NCCL_IB_SPLIT_DATA_ON_QPS=0 \ - -x NCCL_IB_QPS_PER_CONNECTION=2 \ - -x NCCL_IB_HCA=mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1,mlx5_8:1 \ - -x UCX_NET_DEVICES=mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1,mlx5_8:1 \ - /opt/nccl-tests/build/all_reduce_perf -b 8 -e 2G -f 2 -t 1 -g 1 -c 1 -n 100 - else - sleep infinity - fi - -resources: - gpu: A100:8 - shm_size: 16GB -``` - -Pass the configuration to `dstack apply`: - -
- -```shell -$ dstack apply -f crusoe-nccl-tests.dstack.yml - -Provisioning... ----> 100% - -nccl-tests provisioning completed (running) - -# out-of-place in-place -# size count type redop root time algbw busbw #wrong time algbw busbw #wrong -# (B) (elements) (us) (GB/s) (GB/s) (us) (GB/s) (GB/s) - 8 2 float sum -1 27.70 0.00 0.00 0 29.82 0.00 0.00 0 - 16 4 float sum -1 28.78 0.00 0.00 0 28.99 0.00 0.00 0 - 32 8 float sum -1 28.49 0.00 0.00 0 28.16 0.00 0.00 0 - 64 16 float sum -1 28.41 0.00 0.00 0 28.69 0.00 0.00 0 - 128 32 float sum -1 28.94 0.00 0.01 0 28.58 0.00 0.01 0 - 256 64 float sum -1 29.46 0.01 0.02 0 29.45 0.01 0.02 0 - 512 128 float sum -1 30.23 0.02 0.03 0 29.85 0.02 0.03 0 - 1024 256 float sum -1 30.79 0.03 0.06 0 34.03 0.03 0.06 0 - 2048 512 float sum -1 37.90 0.05 0.10 0 33.22 0.06 0.12 0 - 4096 1024 float sum -1 35.91 0.11 0.21 0 35.30 0.12 0.22 0 - 8192 2048 float sum -1 36.84 0.22 0.42 0 38.30 0.21 0.40 0 - 16384 4096 float sum -1 47.08 0.35 0.65 0 37.26 0.44 0.82 0 - 32768 8192 float sum -1 45.20 0.72 1.36 0 48.70 0.67 1.26 0 - 65536 16384 float sum -1 49.43 1.33 2.49 0 50.97 1.29 2.41 0 - 131072 32768 float sum -1 51.08 2.57 4.81 0 50.17 2.61 4.90 0 - 262144 65536 float sum -1 192.78 1.36 2.55 0 100.00 2.62 4.92 0 - 524288 131072 float sum -1 68.02 7.71 14.45 0 69.40 7.55 14.16 0 - 1048576 262144 float sum -1 81.71 12.83 24.06 0 88.58 11.84 22.20 0 - 2097152 524288 float sum -1 113.03 18.55 34.79 0 102.21 20.52 38.47 0 - 4194304 1048576 float sum -1 123.50 33.96 63.68 0 131.71 31.84 59.71 0 - 8388608 2097152 float sum -1 189.42 44.29 83.04 0 183.01 45.84 85.95 0 - 16777216 4194304 float sum -1 274.05 61.22 114.79 0 265.91 63.09 118.30 0 - 33554432 8388608 float sum -1 490.77 68.37 128.20 0 490.53 68.40 128.26 0 - 67108864 16777216 float sum -1 854.62 78.52 147.23 0 853.49 78.63 147.43 0 - 134217728 33554432 float sum -1 1483.43 90.48 169.65 0 1479.22 90.74 170.13 0 - 268435456 67108864 float sum -1 2700.36 99.41 186.39 0 2700.49 99.40 186.38 0 - 536870912 134217728 float sum -1 5300.49 101.29 189.91 0 5314.91 101.01 189.40 0 - 1073741824 268435456 float sum -1 10472.2 102.53 192.25 0 10485.6 102.40 192.00 0 - 2147483648 536870912 float sum -1 20749.1 103.50 194.06 0 20745.7 103.51 194.09 0 -# Out of bounds values : 0 OK -# Avg bus bandwidth : 53.7387 -``` - -
- -## What's next - -1. Learn about [dev environments](https://dstack.ai/docs/concepts/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), [services](https://dstack.ai/docs/concepts/services) -2. Read the [Kuberentes](https://dstack.ai/docs/guides/kubernetes), and [Clusters](https://dstack.ai/docs/guides/clusters) guides -3. Check the docs on [Crusoe's networking](https://docs.crusoecloud.com/networking/infiniband/) and ["Crusoe Managed" Kubernetes](https://docs.crusoecloud.com/orchestration/cmk/index.html) diff --git a/examples/clusters/gcp/a3-fleet.dstack.yml b/examples/clusters/gcp/a3-fleet.dstack.yml deleted file mode 100644 index 483877068d..0000000000 --- a/examples/clusters/gcp/a3-fleet.dstack.yml +++ /dev/null @@ -1,7 +0,0 @@ -type: fleet -name: a3mega-cluster -nodes: 2 -placement: cluster -instance_types: - - a3-megagpu-8g -spot_policy: auto diff --git a/examples/clusters/gcp/a3high-fleet.dstack.yml b/examples/clusters/gcp/a3high-fleet.dstack.yml deleted file mode 100644 index e9f0a9dbc2..0000000000 --- a/examples/clusters/gcp/a3high-fleet.dstack.yml +++ /dev/null @@ -1,7 +0,0 @@ -type: fleet -name: a3high-cluster -nodes: 2 -placement: cluster -instance_types: - - a3-highgpu-8g -spot_policy: auto diff --git a/examples/clusters/gcp/a3high-nccl-tests.dstack.yml b/examples/clusters/gcp/a3high-nccl-tests.dstack.yml deleted file mode 100644 index 6cacbdf54b..0000000000 --- a/examples/clusters/gcp/a3high-nccl-tests.dstack.yml +++ /dev/null @@ -1,37 +0,0 @@ -type: task -name: nccl-tests -nodes: 2 -image: us-docker.pkg.dev/gce-ai-infra/gpudirect-tcpx/nccl-plugin-gpudirecttcpx -commands: - - | - export NCCL_DEBUG=INFO - export LD_LIBRARY_PATH=/usr/local/tcpx/lib64:$LD_LIBRARY_PATH - # We use FIFO for inter-node communication - FIFO=/tmp/dstack_job - if [ ${DSTACK_NODE_RANK} -eq 0 ]; then - mkdir -p /scripts/hostfiles2 - : > /scripts/hostfiles2/hostfile8 - for ip in ${DSTACK_NODES_IPS}; do - echo "${ip} slots=${DSTACK_GPUS_PER_NODE}" >> /scripts/hostfiles2/hostfile8 - done - MPIRUN='mpirun --allow-run-as-root --hostfile /scripts/hostfiles2/hostfile8' - # Wait for other nodes - while true; do - if ${MPIRUN} -n ${DSTACK_NODES_NUM} -N 1 true >/dev/null 2>&1; then - break - fi - echo 'Waiting for nodes...' - sleep 5 - done - # Run NCCL Tests - NCCL_GPUDIRECTTCPX_FORCE_ACK=0 /scripts/run-allgather.sh 8 eth1,eth2,eth3,eth4 8M 8GB 2 - # Notify nodes the job is done - ${MPIRUN} -n ${DSTACK_NODES_NUM} -N 1 sh -c "echo done > ${FIFO}" - else - mkfifo ${FIFO} - # Wait for a message from the first node - cat ${FIFO} - fi -spot_policy: auto -resources: - shm_size: 16GB diff --git a/examples/clusters/gcp/a3mega-nccl-tests.dstack.yml b/examples/clusters/gcp/a3mega-nccl-tests.dstack.yml deleted file mode 100644 index 8c7e49d3f9..0000000000 --- a/examples/clusters/gcp/a3mega-nccl-tests.dstack.yml +++ /dev/null @@ -1,50 +0,0 @@ -type: task -name: nccl-tests -nodes: 2 -image: nvcr.io/nvidia/pytorch:24.04-py3 -entrypoint: "bash -c" # Need to use bash instead of default dash for nccl-env-profile.sh -commands: - - | - # Setup TCPXO NCCL env variables - NCCL_LIB_DIR="/var/lib/tcpxo/lib64" - source ${NCCL_LIB_DIR}/nccl-env-profile-ll128.sh - export NCCL_FASTRAK_CTRL_DEV=enp0s12 - export NCCL_FASTRAK_IFNAME=enp6s0,enp7s0,enp13s0,enp14s0,enp134s0,enp135s0,enp141s0,enp142s0 - export NCCL_SOCKET_IFNAME=enp0s12 - export NCCL_FASTRAK_LLCM_DEVICE_DIRECTORY="/dev/aperture_devices" - export LD_LIBRARY_PATH="${NCCL_LIB_DIR}:${LD_LIBRARY_PATH}" - # Build NCCL Tests - git clone https://github.com/NVIDIA/nccl-tests.git - cd nccl-tests - MPI=1 CC=mpicc CXX=mpicxx make -j - cd build - # We use FIFO for inter-node communication - FIFO=/tmp/dstack_job - if [ ${DSTACK_NODE_RANK} -eq 0 ]; then - sleep 10 - echo "${DSTACK_NODES_IPS}" > hostfile - MPIRUN='mpirun --allow-run-as-root --hostfile hostfile' - # Wait for other nodes - while true; do - if ${MPIRUN} -n ${DSTACK_NODES_NUM} -N 1 true >/dev/null 2>&1; then - break - fi - echo 'Waiting for nodes...' - sleep 5 - done - # Run NCCL Tests - ${MPIRUN} \ - -n ${DSTACK_GPUS_NUM} -N ${DSTACK_GPUS_PER_NODE} \ - --mca btl tcp,self --mca btl_tcp_if_exclude lo,docker0 \ - $(env | awk -F= '{print "-x", $1}' | xargs) \ - ./all_gather_perf -b 8M -e 8G -f 2 -g 1 -w 5 --iters 200 -c 0; - # Notify nodes the job is done - ${MPIRUN} -n ${DSTACK_NODES_NUM} -N 1 sh -c "echo done > ${FIFO}" - else - mkfifo ${FIFO} - # Wait for a message from the first node - cat ${FIFO} - fi -spot_policy: auto -resources: - shm_size: 16GB diff --git a/examples/clusters/gcp/a4-fleet.dstack.yml b/examples/clusters/gcp/a4-fleet.dstack.yml deleted file mode 100644 index ac97e22def..0000000000 --- a/examples/clusters/gcp/a4-fleet.dstack.yml +++ /dev/null @@ -1,13 +0,0 @@ -type: fleet -name: a4-cluster - -nodes: 2 -placement: cluster - -# Specify the zone where you have configured the RoCE VPC -availability_zones: [us-west2-c] -backends: [gcp] -spot_policy: auto - -resources: - gpu: B200:8 diff --git a/examples/clusters/nccl-rccl-tests/README.md b/examples/clusters/nccl-rccl-tests/README.md deleted file mode 100644 index 7248a4f422..0000000000 --- a/examples/clusters/nccl-rccl-tests/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# NCCL/RCCL tests - -This example shows how to run [NCCL](https://github.com/NVIDIA/nccl-tests) or [RCCL](https://github.com/ROCm/rccl-tests) tests on a cluster using [distributed tasks](https://dstack.ai/docs/concepts/tasks#distributed-tasks). - -!!! info "Prerequisites" - Before running a distributed task, make sure to create a fleet with `placement` set to `cluster` (can be a [managed fleet](https://dstack.ai/docs/concepts/fleets#backend-placement) or an [SSH fleet](https://dstack.ai/docs/concepts/fleets#ssh-placement)). - -## Running as a task - -Here's an example of a task that runs AllReduce test on 2 nodes, each with 4 GPUs (8 processes in total). - -=== "NCCL tests" - -
- - ```yaml - type: task - name: nccl-tests - - nodes: 2 - - startup_order: workers-first - stop_criteria: master-done - - env: - - NCCL_DEBUG=INFO - commands: - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun \ - --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --bind-to none \ - /opt/nccl-tests/build/all_reduce_perf -b 8 -e 8G -f 2 -g 1 - else - sleep infinity - fi - - # Uncomment if the `kubernetes` backend requires it for `/dev/infiniband` access - #privileged: true - - resources: - gpu: nvidia:1..8 - shm_size: 16GB - ``` - -
- - !!! info "Default image" - If you don't specify `image`, `dstack` uses its [base](https://github.com/dstackai/dstack/tree/master/docker/base) Docker image pre-configured with - `uv`, `python`, `pip`, essential CUDA drivers, `mpirun`, and NCCL tests (under `/opt/nccl-tests/build`). - -=== "RCCL tests" - -
- - ```yaml - type: task - name: rccl-tests - - nodes: 2 - startup_order: workers-first - stop_criteria: master-done - - # Mount the system libraries folder from the host - volumes: - - /usr/local/lib:/mnt/lib - - image: rocm/dev-ubuntu-22.04:6.4-complete - env: - - NCCL_DEBUG=INFO - - OPEN_MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi - commands: - # Setup MPI and build RCCL tests - - apt-get install -y git libopenmpi-dev openmpi-bin - - git clone https://github.com/ROCm/rccl-tests.git - - cd rccl-tests - - make MPI=1 MPI_HOME=$OPEN_MPI_HOME - - # Preload the RoCE driver library from the host (for Broadcom driver compatibility) - - export LD_PRELOAD=/mnt/lib/libbnxt_re-rdmav34.so - - # Run RCCL tests via MPI - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --mca btl_tcp_if_include ens41np0 \ - -x LD_PRELOAD \ - -x NCCL_IB_HCA=mlx5_0/1,bnxt_re0,bnxt_re1,bnxt_re2,bnxt_re3,bnxt_re4,bnxt_re5,bnxt_re6,bnxt_re7 \ - -x NCCL_IB_GID_INDEX=3 \ - -x NCCL_IB_DISABLE=0 \ - ./build/all_reduce_perf -b 8M -e 8G -f 2 -g 1 -w 5 --iters 20 -c 0; - else - sleep infinity - fi - - resources: - gpu: MI300X:8 - ``` - -
- - !!! info "RoCE library" - Broadcom RoCE drivers require the `libbnxt_re` userspace library inside the container to be compatible with the host’s Broadcom - kernel driver `bnxt_re`. To ensure this compatibility, we mount `libbnxt_re-rdmav34.so` from the host and preload it - using `LD_PRELOAD` when running MPI. - - -!!! info "Privileged" - In some cases, the backend (e.g., `kubernetes`) may require `privileged: true` to access the high-speed interconnect (e.g., InfiniBand). - -### Apply a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply/) command. - -
- -```shell -$ dstack apply -f examples/clusters/nccl-rccl-tests/nccl-tests.dstack.yml - - # BACKEND REGION INSTANCE RESOURCES SPOT PRICE - 1 aws us-east-1 g4dn.12xlarge 48xCPU, 192GB, 4xT4 (16GB), 100.0GB (disk) no $3.912 - 2 aws us-west-2 g4dn.12xlarge 48xCPU, 192GB, 4xT4 (16GB), 100.0GB (disk) no $3.912 - 3 aws us-east-2 g4dn.12xlarge 48xCPU, 192GB, 4xT4 (16GB), 100.0GB (disk) no $3.912 - -Submit the run nccl-tests? [y/n]: y -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/clusters/nccl-rccl-tests`](https://github.com/dstackai/dstack/blob/master/examples/clusters/nccl-rccl-tests). - -## What's next? - -1. Check [dev environments](https://dstack.ai/docs/concepts/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), - [services](https://dstack.ai/docsconcepts/services), and [fleets](https://dstack.ai/docs/concepts/fleets). diff --git a/examples/clusters/nccl-rccl-tests/nccl-tests.dstack.yml b/examples/clusters/nccl-rccl-tests/nccl-tests.dstack.yml deleted file mode 100644 index 4232e60a9e..0000000000 --- a/examples/clusters/nccl-rccl-tests/nccl-tests.dstack.yml +++ /dev/null @@ -1,29 +0,0 @@ -type: task -name: nccl-tests - -nodes: 2 -startup_order: workers-first -stop_criteria: master-done - -env: - - NCCL_DEBUG=INFO -commands: - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun \ - --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --bind-to none \ - /opt/nccl-tests/build/all_reduce_perf -b 8 -e 8G -f 2 -g 1 - else - sleep infinity - fi - -# Uncomment if the `kubernetes` backend requires it for `/dev/infiniband` access -#privileged: true - -resources: - gpu: nvidia:1..8 - shm_size: 16GB diff --git a/examples/clusters/nccl-rccl-tests/rccl-tests.dstack.yml b/examples/clusters/nccl-rccl-tests/rccl-tests.dstack.yml deleted file mode 100644 index 5beb1cd3ee..0000000000 --- a/examples/clusters/nccl-rccl-tests/rccl-tests.dstack.yml +++ /dev/null @@ -1,44 +0,0 @@ -type: task -name: rccl-tests - -nodes: 2 -startup_order: workers-first -stop_criteria: master-done - -# Mount the system libraries folder from the host -volumes: - - /usr/local/lib:/mnt/lib - -image: rocm/dev-ubuntu-22.04:6.4-complete -env: - - NCCL_DEBUG=INFO - - OPEN_MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi -commands: - # Setup MPI and build RCCL tests - - apt-get install -y git libopenmpi-dev openmpi-bin - - git clone https://github.com/ROCm/rccl-tests.git - - cd rccl-tests - - make MPI=1 MPI_HOME=$OPEN_MPI_HOME - - # Preload the RoCE driver library from the host (for Broadcom driver compatibility) - - export LD_PRELOAD=/mnt/lib/libbnxt_re-rdmav34.so - - # Run RCCL tests via MPI - - | - if [ $DSTACK_NODE_RANK -eq 0 ]; then - mpirun --allow-run-as-root \ - --hostfile $DSTACK_MPI_HOSTFILE \ - -n $DSTACK_GPUS_NUM \ - -N $DSTACK_GPUS_PER_NODE \ - --mca btl_tcp_if_include ens41np0 \ - -x LD_PRELOAD \ - -x NCCL_IB_HCA=mlx5_0/1,bnxt_re0,bnxt_re1,bnxt_re2,bnxt_re3,bnxt_re4,bnxt_re5,bnxt_re6,bnxt_re7 \ - -x NCCL_IB_GID_INDEX=3 \ - -x NCCL_IB_DISABLE=0 \ - ./build/all_reduce_perf -b 8M -e 8G -f 2 -g 1 -w 5 --iters 20 -c 0; - else - sleep infinity - fi - -resources: - gpu: MI300X:8 diff --git a/examples/distributed-training/axolotl/.dstack.yml b/examples/distributed-training/axolotl/.dstack.yml deleted file mode 100644 index 6192c689d0..0000000000 --- a/examples/distributed-training/axolotl/.dstack.yml +++ /dev/null @@ -1,49 +0,0 @@ -type: task -name: axolotl-multi-node-qlora-llama3-70b - -# Size of the cluster -nodes: 2 - -# The axolotlai/axolotl:main-latest image does not include InfiniBand or RDMA libraries, so we need to use the NGC container. -image: nvcr.io/nvidia/pytorch:25.01-py3 -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - HUB_MODEL_ID - - NCCL_DEBUG=INFO - - CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 - - ACCELERATE_LOG_LEVEL=info -# Commands of the task -commands: - # Replacing the default Torch and FlashAttention in the NCG container with Axolotl-compatible versions. - # The preinstalled versions are incompatible with Axolotl. - - pip uninstall -y torch flash-attn - - pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/test/cu124 - - pip install --no-build-isolation axolotl[flash-attn,deepspeed] - - wget https://raw.githubusercontent.com/huggingface/trl/main/examples/accelerate_configs/fsdp1.yaml - - wget https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/qlora-fsdp-70b.yaml - # Axolotl includes hf-xet version 1.1.0, which fails during downloads. Replacing it with the latest version (1.1.2). - - pip uninstall -y hf-xet - - pip install hf-xet --no-cache-dir - - | - accelerate launch \ - --config_file=fsdp1.yaml \ - -m axolotl.cli.train qlora-fsdp-70b.yaml \ - --hub-model-id $HUB_MODEL_ID \ - --output-dir /checkpoints/qlora-llama3-70b \ - --wandb-project $DSTACK_RUN_NAME \ - --wandb-name $WANDB_NAME \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM - -resources: - gpu: 80GB:8 - shm_size: 128GB - -volumes: - - /checkpoints:/checkpoints diff --git a/examples/distributed-training/axolotl/README.md b/examples/distributed-training/axolotl/README.md deleted file mode 100644 index 9ddd77a363..0000000000 --- a/examples/distributed-training/axolotl/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Axolotl - -This example walks you through how to run distributed fine-tune using [Axolotl](https://github.com/axolotl-ai-cloud/axolotl) and [distributed tasks](https://dstack.ai/docs/concepts/tasks#distributed-tasks). - -!!! info "Prerequisites" - Before running a distributed task, make sure to create a fleet with `placement` set to `cluster` (can be a [managed fleet](https://dstack.ai/docs/concepts/fleets#backend-placement) or an [SSH fleet](https://dstack.ai/docs/concepts/fleets#ssh-placement)). - -## Define a configuration - -Once the fleet is created, define a distributed task configuration. Here's an example of distributed `QLORA` task using `FSDP`. - -
- -```yaml -type: task -name: axolotl-multi-node-qlora-llama3-70b - -nodes: 2 - -image: nvcr.io/nvidia/pytorch:25.01-py3 - -env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - HUB_MODEL_ID - - CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 - - NCCL_DEBUG=INFO - - ACCELERATE_LOG_LEVEL=info - -commands: - # Replacing the default Torch and FlashAttention in the NCG container with Axolotl-compatible versions. - # The preinstalled versions are incompatible with Axolotl. - - pip uninstall -y torch flash-attn - - pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/test/cu124 - - pip install --no-build-isolation axolotl[flash-attn,deepspeed] - - wget https://raw.githubusercontent.com/huggingface/trl/main/examples/accelerate_configs/fsdp1.yaml - - wget https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-3/qlora-fsdp-70b.yaml - # Axolotl includes hf-xet version 1.1.0, which fails during downloads. Replacing it with the latest version (1.1.2). - - pip uninstall -y hf-xet - - pip install hf-xet --no-cache-dir - - | - accelerate launch \ - --config_file=fsdp1.yaml \ - -m axolotl.cli.train qlora-fsdp-70b.yaml \ - --hub-model-id $HUB_MODEL_ID \ - --output-dir /checkpoints/qlora-llama3-70b \ - --wandb-project $WANDB_PROJECT \ - --wandb-name $DSTACK_RUN_NAME \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM - -resources: - gpu: 80GB:8 - shm_size: 128GB - -volumes: - - /checkpoints:/checkpoints -``` -
- -!!! info "Docker image" - We are using `nvcr.io/nvidia/pytorch:25.01-py3` from NGC because it includes the necessary libraries and packages for RDMA and InfiniBand support. - -### Apply the configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ WANDB_PROJECT=... -$ HUB_MODEL_ID=... -$ dstack apply -f examples/distributed-training/trl/fsdp.dstack.yml - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 ssh (remote) cpu=208 mem=1772GB H100:80GB:8 instance $0 idle - 2 ssh (remote) cpu=208 mem=1772GB H100:80GB:8 instance $0 idle - -Submit the run trl-train-fsdp-distrib? [y/n]: y - -Provisioning... ----> 100% -``` -
- -## Source code - -The source-code of this example can be found in -[`examples/distributed-training/axolotl`](https://github.com/dstackai/dstack/blob/master/examples/distributed-training/axolotl). - -!!! info "What's next?" - 1. Read the [clusters](https://dstack.ai/docs/guides/clusters) guide - 2. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), - [services](https://dstack.ai/docs/concepts/services), and [fleets](https://dstack.ai/docs/concepts/fleets) diff --git a/examples/distributed-training/axolotl/fleet.dstack.yml b/examples/distributed-training/axolotl/fleet.dstack.yml deleted file mode 100644 index a522642091..0000000000 --- a/examples/distributed-training/axolotl/fleet.dstack.yml +++ /dev/null @@ -1,9 +0,0 @@ -type: fleet -name: axolotl-fleet - -nodes: 2 -placement: cluster - -resources: - gpu: 80GB:8 - shm_size: 128GB diff --git a/examples/distributed-training/ray-ragen/.dstack.yml b/examples/distributed-training/ray-ragen/.dstack.yml deleted file mode 100644 index 8dabde9e04..0000000000 --- a/examples/distributed-training/ray-ragen/.dstack.yml +++ /dev/null @@ -1,39 +0,0 @@ -type: task -name: ray-ragen-cluster - -nodes: 2 - -env: -- WANDB_API_KEY -image: whatcanyousee/verl:ngc-cu124-vllm0.8.5-sglang0.4.6-mcore0.12.0-te2.2 -commands: - - wget -O miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh - - bash miniconda.sh -b -p /workflow/miniconda - - eval "$(/workflow/miniconda/bin/conda shell.bash hook)" - - git clone https://github.com/RAGEN-AI/RAGEN.git - - cd RAGEN - - bash scripts/setup_ragen.sh - - conda activate ragen - - cd verl - - pip install --no-deps -e . - - pip install hf_transfer hf_xet - - pip uninstall -y ray - - pip install -U "ray[default]" - - | - if [ $DSTACK_NODE_RANK = 0 ]; then - ray start --head --port=6379; - else - ray start --address=$DSTACK_MASTER_NODE_IP:6379 - fi - -# Expose Ray dashboard port -ports: - - 8265 - -resources: - gpu: 80GB:8 - shm_size: 128GB - -# Save checkpoints on the instance -volumes: - - /checkpoints:/checkpoints diff --git a/examples/distributed-training/ray-ragen/fleet.dstack.yml b/examples/distributed-training/ray-ragen/fleet.dstack.yml deleted file mode 100644 index 04cd389254..0000000000 --- a/examples/distributed-training/ray-ragen/fleet.dstack.yml +++ /dev/null @@ -1,9 +0,0 @@ -type: fleet -name: ray-ragen-cluster-fleet - -nodes: 2 -placement: cluster - -resources: - gpu: 80GB:8 - shm_size: 128GB diff --git a/examples/distributed-training/trl/README.md b/examples/distributed-training/trl/README.md deleted file mode 100644 index c6231e5170..0000000000 --- a/examples/distributed-training/trl/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# TRL - -This example walks you through how to run distributed fine-tune using [TRL](https://github.com/huggingface/trl), [Accelerate](https://github.com/huggingface/accelerate) and [Deepspeed](https://github.com/deepspeedai/DeepSpeed). - -!!! info "Prerequisites" - Before running a distributed task, make sure to create a fleet with `placement` set to `cluster` (can be a [managed fleet](https://dstack.ai/docs/concepts/fleets#backend-placement) or an [SSH fleet](https://dstack.ai/docs/concepts/fleets#ssh-placement)). - -## Define a configuration - -Once the fleet is created, define a distributed task configuration. Here's an example of such a task. - -=== "FSDP" - -
- ```yaml - type: task - name: trl-train-fsdp-distrib - - nodes: 2 - - image: nvcr.io/nvidia/pytorch:25.01-py3 - - env: - - HF_TOKEN - - ACCELERATE_LOG_LEVEL=info - - WANDB_API_KEY - - MODEL_ID=meta-llama/Llama-3.1-8B - - HUB_MODEL_ID - - commands: - - pip install transformers bitsandbytes peft wandb - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/fsdp1.yaml \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM \ - trl/scripts/sft.py \ - --model_name $MODEL_ID \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --attn_implementation flash_attention_2 \ - --logging_steps=10 \ - --output_dir /checkpoints/llama31-ft \ - --hub_model_id $HUB_MODEL_ID \ - --torch_dtype bfloat16 - - resources: - gpu: 80GB:8 - shm_size: 128GB - - volumes: - - /checkpoints:/checkpoints - ``` -
- -=== "Deepseed ZeRO-3" - -
- ```yaml - type: task - name: trl-train-deepspeed-distrib - - nodes: 2 - - image: nvcr.io/nvidia/pytorch:25.01-py3 - - env: - - HF_TOKEN - - WANDB_API_KEY - - HUB_MODEL_ID - - MODEL_ID=meta-llama/Llama-3.1-8B - - ACCELERATE_LOG_LEVEL=info - - commands: - - pip install transformers bitsandbytes peft wandb deepspeed - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/deepspeed_zero3.yaml \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM \ - trl/scripts/sft.py \ - --model_name $MODEL_ID \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --attn_implementation flash_attention_2 \ - --logging_steps=10 \ - --output_dir /checkpoints/llama31-ft \ - --hub_model_id $HUB_MODEL_ID \ - --torch_dtype bfloat16 - - resources: - gpu: 80GB:8 - shm_size: 128GB - - volumes: - - /checkpoints:/checkpoints - ``` -
- -!!! info "Docker image" - We are using `nvcr.io/nvidia/pytorch:25.01-py3` from NGC because it includes the necessary libraries and packages for RDMA and InfiniBand support. - -### Apply the configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ HUB_MODEL_ID=... -$ dstack apply -f examples/distributed-training/trl/fsdp.dstack.yml - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 ssh (remote) cpu=208 mem=1772GB H100:80GB:8 instance $0 idle - 2 ssh (remote) cpu=208 mem=1772GB H100:80GB:8 instance $0 idle - -Submit the run trl-train-fsdp-distrib? [y/n]: y - -Provisioning... ----> 100% -``` -
- -## Source code - -The source-code of this example can be found in -[`examples/distributed-training/trl`](https://github.com/dstackai/dstack/blob/master/examples/distributed-training/trl). - -!!! info "What's next?" - 1. Read the [clusters](https://dstack.ai/docs/guides/clusters) guide - 2. Check [dev environments](https://dstack.ai/docs/concepts/dev-environments), [tasks](https://dstack.ai/docs/concepts/tasks), - [services](https://dstack.ai/docs/concepts/services), and [fleets](https://dstack.ai/docs/concepts/fleets) diff --git a/examples/distributed-training/trl/deepspeed.dstack.yml b/examples/distributed-training/trl/deepspeed.dstack.yml deleted file mode 100644 index 972351f6ac..0000000000 --- a/examples/distributed-training/trl/deepspeed.dstack.yml +++ /dev/null @@ -1,52 +0,0 @@ -type: task -name: trl-train-deepspeed-distrib - -# Size of the cluster -nodes: 2 - -image: nvcr.io/nvidia/pytorch:25.01-py3 - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - HUB_MODEL_ID - - MODEL_ID=meta-llama/Llama-3.1-8B - - ACCELERATE_LOG_LEVEL=info -# Commands of the task -commands: - - pip install transformers bitsandbytes peft wandb deepspeed - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/deepspeed_zero3.yaml \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM \ - trl/scripts/sft.py \ - --model_name $MODEL_ID \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --attn_implementation flash_attention_2 \ - --logging_steps=10 \ - --output_dir /checkpoints/llama31-ft \ - --hub_model_id $HUB_MODEL_ID \ - --torch_dtype bfloat16 - -resources: - gpu: 80GB:8 - shm_size: 128GB - -volumes: - - /checkpoints:/checkpoints diff --git a/examples/distributed-training/trl/fleet.dstack.yml b/examples/distributed-training/trl/fleet.dstack.yml deleted file mode 100644 index 1275794e8c..0000000000 --- a/examples/distributed-training/trl/fleet.dstack.yml +++ /dev/null @@ -1,9 +0,0 @@ -type: fleet -name: trl-train-fleet - -nodes: 2 -placement: cluster - -resources: - gpu: 80GB:8 - shm_size: 128GB diff --git a/examples/distributed-training/trl/fsdp.dstack.yml b/examples/distributed-training/trl/fsdp.dstack.yml deleted file mode 100644 index b00104033b..0000000000 --- a/examples/distributed-training/trl/fsdp.dstack.yml +++ /dev/null @@ -1,52 +0,0 @@ -type: task -name: trl-train-fsdp-distrib - -# Size of the cluster -nodes: 2 - -image: nvcr.io/nvidia/pytorch:25.01-py3 - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - HUB_MODEL_ID - - MODEL_ID=meta-llama/Llama-3.1-8B - - ACCELERATE_LOG_LEVEL=info -# Commands of the task -commands: - - pip install transformers bitsandbytes peft wandb - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/fsdp1.yaml \ - --main_process_ip=$DSTACK_MASTER_NODE_IP \ - --main_process_port=8008 \ - --machine_rank=$DSTACK_NODE_RANK \ - --num_processes=$DSTACK_GPUS_NUM \ - --num_machines=$DSTACK_NODES_NUM \ - trl/scripts/sft.py \ - --model_name $MODEL_ID \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --attn_implementation flash_attention_2 \ - --logging_steps=10 \ - --output_dir /checkpoints/llama31-ft \ - --hub_model_id $HUB_MODEL_ID \ - --torch_dtype bfloat16 - -resources: - gpu: 80GB:8 - shm_size: 128GB - -volumes: - - /checkpoints:/checkpoints diff --git a/examples/inference/nim/.dstack.yml b/examples/inference/nim/.dstack.yml deleted file mode 100644 index 4a7d33406b..0000000000 --- a/examples/inference/nim/.dstack.yml +++ /dev/null @@ -1,27 +0,0 @@ -type: service -name: serve-distill-deepseek - -image: nvcr.io/nim/deepseek-ai/deepseek-r1-distill-llama-8b -env: - - NGC_API_KEY - - NIM_MAX_MODEL_LEN=4096 -registry_auth: - username: $oauthtoken - password: ${{ env.NGC_API_KEY }} -port: 8000 -# Register the model -model: deepseek-ai/deepseek-r1-distill-llama-8b - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Cache downloaded models -volumes: - - instance_path: /root/.cache/nim - path: /opt/nim/.cache - optional: true - -resources: - gpu: A100:40GB - # Uncomment if using multiple GPUs - #shm_size: 16GB diff --git a/examples/inference/nim/README.md b/examples/inference/nim/README.md deleted file mode 100644 index 1b125dda83..0000000000 --- a/examples/inference/nim/README.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: NVIDIA NIM -description: "This example shows how to deploy DeepSeek-R1-Distill-Llama-8B to any cloud or on-premises environment using NVIDIA NIM and dstack." ---- - -# NVIDIA NIM - -This example shows how to deploy DeepSeek-R1-Distill-Llama-8B using [NVIDIA NIM](https://docs.nvidia.com/nim/large-language-models/latest/getting-started.html) and `dstack`. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -Here's an example of a service that deploys DeepSeek-R1-Distill-Llama-8B using NIM. - -
- -```yaml -type: service -name: serve-distill-deepseek - -image: nvcr.io/nim/deepseek-ai/deepseek-r1-distill-llama-8b -env: - - NGC_API_KEY - - NIM_MAX_MODEL_LEN=4096 -registry_auth: - username: $oauthtoken - password: ${{ env.NGC_API_KEY }} -port: 8000 -# Register the model -model: deepseek-ai/deepseek-r1-distill-llama-8b - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Cache downloaded models -volumes: - - instance_path: /root/.cache/nim - path: /opt/nim/.cache - optional: true - -resources: - gpu: A100:40GB - # Uncomment if using multiple GPUs - #shm_size: 16GB -``` -
- -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ NGC_API_KEY=... -$ dstack apply -f examples/inference/nim/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vultr ewr 6xCPU, 60GB, 1xA100 (40GB) no $1.199 - 2 vultr ewr 6xCPU, 60GB, 1xA100 (40GB) no $1.199 - 3 vultr nrt 6xCPU, 60GB, 1xA100 (40GB) no $1.199 - -Submit the run serve-distill-deepseek? [y/n]: y - -Provisioning... ----> 100% -``` -
- -If no gateway is created, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "meta/llama3-8b-instruct", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "max_tokens": 128 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -## Source code - -The source-code of this example can be found in -[`examples/inference/nim`](https://github.com/dstackai/dstack/blob/master/examples/inference/nim). - -## What's next? - -1. Check [services](https://dstack.ai/docs/services) -2. Browse the [DeepSeek AI NIM](https://build.nvidia.com/deepseek-ai) diff --git a/examples/inference/sglang/README.md b/examples/inference/sglang/README.md deleted file mode 100644 index d0c42bf2ee..0000000000 --- a/examples/inference/sglang/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# SGLang - -This example shows how to deploy DeepSeek-R1-Distill-Llama 8B and 70B using [SGLang](https://github.com/sgl-project/sglang) and `dstack`. - -## Apply a configuration - -Here's an example of a service that deploys DeepSeek-R1-Distill-Llama 8B and 70B using SgLang. - -=== "NVIDIA" - -
- - ```yaml - type: service - name: deepseek-r1-nvidia - - image: lmsysorg/sglang:latest - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - resources: - gpu: 24GB - ``` -
- -=== "AMD" - -
- - ```yaml - type: service - name: deepseek-r1-amd - - image: lmsysorg/sglang:v0.4.1.post4-rocm620 - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - resources: - gpu: MI300x - disk: 300GB - ``` -
- -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ dstack apply -f examples/llms/deepseek/sglang/amd/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod EU-RO-1 24xCPU, 283GB, 1xMI300X (192GB) no $2.49 - -Submit the run deepseek-r1-amd? [y/n]: y - -Provisioning... ----> 100% -``` -
- -Once the service is up, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "stream": true, - "max_tokens": 512 - }' -``` -
- -!!! info "SGLang Model Gateway" - If you'd like to use a custom routing policy, e.g. by leveraging the [SGLang Model Gateway](https://docs.sglang.ai/advanced_features/router.html#), create a gateway with `router` set to `sglang`. Check out [gateways](https://dstack.ai/docs/concepts/gateways#router) for more details. - -> If a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured (e.g. to enable auto-scaling or HTTPs, rate-limits, etc), the OpenAI-compatible endpoint is available at `https://gateway./`. - -## Source code - -The source-code of this example can be found in -[`examples/llms/deepseek/sglang`](https://github.com/dstackai/dstack/blob/master/examples/llms/deepseek/sglang). - -## What's next? - -1. Read about [services](https://dstack.ai/docs/concepts/services) and [gateways](https://dstack.ai/docs/concepts/gateways) -2. Browse the [SgLang DeepSeek Usage](https://docs.sglang.ai/references/deepseek.html), [Supercharge DeepSeek-R1 Inference on AMD Instinct MI300X](https://rocm.blogs.amd.com/artificial-intelligence/DeepSeekR1-Part2/README.html) diff --git a/examples/inference/tgi/.dstack.yml b/examples/inference/tgi/.dstack.yml deleted file mode 100644 index 67fe1179d4..0000000000 --- a/examples/inference/tgi/.dstack.yml +++ /dev/null @@ -1,32 +0,0 @@ -type: service -name: llama4-scout - -image: ghcr.io/huggingface/text-generation-inference:latest - -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - MAX_INPUT_LENGTH=8192 - - MAX_TOTAL_TOKENS=16384 - # max_batch_prefill_tokens must be >= max_input_tokens - - MAX_BATCH_PREFILL_TOKENS=8192 -commands: - # Activate the virtual environment at /usr/src/.venv/ - # as required by TGI's latest image. - - . /usr/src/.venv/bin/activate - - NUM_SHARD=$DSTACK_GPUS_NUM text-generation-launcher - -port: 80 -# Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Uncomment to cache downloaded models -#volumes: -# - /data:/data - -resources: - gpu: H200:2 - disk: 500GB.. diff --git a/examples/inference/tgi/README.md b/examples/inference/tgi/README.md deleted file mode 100644 index 6984ec2ff6..0000000000 --- a/examples/inference/tgi/README.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: HuggingFace TGI -description: "This example shows how to deploy Llama 4 Scout to any cloud or on-premises environment using HuggingFace TGI and dstack." ---- - -# HuggingFace TGI - -This example shows how to deploy Llama 4 Scout with `dstack` using [HuggingFace TGI](https://huggingface.co/docs/text-generation-inference/en/index). - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -Here's an example of a service that deploys [`Llama-4-Scout-17B-16E-Instruct`](https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct) using TGI. - -
- -```yaml -type: service -name: llama4-scout - -image: ghcr.io/huggingface/text-generation-inference:latest - -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - MAX_INPUT_LENGTH=8192 - - MAX_TOTAL_TOKENS=16384 - # max_batch_prefill_tokens must be >= max_input_tokens - - MAX_BATCH_PREFILL_TOKENS=8192 -commands: - # Activate the virtual environment at /usr/src/.venv/ - # as required by TGI's latest image. - - . /usr/src/.venv/bin/activate - - NUM_SHARD=$DSTACK_GPUS_NUM text-generation-launcher - -port: 80 -# Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Uncomment to cache downloaded models -#volumes: -# - /data:/data - -resources: - gpu: H200:2 - disk: 500GB.. -``` -
- -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ dstack apply -f examples/inference/tgi/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai is-iceland 48xCPU, 128GB, 2xH200 (140GB) no $7.87 - 2 runpod EU-SE-1 40xCPU, 128GB, 2xH200 (140GB) no $7.98 - -Submit the run llama4-scout? [y/n]: y - -Provisioning... ----> 100% -``` -
- -If no gateway is created, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "meta-llama/Llama-4-Scout-17B-16E-Instruct", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "max_tokens": 128 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -## Source code - -The source-code of this example can be found in -[`examples/inference/tgi`](https://github.com/dstackai/dstack/blob/master/examples/inference/tgi). - -## What's next? - -1. Check [services](https://dstack.ai/docs/services) -2. Browse the [Llama](https://dstack.ai/examples/llms/llama/), [vLLM](https://dstack.ai/examples/inference/vllm/), [SgLang](https://dstack.ai/examples/inference/sglang/) and [NIM](https://dstack.ai/examples/inference/nim/) examples -3. See also [AMD](https://dstack.ai/examples/accelerators/amd/) and - [TPU](https://dstack.ai/examples/accelerators/tpu/) diff --git a/examples/inference/tgi/amd/.dstack.yml b/examples/inference/tgi/amd/.dstack.yml deleted file mode 100644 index 46c2239688..0000000000 --- a/examples/inference/tgi/amd/.dstack.yml +++ /dev/null @@ -1,21 +0,0 @@ -type: service -name: amd-service-tgi - -image: ghcr.io/huggingface/text-generation-inference:sha-a379d55-rocm -env: - - HF_TOKEN - - ROCM_USE_FLASH_ATTN_V2_TRITON=true - - TRUST_REMOTE_CODE=true - - MODEL_ID=meta-llama/Meta-Llama-3.1-70B-Instruct -commands: - - text-generation-launcher --port 8000 -port: 8000 -# Register the model -model: meta-llama/Meta-Llama-3.1-70B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - gpu: MI300X - disk: 150GB diff --git a/examples/inference/tgi/tpu/.dstack.yml b/examples/inference/tgi/tpu/.dstack.yml deleted file mode 100644 index 42ba5ab7fd..0000000000 --- a/examples/inference/tgi/tpu/.dstack.yml +++ /dev/null @@ -1,27 +0,0 @@ -type: service -# The name is optional, if not specified, generated randomly -name: llama31-service-optimum-tpu - -# Using a Docker image with a fix instead of the official one -# More details at https://github.com/huggingface/optimum-tpu/pull/92 -image: dstackai/optimum-tpu:llama31 -# Required environment variables -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_TOTAL_TOKENS=4096 - - MAX_BATCH_PREFILL_TOKENS=4095 -commands: - - text-generation-launcher --port 8000 -port: 8000 -model: - format: tgi - type: chat - name: meta-llama/Meta-Llama-3.1-8B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - # Required resources - gpu: v5litepod-4 diff --git a/examples/inference/trtllm/README.md b/examples/inference/trtllm/README.md deleted file mode 100644 index 02ae55f464..0000000000 --- a/examples/inference/trtllm/README.md +++ /dev/null @@ -1,374 +0,0 @@ ---- -title: TensorRT-LLM -description: "This example shows how to deploy Deepseek models to any cloud or on-premises environment using NVIDIA TensorRT-LLM and dstack." ---- - -# TensorRT-LLM - -This example shows how to deploy both DeepSeek R1 and its distilled version -using [TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) and `dstack`. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -### DeepSeek R1 - -We normally use Triton with the TensorRT-LLM backend to serve models. While this works for the distilled Llama-based -version, DeepSeek R1 isn’t yet compatible. So, for DeepSeek R1, we’ll use `trtllm-serve` with the PyTorch backend instead. - -To use `trtllm-serve`, we first need to build the TensorRT-LLM Docker image from the `main` branch. - -#### Build a Docker image - -Here’s the task config that builds the image and pushes it using the provided Docker credentials. - -
- -```yaml -type: task -name: build-image - -privileged: true -image: dstackai/dind -env: - - DOCKER_USERNAME - - DOCKER_PASSWORD -commands: - - start-dockerd - - apt update && apt-get install -y build-essential make git git-lfs - - git lfs install - - git clone https://github.com/NVIDIA/TensorRT-LLM.git - - cd TensorRT-LLM - - git submodule update --init --recursive - - git lfs pull - # Limit compilation to Hopper for a smaller image - - make -C docker release_build CUDA_ARCHS="90-real" - - docker tag tensorrt_llm/release:latest $DOCKER_USERNAME/tensorrt_llm:latest - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker push "$DOCKER_USERNAME/tensorrt_llm:latest" - -resources: - cpu: 8 - disk: 500GB.. -``` -
- -To run it, pass the task configuration to `dstack apply`. - -
- -```shell -$ dstack apply -f examples/inference/trtllm/build-image.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 cudo ca-montreal-2 8xCPU, 25GB, (500.0GB) yes $0.1073 - -Submit the run build-image? [y/n]: y - -Provisioning... ----> 100% -``` -
- -#### Deploy the model - -Below is the service configuration that deploys DeepSeek R1 using the built TensorRT-LLM image. - -
- - ```yaml - type: service - name: serve-r1 - - # Specify the image built with `examples/inference/trtllm/build-image.dstack.yml` - image: dstackai/tensorrt_llm:9b931c0f6305aefa3660e6fb84a76a42c0eef167 - env: - - MAX_BATCH_SIZE=256 - - MAX_NUM_TOKENS=16384 - - MAX_SEQ_LENGTH=16384 - - EXPERT_PARALLEL=4 - - PIPELINE_PARALLEL=1 - - HF_HUB_ENABLE_HF_TRANSFER=1 - commands: - - pip install -U "huggingface_hub[cli]" - - pip install hf_transfer - - huggingface-cli download deepseek-ai/DeepSeek-R1 --local-dir DeepSeek-R1 - - trtllm-serve - --backend pytorch - --max_batch_size $MAX_BATCH_SIZE - --max_num_tokens $MAX_NUM_TOKENS - --max_seq_len $MAX_SEQ_LENGTH - --tp_size $DSTACK_GPUS_NUM - --ep_size $EXPERT_PARALLEL - --pp_size $PIPELINE_PARALLEL - DeepSeek-R1 - port: 8000 - model: deepseek-ai/DeepSeek-R1 - - resources: - gpu: 8:H200 - shm_size: 32GB - disk: 2000GB.. - ``` -
- - -To run it, pass the configuration to `dstack apply`. - -
- -```shell -$ dstack apply -f examples/inference/trtllm/serve-r1.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai is-iceland 192xCPU, 2063GB, 8xH200 (141GB) yes $25.62 - -Submit the run serve-r1? [y/n]: y - -Provisioning... ----> 100% -``` -
- - -### DeepSeek R1 Distill Llama 8B - -To deploy DeepSeek R1 Distill Llama 8B, follow the steps below. - -#### Convert and upload checkpoints - -Here’s the task config that converts a Hugging Face model to a TensorRT-LLM checkpoint format -and uploads it to S3 using the provided AWS credentials. - -
- - ```yaml - type: task - name: convert-model - - image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - env: - - HF_TOKEN - - MODEL_REPO=https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - commands: - # nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 container uses TensorRT-LLM version 0.17.0, - # therefore we are using branch v0.17.0 - - git clone --branch v0.17.0 --depth 1 https://github.com/triton-inference-server/tensorrtllm_backend.git - - git clone --branch v0.17.0 --single-branch https://github.com/NVIDIA/TensorRT-LLM.git - - git clone https://github.com/triton-inference-server/server.git - - cd TensorRT-LLM/examples/llama - - apt-get -y install git git-lfs - - git lfs install - - git config --global credential.helper store - - huggingface-cli login --token $HF_TOKEN --add-to-git-credential - - git clone $MODEL_REPO - - python3 convert_checkpoint.py --model_dir DeepSeek-R1-Distill-Llama-8B --output_dir tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --dtype bfloat16 --tp_size $DSTACK_GPUS_NUM - # Download the AWS CLI - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 s3://${S3_BUCKET_NAME}/tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --acl public-read - - resources: - gpu: A100:40GB - - ``` -
- - -To run it, pass the configuration to `dstack apply`. - -
- -```shell -$ dstack apply -f examples/inference/trtllm/convert-model.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai us-iowa 12xCPU, 85GB, 1xA100 (40GB) yes $0.66904 - -Submit the run convert-model? [y/n]: y - -Provisioning... ----> 100% -``` -
- - -#### Build and upload the model - -Here’s the task config that builds a TensorRT-LLM model and uploads it to S3 with the provided AWS credentials. - -
- - ```yaml - type: task - name: build-model - - image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - env: - - MODEL=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - - MAX_SEQ_LEN=8192 # Sum of Max Input Length & Max Output Length - - MAX_INPUT_LEN=4096 - - MAX_BATCH_SIZE=256 - - TRITON_MAX_BATCH_SIZE=1 - - INSTANCE_COUNT=1 - - MAX_QUEUE_DELAY_MS=0 - - MAX_QUEUE_SIZE=0 - - DECOUPLED_MODE=true # Set true for streaming - commands: - - huggingface-cli download $MODEL --exclude '*.safetensors' --local-dir tokenizer_dir - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync s3://${S3_BUCKET_NAME}/tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 ./tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 - - trtllm-build --checkpoint_dir tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --gemm_plugin bfloat16 --output_dir tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --max_seq_len $MAX_SEQ_LEN --max_input_len $MAX_INPUT_LEN --max_batch_size $MAX_BATCH_SIZE --gpt_attention_plugin bfloat16 --use_paged_context_fmha enable - - git clone --branch v0.17.0 --single-branch https://github.com/NVIDIA/TensorRT-LLM.git - - python3 TensorRT-LLM/examples/run.py --engine_dir tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --max_output_len 40 --tokenizer_dir tokenizer_dir --input_text "What is Deep Learning?" - - git clone --branch v0.17.0 --depth 1 https://github.com/triton-inference-server/tensorrtllm_backend.git - - mkdir triton_model_repo - - cp -r tensorrtllm_backend/all_models/inflight_batcher_llm/* triton_model_repo/ - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/ensemble/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},logits_datatype:TYPE_BF16 - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/preprocessing/config.pbtxt tokenizer_dir:tokenizer_dir,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},preprocessing_instance_count:${INSTANCE_COUNT} - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},engine_dir:tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16,max_queue_delay_microseconds:${MAX_QUEUE_DELAY_MS},batching_strategy:inflight_fused_batching,max_queue_size:${MAX_QUEUE_SIZE},encoder_input_features_data_type:TYPE_BF16,logits_datatype:TYPE_BF16 - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/postprocessing/config.pbtxt tokenizer_dir:tokenizer_dir,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},postprocessing_instance_count:${INSTANCE_COUNT},max_queue_size:${MAX_QUEUE_SIZE} - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},bls_instance_count:${INSTANCE_COUNT},logits_datatype:TYPE_BF16 - - aws s3 sync triton_model_repo s3://${S3_BUCKET_NAME}/triton_model_repo --acl public-read - - aws s3 sync tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 s3://${S3_BUCKET_NAME}/tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --acl public-read - - resources: - gpu: A100:40GB - ``` -
- -To run it, pass the configuration to `dstack apply`. - -
- -```shell -$ dstack apply -f examples/inference/trtllm/build-model.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai us-iowa 12xCPU, 85GB, 1xA100 (40GB) yes $0.66904 - -Submit the run build-model? [y/n]: y - -Provisioning... ----> 100% -``` -
- -#### Deploy the model - -Below is the service configuration that deploys DeepSeek R1 Distill Llama 8B. - -
- -```yaml - type: service - name: serve-distill - - image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - env: - - MODEL=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - - commands: - - huggingface-cli download $MODEL --exclude '*.safetensors' --local-dir tokenizer_dir - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync s3://${S3_BUCKET_NAME}/tllm_engine_1gpu_bf16 ./tllm_engine_1gpu_bf16 - - git clone https://github.com/triton-inference-server/server.git - - python3 server/python/openai/openai_frontend/main.py --model-repository s3://${S3_BUCKET_NAME}/triton_model_repo --tokenizer tokenizer_dir --openai-port 8000 - port: 8000 - model: ensemble - - resources: - gpu: A100:40GB - -``` -
- -To run it, pass the configuration to `dstack apply`. - -
- -```shell -$ dstack apply -f examples/inference/trtllm/serve-distill.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai us-iowa 12xCPU, 85GB, 1xA100 (40GB) yes $0.66904 - -Submit the run serve-distill? [y/n]: y - -Provisioning... ----> 100% -``` -
- -## Access the endpoint - -If no gateway is created, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "deepseek-ai/DeepSeek-R1", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "stream": true, - "max_tokens": 128 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -## Source code - -The source-code of this example can be found in -[`examples/inference/trtllm`](https://github.com/dstackai/dstack/blob/master/examples/inference/trtllm). - -## What's next? - -1. Check [services](https://dstack.ai/docs/services) -2. Browse [Tensorrt-LLM DeepSeek-R1 with PyTorch Backend](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/deepseek_v3) and [Prepare the Model Repository](https://github.com/triton-inference-server/tensorrtllm_backend?tab=readme-ov-file#prepare-the-model-repository) -3. See also [`trtllm-serve`](https://nvidia.github.io/TensorRT-LLM/commands/trtllm-serve.html#trtllm-serve) diff --git a/examples/inference/trtllm/build-image.dstack.yml b/examples/inference/trtllm/build-image.dstack.yml deleted file mode 100644 index 6761379299..0000000000 --- a/examples/inference/trtllm/build-image.dstack.yml +++ /dev/null @@ -1,25 +0,0 @@ -type: task -name: build-image - -privileged: true -image: dstackai/dind -env: - - DOCKER_USERNAME - - DOCKER_PASSWORD -commands: - - start-dockerd - - apt update && apt-get install -y build-essential make git git-lfs - - git lfs install - - git clone https://github.com/NVIDIA/TensorRT-LLM.git - - cd TensorRT-LLM - - git submodule update --init --recursive - - git lfs pull - # Limit compilation to Hopper for a smaller image - - make -C docker release_build CUDA_ARCHS="90-real" - - docker tag tensorrt_llm/release:latest $DOCKER_USERNAME/tensorrt_llm:latest - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker push "$DOCKER_USERNAME/tensorrt_llm:latest" - -resources: - cpu: 8 - disk: 500GB.. diff --git a/examples/inference/trtllm/build-model.dstack.yml b/examples/inference/trtllm/build-model.dstack.yml deleted file mode 100644 index b02b87644d..0000000000 --- a/examples/inference/trtllm/build-model.dstack.yml +++ /dev/null @@ -1,44 +0,0 @@ -type: task -name: build-model - -image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - - -env: - - MODEL=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - - MAX_SEQ_LEN=8192 # Sum of Max Input Length & Max Output Length - - MAX_INPUT_LEN=4096 - - MAX_BATCH_SIZE=256 - - TRITON_MAX_BATCH_SIZE=1 - - INSTANCE_COUNT=1 - - MAX_QUEUE_DELAY_MS=0 - - MAX_QUEUE_SIZE=0 - - DECOUPLED_MODE=true # Set true for streaming - -commands: - - huggingface-cli download $MODEL --exclude '*.safetensors' --local-dir tokenizer_dir - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync s3://${S3_BUCKET_NAME}/tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 ./tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 - - trtllm-build --checkpoint_dir tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --gemm_plugin bfloat16 --output_dir tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --max_seq_len $MAX_SEQ_LEN --max_input_len $MAX_INPUT_LEN --max_batch_size $MAX_BATCH_SIZE --gpt_attention_plugin bfloat16 --use_paged_context_fmha enable - - git clone --branch v0.17.0 --single-branch https://github.com/NVIDIA/TensorRT-LLM.git - - python3 TensorRT-LLM/examples/run.py --engine_dir tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --max_output_len 40 --tokenizer_dir tokenizer_dir --input_text "What is Deep Learning?" - - git clone --branch v0.17.0 --depth 1 https://github.com/triton-inference-server/tensorrtllm_backend.git - - mkdir triton_model_repo - - cp -r tensorrtllm_backend/all_models/inflight_batcher_llm/* triton_model_repo/ - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/ensemble/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},logits_datatype:TYPE_BF16 - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/preprocessing/config.pbtxt tokenizer_dir:tokenizer_dir,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},preprocessing_instance_count:${INSTANCE_COUNT} - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},engine_dir:tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16,max_queue_delay_microseconds:${MAX_QUEUE_DELAY_MS},batching_strategy:inflight_fused_batching,max_queue_size:${MAX_QUEUE_SIZE},encoder_input_features_data_type:TYPE_BF16,logits_datatype:TYPE_BF16 - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/postprocessing/config.pbtxt tokenizer_dir:tokenizer_dir,triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},postprocessing_instance_count:${INSTANCE_COUNT},max_queue_size:${MAX_QUEUE_SIZE} - - python3 tensorrtllm_backend/tools/fill_template.py -i triton_model_repo/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:${TRITON_MAX_BATCH_SIZE},decoupled_mode:${DECOUPLED_MODE},bls_instance_count:${INSTANCE_COUNT},logits_datatype:TYPE_BF16 - - aws s3 sync triton_model_repo s3://${S3_BUCKET_NAME}/triton_model_repo --acl public-read - - aws s3 sync tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 s3://${S3_BUCKET_NAME}/tllm_engine_${DSTACK_GPUS_NUM}gpu_bf16 --acl public-read - - -resources: - gpu: A100:40GB diff --git a/examples/inference/trtllm/convert-model.dstack.yml b/examples/inference/trtllm/convert-model.dstack.yml deleted file mode 100644 index 262e8f2945..0000000000 --- a/examples/inference/trtllm/convert-model.dstack.yml +++ /dev/null @@ -1,34 +0,0 @@ -type: task -name: convert-model - -image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - -env: - - HF_TOKEN - - MODEL_REPO=https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - -commands: - # nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 container uses TensorRT-LLM version 0.17.0, - # therefore we are using branch v0.17.0 - - git clone --branch v0.17.0 --depth 1 https://github.com/triton-inference-server/tensorrtllm_backend.git - - git clone --branch v0.17.0 --single-branch https://github.com/NVIDIA/TensorRT-LLM.git - - git clone https://github.com/triton-inference-server/server.git - - cd TensorRT-LLM/examples/llama - - apt-get -y install git git-lfs - - git lfs install - - git config --global credential.helper store - - huggingface-cli login --token $HF_TOKEN --add-to-git-credential - - git clone $MODEL_REPO - - python3 convert_checkpoint.py --model_dir DeepSeek-R1-Distill-Llama-8B --output_dir tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --dtype bfloat16 --tp_size $DSTACK_GPUS_NUM - # Download the AWS CLI - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 s3://${S3_BUCKET_NAME}/tllm_checkpoint_${DSTACK_GPUS_NUM}gpu_bf16 --acl public-read - -resources: - gpu: A100:40GB diff --git a/examples/inference/trtllm/serve-distill.dstack.yml b/examples/inference/trtllm/serve-distill.dstack.yml deleted file mode 100644 index bc5ad6d028..0000000000 --- a/examples/inference/trtllm/serve-distill.dstack.yml +++ /dev/null @@ -1,28 +0,0 @@ -type: service -name: serve-distill - -image: nvcr.io/nvidia/tritonserver:25.01-trtllm-python-py3 - -env: - - MODEL=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - S3_BUCKET_NAME - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_DEFAULT_REGION - -commands: - - huggingface-cli download $MODEL --exclude '*.safetensors' --local-dir tokenizer_dir - - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - - unzip awscliv2.zip - - ./aws/install - - aws s3 sync s3://${S3_BUCKET_NAME}/tllm_engine_1gpu_bf16 ./tllm_engine_1gpu_bf16 - - git clone https://github.com/triton-inference-server/server.git - - python3 server/python/openai/openai_frontend/main.py --model-repository s3://${S3_BUCKET_NAME}/triton_model_repo --tokenizer tokenizer_dir --openai-port 8000 - - -port: 8000 - -model: ensemble - -resources: - gpu: A100:40GB diff --git a/examples/inference/trtllm/serve-r1.dstack.yml b/examples/inference/trtllm/serve-r1.dstack.yml deleted file mode 100644 index f5e9091720..0000000000 --- a/examples/inference/trtllm/serve-r1.dstack.yml +++ /dev/null @@ -1,32 +0,0 @@ -type: service -name: serve-r1 - -# Specify the image built with `examples/inference/trtllm/build-image.dstack.yml` -image: dstackai/tensorrt_llm:9b931c0f6305aefa3660e6fb84a76a42c0eef167 -env: - - MAX_BATCH_SIZE=256 - - MAX_NUM_TOKENS=16384 - - MAX_SEQ_LENGTH=16384 - - EXPERT_PARALLEL=4 - - PIPELINE_PARALLEL=1 - - HF_HUB_ENABLE_HF_TRANSFER=1 -commands: - - pip install -U "huggingface_hub[cli]" - - pip install hf_transfer - - huggingface-cli download deepseek-ai/DeepSeek-R1 --local-dir DeepSeek-R1 - - trtllm-serve - --backend pytorch - --max_batch_size $MAX_BATCH_SIZE - --max_num_tokens $MAX_NUM_TOKENS - --max_seq_len $MAX_SEQ_LENGTH - --tp_size $DSTACK_GPUS_NUM - --ep_size $EXPERT_PARALLEL - --pp_size $PIPELINE_PARALLEL - DeepSeek-R1 -port: 8000 -model: deepseek-ai/DeepSeek-R1 - -resources: - gpu: 8:H200 - shm_size: 32GB - disk: 2000GB.. diff --git a/examples/inference/vllm/.dstack.yml b/examples/inference/vllm/.dstack.yml deleted file mode 100644 index 9060cff5ab..0000000000 --- a/examples/inference/vllm/.dstack.yml +++ /dev/null @@ -1,28 +0,0 @@ -type: service -name: llama31 - -python: "3.11" -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_MODEL_LEN=4096 -commands: - - pip install vllm - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --tensor-parallel-size $DSTACK_GPUS_NUM -port: 8000 -# Register the model -model: meta-llama/Meta-Llama-3.1-8B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - -resources: - gpu: 24GB - # Uncomment if using multiple GPUs - #shm_size: 24GB diff --git a/examples/inference/vllm/README.md b/examples/inference/vllm/README.md deleted file mode 100644 index 12bfd18ac1..0000000000 --- a/examples/inference/vllm/README.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -description: "This example shows how to deploy Llama 3.1 to any cloud or on-premises environment using vLLM and dstack." ---- - -# vLLM - -This example shows how to deploy Llama 3.1 8B with `dstack` using [vLLM](https://docs.vllm.ai/en/latest/). - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -Here's an example of a service that deploys Llama 3.1 8B using vLLM. - -
- -```yaml -type: service -name: llama31 - -python: "3.11" -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_MODEL_LEN=4096 -commands: - - pip install vllm - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --tensor-parallel-size $DSTACK_GPUS_NUM -port: 8000 -# Register the model -model: meta-llama/Meta-Llama-3.1-8B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - -resources: - gpu: 24GB - # Uncomment if using multiple GPUs - #shm_size: 24GB -``` - -
- -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ dstack apply -f examples/inference/vllm/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod CA-MTL-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 2 runpod EU-SE-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 3 gcp us-west4 27xCPU, 150GB, A5000:24GB:2 yes $0.23 - -Submit a new run? [y/n]: y - -Provisioning... ----> 100% -``` -
- -If no gateway is created, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "meta-llama/Meta-Llama-3.1-8B-Instruct", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "max_tokens": 128 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -## Source code - -The source-code of this example can be found in -[`examples/inference/vllm`](https://github.com/dstackai/dstack/blob/master/examples/inference/vllm). - -## What's next? - -1. Check [services](https://dstack.ai/docs/services) -2. Browse the [Llama 3.1](https://dstack.ai/examples/llms/llama31/), [TGI](https://dstack.ai/examples/inference/tgi/) - and [NIM](https://dstack.ai/examples/inference/nim/) examples -3. See also [AMD](https://dstack.ai/examples/accelerators/amd/) and - [TPU](https://dstack.ai/examples/accelerators/tpu/) diff --git a/examples/inference/vllm/amd/.dstack.yml b/examples/inference/vllm/amd/.dstack.yml deleted file mode 100644 index f0c488d755..0000000000 --- a/examples/inference/vllm/amd/.dstack.yml +++ /dev/null @@ -1,43 +0,0 @@ -type: service -name: llama31-service-vllm-amd - -image: runpod/pytorch:2.4.0-py3.10-rocm6.1.0-ubuntu22.04 -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-70B-Instruct - - MAX_MODEL_LEN=126192 -commands: - - export PATH=/opt/conda/envs/py_3.10/bin:$PATH - - wget https://github.com/ROCm/hipBLAS/archive/refs/tags/rocm-6.1.0.zip - - unzip rocm-6.1.0.zip - - cd hipBLAS-rocm-6.1.0 - - python rmake.py - - cd .. - - git clone https://github.com/vllm-project/vllm.git - - cd vllm - - pip install triton - - pip uninstall torch -y - - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.1 - - pip install /opt/rocm/share/amd_smi - - pip install --upgrade numba scipy huggingface-hub[cli] - - pip install "numpy<2" - - pip install -r requirements-rocm.txt - - wget -N https://github.com/ROCm/vllm/raw/fa78403/rocm_patch/libamdhip64.so.6 -P /opt/rocm/lib - - rm -f "$(python3 -c 'import torch; print(torch.__path__[0])')"/lib/libamdhip64.so* - - export PYTORCH_ROCM_ARCH="gfx90a;gfx942" - - wget https://dstack-binaries.s3.amazonaws.com/vllm-0.6.0%2Brocm614-cp310-cp310-linux_x86_64.whl - - pip install vllm-0.6.0+rocm614-cp310-cp310-linux_x86_64.whl - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --port 8000 -# Expose the vllm server port -port: 8000 -# Register the model -model: meta-llama/Meta-Llama-3.1-70B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - gpu: MI300X - disk: 200GB diff --git a/examples/inference/vllm/amd/build-vllm.dstack.yml b/examples/inference/vllm/amd/build-vllm.dstack.yml deleted file mode 100644 index 3510d0f6d6..0000000000 --- a/examples/inference/vllm/amd/build-vllm.dstack.yml +++ /dev/null @@ -1,47 +0,0 @@ -type: task -name: build-vllm-rocm - -image: runpod/pytorch:2.4.0-py3.10-rocm6.1.0-ubuntu22.04 - -env: - - HF_TOKEN - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - - AWS_REGION - - BUCKET_NAME - -command: - - apt-get update -y - - apt-get install awscli -y - - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID - - aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY - - aws configure set region $AWS_REGION - - export PATH=/opt/conda/envs/py_3.10/bin:$PATH - - wget https://github.com/ROCm/hipBLAS/archive/refs/tags/rocm-6.1.0.zip - - unzip rocm-6.1.0.zip - - cd hipBLAS-rocm-6.1.0 - - python rmake.py - - cd .. - - git clone https://github.com/vllm-project/vllm.git - - cd vllm - - pip install triton - - pip uninstall torch -y - - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.1 - - pip install /opt/rocm/share/amd_smi - - pip install --upgrade numba scipy huggingface-hub[cli] - - pip install "numpy<2" - - pip install -r requirements-rocm.txt - - wget -N https://github.com/ROCm/vllm/raw/fa78403/rocm_patch/libamdhip64.so.6 -P /opt/rocm/lib - - rm -f "$(python3 -c 'import torch; print(torch.__path__[0])')"/lib/libamdhip64.so* - - export PYTORCH_ROCM_ARCH="gfx90a;gfx942" - - pip install wheel setuptools setuptools_scm ninja - - python setup.py bdist_wheel -d dist/ - - cd dist - - aws s3 cp "$(ls -1 | head -n 1)" s3://$BUCKET_NAME/ --acl public-read - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - gpu: MI300X - disk: 150GB diff --git a/examples/inference/vllm/tpu/.dstack.yml b/examples/inference/vllm/tpu/.dstack.yml deleted file mode 100644 index 5a637ca797..0000000000 --- a/examples/inference/vllm/tpu/.dstack.yml +++ /dev/null @@ -1,23 +0,0 @@ -type: service -# The name is optional, if not specified, generated randomly -name: llama31-service-vllm-tpu -image: vllm/vllm-tpu:nightly -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_MODEL_LEN=4096 -commands: - - vllm serve $MODEL_ID - --tensor-parallel-size 4 - --max-model-len $MAX_MODEL_LEN - --port 8000 -# Expose the vllm server port -port: 8000 -# Register the model -model: meta-llama/Meta-Llama-3.1-8B-Instruct - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - gpu: v5litepod-4 diff --git a/examples/llms/deepseek/README.md b/examples/llms/deepseek/README.md deleted file mode 100644 index bca90ac35c..0000000000 --- a/examples/llms/deepseek/README.md +++ /dev/null @@ -1,607 +0,0 @@ -# Deepseek - -This example walks you through how to deploy and -train [Deepseek](https://huggingface.co/deepseek-ai) -models with `dstack`. - -> We used Deepseek-R1 distilled models and Deepseek-V2-Lite, a 16B model with the same architecture as Deepseek-R1 (671B). Deepseek-V2-Lite retains MLA and DeepSeekMoE but requires less memory, making it ideal for testing and fine-tuning on smaller GPUs. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` -
- -## Deployment - -### AMD - -Here's an example of a service that deploys `Deepseek-R1-Distill-Llama-70B` using [SGLang](https://github.com/sgl-project/sglang) and [vLLM](https://github.com/vllm-project/vllm) with AMD `MI300X` GPU. The below configurations also support `Deepseek-V2-Lite`. - -=== "SGLang" - -
- - ```yaml - type: service - name: deepseek-r1-amd - - image: lmsysorg/sglang:v0.4.1.post4-rocm620 - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - resources: - gpu: MI300X - disk: 300Gb - - ``` -
- -=== "vLLM" - -
- - ```yaml - type: service - name: deepseek-r1-amd - - image: rocm/vllm:rocm6.2_mi300_ubuntu20.04_py3.9_vllm_0.6.4 - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - MAX_MODEL_LEN=126432 - commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --trust-remote-code - port: 8000 - - model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - resources: - gpu: MI300X - disk: 300Gb - ``` -
- -Note, when using `Deepseek-R1-Distill-Llama-70B` with `vLLM` with a 192GB GPU, we must limit the context size to 126432 tokens to fit the memory. - -### Intel Gaudi - -Here's an example of a service that deploys `Deepseek-R1-Distill-Llama-70B` -using [TGI on Gaudi](https://github.com/huggingface/tgi-gaudi) -and [vLLM](https://github.com/HabanaAI/vllm-fork) (Gaudi fork) with Intel Gaudi 2. - -> Both [TGI on Gaudi](https://github.com/huggingface/tgi-gaudi) -> and [vLLM](https://github.com/HabanaAI/vllm-fork) do not support `Deepseek-V2-Lite`. -> See [this](https://github.com/huggingface/tgi-gaudi/issues/271) -> and [this](https://github.com/HabanaAI/vllm-fork/issues/809#issuecomment-2652454824) issues. - -=== "TGI" - -
- ```yaml - type: service - - name: tgi - - image: ghcr.io/huggingface/tgi-gaudi:2.3.1 - - auth: false - port: 8000 - - model: DeepSeek-R1-Distill-Llama-70B - - env: - - HF_TOKEN - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - PORT=8000 - - OMPI_MCA_btl_vader_single_copy_mechanism=none - - TEXT_GENERATION_SERVER_IGNORE_EOS_TOKEN=true - - PT_HPU_ENABLE_LAZY_COLLECTIVES=true - - MAX_TOTAL_TOKENS=2048 - - BATCH_BUCKET_SIZE=256 - - PREFILL_BATCH_BUCKET_SIZE=4 - - PAD_SEQUENCE_TO_MULTIPLE_OF=64 - - ENABLE_HPU_GRAPH=true - - LIMIT_HPU_GRAPH=true - - USE_FLASH_ATTENTION=true - - FLASH_ATTENTION_RECOMPUTE=true - - commands: - - text-generation-launcher - --sharded true - --num-shard 8 - --max-input-length 1024 - --max-total-tokens 2048 - --max-batch-prefill-tokens 4096 - --max-batch-total-tokens 524288 - --max-waiting-tokens 7 - --waiting-served-ratio 1.2 - --max-concurrent-requests 512 - - resources: - gpu: Gaudi2:8 - ``` -
- -=== "vLLM" - -
- ```yaml - type: service - name: deepseek-r1-gaudi - - image: vault.habana.ai/gaudi-docker/1.19.0/ubuntu22.04/habanalabs/pytorch-installer-2.5.1:latest - - - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - HABANA_VISIBLE_DEVICES=all - - OMPI_MCA_btl_vader_single_copy_mechanism=none - - commands: - - git clone https://github.com/HabanaAI/vllm-fork.git - - cd vllm-fork - - git checkout habana_main - - pip install -r requirements-hpu.txt - - python setup.py develop - - vllm serve $MODEL_ID - --tensor-parallel-size 8 - --trust-remote-code - --download-dir /data - - port: 8000 - ``` -
- -### NVIDIA - -Here's an example of a service that deploys `Deepseek-R1-Distill-Llama-8B` -using [SGLang](https://github.com/sgl-project/sglang) -and [vLLM](https://github.com/vllm-project/vllm) with NVIDIA GPUs. -Both SGLang and vLLM also support `Deepseek-V2-Lite`. - -=== "SGLang" -
- - ```yaml - type: service - name: deepseek-r1-nvidia - - image: lmsysorg/sglang:latest - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - resources: - gpu: 24GB - ``` -
- -=== "vLLM" -
- - ```yaml - type: service - name: deepseek-r1-nvidia - - image: vllm/vllm-openai:latest - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - MAX_MODEL_LEN=4096 - commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - port: 8000 - model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - resources: - gpu: 24GB - ``` -
- -Note, to run `Deepseek-R1-Distill-Llama-8B` with `vLLM` with a 24GB GPU, we must limit the context size to 4096 tokens to fit the memory. - -> To run `Deepseek-V2-Lite` with `vLLM`, we must use 40GB GPU and to run `Deepseek-V2-Lite` with SGLang, we must use -> 80GB GPU. For more details on SGlang's memory requirements you can refer to -> this [issue](https://github.com/sgl-project/sglang/issues/3451). - -### Memory requirements - -Approximate memory requirements for loading the model (excluding context and CUDA/ROCm kernel reservations). - -| Model | Size | FP16 | FP8 | INT4 | -|-----------------------------|----------|--------|--------|--------| -| `Deepseek-R1` | **671B** | 1.35TB | 671GB | 336GB | -| `DeepSeek-R1-Distill-Llama` | **70B** | 161GB | 80.5GB | 40B | -| `DeepSeek-R1-Distill-Qwen` | **32B** | 74GB | 37GB | 18.5GB | -| `DeepSeek-V2-Lite` | **16B** | 35GB | 17.5GB | 8.75GB | -| `DeepSeek-R1-Distill-Qwen` | **14B** | 32GB | 16GB | 8GB | -| `DeepSeek-R1-Distill-Llama` | **8B** | 18GB | 9GB | 4.5GB | -| `DeepSeek-R1-Distill-Qwen` | **7B** | 16GB | 8GB | 4GB | - -For example, the FP8 version of Deepseek-R1 671B fits on a single node of MI300X with eight 192GB GPUs, a single node of -H200 with eight 141GB GPUs, or a single node of Intel Gaudi2 with eight 96GB GPUs. - -### Applying the configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ dstack apply -f examples/llms/deepseek/sglang/amd/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod EU-RO-1 24xCPU, 283GB, 1xMI300X (192GB) no $2.49 - -Submit the run deepseek-r1-amd? [y/n]: y - -Provisioning... ----> 100% -``` -
- -Once the service is up, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "stream": true, - "max_tokens": 512 - }' -``` -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -## Fine-tuning - -### AMD - -Here are the examples of LoRA fine-tuning of `Deepseek-V2-Lite` and GRPO fine-tuning of `DeepSeek-R1-Distill-Qwen-1.5B` on `MI300X` GPU using HuggingFace's [TRL](https://github.com/huggingface/trl). - -=== "LoRA" - -
- - ```yaml - type: task - name: trl-train - - image: rocm/pytorch:rocm6.2.3_ubuntu22.04_py3.10_pytorch_release_2.3.0 - - env: - - WANDB_API_KEY - - WANDB_PROJECT - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite - - ACCELERATE_USE_FSDP=False - commands: - - git clone https://github.com/huggingface/peft.git - - pip install trl - - pip install "numpy<2" - - pip install peft - - pip install wandb - - cd peft/examples/sft - - python train.py - --seed 100 - --model_name_or_path "deepseek-ai/DeepSeek-V2-Lite" - --dataset_name "smangrul/ultrachat-10k-chatml" - --chat_template_format "chatml" - --add_special_tokens False - --append_concat_token False - --splits "train,test" - --max_seq_len 512 - --num_train_epochs 1 - --logging_steps 5 - --log_level "info" - --logging_strategy "steps" - --eval_strategy "epoch" - --save_strategy "epoch" - --hub_private_repo True - --hub_strategy "every_save" - --packing True - --learning_rate 1e-4 - --lr_scheduler_type "cosine" - --weight_decay 1e-4 - --warmup_ratio 0.0 - --max_grad_norm 1.0 - --output_dir "deepseek-sft-lora" - --per_device_train_batch_size 8 - --per_device_eval_batch_size 8 - --gradient_accumulation_steps 4 - --gradient_checkpointing True - --use_reentrant True - --dataset_text_field "content" - --use_peft_lora True - --lora_r 16 - --lora_alpha 16 - --lora_dropout 0.05 - --lora_target_modules "all-linear" - - resources: - gpu: MI300X - disk: 150GB - ``` -
- -=== "GRPO" - -
- ```yaml - type: task - name: trl-train-grpo - - image: rocm/pytorch:rocm6.2.3_ubuntu22.04_py3.10_pytorch_release_2.3.0 - - env: - - WANDB_API_KEY - - WANDB_PROJECT - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B - files: - - grpo_train.py - commands: - - pip install trl - - pip install datasets - # numPy version less than 2 is required for the scipy installation with AMD. - - pip install "numpy<2" - - mkdir -p grpo_example - - cp grpo_train.py grpo_example/grpo_train.py - - cd grpo_example - - python grpo_train.py - --model_name_or_path $MODEL_ID - --dataset_name trl-lib/tldr - --per_device_train_batch_size 2 - --logging_steps 25 - --output_dir Deepseek-Distill-Qwen-1.5B-GRPO - --trust_remote_code - - resources: - gpu: MI300X - disk: 150GB - ``` -
- -Note, the `GRPO` fine-tuning of `DeepSeek-R1-Distill-Qwen-1.5B` consumes up to 135GB of VRAM. - -### Intel Gaudi - -Here is an example of LoRA fine-tuning of `DeepSeek-R1-Distill-Qwen-7B` on Intel Gaudi 2 GPUs using -HuggingFace's [Optimum for Intel Gaudi](https://github.com/huggingface/optimum-habana) -and [DeepSpeed](https://github.com/deepspeedai/DeepSpeed). Both also support `LoRA` -fine-tuning of `Deepseek-V2-Lite` with same configuration as below. - -=== "LoRA" - -
- ```yaml - type: task - name: trl-train - - image: vault.habana.ai/gaudi-docker/1.18.0/ubuntu22.04/habanalabs/pytorch-installer-2.4.0 - - env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - - WANDB_API_KEY - - WANDB_PROJECT - commands: - - pip install --upgrade-strategy eager optimum[habana] - - pip install git+https://github.com/HabanaAI/DeepSpeed.git@1.19.0 - - git clone https://github.com/huggingface/optimum-habana.git - - cd optimum-habana/examples/trl - - pip install -r requirements.txt - - pip install wandb - - DEEPSPEED_HPU_ZERO3_SYNC_MARK_STEP_REQUIRED=1 python ../gaudi_spawn.py --world_size 8 --use_deepspeed sft.py - --model_name_or_path $MODEL_ID - --dataset_name "lvwerra/stack-exchange-paired" - --deepspeed ../language-modeling/llama2_ds_zero3_config.json - --output_dir="./sft" - --do_train - --max_steps=500 - --logging_steps=10 - --save_steps=100 - --per_device_train_batch_size=1 - --per_device_eval_batch_size=1 - --gradient_accumulation_steps=2 - --learning_rate=1e-4 - --lr_scheduler_type="cosine" - --warmup_steps=100 - --weight_decay=0.05 - --optim="paged_adamw_32bit" - --lora_target_modules "q_proj" "v_proj" - --bf16 - --remove_unused_columns=False - --run_name="sft_deepseek_70" - --report_to="wandb" - --use_habana - --use_lazy_mode - - resources: - gpu: gaudi2:8 - ``` - -
- - -### NVIDIA - -Here are examples of LoRA fine-tuning of `DeepSeek-R1-Distill-Qwen-1.5B` and QLoRA fine-tuning of `DeepSeek-V2-Lite` -on NVIDIA GPU using HuggingFace's [TRL](https://github.com/huggingface/trl) library. - -=== "LoRA" -
- - ```yaml - type: task - name: trl-train - - python: 3.12 - - env: - - WANDB_API_KEY - - WANDB_PROJECT - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B - commands: - - git clone https://github.com/huggingface/trl.git - - pip install trl - - pip install peft - - pip install wandb - - cd trl/trl/scripts - - python sft.py - --model_name_or_path $MODEL_ID - --dataset_name trl-lib/Capybara - --learning_rate 2.0e-4 - --num_train_epochs 1 - --packing - --per_device_train_batch_size 2 - --gradient_accumulation_steps 8 - --gradient_checkpointing - --logging_steps 25 - --eval_strategy steps - --eval_steps 100 - --use_peft - --lora_r 32 - --lora_alpha 16 - --report_to wandb - --output_dir DeepSeek-R1-Distill-Qwen-1.5B-SFT - - resources: - gpu: 24GB - ``` -
- -=== "QLoRA" -
- - ```yaml - type: task - name: trl-train-deepseek-v2 - - python: 3.12 - nvcc: true - env: - - WANDB_API_KEY - - WANDB_PROJECT - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite - - ACCELERATE_USE_FSDP=False - commands: - - git clone https://github.com/huggingface/peft.git - - pip install trl - - pip install peft - - pip install wandb - - pip install bitsandbytes - - cd peft/examples/sft - - python train.py - --seed 100 - --model_name_or_path "deepseek-ai/DeepSeek-V2-Lite" - --dataset_name "smangrul/ultrachat-10k-chatml" - --chat_template_format "chatml" - --add_special_tokens False - --append_concat_token False - --splits "train,test" - --max_seq_len 512 - --num_train_epochs 1 - --logging_steps 5 - --log_level "info" - --logging_strategy "steps" - --eval_strategy "epoch" - --save_strategy "epoch" - --hub_private_repo True - --hub_strategy "every_save" - --bf16 True - --packing True - --learning_rate 1e-4 - --lr_scheduler_type "cosine" - --weight_decay 1e-4 - --warmup_ratio 0.0 - --max_grad_norm 1.0 - --output_dir "mistral-sft-lora" - --per_device_train_batch_size 8 - --per_device_eval_batch_size 8 - --gradient_accumulation_steps 4 - --gradient_checkpointing True - --use_reentrant True - --dataset_text_field "content" - --use_peft_lora True - --lora_r 16 - --lora_alpha 16 - --lora_dropout 0.05 - --lora_target_modules "all-linear" - --use_4bit_quantization True - --use_nested_quant True - --bnb_4bit_compute_dtype "bfloat16" - - resources: - # Consumes ~25GB of VRAM for QLoRA fine-tuning deepseek-ai/DeepSeek-V2-Lite - gpu: 48GB - ``` -
- -### Memory requirements - -| Model | Size | Full fine-tuning | LoRA | QLoRA | -|-----------------------------|----------|------------------|-------|-------| -| `Deepseek-R1` | **671B** | 10.5TB | 1.4TB | 442GB | -| `DeepSeek-R1-Distill-Llama` | **70B** | 1.09TB | 151GB | 46GB | -| `DeepSeek-R1-Distill-Qwen` | **32B** | 512GB | 70GB | 21GB | -| `DeepSeek-V2-Lite` | **16B** | 256GB | 35GB | 11GB | -| `DeepSeek-R1-Distill-Qwen` | **14B** | 224GB | 30GB | 9GB | -| `DeepSeek-R1-Distill-Llama` | **8B** | 128GB | 17GB | 5GB | -| `DeepSeek-R1-Distill-Qwen` | **7B** | 112GB | 15GB | 4GB | -| `DeepSeek-R1-Distill-Qwen` | **1.5B** | 24GB | 3.2GB | 1GB | - -The memory requirements assume low-rank update matrices are 1% of model parameters. In practice, a 7B model with QLoRA -needs 7–10GB due to intermediate hidden states. - -| Fine-tuning type | Calculation | -|------------------|--------------------------------------------------| -| Full fine-tuning | 671B × 16 bytes = 10.48TB | -| LoRA | 671B × 2 bytes + 1% of 671B × 16 bytes = 1.41TB | -| QLoRA(4-bit) | 671B × 0.5 bytes + 1% of 671B × 16 bytes = 442GB | - -## Source code - -The source-code of this example can be found in -[`examples/llms/deepseek`](https://github.com/dstackai/dstack/blob/master/examples/llms/deepseek). - -!!! info "What's next?" - 1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [protips](https://dstack.ai/docs/protips). diff --git a/examples/llms/deepseek/sglang/amd/.dstack.yml b/examples/llms/deepseek/sglang/amd/.dstack.yml deleted file mode 100644 index 99a19bfee3..0000000000 --- a/examples/llms/deepseek/sglang/amd/.dstack.yml +++ /dev/null @@ -1,18 +0,0 @@ -type: service -name: deepseek-r1-amd - -image: lmsysorg/sglang:v0.4.1.post4-rocm620 -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B -commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - -port: 8000 -model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - -resources: - gpu: mi300x - disk: 300Gb diff --git a/examples/llms/deepseek/sglang/amd/deepseek_v2_lite.dstack.yml b/examples/llms/deepseek/sglang/amd/deepseek_v2_lite.dstack.yml deleted file mode 100644 index 01ef71a6be..0000000000 --- a/examples/llms/deepseek/sglang/amd/deepseek_v2_lite.dstack.yml +++ /dev/null @@ -1,18 +0,0 @@ -type: service -name: deepseek-v2-lite-amd - -image: lmsysorg/sglang:v0.4.1.post4-rocm620 -env: - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite -commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - -port: 8000 -model: deepseek-ai/DeepSeek-V2-Lite - -resources: - gpu: mi300x - disk: 150Gb diff --git a/examples/llms/deepseek/sglang/nvidia/.dstack.yml b/examples/llms/deepseek/sglang/nvidia/.dstack.yml deleted file mode 100644 index d1c92b64d1..0000000000 --- a/examples/llms/deepseek/sglang/nvidia/.dstack.yml +++ /dev/null @@ -1,18 +0,0 @@ -type: service -name: deepseek-r1-nvidia - -image: lmsysorg/sglang:latest -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-8B -commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - -port: 8000 - -model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B - -resources: - gpu: 24GB diff --git a/examples/llms/deepseek/sglang/nvidia/deepseek_v2_lite.dstack.yml b/examples/llms/deepseek/sglang/nvidia/deepseek_v2_lite.dstack.yml deleted file mode 100644 index 8c0adaa41b..0000000000 --- a/examples/llms/deepseek/sglang/nvidia/deepseek_v2_lite.dstack.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Not Working https://github.com/sgl-project/sglang/issues/3451 -type: service -name: deepseek-v2-lite-nvidia - -image: lmsysorg/sglang:latest -env: - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite -commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --port 8000 - --trust-remote-code - -port: 8000 - -model: deepseek-ai/DeepSeek-V2-Lite - -resources: - gpu: 80GB diff --git a/examples/llms/deepseek/tgi/intel/.dstack.yml b/examples/llms/deepseek/tgi/intel/.dstack.yml deleted file mode 100644 index 16d0830924..0000000000 --- a/examples/llms/deepseek/tgi/intel/.dstack.yml +++ /dev/null @@ -1,45 +0,0 @@ -type: service - -name: tgi - -image: ghcr.io/huggingface/tgi-gaudi:2.3.1 - -auth: false -port: 8000 - -model: DeepSeek-R1-Distill-Llama-70B - -env: - - HF_TOKEN - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - PORT=8000 - - OMPI_MCA_btl_vader_single_copy_mechanism=none - - TEXT_GENERATION_SERVER_IGNORE_EOS_TOKEN=true - - PT_HPU_ENABLE_LAZY_COLLECTIVES=true - - MAX_TOTAL_TOKENS=2048 - - BATCH_BUCKET_SIZE=256 - - PREFILL_BATCH_BUCKET_SIZE=4 - - PAD_SEQUENCE_TO_MULTIPLE_OF=64 - - ENABLE_HPU_GRAPH=true - - LIMIT_HPU_GRAPH=true - - USE_FLASH_ATTENTION=true - - FLASH_ATTENTION_RECOMPUTE=true - -commands: - - text-generation-launcher - --sharded true - --num-shard 8 - --max-input-length 1024 - --max-total-tokens 2048 - --max-batch-prefill-tokens 4096 - --max-batch-total-tokens 524288 - --max-waiting-tokens 7 - --waiting-served-ratio 1.2 - --max-concurrent-requests 512 - -resources: - gpu: Gaudi2:8 - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub diff --git a/examples/llms/deepseek/trl/intel/.dstack.yml b/examples/llms/deepseek/trl/intel/.dstack.yml deleted file mode 100644 index 9963e48445..0000000000 --- a/examples/llms/deepseek/trl/intel/.dstack.yml +++ /dev/null @@ -1,46 +0,0 @@ -type: task -# The name is optional, if not specified, generated randomly -name: trl-train - -image: vault.habana.ai/gaudi-docker/1.18.0/ubuntu22.04/habanalabs/pytorch-installer-2.4.0 - -# Required environment variables -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - - WANDB_API_KEY - - WANDB_PROJECT -# Commands of the task -commands: - - pip install --upgrade-strategy eager optimum[habana] - - pip install git+https://github.com/HabanaAI/DeepSpeed.git@1.19.0 - - git clone https://github.com/huggingface/optimum-habana.git - - cd optimum-habana/examples/trl - - pip install -r requirements.txt - - pip install wandb - - DEEPSPEED_HPU_ZERO3_SYNC_MARK_STEP_REQUIRED=1 python ../gaudi_spawn.py --world_size 8 --use_deepspeed sft.py - --model_name_or_path $MODEL_ID - --dataset_name "lvwerra/stack-exchange-paired" - --deepspeed ../language-modeling/llama2_ds_zero3_config.json - --output_dir="./sft" - --do_train - --max_steps=500 - --logging_steps=10 - --save_steps=100 - --per_device_train_batch_size=1 - --per_device_eval_batch_size=1 - --gradient_accumulation_steps=2 - --learning_rate=1e-4 - --lr_scheduler_type="cosine" - --warmup_steps=100 - --weight_decay=0.05 - --optim="paged_adamw_32bit" - --lora_target_modules "q_proj" "v_proj" - --bf16 - --remove_unused_columns=False - --run_name="sft_deepseek_70" - --report_to="wandb" - --use_habana - --use_lazy_mode - -resources: - gpu: gaudi2:8 diff --git a/examples/llms/deepseek/trl/intel/deepseek_v2.dstack.yml b/examples/llms/deepseek/trl/intel/deepseek_v2.dstack.yml deleted file mode 100644 index 7aa13d677a..0000000000 --- a/examples/llms/deepseek/trl/intel/deepseek_v2.dstack.yml +++ /dev/null @@ -1,45 +0,0 @@ -type: task -# The name is optional, if not specified, generated randomly -name: trl-train-deepseek-v2-lite - -image: vault.habana.ai/gaudi-docker/1.18.0/ubuntu22.04/habanalabs/pytorch-installer-2.4.0 - -# Required environment variables -env: - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite - - WANDB_API_KEY - - WANDB_PROJECT -# Commands of the task -commands: - - pip install git+https://github.com/huggingface/optimum-habana.git - - pip install git+https://github.com/HabanaAI/DeepSpeed.git@1.19.0 - - git clone https://github.com/huggingface/optimum-habana.git - - cd optimum-habana/examples/trl - - pip install -r requirements.txt - - DEEPSPEED_HPU_ZERO3_SYNC_MARK_STEP_REQUIRED=1 python ../gaudi_spawn.py --world_size 8 --use_deepspeed sft.py - --model_name_or_path $MODEL_ID - --dataset_name "lvwerra/stack-exchange-paired" - --deepspeed ../language-modeling/llama2_ds_zero3_config.json - --output_dir="./sft" - --do_train - --max_steps=500 - --logging_steps=10 - --save_steps=100 - --per_device_train_batch_size=1 - --per_device_eval_batch_size=1 - --gradient_accumulation_steps=2 - --learning_rate=1e-4 - --lr_scheduler_type="cosine" - --warmup_steps=100 - --weight_decay=0.05 - --optim="paged_adamw_32bit" - --lora_target_modules "q_proj" "v_proj" - --bf16 - --remove_unused_columns=False - --run_name="sft_deepseek_v2lite" - --report_to="wandb" - --use_habana - --use_lazy_mode - -resources: - gpu: gaudi2:8 diff --git a/examples/llms/deepseek/vllm/amd/.dstack.yml b/examples/llms/deepseek/vllm/amd/.dstack.yml deleted file mode 100644 index 23bfb033c0..0000000000 --- a/examples/llms/deepseek/vllm/amd/.dstack.yml +++ /dev/null @@ -1,19 +0,0 @@ -type: service -name: deepseek-r1-amd - -image: rocm/vllm:rocm6.2_mi300_ubuntu20.04_py3.9_vllm_0.6.4 -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - MAX_MODEL_LEN=126432 -commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --trust-remote-code -port: 8000 - -model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - -resources: - gpu: mi300x - disk: 300Gb diff --git a/examples/llms/deepseek/vllm/amd/deepseek_v2_lite.dstack.yml b/examples/llms/deepseek/vllm/amd/deepseek_v2_lite.dstack.yml deleted file mode 100644 index 8937e95266..0000000000 --- a/examples/llms/deepseek/vllm/amd/deepseek_v2_lite.dstack.yml +++ /dev/null @@ -1,18 +0,0 @@ -type: service -name: deepseek-v2-lite-amd - -image: rocm/vllm:rocm6.2_mi300_ubuntu20.04_py3.9_vllm_0.6.4 -env: - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite -commands: - - vllm serve $MODEL_ID - --trust-remote-code - -port: 8000 - -model: deepseek-ai/DeepSeek-V2-Lite - - -resources: - gpu: mi300x - disk: 150Gb diff --git a/examples/llms/deepseek/vllm/intel/.dstack.yml b/examples/llms/deepseek/vllm/intel/.dstack.yml deleted file mode 100644 index d28a0152d8..0000000000 --- a/examples/llms/deepseek/vllm/intel/.dstack.yml +++ /dev/null @@ -1,31 +0,0 @@ -type: service -name: deepseek-r1-gaudi - -image: vault.habana.ai/gaudi-docker/1.19.0/ubuntu22.04/habanalabs/pytorch-installer-2.5.1:latest - -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-70B - - HABANA_VISIBLE_DEVICES=all - - OMPI_MCA_btl_vader_single_copy_mechanism=none - -commands: - - git clone https://github.com/HabanaAI/vllm-fork.git - - cd vllm-fork - - git checkout habana_main - - pip install -r requirements-hpu.txt - - python setup.py develop - - vllm serve $MODEL_ID - --tensor-parallel-size 8 - --trust-remote-code - --download-dir /data - -port: 8000 - -model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B - -resources: - gpu: gaudi2:8 - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub diff --git a/examples/llms/deepseek/vllm/nvidia/.dstack.yml b/examples/llms/deepseek/vllm/nvidia/.dstack.yml deleted file mode 100644 index e623b182c4..0000000000 --- a/examples/llms/deepseek/vllm/nvidia/.dstack.yml +++ /dev/null @@ -1,17 +0,0 @@ -type: service -name: deepseek-r1-nvidia - -image: vllm/vllm-openai:latest -env: - - MODEL_ID=deepseek-ai/DeepSeek-R1-Distill-Llama-8B - - MAX_MODEL_LEN=4096 -commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - -port: 8000 - -model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B - -resources: - gpu: 24GB diff --git a/examples/llms/deepseek/vllm/nvidia/deepseek_v2_lite.dstack.yml b/examples/llms/deepseek/vllm/nvidia/deepseek_v2_lite.dstack.yml deleted file mode 100644 index 06e78f379b..0000000000 --- a/examples/llms/deepseek/vllm/nvidia/deepseek_v2_lite.dstack.yml +++ /dev/null @@ -1,19 +0,0 @@ -type: service -name: deepseek-v2-lite-nvidia - -image: vllm/vllm-openai:latest -env: - - MODEL_ID=deepseek-ai/DeepSeek-V2-Lite - - MAX_MODEL_LEN=4096 -commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --tensor-parallel-size $DSTACK_GPUS_NUM - --trust-remote-code - -port: 8000 - -model: deepseek-ai/DeepSeek-V2-Lite - -resources: - gpu: 48GB diff --git a/examples/llms/llama/README.md b/examples/llms/llama/README.md deleted file mode 100644 index 573d4b8037..0000000000 --- a/examples/llms/llama/README.md +++ /dev/null @@ -1,288 +0,0 @@ -# Llama - -This example walks you through how to deploy Llama 4 Scout model with `dstack`. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -### AMD -Here's an example of a service that deploys -[`Llama-4-Scout-17B-16E-Instruct`](https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct) -using [vLLM](https://github.com/vllm-project/vllm) -with AMD `MI300X` GPUs. - -
- -```yaml -type: service -name: llama4-scout - -image: rocm/vllm-dev:llama4-20250407 -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - VLLM_WORKER_MULTIPROC_METHOD=spawn - - VLLM_USE_MODELSCOPE=False - - VLLM_USE_TRITON_FLASH_ATTN=0 - - MAX_MODEL_LEN=256000 - -commands: - - | - vllm serve $MODEL_ID \ - --tensor-parallel-size $DSTACK_GPUS_NUM \ - --max-model-len $MAX_MODEL_LEN \ - --kv-cache-dtype fp8 \ - --max-num-seqs 64 \ - --override-generation-config='{"attn_temperature_tuning": true}' - - -port: 8000 -# Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -resources: - gpu: Mi300x:2 - disk: 500GB.. -``` -
- -### NVIDIA -Here's an example of a service that deploys -[`Llama-4-Scout-17B-16E-Instruct`](https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct) -using [SGLang](https://github.com/sgl-project/sglang) and [vLLM](https://github.com/vllm-project/vllm) -with NVIDIA `H200` GPUs. - -=== "SGLang" - -
- - ```yaml - type: service - name: llama4-scout - - image: lmsysorg/sglang - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - CONTEXT_LEN=256000 - commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --tp $DSTACK_GPUS_NUM - --context-length $CONTEXT_LEN - --kv-cache-dtype fp8_e5m2 - --port 8000 - - port: 8000 - ## Register the model - model: meta-llama/Llama-4-Scout-17B-16E-Instruct - - resources: - gpu: H200:2 - disk: 500GB.. - ``` -
- -=== "vLLM" - -
- - ```yaml - type: service - name: llama4-scout - - image: vllm/vllm-openai - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - VLLM_DISABLE_COMPILE_CACHE=1 - - MAX_MODEL_LEN=256000 - commands: - - | - vllm serve $MODEL_ID \ - --tensor-parallel-size $DSTACK_GPUS_NUM \ - --max-model-len $MAX_MODEL_LEN \ - --kv-cache-dtype fp8 \ - --override-generation-config='{"attn_temperature_tuning": true}' - - port: 8000 - # Register the model - model: meta-llama/Llama-4-Scout-17B-16E-Instruct - - resources: - gpu: H200:2 - disk: 500GB.. - ``` -
- -!!! info "NOTE:" - With vLLM, add `--override-generation-config='{"attn_temperature_tuning": true}'` to - improve accuracy for [contexts longer than 32K tokens](https://blog.vllm.ai/2025/04/05/llama4.html). - -### Memory requirements - -Below are the approximate memory requirements for loading the model. -This excludes memory for the model context and CUDA kernel reservations. - -| Model | Size | FP16 | FP8 | INT4 | -|---------------|----------|--------|--------|--------| -| `Behemoth` | **2T** | 4TB | 2TB | 1TB | -| `Maverick` | **400B** | 800GB | 200GB | 100GB | -| `Scout` | **109B** | 218GB | 109GB | 54.5GB | - - -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ dstack apply -f examples/llms/llama/sglang/nvidia/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 vastai is-iceland 48xCPU, 128GB, 2xH200 (140GB) no $7.87 - 2 runpod EU-SE-1 40xCPU, 128GB, 2xH200 (140GB) no $7.98 - - -Submit the run llama4-scout? [y/n]: y - -Provisioning... ----> 100% -``` - -
- -Once the service is up, it will be available via the service endpoint -at `/proxy/services///`. - -
- -```shell -curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "meta-llama/Llama-4-Scout-17B-16E-Instruct", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "stream": true, - "max_tokens": 512 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the service endpoint -is available at `https://./`. - -[//]: # (TODO: https://github.com/dstackai/dstack/issues/1777) - -## Fine-tuning - -Here's and example of FSDP and QLoRA fine-tuning of 4-bit Quantized [Llama-4-Scout-17B-16E](https://huggingface.co/axolotl-quants/Llama-4-Scout-17B-16E-Linearized-bnb-nf4-bf16) on 2xH100 NVIDIA GPUs using [Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl) - -
- -```yaml -type: task -# The name is optional, if not specified, generated randomly -name: axolotl-nvidia-llama-scout-train - -# Using the official Axolotl's Docker image -image: axolotlai/axolotl:main-latest - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - WANDB_NAME=axolotl-nvidia-llama-scout-train - - HUB_MODEL_ID -# Commands of the task -commands: - - wget https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-4/scout-qlora-fsdp1.yaml - - axolotl train scout-qlora-fsdp1.yaml - --wandb-project $WANDB_PROJECT - --wandb-name $WANDB_NAME - --hub-model-id $HUB_MODEL_ID - -resources: - # Two GPU (required by FSDP) - gpu: H100:2 - # Shared memory size for inter-process communication - shm_size: 24GB - disk: 500GB.. -``` -
- -The task uses Axolotl's Docker image, where Axolotl is already pre-installed. - -### Memory requirements - -Below are the approximate memory requirements for loading the model. -This excludes memory for the model context and CUDA kernel reservations. - -| Model | Size | Full fine-tuning | LoRA | QLoRA | -|---------------|----------|--------------------|--------|--------| -| `Behemoth` | **2T** | 32TB | 4.3TB | 1.3TB | -| `Maverick` | **400B** | 6.5TB | 864GB | 264GB | -| `Scout` | **109B** | 1.75TB | 236GB | 72GB | - -The memory estimates assume FP16 precision for model weights, with low-rank adaptation (LoRA/QLoRA) layers comprising 1% of the total model parameters. - -| Fine-tuning type | Calculation | -|------------------|--------------------------------------------------| -| Full fine-tuning | 2T × 16 bytes = 32TB | -| LoRA | 2T × 2 bytes + 1% of 2T × 16 bytes = 4.3TB | -| QLoRA(4-bit) | 2T × 0.5 bytes + 1% of 2T × 16 bytes = 1.3TB | - -## Running a configuration - -Once the configuration is ready, run `dstack apply -f `, and `dstack` will automatically provision the -cloud resources and run the configuration. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ WANDB_PROJECT=... -$ WANDB_NAME=axolotl-nvidia-llama-scout-train -$ HUB_MODEL_ID=... -$ dstack apply -f examples/single-node-training/axolotl/.dstack.yml -``` - -
- -## Source code - -The source-code for deployment examples can be found in -[`examples/llms/llama`](https://github.com/dstackai/dstack/blob/master/examples/llms/llama) and the source-code for the finetuning example can be found in [`examples/single-node-training/axolotl`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/axolotl). - -## What's next? - -1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [protips](https://dstack.ai/docs/protips). -2. Browse [Llama 4 with SGLang](https://github.com/sgl-project/sglang/blob/main/docs/references/llama4.md), [Llama 4 with vLLM](https://blog.vllm.ai/2025/04/05/llama4.html), [Llama 4 with AMD](https://rocm.blogs.amd.com/artificial-intelligence/llama4-day-0-support/README.html) and [Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl). diff --git a/examples/llms/llama/sglang/nvidia/.dstack.yml b/examples/llms/llama/sglang/nvidia/.dstack.yml deleted file mode 100644 index b1aea2ce51..0000000000 --- a/examples/llms/llama/sglang/nvidia/.dstack.yml +++ /dev/null @@ -1,23 +0,0 @@ -type: service -name: llama4-scout - -image: lmsysorg/sglang -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - CONTEXT_LEN=256000 -commands: - - python3 -m sglang.launch_server - --model-path $MODEL_ID - --tp $DSTACK_GPUS_NUM - --context-length $CONTEXT_LEN - --port 8000 - --kv-cache-dtype fp8_e5m2 - -port: 8000 -## Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -resources: - gpu: H200:2 - disk: 500GB.. diff --git a/examples/llms/llama/vllm/amd/.dstack.yml b/examples/llms/llama/vllm/amd/.dstack.yml deleted file mode 100644 index eaaf1e6aeb..0000000000 --- a/examples/llms/llama/vllm/amd/.dstack.yml +++ /dev/null @@ -1,29 +0,0 @@ -type: service -name: llama4-scout - -image: rocm/vllm-dev:llama4-20250407 -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - VLLM_WORKER_MULTIPROC_METHOD=spawn - - VLLM_USE_MODELSCOPE=False - - VLLM_USE_TRITON_FLASH_ATTN=0 - - MAX_MODEL_LEN=256000 - -commands: - - | - vllm serve $MODEL_ID \ - --tensor-parallel-size $DSTACK_GPUS_NUM \ - --max-model-len $MAX_MODEL_LEN \ - --kv-cache-dtype fp8 \ - --max-num-seqs 64 \ - --override-generation-config='{"attn_temperature_tuning": true}' - - -port: 8000 -# Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -resources: - gpu: Mi300x:2 - disk: 500GB.. diff --git a/examples/llms/llama/vllm/nvidia/.dstack.yml b/examples/llms/llama/vllm/nvidia/.dstack.yml deleted file mode 100644 index c1f8e4919d..0000000000 --- a/examples/llms/llama/vllm/nvidia/.dstack.yml +++ /dev/null @@ -1,24 +0,0 @@ -type: service -name: llama4-scout - -image: vllm/vllm-openai -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-4-Scout-17B-16E-Instruct - - VLLM_DISABLE_COMPILE_CACHE=1 - - MAX_MODEL_LEN=256000 -commands: - - | - vllm serve $MODEL_ID \ - --tensor-parallel-size $DSTACK_GPUS_NUM \ - --max-model-len $MAX_MODEL_LEN \ - --kv-cache-dtype fp8 \ - --override-generation-config='{"attn_temperature_tuning": true}' - -port: 8000 -# Register the model -model: meta-llama/Llama-4-Scout-17B-16E-Instruct - -resources: - gpu: H200:2 - disk: 500GB.. diff --git a/examples/llms/llama31/README.md b/examples/llms/llama31/README.md deleted file mode 100644 index ff288e3c87..0000000000 --- a/examples/llms/llama31/README.md +++ /dev/null @@ -1,386 +0,0 @@ -# Llama 3.1 - -This example walks you through how to deploy and fine-tuning Llama 3.1 with `dstack`. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -You can use any serving frameworks. -Here's an example of a service that deploys Llama 3.1 8B using vLLM, TGI, and NIM. - -=== "vLLM" - -
- - ```yaml - type: service - name: llama31 - - python: "3.11" - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_MODEL_LEN=4096 - commands: - - pip install vllm - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --tensor-parallel-size $DSTACK_GPUS_NUM - port: 8000 - # Register the model - model: meta-llama/Meta-Llama-3.1-8B-Instruct - - # Uncomment to leverage spot instances - #spot_policy: auto - - # Uncomment to cache downloaded models - #volumes: - # - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - - resources: - gpu: 24GB - # Uncomment if using multiple GPUs - #shm_size: 24GB - ``` - -
- -=== "TGI" - -
- - ```yaml - type: service - name: llama31 - - image: ghcr.io/huggingface/text-generation-inference:latest - env: - - HF_TOKEN - - MODEL_ID=meta-llama/Meta-Llama-3.1-8B-Instruct - - MAX_INPUT_LENGTH=4000 - - MAX_TOTAL_TOKENS=4096 - commands: - - NUM_SHARD=$DSTACK_GPUS_NUM text-generation-launcher - port: 80 - # Register the model - model: meta-llama/Meta-Llama-3.1-8B-Instruct - - # Uncomment to leverage spot instances - #spot_policy: auto - - # Uncomment to cache downloaded models - #volumes: - # - /data:/data - - resources: - gpu: 24GB - # Uncomment if using multiple GPUs - #shm_size: 24GB - ``` - -
- -=== "NIM" - -
- - ```yaml - type: service - name: llama31 - - image: nvcr.io/nim/meta/llama-3.1-8b-instruct:latest - env: - - NGC_API_KEY - - NIM_MAX_MODEL_LEN=4096 - registry_auth: - username: $oauthtoken - password: ${{ env.NGC_API_KEY }} - port: 8000 - # Register the model - model: meta/llama-3.1-8b-instruct - - # Uncomment to leverage spot instances - #spot_policy: auto - - # Cache downloaded models - volumes: - - /root/.cache/nim:/opt/nim/.cache - - resources: - gpu: 24GB - # Uncomment if using multiple GPUs - #shm_size: 24GB - ``` - -
- -Note, when using Llama 3.1 8B with a 24GB GPU, we must limit the context size to 4096 tokens to fit the memory. - -### Memory requirements - -Below are the approximate memory requirements for loading the model. -This excludes memory for the model context and CUDA kernel reservations. - -| Model size | FP16 | FP8 | INT4 | -|------------|-------|-------|-------| -| **8B** | 16GB | 8GB | 4GB | -| **70B** | 140GB | 70GB | 35GB | -| **405B** | 810GB | 405GB | 203GB | - -For example, the FP16 version of Llama 3.1 405B won't fit into a single machine with eight 80GB GPUs, so we'd need at least two -nodes. - -### Quantization - -The INT4 version of Llama 3.1 70B, can fit into two 40GB GPUs. - -[//]: # (TODO: Example: INT4 / 70B / 40GB:2) - -The INT4 version of Llama 3.1 405B can fit into eight 40GB GPUs. - -[//]: # (TODO: Example: INT4 / 405B / 40GB:8) - -Useful links: - - * [Meta's official FP8 quantized version of Llama 3.1 405B](https://huggingface.co/meta-llama/Meta-Llama-3.1-405B-Instruct-FP8) (with minimal accuracy degradation) - * [Llama 3.1 Quantized Models](https://huggingface.co/collections/hugging-quants/llama-31-gptq-awq-and-bnb-quants-669fa7f50f6e713fd54bd198) with quantized checkpoints - -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ dstack apply -f examples/llms/llama31/vllm/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod CA-MTL-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 2 runpod EU-SE-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 3 gcp us-west4 27xCPU, 150GB, A5000:24GB:2 yes $0.23 - -Submit the run llama31? [y/n]: y - -Provisioning... ----> 100% -``` - -
- -Once the service is up, the model will be available via the OpenAI-compatible endpoint -at `/proxy/models//`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/models/main/chat/completions \ - -X POST \ - -H 'Authorization: Bearer <dstack token>' \ - -H 'Content-Type: application/json' \ - -d '{ - "model": "llama3.1", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "What is Deep Learning?" - } - ], - "max_tokens": 128 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the OpenAI-compatible endpoint -is available at `https://gateway./`. - -[//]: # (TODO: How to prompting and tool calling) - -[//]: # (TODO: Syntetic data generation) - -## Fine-tuning - -### Running on multiple GPUs - -Below is the task configuration file of fine-tuning Llama 3.1 8B using TRL on the -[`OpenAssistant/oasst_top1_2023-08-25`](https://huggingface.co/datasets/OpenAssistant/oasst_top1_2023-08-25) dataset. - -
- -```yaml -type: task -name: trl-train - -python: 3.12 -# Ensure nvcc is installed (req. for Flash Attention) -nvcc: true -env: - - HF_TOKEN - - WANDB_API_KEY -commands: - - pip install "transformers>=4.43.2" - - pip install bitsandbytes - - pip install flash-attn --no-build-isolation - - pip install peft - - pip install wandb - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - accelerate launch - --config_file=examples/accelerate_configs/multi_gpu.yaml - --num_processes $DSTACK_GPUS_PER_NODE - examples/scripts/sft.py - --model_name meta-llama/Meta-Llama-3.1-8B - --dataset_name OpenAssistant/oasst_top1_2023-08-25 - --dataset_text_field="text" - --per_device_train_batch_size 1 - --per_device_eval_batch_size 1 - --gradient_accumulation_steps 4 - --learning_rate 2e-4 - --report_to wandb - --bf16 - --max_seq_length 1024 - --lora_r 16 --lora_alpha 32 - --lora_target_modules q_proj k_proj v_proj o_proj - --load_in_4bit - --use_peft - --attn_implementation "flash_attention_2" - --logging_steps=10 - --output_dir models/llama31 - --hub_model_id peterschmidt85/FineLlama-3.1-8B - -resources: -gpu: - # 24GB or more VRAM - memory: 24GB.. - # One or more GPU - count: 1.. -# Shared memory (for multi-gpu) -shm_size: 24GB -``` - -
- -Change the `resources` property to specify more GPUs. - -### Memory requirements - -Below are the approximate memory requirements for fine-tuning Llama 3.1. - -| Model size | Full fine-tuning | LoRA | QLoRA | -|------------|------------------|-------|-------| -| **8B** | 60GB | 16GB | 6GB | -| **70B** | 500GB | 160GB | 48GB | -| **405B** | 3.25TB | 950GB | 250GB | - -The requirements can be significantly reduced with certain optimizations. - -### DeepSpeed - -For more memory-efficient use of multiple GPUs, consider using DeepSpeed and ZeRO Stage 3. - -To do this, use the `examples/accelerate_configs/deepspeed_zero3.yaml` configuration file instead of -`examples/accelerate_configs/multi_gpu.yaml`. - -### Running on multiple nodes - -In case the model doesn't feet into a single GPU, consider running a `dstack` task on multiple nodes. -Below is the corresponding task configuration file. - -
- -```yaml -type: task -name: trl-train-distrib - -# Size of the cluster -nodes: 2 - -python: "3.10" -# Ensure nvcc is installed (req. for Flash Attention) -nvcc: true - -env: - - HF_TOKEN - - WANDB_API_KEY -commands: - - pip install "transformers>=4.43.2" - - pip install bitsandbytes - - pip install flash-attn --no-build-isolation - - pip install peft - - pip install wandb - - git clone https://github.com/huggingface/trl - - cd trl - - pip install . - - accelerate launch - --config_file=examples/accelerate_configs/fsdp_qlora.yaml - --main_process_ip=$DSTACK_MASTER_NODE_IP - --main_process_port=8008 - --machine_rank=$DSTACK_NODE_RANK - --num_processes=$DSTACK_GPUS_NUM - --num_machines=$DSTACK_NODES_NUM - examples/scripts/sft.py - --model_name meta-llama/Meta-Llama-3.1-8B - --dataset_name OpenAssistant/oasst_top1_2023-08-25 - --dataset_text_field="text" - --per_device_train_batch_size 1 - --per_device_eval_batch_size 1 - --gradient_accumulation_steps 4 - --learning_rate 2e-4 - --report_to wandb - --bf16 - --max_seq_length 1024 - --lora_r 16 --lora_alpha 32 - --lora_target_modules q_proj k_proj v_proj o_proj - --load_in_4bit - --use_peft - --attn_implementation "flash_attention_2" - --logging_steps=10 - --output_dir models/llama31 - --hub_model_id peterschmidt85/FineLlama-3.1-8B - --torch_dtype bfloat16 - --use_bnb_nested_quant - -resources: - gpu: - # 24GB or more VRAM - memory: 24GB.. - # One or more GPU - count: 1.. - # Shared memory (for multi-gpu) - shm_size: 24GB -``` - -
- -[//]: # (TODO: Find a better example for a multi-node training) - -## Source code - -The source-code of this example can be found in -[`examples/llms/llama31`](https://github.com/dstackai/dstack/blob/master/examples/llms/llama31) and [`examples/single-node-training/trl`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/trl). - -## What's next? - -1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [protips](https://dstack.ai/docs/protips). -2. Browse [Llama 3.1 on HuggingFace](https://huggingface.co/collections/meta-llama/llama-31-669fc079a0c406a149a5738f), - [HuggingFace's Llama recipes](https://github.com/huggingface/huggingface-llama-recipes), - [Meta's Llama recipes](https://github.com/meta-llama/llama-recipes) - and [Llama Agentic System](https://github.com/meta-llama/llama-agentic-system/). diff --git a/examples/llms/llama32/README.md b/examples/llms/llama32/README.md deleted file mode 100644 index c607139326..0000000000 --- a/examples/llms/llama32/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Llama 3.2 - -This example walks you through how to deploy Llama 3.2 vision model with `dstack` using `vLLM`. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -Here's an example of a service that deploys Llama 3.2 11B using vLLM. - -
- -```yaml -type: service -name: llama32 - -image: vllm/vllm-openai:latest -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-3.2-11B-Vision-Instruct - - MAX_MODEL_LEN=4096 - - MAX_NUM_SEQS=8 -commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --max-num-seqs $MAX_NUM_SEQS - --enforce-eager - --disable-log-requests - --limit-mm-per-prompt "image=1" - --tensor-parallel-size $DSTACK_GPUS_NUM -port: 8000 -# Register the model -model: meta-llama/Llama-3.2-11B-Vision-Instruct - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - -resources: - gpu: 40GB..48GB -``` -
- -[//]: # (TODO: Comment on MAX_MODEL_LEN and MAX_NUM_SEQS) - -### Memory requirements - -Below are the approximate memory requirements for loading the model. -This excludes memory for the model context and CUDA kernel reservations. - -| Model size | FP16 | -|------------|-------| -| **11B** | 40GB | -| **90B** | 180GB | - -[//]: # (TODO: Quantization mention) - -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ dstack apply -f examples/llms/llama32/vllm/.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod CA-MTL-1 9xCPU, 50GB, 1xA40 (48GB) yes $0.24 - 2 runpod EU-SE-1 9xCPU, 50GB, 1xA40 (48GB) yes $0.24 - 3 runpod EU-SE-1 9xCPU, 50GB, 1xA6000 (48GB) yes $0.25 - - -Submit the run llama32? [y/n]: y - -Provisioning... ----> 100% -``` - -
- -Once the service is up, it will be available via the service endpoint -at `/proxy/services///`. - -
- -```shell -$ curl http://127.0.0.1:3000/proxy/services/main/llama32/v1/chat/completions \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer token' \ - --data '{ - "model": "meta-llama/Llama-3.2-11B-Vision-Instruct", - "messages": [ - { - "role": "user", - "content": [ - {"type" : "text", "text": "Describe the image."}, - {"type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/e/ea/Bento_at_Hanabishi%2C_Koyasan.jpg"}} - ] - }], - "max_tokens": 2048 - }' -``` - -
- -When a [gateway](https://dstack.ai/docs/concepts/gateways/) is configured, the service endpoint -is available at `https://./`. - -[//]: # (TODO: https://github.com/dstackai/dstack/issues/1777) - -## Source code - -The source-code of this example can be found in -[`examples/llms/llama32`](https://github.com/dstackai/dstack/blob/master/examples/llms/llama32). - -## What's next? - -1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [protips](https://dstack.ai/docs/protips). -2. Browse [Llama 3.2 on HuggingFace](https://huggingface.co/collections/meta-llama/llama-32-66f448ffc8c32f949b04c8cf) - and [LLama 3.2 on vLLM](https://docs.vllm.ai/en/latest/models/supported_models.html#multimodal-language-models). diff --git a/examples/llms/llama32/vllm/.dstack.yml b/examples/llms/llama32/vllm/.dstack.yml deleted file mode 100644 index 3712a64f80..0000000000 --- a/examples/llms/llama32/vllm/.dstack.yml +++ /dev/null @@ -1,27 +0,0 @@ -type: service -name: llama32 - -image: vllm/vllm-openai:latest -env: - - HF_TOKEN - - MODEL_ID=meta-llama/Llama-3.2-11B-Vision-Instruct - - MAX_MODEL_LEN=4096 - - MAX_NUM_SEQS=8 -commands: - - vllm serve $MODEL_ID - --max-model-len $MAX_MODEL_LEN - --max-num-seqs $MAX_NUM_SEQS - --enforce-eager - --disable-log-requests - --limit-mm-per-prompt "image=1" - --tensor-parallel-size $DSTACK_GPUS_NUM -port: 8000 -# Register the model -model: meta-llama/Llama-3.2-11B-Vision-Instruct - -# Uncomment to cache downloaded models -#volumes: -# - /root/.cache/huggingface/hub:/root/.cache/huggingface/hub - -resources: - gpu: 40GB..48GB diff --git a/examples/misc/docker-compose/.dstack.yml b/examples/misc/docker-compose/.dstack.yml deleted file mode 100644 index 0967f72b81..0000000000 --- a/examples/misc/docker-compose/.dstack.yml +++ /dev/null @@ -1,16 +0,0 @@ -type: dev-environment -name: vscode-docker - -docker: true -env: - - MODEL_ID=meta-llama/Llama-3.2-3B-Instruct - - HF_TOKEN -ide: vscode -files: - - compose.yaml - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - gpu: 24GB diff --git a/examples/misc/docker-compose/README.md b/examples/misc/docker-compose/README.md deleted file mode 100644 index d74dba0304..0000000000 --- a/examples/misc/docker-compose/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# Docker Compose - -All backends except `runpod`, `vastai`, and `kubernetes` allow using [Docker and Docker Compose](https://dstack.ai/docs/guides/protips#docker-and-docker-compose) inside `dstack` runs. - -This example shows how to deploy Hugging Face [Chat UI](https://huggingface.co/docs/chat-ui/index) -with [TGI](https://huggingface.co/docs/text-generation-inference/en/index) -serving [Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) -using [Docker Compose](https://docs.docker.com/compose/). - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Deployment - -### Running as a task - -=== "`task.dstack.yml`" - -
- - ```yaml - type: task - name: chat-ui-task - - docker: true - env: - - MODEL_ID=meta-llama/Llama-3.2-3B-Instruct - - HF_TOKEN - files: - - compose.yaml - commands: - - docker compose up - ports: - - 9000 - - resources: - gpu: "nvidia:24GB" - ``` - -
- -=== "`compose.yaml`" - -
- - ```yaml - services: - app: - image: ghcr.io/huggingface/chat-ui:sha-bf0bc92 - command: - - bash - - -c - - | - echo MONGODB_URL=mongodb://db:27017 > .env.local - echo MODELS='`[{ - "name": "${MODEL_ID?}", - "endpoints": [{"type": "tgi", "url": "http://tgi:8000"}] - }]`' >> .env.local - exec ./entrypoint.sh - ports: - - 127.0.0.1:9000:3000 - depends_on: - - tgi - - db - - tgi: - image: ghcr.io/huggingface/text-generation-inference:sha-704a58c - volumes: - - tgi_data:/data - environment: - HF_TOKEN: ${HF_TOKEN?} - MODEL_ID: ${MODEL_ID?} - PORT: 8000 - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - - db: - image: mongo:latest - volumes: - - db_data:/data/db - - volumes: - tgi_data: - db_data: - ``` - -
- -### Deploying as a service - -If you'd like to deploy Chat UI as an auto-scalable and secure endpoint, -use the service configuration. You can find it at [`examples/misc/docker-compose/service.dstack.yml`](https://github.com/dstackai/dstack/blob/master/examples/misc/docker-compose/service.dstack.yml) - -### Running a configuration - -To run a configuration, use the [`dstack apply`](https://dstack.ai/docs/reference/cli/dstack/apply.md) command. - -
- -```shell -$ HF_TOKEN=... -$ dstack apply -f examples/examples/misc/docker-compose/task.dstack.yml - - # BACKEND REGION RESOURCES SPOT PRICE - 1 runpod CA-MTL-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 2 runpod EU-SE-1 18xCPU, 100GB, A5000:24GB yes $0.12 - 3 gcp us-west4 27xCPU, 150GB, A5000:24GB:2 yes $0.23 - -Submit the run chat-ui-task? [y/n]: y - -Provisioning... ----> 100% -``` - -
- -## Persisting data - -To persist data between runs, create a [volume](https://dstack.ai/docs/concepts/volumes/) and attach it to the run -configuration. - -
- -```yaml -type: task -name: chat-ui-task - -privileged: true -image: dstackai/dind -env: - - MODEL_ID=meta-llama/Llama-3.2-3B-Instruct - - HF_TOKEN -files: - - compose.yaml -commands: - - start-dockerd - - docker compose up -ports: - - 9000 - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - # Required resources - gpu: "nvidia:24GB" - -volumes: - - name: my-dind-volume - path: /var/lib/docker -``` - -
- -With this change, all Docker data—pulled images, containers, and crucially, volumes for database and model storage—will -be persisted. - -## Source code - -The source-code of this example can be found in -[`examples/misc/docker-compose`](https://github.com/dstackai/dstack/blob/master/examples/misc/docker-compose). - -## What's next? - -1. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [protips](https://dstack.ai/docs/protips). diff --git a/examples/misc/docker-compose/compose.yaml b/examples/misc/docker-compose/compose.yaml deleted file mode 100644 index c5c843667c..0000000000 --- a/examples/misc/docker-compose/compose.yaml +++ /dev/null @@ -1,42 +0,0 @@ -services: - app: - image: ghcr.io/huggingface/chat-ui-db:0.9.5 - environment: - HF_TOKEN: ${HF_TOKEN?} - MONGODB_URL: mongodb://db:27017 - MODELS: | - [{ - "name": "${MODEL_ID?}", - "endpoints": [{"type": "openai", "baseURL": "http://tgi:8000/v1"}] - }] - ports: - - 127.0.0.1:9000:3000 - depends_on: - - tgi - - db - - tgi: - image: ghcr.io/huggingface/text-generation-inference:3.3.4 - volumes: - - tgi_data:/data - environment: - HF_TOKEN: ${HF_TOKEN?} - MODEL_ID: ${MODEL_ID?} - PORT: 8000 - shm_size: 1g - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - - db: - image: mongo:latest - volumes: - - db_data:/data/db - -volumes: - tgi_data: - db_data: diff --git a/examples/misc/docker-compose/service.dstack.yml b/examples/misc/docker-compose/service.dstack.yml deleted file mode 100644 index b33b900fbd..0000000000 --- a/examples/misc/docker-compose/service.dstack.yml +++ /dev/null @@ -1,26 +0,0 @@ -type: service -name: chat-ui-service - -docker: true -env: - - MODEL_ID=meta-llama/Llama-3.2-3B-Instruct - - HF_TOKEN -files: - - compose.yaml -commands: - - docker compose up -port: 9000 -auth: false - -# Uncomment to leverage spot instances -#spot_policy: auto - -resources: - # Required resources - gpu: 1 - -# Cache the Docker data -volumes: - - instance_path: /root/.cache/docker-data - path: /var/lib/docker - optional: true diff --git a/examples/misc/docker-compose/task.dstack.yml b/examples/misc/docker-compose/task.dstack.yml deleted file mode 100644 index e7af43f383..0000000000 --- a/examples/misc/docker-compose/task.dstack.yml +++ /dev/null @@ -1,25 +0,0 @@ -type: task -name: chat-ui-task - -docker: true -env: - - MODEL_ID=meta-llama/Llama-3.2-3B-Instruct - - HF_TOKEN -files: - - compose.yaml -commands: - - docker compose up -ports: - - 9000 - -# Use either spot or on-demand instances -spot_policy: auto - -resources: - gpu: 1 - -# Cache the Docker data -volumes: - - instance_path: /root/.cache/docker-data - path: /var/lib/docker - optional: true diff --git a/examples/misc/docker-compose/volume.dstack.yml b/examples/misc/docker-compose/volume.dstack.yml deleted file mode 100644 index d5ddc1cc67..0000000000 --- a/examples/misc/docker-compose/volume.dstack.yml +++ /dev/null @@ -1,8 +0,0 @@ -type: volume -name: my-dind-volume - -backend: aws -region: eu-west-1 - -# Required size -size: 100GB diff --git a/examples/models/wan22/.dstack.yml b/examples/models/wan22/.dstack.yml deleted file mode 100644 index 528e11e0d8..0000000000 --- a/examples/models/wan22/.dstack.yml +++ /dev/null @@ -1,63 +0,0 @@ -type: task -name: wan22 - -repos: - # Clones it to `/workflow` (the default working directory) - - https://github.com/Wan-Video/Wan2.2.git - -python: 3.12 -nvcc: true - -env: - - PROMPT="Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage." - # Required for storing cache on a volume - - UV_LINK_MODE=copy -commands: - # Install flash-attn - - | - uv pip install torch - uv pip install flash-attn --no-build-isolation - # Install dependencies - - | - uv pip install . decord librosa - uv pip install "huggingface_hub[cli]" - hf download Wan-AI/Wan2.2-T2V-A14B --local-dir /root/.cache/Wan2.2-T2V-A14B - # Generate video - - | - if [ ${DSTACK_GPUS_NUM} -gt 1 ]; then - torchrun \ - --nproc_per_node=${DSTACK_GPUS_NUM} \ - generate.py \ - --task t2v-A14B \ - --size 1280*720 \ - --ckpt_dir /root/.cache/Wan2.2-T2V-A14B \ - --dit_fsdp --t5_fsdp --ulysses_size ${DSTACK_GPUS_NUM} \ - --save_file ${DSTACK_RUN_NAME}.mp4 \ - --prompt "${PROMPT}" - else - python generate.py \ - --task t2v-A14B \ - --size 1280*720 \ - --ckpt_dir /root/.cache/Wan2.2-T2V-A14B \ - --offload_model True \ - --convert_model_dtype \ - --save_file ${DSTACK_RUN_NAME}.mp4 \ - --prompt "${PROMPT}" - fi - # Upload video - - curl https://bashupload.com/ -T ./${DSTACK_RUN_NAME}.mp4 - -resources: - gpu: - name: [H100, H200] - count: 1..8 - disk: 300GB - -# Change to on-demand for disabling spot -spot_policy: auto - -volumes: - # Cache pip packages and HF models - - instance_path: /root/dstack-cache - path: /root/.cache/ - optional: true diff --git a/examples/models/wan22/README.md b/examples/models/wan22/README.md deleted file mode 100644 index 47e9f4230d..0000000000 --- a/examples/models/wan22/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# Wan2.2 - -[Wan2.2](https://github.com/Wan-Video/Wan2.2) is an open-source SOTA foundational video model. This example shows how to run the T2V-A14B model variant via `dstack` for text-to-video generation. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Define a configuration - -Below is a task configuration that generates a video using Wan2.2, uploads it, and provides the download link. - -
- -```yaml -type: task -name: wan22 - -repos: - # Clones it to `/workflow` (the default working directory) - - https://github.com/Wan-Video/Wan2.2.git - -python: 3.12 -nvcc: true - -env: - - PROMPT="Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage." - # Required for storing cache on a volume - - UV_LINK_MODE=copy -commands: - # Install flash-attn - - | - uv pip install torch - uv pip install flash-attn --no-build-isolation - # Install dependencies - - | - uv pip install . decord librosa - uv pip install "huggingface_hub[cli]" - hf download Wan-AI/Wan2.2-T2V-A14B --local-dir /root/.cache/Wan2.2-T2V-A14B - # Generate video - - | - if [ ${DSTACK_GPUS_NUM} -gt 1 ]; then - torchrun \ - --nproc_per_node=${DSTACK_GPUS_NUM} \ - generate.py \ - --task t2v-A14B \ - --size 1280*720 \ - --ckpt_dir /root/.cache/Wan2.2-T2V-A14B \ - --dit_fsdp --t5_fsdp --ulysses_size ${DSTACK_GPUS_NUM} \ - --save_file ${DSTACK_RUN_NAME}.mp4 \ - --prompt "${PROMPT}" - else - python generate.py \ - --task t2v-A14B \ - --size 1280*720 \ - --ckpt_dir /root/.cache/Wan2.2-T2V-A14B \ - --offload_model True \ - --convert_model_dtype \ - --save_file ${DSTACK_RUN_NAME}.mp4 \ - --prompt "${PROMPT}" - fi - # Upload video - - curl https://bashupload.com/ -T ./${DSTACK_RUN_NAME}.mp4 - -resources: - gpu: - name: [H100, H200] - count: 1..8 - disk: 300GB - -# Change to on-demand for disabling spot -spot_policy: auto - -volumes: - # Cache pip packages and HF models - - instance_path: /root/dstack-cache - path: /root/.cache/ - optional: true -``` - -
- -## Run the configuration - -Once the configuration is ready, run `dstack apply -f `, and `dstack` will automatically provision the -cloud resources and run the configuration. - -
- -```shell -$ dstack apply -f examples/models/wan22/.dstack.yml - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 datacrunch (FIN-01) cpu=30 mem=120GB disk=200GB H100:80GB:1 (spot) 1H100.80S.30V $0.99 - 2 datacrunch (FIN-01) cpu=30 mem=120GB disk=200GB H100:80GB:1 (spot) 1H100.80S.30V $0.99 - 3 datacrunch (FIN-02) cpu=44 mem=182GB disk=200GB H200:141GB:1 (spot) 1H200.141S.44V $0.99 - ----> 100% - -Uploaded 1 file, 8 375 523 bytes - -wget https://bashupload.com/fIo7l/wan22.mp4 -``` - -
- -If you want you can override the default GPU, spot policy, and even the prompt via the CLI. - -
- -```shell -$ PROMPT=... -$ dstack apply -f examples/models/wan22/.dstack.yml --spot --gpu H100,H200:8 - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 aws (us-east-2) cpu=192 mem=2048GB disk=300GB H100:80GB:8 (spot) p5.48xlarge $6.963 - 2 datacrunch (FIN-02) cpu=176 mem=1480GB disk=300GB H100:80GB:8 (spot) 8H100.80S.176V $7.93 - 3 datacrunch (ICE-01) cpu=176 mem=1450GB disk=300GB H200:141GB:8 (spot) 8H200.141S.176V $7.96 - ----> 100% - -Uploaded 1 file, 8 375 523 bytes - -wget https://bashupload.com/fIo7l/wan22.mp4 -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/models/wan22`](https://github.com/dstackai/dstack/blob/master/examples/models/wan22). diff --git a/examples/plugins/example_plugin/pyproject.toml b/examples/plugins/example_plugin/pyproject.toml index bc83d509a4..66f954a59a 100644 --- a/examples/plugins/example_plugin/pyproject.toml +++ b/examples/plugins/example_plugin/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" authors = [ { name = "Victor Skvortsov", email = "victor@dstack.ai" } ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [] [build-system] diff --git a/examples/plugins/example_plugin_server/pyproject.toml b/examples/plugins/example_plugin_server/pyproject.toml index c910b172b5..cc5d3b1c0a 100644 --- a/examples/plugins/example_plugin_server/pyproject.toml +++ b/examples/plugins/example_plugin_server/pyproject.toml @@ -3,7 +3,7 @@ name = "dstack-plugin-server" version = "0.1.0" description = "Example plugin server" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "fastapi", "uvicorn", diff --git a/examples/single-node-training/axolotl/.dstack.yml b/examples/single-node-training/axolotl/.dstack.yml deleted file mode 100644 index dd28618904..0000000000 --- a/examples/single-node-training/axolotl/.dstack.yml +++ /dev/null @@ -1,28 +0,0 @@ -type: task -# The name is optional, if not specified, generated randomly -name: axolotl-nvidia-llama-scout-train - -# Using the official Axolotl's Docker image -image: axolotlai/axolotl:main-latest - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - HUB_MODEL_ID -# Commands of the task -commands: - - wget https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml - - | - axolotl train scout-qlora-flexattn-fsdp2.yaml \ - --wandb-project $WANDB_PROJECT \ - --wandb-name $DSTACK_RUN_NAME \ - --hub-model-id $HUB_MODEL_ID - -resources: - # Four GPU (required by FSDP) - gpu: H100:4 - # Shared memory size for inter-process communication - shm_size: 64GB - disk: 500GB.. diff --git a/examples/single-node-training/axolotl/README.md b/examples/single-node-training/axolotl/README.md deleted file mode 100644 index c9c7917f0a..0000000000 --- a/examples/single-node-training/axolotl/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Axolotl - -This example shows how to use [Axolotl](https://github.com/OpenAccess-AI-Collective/axolotl) with `dstack` to fine-tune 4-bit Quantized `Llama-4-Scout-17B-16E` using SFT with FSDP and QLoRA. - -??? info "Prerequisites" - Once `dstack` is [installed](https://dstack.ai/docs/installation), clone the repo with examples. - -
- - ```shell - $ git clone https://github.com/dstackai/dstack - $ cd dstack - ``` - -
- -## Define a configuration - -Axolotl reads the model, QLoRA, and dataset arguments, as well as trainer configuration from a [`scout-qlora-flexattn-fsdp2.yaml`](https://github.com/axolotl-ai-cloud/axolotl/blob/main/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml) file. The configuration uses 4-bit axolotl quantized version of `meta-llama/Llama-4-Scout-17B-16E`, requiring only ~43GB VRAM/GPU with 4K context length. - -Below is a task configuration that does fine-tuning. - -
- -```yaml -type: task -# The name is optional, if not specified, generated randomly -name: axolotl-nvidia-llama-scout-train - -# Using the official Axolotl's Docker image -image: axolotlai/axolotl:main-latest - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - WANDB_PROJECT - - HUB_MODEL_ID -# Commands of the task -commands: - - wget https://raw.githubusercontent.com/axolotl-ai-cloud/axolotl/main/examples/llama-4/scout-qlora-flexattn-fsdp2.yaml - - | - axolotl train scout-qlora-flexattn-fsdp2.yaml \ - --wandb-project $WANDB_PROJECT \ - --wandb-name $DSTACK_RUN_NAME \ - --hub-model-id $HUB_MODEL_ID - -resources: - # Four GPU (required by FSDP) - gpu: H100:4 - # Shared memory size for inter-process communication - shm_size: 64GB - disk: 500GB.. -``` - -
- -The task uses Axolotl's Docker image, where Axolotl is already pre-installed. - -!!! info "AMD" - The example above uses NVIDIA accelerators. To use it with AMD, check out [AMD](https://dstack.ai/examples/accelerators/amd#axolotl). - -## Run the configuration - -Once the configuration is ready, run `dstack apply -f `, and `dstack` will automatically provision the -cloud resources and run the configuration. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ WANDB_PROJECT=... -$ HUB_MODEL_ID=... -$ dstack apply -f examples/single-node-training/axolotl/.dstack.yml - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 vastai (cz-czechia) cpu=64 mem=128GB H100:80GB:2 18794506 $3.8907 - 2 vastai (us-texas) cpu=52 mem=64GB H100:80GB:2 20442365 $3.6926 - 3 vastai (fr-france) cpu=64 mem=96GB H100:80GB:2 20379984 $3.7389 - -Submit the run axolotl-nvidia-llama-scout-train? [y/n]: - -Provisioning... ----> 100% -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/single-node-training/axolotl`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/axolotl) and [`examples/distributed-training/axolotl`](https://github.com/dstackai/dstack/blob/master/examples/distributed-training/axolotl). - -## What's next? - -1. Browse the [Axolotl distributed training](https://dstack.ai/docs/examples/distributed-training/axolotl) example -2. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), [fleets](https://dstack.ai/docs/concepts/fleets) -3. See the [AMD](https://dstack.ai/examples/accelerators/amd#axolotl) example diff --git a/examples/single-node-training/trl/README.md b/examples/single-node-training/trl/README.md deleted file mode 100644 index 2084082818..0000000000 --- a/examples/single-node-training/trl/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# TRL - -This example walks you through how to use [TRL](https://github.com/huggingface/trl) to fine-tune `Llama-3.1-8B` with `dstack` using SFT with QLoRA. - -## Define a configuration - -Below is a task configuration that does fine-tuning. - -
- -```yaml -type: task -name: trl-train - -python: 3.12 -# Ensure nvcc is installed (req. for Flash Attention) -nvcc: true - -env: - - HF_TOKEN - - WANDB_API_KEY - - HUB_MODEL_ID -commands: - # Pin torch==2.6.0 to avoid building Flash Attention from source. - # Prebuilt Flash Attention wheels are not available for the latest torch==2.7.0. - - uv pip install torch==2.6.0 - - uv pip install transformers bitsandbytes peft wandb - - uv pip install flash_attn --no-build-isolation - - git clone https://github.com/huggingface/trl - - cd trl - - uv pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/multi_gpu.yaml \ - --num_processes $DSTACK_GPUS_PER_NODE \ - trl/scripts/sft.py \ - --model_name meta-llama/Meta-Llama-3.1-8B \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --lora_r 16 \ - --lora_alpha 32 \ - --lora_target_modules q_proj k_proj v_proj o_proj \ - --load_in_4bit \ - --use_peft \ - --attn_implementation "flash_attention_2" \ - --logging_steps=10 \ - --output_dir models/llama31 \ - --hub_model_id peterschmidt85/FineLlama-3.1-8B - -resources: - gpu: - # 24GB or more VRAM - memory: 24GB.. - # One or more GPU - count: 1.. - # Shared memory (for multi-gpu) - shm_size: 24GB -``` - -
- -Change the `resources` property to specify more GPUs. - -!!! info "AMD" - The example above uses NVIDIA accelerators. To use it with AMD, check out [AMD](https://dstack.ai/examples/accelerators/amd#trl). - -??? info "DeepSpeed" - For more memory-efficient use of multiple GPUs, consider using DeepSpeed and ZeRO Stage 3. - - To do this, use the `examples/accelerate_configs/deepspeed_zero3.yaml` configuration file instead of - `examples/accelerate_configs/multi_gpu.yaml`. - -## Run the configuration - -Once the configuration is ready, run `dstack apply -f `, and `dstack` will automatically provision the -cloud resources and run the configuration. - -
- -```shell -$ HF_TOKEN=... -$ WANDB_API_KEY=... -$ HUB_MODEL_ID=... -$ dstack apply -f examples/single-node-training/trl/train.dstack.yml - - # BACKEND RESOURCES INSTANCE TYPE PRICE - 1 vastai (cz-czechia) cpu=64 mem=128GB H100:80GB:2 18794506 $3.8907 - 2 vastai (us-texas) cpu=52 mem=64GB H100:80GB:2 20442365 $3.6926 - 3 vastai (fr-france) cpu=64 mem=96GB H100:80GB:2 20379984 $3.7389 - -Submit the run trl-train? [y/n]: - -Provisioning... ----> 100% -``` - -
- -## Source code - -The source-code of this example can be found in -[`examples/llms/llama31`](https://github.com/dstackai/dstack/blob/master/examples/llms/llama31) and [`examples/single-node-training/trl`](https://github.com/dstackai/dstack/blob/master/examples/single-node-training/trl). - -## What's next? - -1. Browse the [TRL distributed training](https://dstack.ai/docs/examples/distributed-training/trl) example -2. Check [dev environments](https://dstack.ai/docs/dev-environments), [tasks](https://dstack.ai/docs/tasks), - [services](https://dstack.ai/docs/services), and [fleets](https://dstack.ai/docs/fleets) -3. See the [AMD](https://dstack.ai/examples/accelerators/amd#trl) example diff --git a/examples/single-node-training/trl/train.dstack.yml b/examples/single-node-training/trl/train.dstack.yml deleted file mode 100644 index 9b24ae6131..0000000000 --- a/examples/single-node-training/trl/train.dstack.yml +++ /dev/null @@ -1,54 +0,0 @@ -type: task -# The name is optional, if not specified, generated randomly -name: trl-train - -python: 3.12 - -# Required environment variables -env: - - HF_TOKEN - - WANDB_API_KEY - - HUB_MODEL_ID - - ACCELERATE_LOG_LEVEL=info -# Commands of the task -commands: - # Pin torch==2.6.0 to avoid building Flash Attention from source. - # Prebuilt Flash Attention wheels are not available for the latest torch==2.7.0. - - uv pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 - - uv pip install transformers bitsandbytes peft wandb - - uv pip install flash_attn --no-build-isolation - - git clone https://github.com/huggingface/trl - - cd trl - - uv pip install . - - | - accelerate launch \ - --config_file=examples/accelerate_configs/multi_gpu.yaml \ - --num_processes $DSTACK_GPUS_PER_NODE \ - trl/scripts/sft.py \ - --model_name meta-llama/Meta-Llama-3.1-8B \ - --dataset_name OpenAssistant/oasst_top1_2023-08-25 \ - --dataset_text_field="text" \ - --per_device_train_batch_size 1 \ - --per_device_eval_batch_size 1 \ - --gradient_accumulation_steps 4 \ - --learning_rate 2e-4 \ - --report_to wandb \ - --bf16 \ - --max_seq_length 1024 \ - --lora_r 16 \ - --lora_alpha 32 \ - --lora_target_modules q_proj k_proj v_proj o_proj \ - --load_in_4bit \ - --use_peft \ - --attn_implementation "flash_attention_2" \ - --logging_steps=10 \ - --output_dir models/llama31 \ - --hub_model_id peterschmidt85/FineLlama-3.1-8B -resources: - gpu: - # 24GB or more VRAM - memory: 24GB.. - # One or more GPU - count: 1.. - # Shared memory (for multi-gpu) - shm_size: 24GB diff --git a/frontend/eslint.config.cjs b/frontend/eslint.config.cjs index 3cfe1c41e4..9750398241 100644 --- a/frontend/eslint.config.cjs +++ b/frontend/eslint.config.cjs @@ -1,6 +1,7 @@ const { defineConfig, globalIgnores } = require('eslint/config'); const i18N = require('eslint-plugin-i18n'); const simpleImportSort = require('eslint-plugin-simple-import-sort'); +const react = require('eslint-plugin-react'); const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const typescriptEslint = require('@typescript-eslint/eslint-plugin'); @@ -18,20 +19,25 @@ const BASE_CONFIG = { 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'prettier', - 'plugin:prettier/recommended' + 'plugin:prettier/recommended', + 'plugin:react/recommended', ), plugins: { '@typescript-eslint': typescriptEslint, i18n: i18N, 'simple-import-sort': simpleImportSort, + react: react, }, languageOptions: { parser: tsParser, }, + settings: {}, rules: { + 'react/jsx-no-target-blank': 'off', + 'react/no-unescaped-entities': 'off', 'i18n/no-russian-character': 1, 'simple-import-sort/imports': [ diff --git a/frontend/jest.auth.config.cjs b/frontend/jest.auth.config.cjs new file mode 100644 index 0000000000..c8616f64a9 --- /dev/null +++ b/frontend/jest.auth.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + rootDir: '.', + clearMocks: true, + testEnvironment: 'node', + moduleDirectories: ['node_modules', 'src'], + testMatch: [ + '/src/App/auth.test.tsx', + '/src/App/login.test.tsx', + '/src/services/preset.test.tsx', + ], + transform: { + '\\.[jt]sx?$': 'babel-jest', + }, +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e66eba969c..e76e6e8b13 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,7 @@ "@cloudscape-design/chat-components": "^1.0.62", "@cloudscape-design/collection-hooks": "^1.0.74", "@cloudscape-design/component-toolkit": "^1.0.0-beta.120", - "@cloudscape-design/components": "^3.0.1091", + "@cloudscape-design/components": "^3.0.1188", "@cloudscape-design/design-tokens": "^3.0.60", "@cloudscape-design/global-styles": "^1.0.45", "@hookform/resolvers": "^2.9.10", @@ -30,6 +30,7 @@ "rc-tooltip": "^5.2.2", "react": "^18.3.1", "react-avatar": "^5.0.3", + "react-bus": "^4.0.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", "react-hook-form": "^7.53.0", @@ -81,10 +82,11 @@ "cross-env": "^7.0.3", "css-loader": "^6.7.3", "enzyme": "^3.11.0", - "eslint": "^9.32.0", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.5", "eslint-plugin-i18n": "^2.4.0", "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-simple-import-sort": "^12.1.1", "favicons": "^7.2.0", "favicons-webpack-plugin": "^6.0.1", @@ -160,9 +162,9 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -236,14 +238,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -524,19 +527,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -565,25 +570,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -2017,25 +2024,24 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -2060,13 +2066,14 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2131,9 +2138,9 @@ } }, "node_modules/@cloudscape-design/components": { - "version": "3.0.1091", - "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.1091.tgz", - "integrity": "sha512-ESV83m/laX9OkuITjeucYRBi4WQSu9w8yniRZjRapiTH+zTlBxQv8Gcnvr9UYPo3cbYyig2HIdbAlOagDplgfA==", + "version": "3.0.1188", + "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.1188.tgz", + "integrity": "sha512-Ajk7wFr2boPO7v4pgBLjdLVcA7myBiCk5hzpzURGcg+oogX9lYtXHv80k60dqyn1kzx+J0xnZwEjLa0oRmflmA==", "license": "Apache-2.0", "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", @@ -2144,7 +2151,6 @@ "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "ace-builds": "^1.34.0", - "balanced-match": "^1.0.2", "clsx": "^1.1.0", "d3-shape": "^1.3.7", "date-fns": "^2.25.0", @@ -2549,10 +2555,11 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -2583,9 +2590,9 @@ "peer": true }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2611,13 +2618,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -2626,19 +2633,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2710,9 +2720,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -2723,9 +2733,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2733,13 +2743,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -3280,9 +3290,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -3827,6 +3837,19 @@ "dev": true, "optional": true }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4143,6 +4166,165 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", @@ -4258,9 +4440,10 @@ } }, "node_modules/@remix-run/router": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", - "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -4867,15 +5050,16 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { @@ -5044,15 +5228,6 @@ "form-data": "^4.0.0" } }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -5441,9 +5616,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5982,13 +6157,14 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -6001,7 +6177,31 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/array-union": { "version": "2.1.0", @@ -6032,16 +6232,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6050,20 +6291,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -6072,12 +6330,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6143,6 +6426,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -6154,13 +6438,14 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -6694,7 +6979,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/batch": { "version": "0.6.1", @@ -6724,43 +7010,36 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -6770,6 +7049,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -6781,7 +7061,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bonjour-service": { "version": "1.2.1", @@ -6799,10 +7080,11 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6893,25 +7175,66 @@ } }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -7387,6 +7710,7 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -7395,17 +7719,18 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -7417,6 +7742,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -7425,13 +7751,18 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -7453,6 +7784,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -7465,6 +7797,7 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -7476,19 +7809,21 @@ "dev": true }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", @@ -8043,14 +8378,15 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8060,29 +8396,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -8239,6 +8577,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8257,6 +8596,7 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -8323,10 +8663,11 @@ "dev": true }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -8370,6 +8711,19 @@ "node": ">=6" } }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -8464,6 +8818,20 @@ "tslib": "^2.0.3" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8528,6 +8896,7 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8659,57 +9028,66 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", + "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -8725,13 +9103,10 @@ "dev": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -8740,7 +9115,34 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, "engines": { "node": ">= 0.4" } @@ -8751,10 +9153,10 @@ "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -8763,37 +9165,43 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -8835,25 +9243,24 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -8951,6 +9358,67 @@ } } }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", @@ -9166,6 +9634,7 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9241,45 +9710,50 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -9287,6 +9761,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -9295,7 +9770,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -9531,10 +10007,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -9573,17 +10050,18 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -9595,6 +10073,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -9603,7 +10082,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/find-cache-dir": { "version": "2.1.0", @@ -9692,12 +10172,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/fork-ts-checker-webpack-plugin": { @@ -9826,12 +10313,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -9860,6 +10350,7 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9882,6 +10373,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9937,21 +10429,23 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -9969,6 +10463,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10001,16 +10505,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10028,6 +10537,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -10041,14 +10563,15 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -10188,12 +10711,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10247,10 +10770,14 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10276,10 +10803,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -10288,10 +10819,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10303,7 +10834,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -10318,7 +10848,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -10527,19 +11056,24 @@ "dev": true }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -10563,10 +11097,11 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -10849,14 +11384,15 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10892,13 +11428,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -10913,13 +11451,37 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10938,13 +11500,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -10971,10 +11534,11 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -10986,11 +11550,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -11001,12 +11568,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11039,6 +11608,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -11057,6 +11642,26 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -11102,6 +11707,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -11136,12 +11754,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11175,13 +11795,16 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -11204,13 +11827,27 @@ "node": ">=6" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -11232,12 +11869,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11253,12 +11892,15 @@ "dev": true }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -11268,13 +11910,27 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11283,12 +11939,33 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11310,7 +11987,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -11441,6 +12119,24 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -12339,9 +13035,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -12409,6 +13105,22 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12703,14 +13415,16 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -12988,6 +13702,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -13008,6 +13731,7 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13029,6 +13753,7 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -13052,6 +13777,7 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13074,6 +13800,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -13178,6 +13905,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mnth": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mnth/-/mnth-2.0.0.tgz", @@ -13232,15 +13965,16 @@ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -13362,15 +14096,6 @@ "node": "4.x || >=6.0.0" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -18584,10 +19309,11 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -18621,14 +19347,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -18639,26 +19368,49 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -18688,10 +19440,11 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -18799,6 +19552,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -18997,10 +19768,11 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -19210,11 +19982,30 @@ "node": ">=4" } }, + "node_modules/pkijs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", + "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -20427,6 +21218,7 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -20440,6 +21232,7 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -20474,13 +21267,34 @@ } ] }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -20555,34 +21369,27 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -20692,6 +21499,19 @@ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-bus": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-bus/-/react-bus-4.0.1.tgz", + "integrity": "sha512-tzPWE23WN0U9v3YaGKAlLW7GXYv1YkCUgWcnzm8HDtfBeD2vDvK8PYHkJVrwMdzg5BleHcxejtdKYfIYZxD7PQ==", + "license": "MIT", + "dependencies": { + "@types/react": "^18.0.8", + "mitt": "^3.0.1" + }, + "peerDependencies": { + "react": ">=17.0.0 || ^19.0.0-0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -20865,11 +21685,12 @@ } }, "node_modules/react-router": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", - "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.20.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -20879,12 +21700,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", - "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.20.0", - "react-router": "6.27.0" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -21020,6 +21842,36 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/reftools": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", @@ -21047,11 +21899,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -21068,14 +21915,17 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" }, "engines": { @@ -21490,14 +22340,16 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -21526,15 +22378,33 @@ } ] }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -21676,16 +22546,17 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { @@ -21698,24 +22569,25 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -21726,6 +22598,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -21734,16 +22607,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -21832,15 +22697,16 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, + "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -21868,6 +22734,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -21878,11 +22745,27 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -22048,15 +22931,73 @@ "dev": true }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -22260,14 +23201,29 @@ "dev": true }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -22314,16 +23270,59 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -22333,15 +23332,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -22879,6 +23883,7 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } @@ -23026,9 +24031,30 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -23068,6 +24094,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -23077,30 +24104,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -23110,17 +24139,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -23130,17 +24161,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -23163,25 +24195,30 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.17" } @@ -23245,6 +24282,7 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -23310,6 +24348,7 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -23348,6 +24387,7 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -23565,14 +24605,16 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", - "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", + "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", @@ -23581,18 +24623,17 @@ "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.19.2", + "express": "^4.22.1", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", @@ -23621,6 +24662,19 @@ } } }, + "node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -23849,31 +24903,85 @@ } }, "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index 9d581114df..9b55459c1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,16 @@ "scripts": { "start": "cross-env NODE_ENV=development webpack serve --config webpack.config.js", "start-sky": "cross-env NODE_ENV=development UI_VERSION=sky webpack serve --config webpack.config.js", + "start-factory": "cross-env NODE_ENV=development UI_VERSION=factory webpack serve --config webpack.config.js", + "start-enterprise": "cross-env NODE_ENV=development UI_VERSION=enterprise webpack serve --config webpack.config.js", "build": "cross-env NODE_ENV=production webpack build --config webpack.config.js", "build-sky": "cross-env NODE_ENV=production UI_VERSION=sky webpack build --config webpack.config.js", + "build-factory": "cross-env NODE_ENV=production UI_VERSION=factory webpack build --config webpack.config.js", + "build-enterprise": "cross-env NODE_ENV=production UI_VERSION=enterprise webpack build --config webpack.config.js", "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "eslint-fix": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix", "test": "jest", + "test:auth": "cross-env NODE_ENV=test jest --config jest.auth.config.cjs", "test:update-snapshots": "jest -u", "generate-api": "npx @rtk-query/codegen-openapi openapi-config.ts", "pre-commit": "lint-staged" @@ -59,10 +64,11 @@ "cross-env": "^7.0.3", "css-loader": "^6.7.3", "enzyme": "^3.11.0", - "eslint": "^9.32.0", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.5", "eslint-plugin-i18n": "^2.4.0", "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-simple-import-sort": "^12.1.1", "favicons": "^7.2.0", "favicons-webpack-plugin": "^6.0.1", @@ -100,7 +106,7 @@ "@cloudscape-design/chat-components": "^1.0.62", "@cloudscape-design/collection-hooks": "^1.0.74", "@cloudscape-design/component-toolkit": "^1.0.0-beta.120", - "@cloudscape-design/components": "^3.0.1091", + "@cloudscape-design/components": "^3.0.1188", "@cloudscape-design/design-tokens": "^3.0.60", "@cloudscape-design/global-styles": "^1.0.45", "@hookform/resolvers": "^2.9.10", @@ -118,6 +124,7 @@ "rc-tooltip": "^5.2.2", "react": "^18.3.1", "react-avatar": "^5.0.3", + "react-bus": "^4.0.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", "react-hook-form": "^7.53.0", diff --git a/frontend/product.config.cjs b/frontend/product.config.cjs new file mode 100644 index 0000000000..da846b9f15 --- /dev/null +++ b/frontend/product.config.cjs @@ -0,0 +1,27 @@ +/* global module */ +const oss = { + id: 'oss', + name: 'dstack', + hasEvents: false, + hasBilling: false, + hasPresets: false, + isSky: false, +}; +const enterprise = { ...oss, id: 'enterprise', name: 'dstack Enterprise', hasEvents: true }; +const factory = { ...enterprise, id: 'factory', name: 'dstack Factory', hasBilling: true, hasPresets: true }; +const sky = { ...factory, id: 'sky', name: 'dstack Sky', isSky: true }; + +function getProductConfig(version) { + switch (version) { + case 'enterprise': + return enterprise; + case 'factory': + return factory; + case 'sky': + return sky; + default: + return oss; + } +} + +module.exports = { getProductConfig }; diff --git a/frontend/src/App/Login/EnterpriseLogin/index.tsx b/frontend/src/App/Login/EnterpriseLogin/index.tsx deleted file mode 100644 index 7cb9b530af..0000000000 --- a/frontend/src/App/Login/EnterpriseLogin/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import cn from 'classnames'; - -import { Box, NavigateLink, SpaceBetween } from 'components'; -import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout'; - -import { ROUTES } from 'routes'; -import { useGetEntraInfoQuery, useGetGoogleInfoQuery, useGetOktaInfoQuery } from 'services/auth'; - -import { LoginByEntraID } from '../EntraID/LoginByEntraID'; -import { LoginByGoogle } from '../LoginByGoogle'; -import { LoginByOkta } from '../LoginByOkta'; -import { LoginByTokenForm } from '../LoginByTokenForm'; - -import styles from './styles.module.scss'; - -export const EnterpriseLogin: React.FC = () => { - const { t } = useTranslation(); - const { data: oktaData, isLoading: isLoadingOkta } = useGetOktaInfoQuery(); - const { data: entraData, isLoading: isLoadingEntra } = useGetEntraInfoQuery(); - const { data: googleData, isLoading: isLoadingGoogle } = useGetGoogleInfoQuery(); - - const oktaEnabled = oktaData?.enabled; - const entraEnabled = entraData?.enabled; - const googleEnabled = googleData?.enabled; - - const isLoading = isLoadingOkta || isLoadingEntra; - const isShowTokenForm = !oktaEnabled && !entraEnabled; - - return ( - -
- - - {t('auth.sign_in_to_dstack_enterprise')} - - - {!isLoading && isShowTokenForm && } - {!isLoadingOkta && oktaEnabled && } - {!isLoadingEntra && entraEnabled && } - {!isLoadingGoogle && googleEnabled && } - - {!isLoading && !isShowTokenForm && ( - - {t('auth.login_by_token')} - - )} - -
-
- ); -}; diff --git a/frontend/src/App/Login/EnterpriseLogin/styles.module.scss b/frontend/src/App/Login/EnterpriseLogin/styles.module.scss deleted file mode 100644 index 72a104a848..0000000000 --- a/frontend/src/App/Login/EnterpriseLogin/styles.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -.form { - max-width: 440px; - width: 100%; - margin-left: auto; - margin-right: auto; - padding-top: 120px; -} -.token { - -} -.okta { - margin-top: 20px; -} -.entra { - margin-top: 20px; -} diff --git a/frontend/src/App/Login/EntraID/LoginByEntraID/index.tsx b/frontend/src/App/Login/EntraID/LoginByEntraID/index.tsx index c52d14fe10..19d6afef04 100644 --- a/frontend/src/App/Login/EntraID/LoginByEntraID/index.tsx +++ b/frontend/src/App/Login/EntraID/LoginByEntraID/index.tsx @@ -28,7 +28,7 @@ export const LoginByEntraID: React.FC<{ className?: string }> = ({ className }) return (
- - - - No credit card required - - - -
- - - - - - - - - - ); -} - -function ProductOverview() { - return ( -
- -
- Overview -
-
- - dstack is an open-source container orchestrator that lets ML teams easily manage - clusters, volumes, dev environments, training, and inference. Its container-native interface boosts - productivity, maximizes GPU efficiency, and lowers costs. - - - dstack Sky adds a managed service, letting you use the cheapest GPUs from our - marketplace or connect your own cloud accounts. - -
- -
- - Features - - -
-
-
- - Open-source - -
-
dstack Sky
- -
Your cloud accounts
-
- -
-
- -
- -
- SSH fleets{' '} - - - - - -
-
- -
-
- -
- -
- GPU marketplace{' '} - - - - - -
-
-
- -
- -
- Gateway{' '} - - - - - -
-
Configure your own domain
-
Pre-configured *.sky.dstack.ai
- -
Pricing
-
Free
-
Pay only for marketplace GPU
- -
-
Self-hosted
-
Hosted by dstack
-
-
-
- -
-
- Highlights -
- -
    -
  • Use your own cloud accounts or access the cheapest GPUs from our marketplace.
  • -
  • Create dev environments, run training tasks, and deploy inference services.
  • -
  • Manage volumes and fleets.
  • -
  • Manage multiple projects and teams.
  • -
-
-
- -
-
- Documentation -
- - - Want to learn more about dstack? Check out the{' '} - - documentation - - - -
-
-
- ); -} - -function OtherVersions() { - return ( -
- - Other versions - -
    -
  • - - - - Open-source - Self-hosted - - Fully customizable and self-hosted open-source version. - - - -
  • -
  • - - - - dstack Enterprise - Self-hosted - - Single sign-on, advanced governance controls, and dedicated support. - - - -
  • -
-
- ); -} - -export const LoginByGithub: React.FC = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const systemMode = useAppSelector(selectSystemMode) ?? ''; - const ThemeIcon = THEME_ICON_MAP[systemMode]; - - const onChangeSystemModeToggle = (event: React.MouseEvent) => { - event.preventDefault(); - switch (systemMode) { - case Mode.Light: - dispatch(setSystemMode(Mode.Dark)); - return; - default: - dispatch(setSystemMode(Mode.Light)); - } - }; - return ( <> - -
- goToUrl('https://dstack.ai/docs/', true), - }, - { - type: 'button', - text: t('common.discord'), - external: true, - onClick: () => goToUrl(DISCORD_URL, true), - }, - { - href: 'theme-button', - type: 'button', - iconSvg: , - onClick: onChangeSystemModeToggle, - }, - { - type: 'button', - iconName: 'gen-ai', - text: t('common.ask_ai'), - title: t('common.ask_ai'), - onClick: askAi, - }, - ]} - /> -
-
- - - - } - headerVariant="high-contrast" - header={} - defaultPadding={true} - maxContentWidth={1040} - disableOverlap={true} - > -
-
- -
- - - -
- - -
-
-
-
+ {isError && Unable to start GitHub authentication} + ); }; diff --git a/frontend/src/App/Login/LoginByGithub/styles.module.scss b/frontend/src/App/Login/LoginByGithub/styles.module.scss deleted file mode 100644 index 9904b2bd4d..0000000000 --- a/frontend/src/App/Login/LoginByGithub/styles.module.scss +++ /dev/null @@ -1,121 +0,0 @@ -@use '~@cloudscape-design/design-tokens' as cs; - -body { - background: cs.$color-background-layout-main; - position: relative; -} - -$viewport-breakpoint-s: 912px; - -body { - // Note: This token will be themed (see the product page index.tsx) - background: cs.$color-background-layout-main; -} - -.productPageContentGrid { - display: grid; - grid-template-columns: 3fr 1fr; - grid-template-rows: 0 auto 0; - margin-block-start: cs.$space-static-xxl; -} - -.onThisPageMobile { - grid-row: 1; - grid-column: 1 / 3; - display: none; - margin-block-end: cs.$space-static-xxl; -} - -.productPageAside { - grid-row: 2; - grid-column: 2 / 3; - padding-inline-start: calc(#{cs.$space-scaled-xxxl} /2); -} - -.productPageContent { - grid-row: 2; - grid-column: 1 / 2; - padding-inline-end: calc(#{cs.$space-scaled-xxxl} /2); - margin-bottom: 20px; -} - - -.productPageAsideSticky { - position: sticky; - inset-block-start: 40px; -} - -@media only screen and (max-width: $viewport-breakpoint-s) { - .productPageContentGrid { - grid-template-columns: 100%; - grid-template-rows: auto auto auto; - } - - .onThisPageMobile { - display: block; - } - - .productPageMobile { - display: block; - } - - .productPageAside { - display: none; - } -} - - -/* High-level sections of the main content area */ -.pageSection { - padding-block-end: cs.$space-static-xxxl; - margin-block-end: cs.$space-static-xxl; - border-bottom: 1px solid cs.$color-border-divider-default; - - &:last-child { - border: none; - margin-block-end: 0; - } -} - -/* Product details list containing keys and values */ -.productDetails { - display: grid; - grid-template-columns: 30% 35% 35%; - margin: 0; - padding: 0; - - dt { - color: cs.$color-text-body-secondary; - font-weight: bold; - } - - dt, - dd { - margin: 0; - padding: 0; - padding-block: cs.$space-scaled-xs; - border-block-end: 1px solid cs.$color-border-divider-default; - } -} - -/* List of product cards */ -.productCardsList { - display: flex; - flex-wrap: wrap; - column-gap: cs.$space-scaled-l; - row-gap: cs.$space-scaled-l; - - list-style-type: none; - margin: 0; - padding: 0; -} - -.productCardsListItem { - flex: 1; - flex-basis: 250px; - max-inline-size: 312px; - - list-style-type: none; - margin: 0; - padding: 0; -} diff --git a/frontend/src/App/Login/LoginByGithubCallback/index.tsx b/frontend/src/App/Login/LoginByGithubCallback/index.tsx index af88aa72f1..07eac4ee51 100644 --- a/frontend/src/App/Login/LoginByGithubCallback/index.tsx +++ b/frontend/src/App/Login/LoginByGithubCallback/index.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { product } from 'product'; import { NavigateLink } from 'components'; import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout'; @@ -21,6 +22,7 @@ export const LoginByGithubCallback: React.FC = () => { const code = searchParams.get('code'); const state = searchParams.get('state'); const [isInvalidCode, setIsInvalidCode] = useState(false); + const callbackStarted = useRef(false); const dispatch = useAppDispatch(); const [getNextRedirect] = useGetNextRedirectMutation(); @@ -40,9 +42,9 @@ export const LoginByGithubCallback: React.FC = () => { .unwrap() .then(async ({ creds: { token } }) => { dispatch(setAuthData({ token })); - if (process.env.UI_VERSION === 'sky') { - const result = await getProjects().unwrap(); - if (result?.length === 0) { + if (product.hasPresets) { + const result = await getProjects({}).unwrap(); + if (result.data.length === 0) { navigate(ROUTES.PROJECT.ADD); return; } @@ -60,7 +62,11 @@ export const LoginByGithubCallback: React.FC = () => { }; useEffect(() => { - if (code) { + // OAuth codes are single-use, including when StrictMode replays this effect. + if (callbackStarted.current) return; + callbackStarted.current = true; + + if (code && state) { checkCode(); } else { setIsInvalidCode(true); @@ -78,7 +84,7 @@ export const LoginByGithubCallback: React.FC = () => { return ( - ; + ); }; diff --git a/frontend/src/App/Login/LoginByGoogle/index.tsx b/frontend/src/App/Login/LoginByGoogle/index.tsx index b83a93f187..89dee1074e 100644 --- a/frontend/src/App/Login/LoginByGoogle/index.tsx +++ b/frontend/src/App/Login/LoginByGoogle/index.tsx @@ -26,7 +26,7 @@ export const LoginByGoogle: React.FC<{ className?: string }> = ({ className }) = return (
- -
-
- - +
+ + + + +
); }; diff --git a/frontend/src/App/Login/LoginByTokenForm/styles.module.scss b/frontend/src/App/Login/LoginByTokenForm/styles.module.scss deleted file mode 100644 index 859b39ebcc..0000000000 --- a/frontend/src/App/Login/LoginByTokenForm/styles.module.scss +++ /dev/null @@ -1,24 +0,0 @@ -.form { - max-width: 440px; - margin-left: auto; - margin-right: auto; -} -.token { - display: flex; - align-items: flex-start; - gap: 12px; -} -.fieldWrap { - flex-grow: 1; - min-width: 0; -} -.buttonWrap { - display: flex; - flex-shrink: 0; - width: 104px; - margin-right: -20px; - - button { - white-space: nowrap !important; - } -} diff --git a/frontend/src/App/Login/TokenLogin/index.tsx b/frontend/src/App/Login/TokenLogin/index.tsx index b41393ba39..8c033dd469 100644 --- a/frontend/src/App/Login/TokenLogin/index.tsx +++ b/frontend/src/App/Login/TokenLogin/index.tsx @@ -1,34 +1,5 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; -import cn from 'classnames'; -import { Box, NavigateLink, SpaceBetween } from 'components'; -import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout'; +import { Login } from '../index'; -import { ROUTES } from 'routes'; - -import { LoginByTokenForm } from '../LoginByTokenForm'; - -import styles from './styles.module.scss'; - -export const TokenLogin: React.FC = () => { - const { t } = useTranslation(); - - return ( - -
- - - {t('auth.sign_in_to_dstack_enterprise')} - - - - - - {t('auth.another_login_methods')} - - -
-
- ); -}; +export const TokenLogin: React.FC = () => ; diff --git a/frontend/src/App/Login/TokenLogin/styles.module.scss b/frontend/src/App/Login/TokenLogin/styles.module.scss deleted file mode 100644 index b648c81066..0000000000 --- a/frontend/src/App/Login/TokenLogin/styles.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.form { - max-width: 440px; - width: 100%; - margin-left: auto; - margin-right: auto; - padding-top: 120px; -} diff --git a/frontend/src/App/Login/index.tsx b/frontend/src/App/Login/index.tsx new file mode 100644 index 0000000000..98b242070d --- /dev/null +++ b/frontend/src/App/Login/index.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Navigate } from 'react-router-dom'; +import { product } from 'product'; +import { PublicApp } from 'PublicApp'; +import { colorBackgroundHomeHeader } from '@cloudscape-design/design-tokens'; + +import { Box, Container, ContentLayout, Header, Link, NavigateLink, SpaceBetween, Spinner } from 'components'; + +import { useAppSelector } from 'hooks'; +import { ROUTES } from 'routes'; +import { useGetAuthProvidersQuery } from 'services/auth'; +import { useGetUserDataQuery } from 'services/user'; + +import { Loading } from 'App/Loading'; +import { selectAuthToken } from 'App/slice'; + +import { LoginByEntraID } from './EntraID/LoginByEntraID'; +import { LoginByGithub } from './LoginByGithub'; +import { LoginByGoogle } from './LoginByGoogle'; +import { LoginByOkta } from './LoginByOkta'; +import { LoginByTokenForm } from './LoginByTokenForm'; + +const providerButtons = { + github: LoginByGithub, + okta: LoginByOkta, + entra: LoginByEntraID, + google: LoginByGoogle, +}; + +export const Login: React.FC<{ tokenOnly?: boolean }> = ({ tokenOnly = false }) => { + const { t } = useTranslation(); + const token = useAppSelector(selectAuthToken); + const localStorageIsAvailable = 'localStorage' in window; + const { + currentData: userData, + error, + isFetching, + } = useGetUserDataQuery({ token }, { skip: tokenOnly || !token || !localStorageIsAvailable }); + const { data: providers, isLoading } = useGetAuthProvidersQuery(undefined, { skip: tokenOnly }); + const enabledProviders = Object.entries(providerButtons).filter(([name]) => + providers?.some((provider) => provider.name === name && provider.enabled), + ); + const showTokenForm = tokenOnly || (!isLoading && enabledProviders.length === 0); + + if (!tokenOnly && token && localStorageIsAvailable) { + if (isFetching && !userData) return ; + if (userData?.username && !error) return ; + } + + const content = ( + + {t('auth.welcome', { product: product.name })} + + } + > + +
{showTokenForm ? 'Sign in with a token' : t('common.login')}
+ + } + > + + {showTokenForm && } + {!tokenOnly && isLoading && } + {!tokenOnly && + !isLoading && + enabledProviders.map(([name, ProviderButton]) => )} + {!tokenOnly && !isLoading && enabledProviders.length > 0 && product.isSky && ( + + By continuing, you agree to the{' '} + + Terms + {' '} + and{' '} + + Privacy policy + + + )} + {tokenOnly ? ( + {t('auth.another_login_methods')} + ) : ( + !showTokenForm && Sign in with a token + )} + +
+
+ ); + return product.hasPresets ? content : {content}; +}; diff --git a/frontend/src/App/Logout/index.tsx b/frontend/src/App/Logout/index.tsx index 0526c92cca..f7a86c45a3 100644 --- a/frontend/src/App/Logout/index.tsx +++ b/frontend/src/App/Logout/index.tsx @@ -3,6 +3,7 @@ import { Navigate } from 'react-router-dom'; import { useAppDispatch } from 'hooks'; import { ROUTES } from 'routes'; +import { presetApi } from 'services/preset'; import { projectApi } from 'services/project'; import { userApi } from 'services/user'; @@ -16,6 +17,7 @@ export const Logout: React.FC = () => { dispatch(userApi.util.resetApiState()); dispatch(projectApi.util.resetApiState()); + dispatch(presetApi.util.resetApiState()); }, []); return ; diff --git a/frontend/src/App/auth.test.tsx b/frontend/src/App/auth.test.tsx new file mode 100644 index 0000000000..67f195e06e --- /dev/null +++ b/frontend/src/App/auth.test.tsx @@ -0,0 +1,236 @@ +/** @jest-environment node */ +import React from 'react'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; +import { act, create, ReactTestRenderer } from 'react-test-renderer'; + +import { ROUTES } from 'routes'; +import { useGetUserDataQuery } from 'services/user'; + +import { Login } from 'App/Login'; + +const mockDispatch = jest.fn(); +const mockPrivateQuery = jest.fn(); +let mockToken: string | undefined; +let mockUserQuery: { + currentData?: { username: string }; + data?: { username: string }; + error?: { status: number }; + isFetching: boolean; +}; + +jest.mock('hooks', () => ({ + useAppDispatch: () => mockDispatch, + useAppSelector: () => mockToken, +})); + +jest.mock('libs', () => ({ goToUrl: jest.fn() })); + +jest.mock('services/auth', () => ({ + useGetAuthProvidersQuery: () => ({ data: [{ name: 'github', enabled: true }], isLoading: false }), + useGithubAuthorizeMutation: () => [jest.fn(), { isLoading: false }], +})); + +jest.mock('services/user', () => ({ + useGetUserDataQuery: jest.fn((_, { skip }: { skip: boolean }) => (skip ? { isFetching: false } : mockUserQuery)), +})); + +jest.mock('./slice', () => ({ + selectAuthToken: jest.fn(), + setUserData: (payload: unknown) => ({ type: 'app/setUserData', payload }), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { product: string }) => (options ? `Welcome to ${options.product}` : key), + }), +})); + +jest.mock('layouts/AppLayout', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => { + // The real layout starts account, project, billing, and onboarding requests when mounted. + mockPrivateQuery(); + return
{children}
; + }, +})); + +jest.mock('@cloudscape-design/design-tokens', () => ({ colorBackgroundHomeHeader: 'transparent' })); + +jest.mock('components', () => { + const Wrapper = ({ children }: { children: React.ReactNode }) => <>{children}; + return { + Box: ({ variant, children }: { variant?: string; children: React.ReactNode }) => + variant === 'h1' ?

{children}

: <>{children}, + Button: Wrapper, + Alert: Wrapper, + Container: Wrapper, + Header: Wrapper, + NavigateLink: Wrapper, + ContentLayout: ({ header, children }: { header: React.ReactNode; children: React.ReactNode }) => ( + <> + {header} + {children} + + ), + Spinner: Wrapper, + Link: Wrapper, + SpaceBetween: Wrapper, + }; +}); + +jest.mock('product', () => ({ product: { ...jest.requireActual('../../product.config.cjs').getProductConfig('sky') } })); +jest.mock('PublicApp', () => ({ PublicApp: ({ children }: { children: React.ReactNode }) => <>{children} })); +jest.mock('./Login/EntraID/LoginByEntraID', () => ({ LoginByEntraID: () => })); +jest.mock('./Login/LoginByGoogle', () => ({ LoginByGoogle: () => })); +jest.mock('./Login/LoginByOkta', () => ({ LoginByOkta: () => })); +jest.mock('./Login/LoginByTokenForm', () => ({ LoginByTokenForm: () =>
Token
})); +jest.mock('./Loading', () => ({ Loading: () =>

Loading

})); +jest.mock('./AuthErrorMessage', () => ({ AuthErrorMessage: () =>

Storage unavailable

})); + +const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); +let App: React.FC; +let rendered: ReactTestRenderer | undefined; + +const CurrentPath = () => {useLocation().pathname}; + +const renderApp = (path: string) => { + act(() => { + rendered = create( + + + } /> + }> + Runs} /> + + + + , + ); + }); + return rendered!; +}; + +beforeAll(() => { + jest.replaceProperty(process, 'env', { ...process.env, UI_VERSION: 'sky' }); + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { localStorage: {} }, + }); + App = jest.requireActual('./index').default; +}); + +beforeEach(() => { + Object.defineProperty(globalThis, 'window', { value: { localStorage: {} } }); + mockToken = undefined; + mockUserQuery = { isFetching: false }; +}); + +afterEach(() => { + act(() => rendered?.unmount()); + rendered = undefined; +}); + +afterAll(() => { + jest.restoreAllMocks(); + if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow); + else Reflect.deleteProperty(globalThis, 'window'); +}); + +describe('Sky public and protected pages', () => { + test('visitors can open sign-in without loading account data', () => { + const view = renderApp(ROUTES.BASE); + + expect(view.root.findByType('output').children).toEqual([ROUTES.BASE]); + expect(view.root.findByType('h1').children).toEqual(['Welcome to dstack Sky']); + expect(useGetUserDataQuery).toHaveBeenCalledWith({ token: undefined }, { skip: true }); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test('a validated user opening sign-in reaches their runs', () => { + mockToken = 'valid-token'; + mockUserQuery.currentData = mockUserQuery.data = { username: 'alice' }; + const view = renderApp(ROUTES.BASE); + + expect(view.root.findByType('output').children).toEqual(['/runs']); + expect(view.root.findByType('h1').children).toEqual(['Runs']); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'app/setUserData', payload: { username: 'alice' } }); + }); + + test.each([ + [ROUTES.BASE, ROUTES.BASE], + [ROUTES.RUNS.LIST, ROUTES.BASE], + ])('a rejected token at %s settles on %s', (path, expectedPath) => { + mockToken = 'expired-token'; + mockUserQuery.error = { status: 401 }; + const view = renderApp(path); + + expect(view.root.findByType('output').children).toEqual([expectedPath]); + expect(view.root.findByType('h1').children).toEqual(['Welcome to dstack Sky']); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test('a rejected token stays at sign-in even when its user data is cached', () => { + mockToken = 'expired-token'; + mockUserQuery.currentData = mockUserQuery.data = { username: 'alice' }; + mockUserQuery.error = { status: 401 }; + const view = renderApp(ROUTES.BASE); + + expect(view.root.findByType('output').children).toEqual([ROUTES.BASE]); + expect(view.root.findByType('h1').children).toEqual(['Welcome to dstack Sky']); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test('a rejected token cannot restore cached account data on a protected page', () => { + mockToken = 'expired-token'; + mockUserQuery.currentData = mockUserQuery.data = { username: 'alice' }; + mockUserQuery.error = { status: 403 }; + const view = renderApp(ROUTES.RUNS.LIST); + + expect(view.root.findByType('output').children).toEqual([ROUTES.BASE]); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test('a visitor opening a protected page reaches sign-in', () => { + const view = renderApp('/runs'); + + expect(view.root.findByType('output').children).toEqual([ROUTES.BASE]); + expect(view.root.findByType('h1').children).toEqual(['Welcome to dstack Sky']); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test.each([ROUTES.BASE, ROUTES.RUNS.LIST])('%s waits for token validation before showing private content', (path) => { + mockToken = 'pending-token'; + mockUserQuery.isFetching = true; + const view = renderApp(path); + + expect(view.root.findByProps({ role: 'status' }).children).toEqual(['Loading']); + expect(view.root.findByType('output').children).toEqual([path]); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); + + test.each([ROUTES.BASE, ROUTES.RUNS.LIST])( + '%s waits for a new token even when data from the previous token is available', + (path) => { + mockToken = 'new-token'; + mockUserQuery.data = { username: 'previous-user' }; + mockUserQuery.isFetching = true; + const view = renderApp(path); + + expect(view.root.findByProps({ role: 'status' }).children).toEqual(['Loading']); + expect(view.root.findByType('output').children).toEqual([path]); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }, + ); + + test('sign-in remains available when local storage is unavailable', () => { + Object.defineProperty(globalThis, 'window', { value: {} }); + mockToken = 'saved-token'; + const view = renderApp(ROUTES.BASE); + + expect(view.root.findByType('h1').children).toEqual(['Welcome to dstack Sky']); + expect(useGetUserDataQuery).toHaveBeenCalledWith({ token: 'saved-token' }, { skip: true }); + expect(mockPrivateQuery).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index de0f919079..78afd5bc15 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -1,20 +1,19 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { product } from 'product'; import AppLayout from 'layouts/AppLayout'; import { useAppDispatch, useAppSelector } from 'hooks'; import { useGetUserDataQuery } from 'services/user'; -import { EnterpriseLogin } from './Login/EnterpriseLogin'; -import { LoginByGithub } from './Login/LoginByGithub'; import { ROUTES } from '../routes'; import { AuthErrorMessage } from './AuthErrorMessage'; +import { Loading } from './Loading'; +import { Login } from './Login'; import { selectAuthToken, setUserData } from './slice'; -const localStorageIsAvailable = 'localStorage' in window; - const IGNORED_AUTH_PATHS = [ ROUTES.AUTH.GITHUB_CALLBACK, ROUTES.AUTH.OKTA_CALLBACK, @@ -23,58 +22,47 @@ const IGNORED_AUTH_PATHS = [ ROUTES.AUTH.TOKEN, ]; -const LoginFormComponent = process.env.UI_VERSION === 'enterprise' ? EnterpriseLogin : LoginByGithub; +const LoginFormComponent = product.hasPresets ? () => : Login; const App: React.FC = () => { const { t } = useTranslation(); const token = useAppSelector(selectAuthToken); - const isAuthenticated = Boolean(token); + const localStorageIsAvailable = 'localStorage' in window; const dispatch = useAppDispatch(); const { pathname } = useLocation(); const { - isLoading, - data: userData, + isFetching, + currentData: userData, error: getUserError, } = useGetUserDataQuery( { token }, { - skip: !isAuthenticated || !localStorageIsAvailable, + skip: !token || !localStorageIsAvailable, }, ); useEffect(() => { - if (userData?.username || getUserError) { - if (userData?.username) { - dispatch(setUserData(userData)); - } + if (userData?.username && !getUserError) { + dispatch(setUserData(userData)); } - }, [userData, getUserError, isLoading]); + }, [userData, getUserError, dispatch]); + + if (IGNORED_AUTH_PATHS.includes(pathname)) { + return ; + } - const renderLocalstorageError = () => { + if (!localStorageIsAvailable) { return ( ); - }; - - const renderTokenError = () => { - return ; - }; - - const renderNotAuthorizedError = () => { - return ; - }; - - if (IGNORED_AUTH_PATHS.includes(pathname)) { - return ; } - - if (!localStorageIsAvailable) return renderLocalstorageError(); - if (getUserError) return renderTokenError(); - if (!isAuthenticated) return renderNotAuthorizedError(); + if (!token || getUserError) return ; + if (isFetching && !userData) return ; + if (!userData?.username) return ; return ( diff --git a/frontend/src/App/login.test.tsx b/frontend/src/App/login.test.tsx new file mode 100644 index 0000000000..39e127d7f2 --- /dev/null +++ b/frontend/src/App/login.test.tsx @@ -0,0 +1,156 @@ +/** @jest-environment node */ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { act, create, ReactTestRenderer } from 'react-test-renderer'; +import { product } from 'product'; + +import { ROUTES } from 'routes'; +import { useGetAuthProvidersQuery } from 'services/auth'; + +import { getProductConfig } from '../../product.config.cjs'; +import { Login } from './Login'; + +type ProvidersResult = { + data?: { name: string; enabled: boolean }[]; + isLoading: boolean; + isError?: boolean; +}; +let mockProviders: ProvidersResult; +let rendered: ReactTestRenderer | undefined; +const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + +jest.mock('product', () => ({ product: { ...jest.requireActual('../../product.config.cjs').getProductConfig('factory') } })); +jest.mock('hooks', () => ({ useAppSelector: () => undefined })); +jest.mock('services/auth', () => ({ useGetAuthProvidersQuery: jest.fn(() => mockProviders) })); +jest.mock('services/user', () => ({ useGetUserDataQuery: () => ({ isFetching: false }) })); +jest.mock('./slice', () => ({ selectAuthToken: jest.fn() })); +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { product: string }) => (options ? `Welcome to ${options.product}` : key), + }), +})); +jest.mock('PublicApp', () => ({ PublicApp: ({ children }: { children: React.ReactNode }) => <>{children} })); +jest.mock('@cloudscape-design/design-tokens', () => ({ colorBackgroundHomeHeader: 'transparent' })); +jest.mock('components', () => { + const Wrapper = ({ children }: { children: React.ReactNode }) => <>{children}; + const Anchor = ({ href, children }: { href: string; children: React.ReactNode }) => {children}; + return { + Box: ({ variant, children }: { variant?: string; children: React.ReactNode }) => + variant === 'h1' ?

{children}

: <>{children}, + Container: Wrapper, + Header: Wrapper, + ContentLayout: ({ header, children }: { header: React.ReactNode; children: React.ReactNode }) => ( + <> + {header} + {children} + + ), + Link: Anchor, + NavigateLink: Anchor, + SpaceBetween: Wrapper, + Spinner: () =>

Loading providers

, + }; +}); +jest.mock('./Login/LoginByGithub', () => ({ LoginByGithub: () => })); +jest.mock('./Login/EntraID/LoginByEntraID', () => ({ LoginByEntraID: () => })); +jest.mock('./Login/LoginByGoogle', () => ({ LoginByGoogle: () => })); +jest.mock('./Login/LoginByOkta', () => ({ LoginByOkta: () => })); +jest.mock('./Login/LoginByTokenForm', () => ({ LoginByTokenForm: () =>
Token
})); +jest.mock('./Loading', () => ({ Loading: () =>

Loading user

})); + +const renderLogin = (tokenOnly = false) => { + act(() => { + rendered = create( + + + , + ); + }); + return rendered!.root; +}; + +beforeEach(() => { + Object.defineProperty(globalThis, 'window', { configurable: true, value: { localStorage: {} } }); + Object.assign(product, getProductConfig('factory')); + mockProviders = { data: [], isLoading: false }; +}); +afterEach(() => { + act(() => rendered?.unmount()); + rendered = undefined; +}); +afterAll(() => { + if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow); + else Reflect.deleteProperty(globalThis, 'window'); +}); + +test.each([ + ['oss', 'dstack'], + ['enterprise', 'dstack Enterprise'], + ['factory', 'dstack Factory'], + ['sky', 'dstack Sky'], +])('%s login uses its branding and the server provider list', (version, name) => { + Object.assign(product, getProductConfig(version)); + mockProviders.data = [ + { name: 'google', enabled: true }, + { name: 'github', enabled: false }, + ]; + const view = renderLogin(); + expect(view.findByType('h1').children).toEqual([`Welcome to ${name}`]); + expect(view.findAllByType('button').map((button) => button.children)).toEqual([['Google']]); + expect(view.findByProps({ href: ROUTES.AUTH.TOKEN })).toBeDefined(); +}); + +test('Factory supports all configured providers without Sky terms', () => { + mockProviders.data = ['github', 'okta', 'entra', 'google'].map((name) => ({ name, enabled: true })); + const view = renderLogin(); + expect(view.findAllByType('button').map((button) => button.children)).toEqual([ + ['GitHub'], + ['Okta'], + ['Entra'], + ['Google'], + ]); + expect(view.findAllByProps({ href: 'https://dstack.ai/terms/' })).toHaveLength(0); +}); + +test('Sky keeps its terms with GitHub login', () => { + Object.assign(product, getProductConfig('sky')); + mockProviders.data = [{ name: 'github', enabled: true }]; + const view = renderLogin(); + expect(view.findByType('button').children).toEqual(['GitHub']); + expect(view.findByProps({ href: 'https://dstack.ai/terms/' })).toBeDefined(); +}); + +test.each([ + { data: [], isLoading: false }, + { data: [{ name: 'github', enabled: false }], isLoading: false }, + { data: [{ name: 'unknown', enabled: true }], isLoading: false }, + { isError: true, isLoading: false }, +])('falls back to token login when no supported provider is available: %j', (result) => { + mockProviders = result; + const view = renderLogin(); + expect(view.findByType('form').children).toEqual(['Token']); + expect(view.findAllByType('button')).toHaveLength(0); +}); + +test('token login remains accessible while provider discovery loads', () => { + mockProviders = { isLoading: true }; + const view = renderLogin(); + expect(view.findByProps({ role: 'status' }).children).toEqual(['Loading providers']); + expect(view.findByProps({ href: ROUTES.AUTH.TOKEN })).toBeDefined(); +}); + +test('the token page does not wait for provider discovery', () => { + mockProviders = { isLoading: true }; + const view = renderLogin(true); + expect(view.findByType('form').children).toEqual(['Token']); + expect(useGetAuthProvidersQuery).toHaveBeenCalledWith(undefined, { skip: true }); + expect(view.findByProps({ href: ROUTES.BASE })).toBeDefined(); +}); + +test('Factory inherits Enterprise features and Sky inherits Factory features', () => { + expect(getProductConfig('enterprise')).toMatchObject({ hasEvents: true, hasBilling: false, hasPresets: false }); + for (const version of ['factory', 'sky']) { + expect(getProductConfig(version)).toMatchObject({ hasEvents: true, hasBilling: true, hasPresets: true }); + } + expect(getProductConfig(undefined)).toMatchObject({ id: 'oss', hasEvents: false, hasBilling: false, hasPresets: false }); +}); diff --git a/frontend/src/PublicApp/PresetApp.tsx b/frontend/src/PublicApp/PresetApp.tsx new file mode 100644 index 0000000000..55f66f84f9 --- /dev/null +++ b/frontend/src/PublicApp/PresetApp.tsx @@ -0,0 +1,26 @@ +import React, { createContext, useContext } from 'react'; + +import { useAppSelector } from 'hooks'; +import { useGetUserDataQuery } from 'services/user'; + +import App from 'App'; +import { Loading } from 'App/Loading'; +import { selectAuthToken } from 'App/slice'; + +import { PublicApp } from './index'; + +const PresetViewer = createContext({ isAuthenticated: false }); + +export const usePresetViewer = () => useContext(PresetViewer); + +export const PresetApp: React.FC = () => { + const token = useAppSelector(selectAuthToken); + const { currentData, error, isFetching } = useGetUserDataQuery({ token }, { skip: !token || !('localStorage' in window) }); + const isAuthenticated = Boolean(token && currentData?.username && !error); + + if (token && isFetching && !currentData) return ; + + return ( + {isAuthenticated ? : } + ); +}; diff --git a/frontend/src/PublicApp/index.tsx b/frontend/src/PublicApp/index.tsx new file mode 100644 index 0000000000..33f29c95c7 --- /dev/null +++ b/frontend/src/PublicApp/index.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { product } from 'product'; +import enMessages from '@cloudscape-design/components/i18n/messages/all.en.json'; +import { applyMode, Mode } from '@cloudscape-design/global-styles'; + +import { AppLayout, BreadcrumbGroup, BreadcrumbGroupProps, Button, I18nProvider, TopNavigation } from 'components'; +import { DarkThemeIcon, LightThemeIcon } from 'layouts/AppLayout/themeIcons'; + +import { DISCORD_URL } from 'consts'; +import { useAppDispatch, useAppSelector } from 'hooks'; +import { goToUrl } from 'libs'; +import { ROUTES } from 'routes'; + +import { selectBreadcrumbs, selectSystemMode, setSystemMode } from 'App/slice'; + +import logo from 'assets/images/logo.svg'; +import styles from './styles.module.scss'; + +type PortalProps = { + children: React.ReactNode; +}; + +const i18nStrings = { + overflowMenuTriggerText: '', + overflowMenuTitleText: '', + overflowMenuBackIconAriaLabel: '', + overflowMenuDismissIconAriaLabel: '', +}; + +const THEME_ICON_MAP: Record = { + [Mode.Dark]: DarkThemeIcon, + [Mode.Light]: LightThemeIcon, +}; + +const askAi = () => { + window.document.body.focus(); + window?.Kapa?.open(); +}; + +const HeaderPortal = ({ children }: PortalProps) => { + const domNode = document.querySelector('#header'); + if (domNode) return createPortal(children, domNode); + return null; +}; + +export const PublicApp: React.FC = ({ children }) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const breadcrumbs = useAppSelector(selectBreadcrumbs); + const isAuth = pathname === ROUTES.BASE || pathname === ROUTES.AUTH.TOKEN; + const onFollow: BreadcrumbGroupProps['onFollow'] = (event) => { + event.preventDefault(); + navigate(event.detail.href); + }; + const systemMode = useAppSelector(selectSystemMode) ?? ''; + const ThemeIcon = THEME_ICON_MAP[systemMode]; + + const onChangeSystemModeToggle = (event: { preventDefault: () => void }) => { + event.preventDefault(); + switch (systemMode) { + case Mode.Light: + dispatch(setSystemMode(Mode.Dark)); + return; + default: + dispatch(setSystemMode(Mode.Light)); + } + }; + + return ( + <> + +
{ + if (element) applyMode(Mode.Dark, element); + }} + > + goToUrl('https://dstack.ai/docs/', true), + }, + { + type: 'button', + text: t('common.discord'), + external: true, + onClick: () => goToUrl(DISCORD_URL, true), + }, + { + href: 'theme-button', + type: 'button', + iconSvg: , + onClick: onChangeSystemModeToggle, + }, + { + type: 'button', + iconName: 'gen-ai', + text: t('common.ask_ai'), + title: t('common.ask_ai'), + onClick: askAi, + }, + ]} + /> + {product.hasPresets && !isAuth && ( +
+ +
+ )} +
+
+ + + } + breadcrumbs={ + product.hasPresets && pathname.startsWith(ROUTES.PRESETS.LIST) && breadcrumbs ? ( + + ) : undefined + } + /> + + + ); +}; diff --git a/frontend/src/PublicApp/styles.module.scss b/frontend/src/PublicApp/styles.module.scss new file mode 100644 index 0000000000..5ae0bcb23a --- /dev/null +++ b/frontend/src/PublicApp/styles.module.scss @@ -0,0 +1,24 @@ +@use '@cloudscape-design/design-tokens/index' as awsui; + +body { + background: awsui.$color-background-layout-main; + position: relative; +} + +.header { + display: flex; + background: awsui.$color-background-container-content; +} + +.navigation { + flex: 1; + min-width: 0; +} + +.signIn { + display: flex; + align-items: center; + flex-shrink: 0; + padding-inline-end: awsui.$space-static-l; + border-block-end: 1px solid awsui.$color-border-divider-default; +} diff --git a/frontend/src/analytics.ts b/frontend/src/analytics.ts new file mode 100644 index 0000000000..9b793c3787 --- /dev/null +++ b/frontend/src/analytics.ts @@ -0,0 +1,46 @@ +import { product } from 'product'; + +import { router } from './router'; + +const MEASUREMENT_ID = process.env.GA_MEASUREMENT_ID; + +export const initAnalytics = () => { + if (!product.isSky || !MEASUREMENT_ID) return; + + window.dataLayer = window.dataLayer || []; + window.gtag = function () { + // eslint-disable-next-line prefer-rest-params -- gtag expects an arguments object. + window.dataLayer?.push(arguments); + }; + window.gtag('js', new Date()); + window.gtag('set', { + page_location: window.location.origin, + page_referrer: '', + page_title: 'dstack Sky', + }); + // Enhanced Measurement must be disabled for the Sky stream. + window.gtag('config', MEASUREMENT_ID, { send_page_view: false }); + + let lastLocationKey: string | undefined; + let previousPageLocation = ''; + const trackPageView = () => { + const { location, matches, errors } = router.state; + if (location.key === lastLocationKey) return; + + // Route templates exclude names, IDs, and OAuth query parameters. + const path = errors ? '/404' : matches.reduce((path, { route }) => route.path || path, '/404'); + const pageLocation = window.location.origin + path; + window.gtag?.('set', { page_location: pageLocation, page_referrer: previousPageLocation }); + window.gtag?.('event', 'page_view'); + previousPageLocation = pageLocation; + lastLocationKey = location.key; + }; + + router.subscribe(trackPageView); + trackPageView(); + + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`; + document.head.appendChild(script); +}; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index d58dbc7d38..bd9fc32330 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -5,6 +5,7 @@ export const API = { AUTH: { BASE: () => `${API.BASE()}/auth`, + LIST_PROVIDERS: () => `${API.AUTH.BASE()}/list_providers`, NEXT_REDIRECT: () => `${API.AUTH.BASE()}/get_next_redirect`, GITHUB: { BASE: () => `${API.AUTH.BASE()}/github`, @@ -73,6 +74,7 @@ export const API = { ADD_MEMBERS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/add_members`, REMOVE_MEMBERS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/remove_members`, UPDATE: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/update`, + UPDATE_PUBLIC_PRESETS: (name: IProject['project_name']) => `${API.PROJECTS.DETAILS(name)}/update_public_presets`, // Repos REPOS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/repos`, @@ -87,7 +89,6 @@ export const API = { RUN_GET_PLAN: (projectName: IProject['project_name']) => `${API.PROJECTS.RUNS(projectName)}/get_plan`, RUNS_DELETE: (projectName: IProject['project_name']) => `${API.PROJECTS.RUNS(projectName)}/delete`, RUNS_STOP: (projectName: IProject['project_name']) => `${API.PROJECTS.RUNS(projectName)}/stop`, - RUNS_SUBMIT: (projectName: IProject['project_name']) => `${API.PROJECTS.RUNS(projectName)}/submit`, RUNS_APPLY: (projectName: IProject['project_name']) => `${API.PROJECTS.RUNS(projectName)}/apply`, // Logs @@ -99,6 +100,7 @@ export const API = { // Fleets FLEETS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/list`, FLEETS_DETAILS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/get`, + FLEETS_APPLY: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/apply`, FLEETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/delete`, FLEET_INSTANCES_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/delete_instances`, @@ -106,6 +108,10 @@ export const API = { // Fleets VOLUMES_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/volumes/delete`, + // PRESETS + PRESETS_GET: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/presets/get`, + PRESETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/presets/delete`, + // METRICS JOB_METRICS: (projectName: IProject['project_name'], runName: IRun['run_spec']['run_name']) => `${API.BASE()}/project/${projectName}/metrics/job/${runName}`, @@ -118,6 +124,8 @@ export const API = { SECRETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/secrets/delete`, // GPUS GPUS_LIST: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/gpus/list`, + // GPUS + TEMPLATES_LIST: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/templates/list`, }, BACKENDS: { @@ -168,6 +176,7 @@ export const API = { INSTANCES: { BASE: () => `${API.BASE()}/instances`, LIST: () => `${API.INSTANCES.BASE()}/list`, + DETAILS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/instances/get`, }, SERVER: { @@ -179,4 +188,16 @@ export const API = { BASE: () => `${API.BASE()}/volumes`, LIST: () => `${API.VOLUME.BASE()}/list`, }, + + PRESET: { + BASE: () => `${API.BASE()}/presets`, + LIST: () => `${API.PRESET.BASE()}/list`, + }, + + USER_PUBLIC_KEYS: { + BASE: () => `${API.BASE()}/users/public_keys`, + LIST: () => `${API.USER_PUBLIC_KEYS.BASE()}/list`, + ADD: () => `${API.USER_PUBLIC_KEYS.BASE()}/add`, + DELETE: () => `${API.USER_PUBLIC_KEYS.BASE()}/delete`, + }, }; diff --git a/frontend/src/components/ButtonWithConfirmation/index.tsx b/frontend/src/components/ButtonWithConfirmation/index.tsx index 78c2793d9c..56ae78ad59 100644 --- a/frontend/src/components/ButtonWithConfirmation/index.tsx +++ b/frontend/src/components/ButtonWithConfirmation/index.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import Box from '@cloudscape-design/components/box'; import { Button } from '../Button'; @@ -13,20 +14,31 @@ export const ButtonWithConfirmation: React.FC = ({ confirmButtonLabel, ...props }) => { + const { t } = useTranslation(); const [showDeleteConfirm, setShowConfirmDelete] = useState(false); const toggleDeleteConfirm = () => { setShowConfirmDelete((val) => !val); }; - const content = typeof confirmContent === 'string' ? {confirmContent} : confirmContent; - const onConfirm = () => { if (onClick) onClick(); setShowConfirmDelete(false); }; + const getContent = () => { + if (!confirmContent) { + return {t('confirm_dialog.message')}; + } + + if (typeof confirmContent === 'string') { + return {confirmContent}; + } + + return confirmContent; + }; + return ( <> + + ), + }, + ].filter(tab => !marketplaceOnly || tab.id === 'marketplace')} + /> + ); +} diff --git a/website/src/pages/Sky/SkyPage.tsx b/website/src/pages/Sky/SkyPage.tsx new file mode 100644 index 0000000000..51585b1eff --- /dev/null +++ b/website/src/pages/Sky/SkyPage.tsx @@ -0,0 +1,137 @@ +import Button from '@cloudscape-design/components/button'; +import { useLayoutContext } from '../../App'; +import { heroButtonStyle, mainButtonStyle } from '../../cloudscape-theme'; +import { AlternatingDocBlock } from '../../components/AlternatingDocBlock'; +import { ComputeSourcesTabs } from '../../components/ComputeSourcesTabs'; +import { highlightTerms } from '../../components/highlightTerms'; +import { KeyConceptsBlock } from '../Home/ExploreSection'; +import { FaqSection } from '../Home/FaqSection'; +import { TrustedBySection } from '../Home/TrustedBySection'; +import { HeroSquircle } from './HeroSquircle'; +import { MarketplaceTabs } from './MarketplaceTabs'; + +export function SkyPage() { + const { theme } = useLayoutContext(); + + return ( +
+
+ +
+

One account across GPU clouds

+

+ {highlightTerms( + 'dstack Sky combines GPU capacity from multiple cloud partners with competitive pricing, ' + + 'unified billing, and AI-native orchestration. We host and manage the dstack server for you, ' + + 'whether you use the marketplace or bring your own cloud and on-prem compute.', + )} +

+
+ +
+
+
+ +
+
+
+
+ } + title="Better prices, unified billing" + imageFirst + > + The dstack Sky marketplace offers on-demand and spot GPUs from a wide set of partners. + You get competitive pricing and capacity across regions, without depending on a + single cloud. +
+
+ One dstack Sky account gives you access to all marketplace partners. Billing is unified, + with no separate cloud accounts to manage. +
+ + } + title="Bring your own compute" + > + dstack Sky provisions and manages compute in your own GPU cloud accounts using your + credentials. Compute is billed directly to those accounts. +
+
+ You can also bring on-prem compute, including Kubernetes clusters, pre-provisioned + VMs, and bare-metal clusters. We host and maintain the dstack server for you. +
+ + + dstack Sky provides first-class primitives for compute management, training, + inference, and observability across heterogeneous AI compute. +
+
+ dstack Sky includes all the features of open-source dstack and uses the same CLI and + YAML configurations. It adds a built-in gateway with an HTTPS domain, advanced + observability, and usage metering. +
+
+ + + + + + +
+ } + items={[ + { + q: 'How is dstack Sky different from dstack?', + a: [ + 'With dstack Sky, we host and maintain the server for you. ' + + 'With open-source dstack, you host the server yourself.', + 'dstack Sky includes all open-source features and adds a built-in gateway with an HTTPS domain, advanced observability, and usage metering.', + 'dstack Sky also provides a GPU marketplace with unified billing. ' + + 'Both support your own cloud accounts and on-prem compute. ' + + 'The CLI and YAML configurations are the same.', + ], + }, + ]} + /> + +
+ +
+ ); +} diff --git a/website/src/pages/Sky/pricing.ts b/website/src/pages/Sky/pricing.ts new file mode 100644 index 0000000000..744a1e469e --- /dev/null +++ b/website/src/pages/Sky/pricing.ts @@ -0,0 +1,13 @@ +// Indicative USD per GPU-hour from Sky API quotes, 2026-09-14; availability varies. +export const gpuPrices = [ + { name: 'B300', memory: '288GB', onDemand: '$7.58–17.80', spot: '$3.79–6.49' }, + { name: 'B200', memory: '192GB', onDemand: '$6.23–17.91', spot: '$2.41–5.44' }, + { name: 'H200', memory: '141GB', onDemand: '$4.20–9.89', spot: '$2.10–9.89' }, + { name: 'H100', memory: '80GB', onDemand: '$2.89–12.74', spot: '$1.09–8.94' }, + // Includes AMD Developer Cloud and Hot Aisle backend quotes. + { name: 'MI300X', memory: '192GB', onDemand: '$1.99–2.99', spot: null }, + { name: 'RTX PRO 6000', memory: '96GB', onDemand: '$0.97–7.20', spot: '$0.50–3.17' }, + { name: 'A100', memory: '80GB', onDemand: '$1.59–5.58', spot: '$0.87–4.29' }, + { name: 'A100', memory: '40GB', onDemand: '$1.99–3.75', spot: '$0.34–3.57' }, + { name: 'L40S', memory: '48GB', onDemand: '$1.09–4.71', spot: '$0.54–4.71' }, +]; diff --git a/website/src/router.tsx b/website/src/router.tsx new file mode 100644 index 0000000000..6c9e810c80 --- /dev/null +++ b/website/src/router.tsx @@ -0,0 +1,20 @@ +import { Navigate, createBrowserRouter } from 'react-router-dom'; +import { App } from './App'; +import { FactoryPage } from './pages/Factory/FactoryPage'; +import { HomePage } from './pages/Home'; +import { SkyPage } from './pages/Sky/SkyPage'; +import { ROUTES } from './routes'; + +// The landing and product pages share one layout. Docs and blog are served by MkDocs +// on the same origin; stray paths redirect home. +export const router = createBrowserRouter([ + { + element: , + children: [ + { index: true, element: }, + { path: ROUTES.SKY, element: }, + { path: ROUTES.FACTORY, element: }, + { path: '*', element: }, + ], + }, +], { basename: import.meta.env.BASE_URL.replace(/\/$/, '') || '/' }); diff --git a/website/src/routes.ts b/website/src/routes.ts new file mode 100644 index 0000000000..8a1309ac44 --- /dev/null +++ b/website/src/routes.ts @@ -0,0 +1,24 @@ +// Central route table + cross-links to the MkDocs-served parts of the site. +// +// This app owns `/` and `/products/*`. Docs and blog are served by MkDocs on the +// SAME origin in production (`/docs`, `/blog`). For standalone landing development you +// can point those links at the live site by setting VITE_DOCS_BASE, e.g. +// VITE_DOCS_BASE=https://dstack.ai npm run dev +const SITE_BASE = (import.meta.env.VITE_DOCS_BASE ?? '').replace(/\/+$/, ''); + +export const ROUTES = { + HOME: '/', + SKY: '/products/sky', + FACTORY: '/products/factory', +} as const; + +export type Route = (typeof ROUTES)[keyof typeof ROUTES]; + +// Cross-links into the MkDocs site (same origin unless VITE_DOCS_BASE is set). +export const DOCS_URL = `${SITE_BASE}/docs`; +export const BLOG_URL = `${SITE_BASE}/blog`; +export const TERMS_URL = `${SITE_BASE}/terms`; +export const PRIVACY_URL = `${SITE_BASE}/privacy`; + +// Deep link into the docs, e.g. docsUrl('concepts/fleets') -> `${SITE_BASE}/docs/concepts/fleets`. +export const docsUrl = (path: string) => `${DOCS_URL}/${path.replace(/^\/+/, '')}`; diff --git a/website/src/styles.css b/website/src/styles.css new file mode 100644 index 0000000000..f82cf21f18 --- /dev/null +++ b/website/src/styles.css @@ -0,0 +1,2036 @@ +@font-face { + font-family: 'Geist'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('./fonts/Geist-Variable.woff2') format('woff2'); +} + +@font-face { + font-family: 'Geist Pixel Square'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('./fonts/GeistPixel-Square.woff2') format('woff2'); +} + +:root { + color-scheme: light; + --cs-text: #16191f; + --font-base: Geist, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + --cs-bg: #ffffff; + --cs-muted: #5f6b7a; + --cs-link: #0972d3; + --cs-border: #16191f; + --cs-panel: #f4f4f7; + --font-small: 15px; /* small body text: action-card descriptions, quotes, FAQ answers */ + --font-card-heading: 16px; /* card titles: action cards, dstack Sky/Factory, quote names */ + --cs-hover: rgba(22, 25, 31, 0.03); /* shared faint hover tint: normal buttons, cards, dropdown items (softened from 0.05) */ + --cs-btn-hover: #272d38; /* filled (primary) button hover fill — shared with the split-button override */ + --cs-seg-divider: rgba(255, 255, 255, 0.22); /* split-button segment divider (light line on the dark fill) */ + --cs-dark: #0f141d; + --nav-height: 4.875rem; + --frame: 80rem; + --page-gutter: clamp(1.5rem, 5.5vw, 5rem); + --card-gap: 1.25rem; + --radius: 0; + --doc-article-width: 49.5rem; + --doc-rail-width: 17.5rem; + --doc-right-gap: 2.5rem; + --doc-gap: var(--doc-right-gap); +} + +:root[data-theme='dark'] { + color-scheme: dark; + --cs-text: #f2f3f3; + --cs-bg: #0f141d; + --cs-muted: #c6c6cd; + --cs-link: #58a6ff; + --cs-border: #f2f3f3; + --cs-panel: #1b212d; + --cs-hover: rgba(242, 243, 243, 0.05); /* softened from 0.085 */ + --cs-btn-hover: #e2e5e8; + --cs-seg-divider: rgba(0, 0, 0, 0.18); /* split-button segment divider (dark line on the light fill) */ +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + /* Offset anchor / scrollIntoView landings by the sticky header (banner + nav) so a targeted + section isn't hidden behind it — e.g. "Get started" scrolling to #resources. */ + scroll-padding-top: calc(var(--nav-height) + 3.25rem); +} + +body { + margin: 0; + min-width: 320px; + color: var(--cs-text); + background: var(--cs-bg, #ffffff); + font-family: var(--font-base); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Cloudscape's own font family is set via the theming API (see cloudscape-theme.ts); + this base rule covers the non-Cloudscape markup. */ + +img { + display: block; + max-width: 100%; +} + +h1, +h2, +h3, +p { + margin: 0; +} + +p { + line-height: 1.5; +} + +/* Inline highlight for key terms (e.g. "dstack"): a thin rounded outline with minor padding and + a transparent fill. Border takes a muted text color so it stays subtle and theme-adaptive. */ +.highlight { + padding: 0.06em 0.36em; + border: 0.25px solid currentColor; + border-radius: 6px; + background: transparent; + white-space: nowrap; +} + +.site-frame { + width: min(var(--frame), calc(100vw - (2 * var(--page-gutter)))); + margin: 0 auto; +} + +/* Top announcement banner. Filled with the text color and an inverted label — the same + treatment as the primary buttons — so it reads as a deliberate strip in either theme. It + sticks to the top together with the nav (see .site-header). */ +.site-banner { + display: flex; + align-items: center; + justify-content: center; + min-height: 38px; + padding: 9px 16px 7.5px; + background: var(--cs-text); + color: var(--cs-bg); + text-align: center; +} + +/* Keep the whole banner (label + arrow) on a single line — never wrap. */ +.site-banner__link { + color: var(--cs-bg) !important; + font-size: 16.5px; + font-weight: 300; + text-decoration: none !important; + white-space: nowrap; +} + +/* On phones the announcement is wider than the viewport; wrap it (the sticky .site-header is + built for the banner's variable height) instead of clipping the text at both edges. */ +@media (max-width: 640px) { + .site-banner__link { + white-space: normal; + text-wrap: balance; + font-size: 15px; + } +} + +/* Light-on-dark text looks thinner than dark-on-light at the same weight; use a heavier weight in + dark mode so the banner reads with the same emphasis as in light. */ +:root[data-theme='dark'] .site-banner__link { + font-weight: 500; +} + +/* Icons follow the banner's (inverted) text color in both themes. */ +.site-banner [class*='awsui_icon'] { + color: var(--cs-bg) !important; +} + +.site-banner__arrow { + display: inline-flex; + vertical-align: middle; + margin-inline-start: 6px; + transition: transform 0.2s ease; +} + +.site-banner__link:hover .site-banner__arrow { + transform: translateX(3px); +} + +/* Compact buttons give the arrow a little extra room at the button edge. + Hero buttons supply their own symmetric padding around the whole label. */ +.cta-with-arrow { + --cta-padding-end: 10px; + display: inline-flex; + flex: 0 0 auto; +} +.cta-with-arrow__label { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; + vertical-align: top; +} +.cta-with-arrow__icon { + display: block; + flex: 0 0 12px; + width: 12px; + height: 12px; +} + +/* Banner + nav stick to the top together. Sticking the wrapper (rather than each child) + handles the banner's variable height when its text wraps on narrow screens, and keeps it + above the hero gradient (which can extend up into this area). */ +.site-header { + position: sticky; + top: 0; + z-index: 901; +} + +.site-nav { + position: relative; + z-index: 900; + height: var(--nav-height); + border-bottom: 0.5px solid var(--cs-border); + background: var(--cs-bg, #ffffff); +} + +.site-nav--home { + border-bottom: 0; + background: transparent; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); +} + +.site-nav::after { + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 4px; + content: ''; + background: linear-gradient(90deg, #8f41ff 0 29%, #5668ff 29% 61%, #2d49d8 61% 78%, #102f80 78% 100%); +} + +.site-nav--home::after { + display: none; +} + +.site-nav__inner { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 20px; + height: 100%; + padding: 14px 24px; +} + +.site-logo { + border: 0; + background: transparent; + color: var(--cs-text); + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.site-logo { + display: inline-flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + font-size: 20px; + line-height: 1; +} + +.site-logo img { + width: 28px; + height: 28px; +} + +.site-logo span { + font-family: 'Geist Pixel Square', var(--font-base); + font-weight: 700; + font-size: 25px; +} + +.site-menu { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1 1 auto; + min-width: 0; +} + +.site-mobile-trigger, +.site-mobile-spacer { + display: none; +} + +.site-menu-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; + min-height: 42px; + /* No horizontal padding: borderless text items are spaced purely by the nav's + SpaceBetween gap, so the whitespace to a neighboring text item matches the whitespace + to a bordered button (whose padding sits inside its border) — giving uniform gaps. */ + padding: 10px 0; + border-radius: 21px; + color: var(--cs-text); + font-weight: 700; + text-decoration: none; +} + +.site-menu-link:hover { + text-decoration: underline; + text-underline-offset: 0.2em; +} + +/* Keyboard focus only on the top-menu links. These are our own elements (not Cloudscape + components), so use plain values rather than Cloudscape's internal token variables. */ +.site-menu-link:focus-visible { + background: var(--cs-panel); + color: var(--cs-text); +} + +/* The Products secondary button anchors the click-to-open popup. */ +.site-products-dropdown { + --products-arrow-gap: 10px; + position: relative; + display: inline-flex; + align-items: center; +} + +/* The text-to-caret gap and trailing button padding use the same value. */ +.site-products-dropdown__label { + display: inline-flex; + align-items: center; + gap: var(--products-arrow-gap); + vertical-align: top; +} +/* Dropdown caret on the Products trigger — rotates 180° while the menu is open. */ +.site-products-dropdown__caret { + display: block; + width: 12px; + height: 12px; + opacity: 1; /* full text colour — the caret reads as black like the menu label */ + transition: transform 0.15s ease; +} +.site-products-dropdown[data-open='true'] .site-products-dropdown__caret { + transform: rotate(180deg); +} + +/* The Products popup frames the shared grouped rail with one outer border. */ +.site-products-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + z-index: 1000; + width: 360px; + /* A floating popup can't show a see-through hole between the groups (arbitrary page content + sits behind it) — back the rail with the unselected-row color so the gap reads as ground. */ + background: var(--cs-panel); + border: 1px solid var(--cs-border); + border-radius: 8px; + overflow: hidden; +} +.site-products-menu .gs-rail { + border-radius: 7px; +} +/* And no hole at all in the popup: the groups sit flush, like one continuous menu. */ +.site-products-menu .gs-opt + .gs-rail__group { + margin-top: 0; +} + +.home-hero { + position: relative; + overflow: visible; +} + +.home-hero__content { + position: relative; + z-index: 1; + padding: 96px 0 64px; + color: var(--cs-text); + /* The content column overlaps the absolutely-positioned art box on the right; let pointer + events fall through its empty areas so the live hero scene stays hoverable. The + interactive children (text + CTAs) opt back in below. */ + pointer-events: none; +} + +.home-hero h1, +.home-hero h2, +.home-hero p, +.home-hero__actions { + pointer-events: auto; +} + +.home-hero h1, +.home-hero h2 { + margin-top: 0; + max-width: 720px; + color: var(--cs-text); + font-size: 42px; + line-height: 1.16; + font-weight: 700; +} + +.home-hero p { + margin-top: 16px; + max-width: 600px; + font-size: 17px; +} + +.home-hero .highlight { + font-weight: 600; +} + +/* Paired actions share equal columns; single actions keep their natural width. */ +.home-hero__actions { + margin-top: 28px; + display: inline-flex; + flex-wrap: wrap; + gap: 8px; +} + +.cta-pair { + display: inline-grid; + grid-auto-flow: column; + grid-auto-columns: minmax(0, 1fr); + gap: 8px; + max-width: 100%; +} + +.home-hero__art { + position: absolute; + inset: 0; + z-index: 0; + opacity: 1; +} + +:root[data-theme='dark'] .home-hero__art { + opacity: 0.92; +} + +/* Centered content frame for the art, so it aligns with the columns of the blocks below. */ +.home-hero__art-frame { + position: relative; + height: 100%; +} + +/* Live squircle hero scene occupies the second content column: right edge at the frame + edge, width of one column, so it lines up with the second column of the blocks below. + The SVG inside is width:100%, so it scales to this box. */ +.hero-squircle { + position: absolute; + top: 40px; + right: 0; + width: calc((100% - var(--doc-gap)) / 2); +} + +/* Lifted above the footer gradient so the content/cards stay clean while the gradient + shows through behind them. */ +.home-main { + position: relative; + z-index: 1; +} + +.home-main--sky::before { + position: absolute; + top: calc(-1 * var(--nav-height)); + left: 0; + right: 0; + height: 36rem; + z-index: -1; + content: ''; + pointer-events: none; + background: linear-gradient( + 180deg, + rgba(65, 157, 255, 0.5) 0%, + rgba(176, 65, 255, 0.06) 35%, + rgba(176, 65, 255, 0) 100% + ); +} + +.home-stack { + padding: 0 0 96px; +} + +.home-with-rail .docs-body { + grid-template-columns: + minmax(0, calc(100% - var(--doc-right-gap) - var(--doc-rail-width))) + var(--doc-right-gap) + var(--doc-rail-width); +} + +.media-card h3, +.doc-media-card h3 { + margin-bottom: 8px; + font-size: 20px; + line-height: 1.2; +} + +.docs-section > h2 { + margin-bottom: 12px; + font-size: 28px; + line-height: 1.25; +} + +/* "Get started" and "Trusted by" section headings: larger than the in-flow block + titles, closer to the hero size. */ +#resources > h2, +#trusted-by > h2 { + margin-bottom: 32px; + font-size: 36px; +} + +/* Keep the lower sections flowing as one continuous sequence (no dividers between them). */ +#faq, +#trusted-by { + border-bottom: 0; +} + +/* One outer frame for the FAQ; individual questions only draw internal dividers + (see cloudscape-overrides). Clip their backgrounds to the shared rounded corners. */ +.faq-list { + border: 1px solid var(--cs-border); + border-radius: 12px; + overflow: hidden; +} + +/* FAQ question headers. */ +.faq-list [class*="awsui_header-text"] { + font-size: 16px; +} + +.media-card { + border: 1px solid var(--cs-border); /* 1px / 12px */ + border-radius: 12px; + overflow: hidden; + background: var(--cs-bg, #ffffff); +} + +.media-card img { + width: 100%; + height: 120px; + object-fit: cover; + background: #f3f5f9; +} + +.media-card h3, +.media-card p { + padding-inline: 20px; +} + +.media-card h3 { + padding-top: 16px; +} + +.media-card p { + padding-bottom: 22px; + font-size: var(--font-small); +} + +.docs-body { + display: grid; + grid-template-columns: minmax(0, var(--doc-article-width)) var(--doc-right-gap) var(--doc-rail-width); + align-items: start; +} + +.docs-body--no-rail { + grid-template-columns: minmax(0, 1fr); +} + +.docs-article { + grid-column: 1; +} + +.docs-section { + padding: 1.5rem 0 3rem; + border-bottom: 0.5px solid var(--cs-border); +} + +.docs-section:last-child { + border-bottom: 0; +} + +/* No divider between the explore section and Access GPU marketplace below it. */ +.docs-section.explore-section { + border-bottom: 0; +} + +.doc-alternating { + display: grid; + grid-template-columns: minmax(16rem, 1fr) minmax(0, 1fr); + gap: var(--doc-gap); + align-items: center; + margin-top: 18px; +} + +.doc-alternating + .doc-alternating { + margin-top: 48px; +} + +.doc-alternating:not(.image-first) { + grid-template-columns: minmax(0, 1fr) minmax(16rem, 1fr); +} + +.doc-alternating:not(.image-first) .doc-visual { + order: 2; +} + +.doc-visual { + min-width: 0; +} + +/* CodeView has no prop to disable its built-in fill; let the Container surface show through. */ +.code-snippet > div { + background-color: transparent !important; +} + +/* Cloudscape buttons render as when given href; exclude them from the docs underline rule. + Higher specificity than `.docs-article a` so the reset wins regardless of source order. */ +.docs-article .doc-action a, +.docs-article .figma-card a, +.docs-article .gs-boxfoot a, +.docs-article .gs-deploy a { + text-decoration: none !important; +} + +.doc-action { + margin-top: 20px; +} + +.doc-alternating h2 { + margin-bottom: 12px; + font-size: 28px; +} + +/* Body copy in the alternating marketing blocks (normal page text). */ +.doc-alternating p { + font-size: 17px; +} + +/* "Vendor-agnostic, open-source" architecture diagram (components/ArchitectureDiagram.tsx), + rebuilt from a static SVG into HTML/CSS and sized 1:1 with it. Every dimension is in cqw + (% of the diagram's own width, resolved against the .arch-diagram-wrap container) so the + whole thing scales proportionally like the original SVG. Source design width is 901.8 + units, so 1 unit ≈ 0.111cqw. Logos are monochrome + theme-adaptive (see .arch-logo). */ +.arch-diagram-wrap { + container-type: inline-size; + width: 100%; +} + +.arch-diagram { + display: flex; + flex-direction: column; + gap: 2.66cqw; /* 24u between the diagram blocks */ + width: 100%; + color: var(--cs-text); + --arch-border-width: max(1px, 0.22cqw); /* placeholder badge stroke */ + --arch-gradient: var(--cs-text); + --arch-label-padding: 2cqw; +} + +.arch-stack { + display: flex; + flex-direction: column; + overflow: hidden; + border: 0.5px dotted currentColor; + border-radius: 12px; + background: var(--cs-panel); +} + +.arch-row { + display: grid; + gap: 1.72cqw; /* 15.5u */ +} + +/* Framework/model inputs use equal-width blocks. */ +.arch-row--inputs { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +/* Give cloud examples the remaining space after the compact compute labels. */ +.arch-row--compute { + grid-template-columns: minmax(0, 1fr) repeat(4, max-content); + gap: 1.26cqw; + padding: 3.2cqw var(--arch-label-padding) 3.48cqw; +} + +.arch-row--compute .arch-cell { + justify-content: center; + gap: 0.55cqw; + min-height: calc(8cqw - 6px); + padding-inline: 1.5cqw; + border: 1px solid currentColor; + border-radius: 9px; + background: var(--cs-bg); +} + +.arch-row--compute .arch-cell--cloud { + justify-content: space-between; + gap: 1.55cqw; +} + +.arch-cell--cloud .arch-logos { + flex-shrink: 0; +} + +/* Pin both input labels left and their logo groups right. */ +.arch-row--inputs .arch-cell { + justify-content: space-between; +} + +.arch-cell { + position: relative; + display: flex; + align-items: center; + gap: 1.55cqw; + min-height: 9.83cqw; /* 88.7u */ + padding: 0 var(--arch-label-padding); + border: 1px solid currentColor; + border-radius: 12px; +} + +.arch-cell--center { + justify-content: center; +} + +/* Centered labels (e.g. "On-prem clusters") may wrap rather than overflow the narrow column. */ +.arch-cell--center .arch-cell__label { + white-space: normal; + text-align: center; +} + +.arch-cell--full { + min-height: 9.83cqw; +} + +.arch-cell--platform { + justify-content: center; + gap: 0.55cqw; + padding-inline: 2.1cqw; +} + +.arch-cell--platform .arch-cell__label { + font-size: 2.55cqw; + text-align: center; + white-space: normal; +} + +/* "Any accelerator": label pinned left, logos centered across the full row. */ +.arch-cell--hw { + justify-content: center; +} +.arch-cell--hw > .arch-cell__label { + position: absolute; + left: var(--arch-label-padding); +} + +.arch-cell__label { + font-size: 2.55cqw; + font-weight: 500; + line-height: 1.2; + white-space: nowrap; +} + +/* Orchestration and infrastructure share an outer border and aligned label padding. */ +.arch-orchestration { + display: flex; + flex-direction: column; + align-items: center; + gap: 3cqw; + padding: 3.2cqw var(--arch-label-padding) 3.48cqw; + border-radius: 0 0 12px 12px; + background: var(--arch-gradient); + color: var(--cs-bg); +} + +/* Same size/weight as the cell labels (e.g. "Any framework"). */ +.arch-orchestration__title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 2cqw; + width: 100%; /* stretch full width so the title pins left and the Docker marker pins right */ + font-size: 2.55cqw; + font-weight: 500; +} + +/* Five top-level orchestration primitives. */ +.arch-orchestration__cells { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 1.26cqw; /* 11.4u */ + width: 100%; +} + +.arch-subcell { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 6cqw; + padding: 0 0.8cqw; + border-radius: 12px; + text-align: center; + font-size: 2.25cqw; + font-weight: 500; + line-height: 1.2; + border: 1px dotted currentColor; +} + +.arch-diagram .arch-link { + color: inherit; + text-decoration: none !important; +} + +.arch-diagram .arch-link:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.arch-logos { + display: inline-flex; + align-items: center; + gap: 1.15cqw; /* ~10u between logos in the SVG */ + flex-wrap: nowrap; /* keep each cell's logos on one line (mobile reflow re-enables wrap) */ +} + +/* Real logo: source SVG (or webp) used as a mask filled with the current text color, so it reads + monochrome and flips with the theme. The mask image URL is the only inline bit (it needs the + runtime base path); size + aspect live in the per-logo classes below. */ +.arch-logo { + display: inline-block; + background-color: currentColor; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; +} + +/* Per-logo height (cqw, so it scales) + aspect from each source's viewBox. Wide marks (NVIDIA) + are set shorter so they don't dominate; tall ones (PyTorch) a touch taller. */ +.arch-logo--nebius { height: 4.6cqw; aspect-ratio: 1; } +.arch-logo--crusoe { height: 4.8cqw; aspect-ratio: 1; } +.arch-logo--runpod { height: calc(5.04cqw - 1.5px); aspect-ratio: 120 / 130; } +.arch-logo--aws { height: calc(4.05cqw - 1.5px); aspect-ratio: 304 / 182; } +.arch-logo--pytorch { height: 4.9cqw; aspect-ratio: 90.3 / 109.1; } +.arch-logo--vllm { height: 4.8cqw; aspect-ratio: 1; } +.arch-logo--sglang { height: 4.8cqw; aspect-ratio: 596 / 683; } +.arch-logo--smg { height: 3.75cqw; aspect-ratio: 28 / 29; } +.arch-logo--nvidia { height: 4cqw; aspect-ratio: 271.7 / 179.7; } +.arch-logo--amd { height: 4.8cqw; margin-left: 2px; aspect-ratio: 1600 / 1408; } +.arch-logo--tenstorrent { height: 4.8cqw; aspect-ratio: 170 / 169; } +.arch-logo--tpu { height: calc(4.2cqw - 1px); aspect-ratio: 256 / 206; } /* shares the GCP mark, sized down a touch */ +/* "Any model" marks (top row) — square icons; plus the Docker marker on the orchestration band. */ +/* Heights tuned per-logo for similar optical weight, compensating for differing padding and + visual density inside each source viewBox. */ +.arch-logo--glm { height: 3.8cqw; aspect-ratio: 19 / 16; } +.arch-logo--deepseek { height: 5.75cqw; aspect-ratio: 1; transform: translateY(-1px); } +.arch-logo--qwen { + width: 5.4cqw; + height: 6.3cqw; + aspect-ratio: 1; + -webkit-mask-size: auto 100%; + mask-size: auto 100%; + transform: translateY(1px); +} +.arch-logo--kimi { height: 5.2cqw; aspect-ratio: 1; transform: translateY(-1px); } +.arch-logo--docker { height: 2.8cqw; aspect-ratio: 122.88 / 88.17; } + +/* Placeholder badge (fallback if a logo ever lacks a source). Overrides the mask fill. */ +.arch-logo--placeholder { + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + /* height + min-width set inline per logo */ + padding: 0 0.6cqw; + border: var(--arch-border-width) dashed color-mix(in srgb, var(--cs-text) 42%, transparent); + border-radius: 0.7cqw; + background: none; + font-size: 1.5cqw; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--cs-muted); +} + +/* Safety net for narrow containers (phones): stop shrinking, stack the rows, drop the + primitives to two columns, and switch to fixed legible type. */ +@container (max-width: 480px) { + .arch-diagram { gap: 12px; --arch-label-padding: 14px; } + .arch-stack { border-radius: 8px; } + /* Stack the inputs and cloud examples, then pair the other compute sources. */ + .arch-row { grid-template-columns: 1fr 1fr; gap: 8px; } + .arch-row > :first-child { grid-column: 1 / -1; } + .arch-row--inputs > .arch-cell { grid-column: 1 / -1; } + .arch-row--compute { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; padding: 16px var(--arch-label-padding); } + .arch-row--compute > :first-child { grid-column: 1 / -1; } + .arch-row--compute .arch-cell { min-height: 54px; padding: 11px 8px; } + .arch-cell { min-height: 52px; padding: 10px var(--arch-label-padding); gap: 12px; border-width: 1px; border-radius: 8px; } + .arch-cell__label { font-size: 16px; white-space: normal; } + .arch-cell--platform { min-height: 52px; gap: 6px; padding: 8px 4px; } + .arch-cell--platform .arch-cell__label { font-size: 16px; } + /* "Any accelerator": label left, logos pushed to the right (the desktop layout centers them). */ + .arch-cell--hw { justify-content: space-between; } + .arch-cell--hw > .arch-cell__label { position: static; } + .arch-orchestration { gap: 12px; padding: 16px var(--arch-label-padding); border-radius: 0 0 8px 8px; } + .arch-orchestration__title { font-size: 15px; } + .arch-orchestration__cells { grid-template-columns: repeat(2, 1fr); gap: 8px; } + .arch-orchestration__cells > :last-child { grid-column: 1 / -1; } + .arch-subcell { min-height: 44px; font-size: 13px; border-width: 1px; border-radius: 7px; } + .arch-logos { gap: 12px; } + /* !important to beat the per-logo cqw heights. */ + .arch-logo { height: 22px !important; } + .arch-logo--runpod, + .arch-logo--aws { height: 20.5px !important; } + .arch-logo--tpu { height: 21px !important; } + .arch-logo--placeholder { height: 22px !important; min-width: 22px !important; font-size: 12px; border-width: 1px; border-radius: 5px; } +} + +.doc-card-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--card-gap); + margin-top: 24px; +} + +.doc-media-card img { + height: 98px; +} + +.doc-media-card h3 { + padding-top: 24px; +} + +.doc-media-card { + min-height: 272px; +} + +.alert-block { + margin-top: 1.5rem; +} + +.figma-card { + width: min(24rem, 100%); + margin-top: 1.5rem; + border: 1px solid var(--cs-border); /* 1px / 12px */ + border-radius: 12px; + overflow: hidden; + background: var(--cs-bg, #ffffff); +} + +.figma-card img { + width: 100%; + aspect-ratio: 980 / 560; + height: auto; + object-fit: cover; +} + +.figma-card div { + padding: 24px; +} + +.figma-card h3 { + margin-bottom: 12px; + font-size: var(--font-card-heading); +} + +.figma-card p { + margin-bottom: 20px; + font-size: var(--font-small); +} + +/* Quote author name reuses the card-heading size. */ +.testimonial-name { + font-size: var(--font-card-heading) !important; +} + +/* Product cards reusing the figma-card style. Used inside an AlternatingDocBlock + visual slot, so it stays vertically centered (no top margin). */ +.product-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--card-gap); +} + +/* Two-up variant: dstack Sky and Factory side by side. */ +.product-grid--pair { + grid-template-columns: repeat(2, 1fr); +} + +/* Testimonial cards in the "Trusted by" section. */ +.testimonial-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--card-gap); + margin-top: 24px; +} + +/* Small body text — quotes and FAQ answers — reuse the action-card description's size, line + height, and color so they read identically. !important to beat Cloudscape's boosted classes. */ +.testimonial-grid p, +.faq-list [class*='awsui_content'] { + font-size: var(--font-small) !important; + line-height: 1.5 !important; + color: var(--cs-text) !important; +} + +.faq-list p { + font-size: inherit; +} + +.faq-list p + p { + margin-top: 1em; +} + + +.testimonial-person { + display: flex; + align-items: center; + gap: 12px; +} + +.testimonial-photo { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + flex: 0 0 auto; +} + +.product-grid .figma-card { + width: auto; + margin-top: 0; +} + +/* Product selector and its matching preview. */ +.gs-deploy { + display: grid; + grid-template-columns: 1fr 2fr; /* selector 1/3, detail 2/3 */ + align-items: start; /* the box sets its own (taller) height; the rail sits at the top, not stretched */ + gap: 36px; + margin-top: 8px; +} + +/* Left: the selector — a bare column of individually-bordered option cards (no panel chrome). */ +.gs-rail { + display: flex; + flex-direction: column; + padding: 0; + border-radius: 8px; + overflow: hidden; /* clip the full-width group bars to the rounded corners */ + background: transparent; /* the rows carry the panel ground, so the gap between groups is a true hole */ +} +/* Product links share the same styling in the section and popup. */ +.gs-rail a.gs-opt { + color: var(--cs-text); + text-decoration: none; + cursor: pointer; +} +.gs-rail__group { + position: relative; + z-index: 4; +} +.gs-opt { + position: relative; + display: block; + width: 100%; + border: 0; + border-radius: 8px; /* same radius as the tab chips */ + background: var(--cs-panel); /* each row paints its own ground (the rail is transparent) */ + color: var(--cs-text); + font: inherit; + line-height: 1.15; /* pinned (not inherited) so the rail renders identically on the docs site */ + text-align: left; + cursor: pointer; +} + +/* Adjacent rows inside a group square off their shared edge, so the group's ground reads as one + block (rounded corners there would open see-through notches between the rows). */ +.gs-opt + .gs-opt { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.gs-opt:has(+ .gs-opt) { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.gs-rail:not(:has(a.gs-opt:hover, a.gs-opt:focus-visible)) .gs-opt.gs-opt--on, +.gs-rail a.gs-opt:hover, +.gs-rail a.gs-opt:focus-visible { + border-radius: 8px; + background: var(--cs-bg); + box-shadow: inset 0 0 0 1px var(--cs-text); +} +.site-products-menu .gs-rail:not(:has(a.gs-opt:hover, a.gs-opt:focus-visible)) .gs-opt.gs-opt--on, +.site-products-menu .gs-rail a.gs-opt:hover, +.site-products-menu .gs-rail a.gs-opt:focus-visible { + box-shadow: none; +} +.gs-rail button.gs-opt:focus-visible { + outline: 2px solid var(--cs-text); + outline-offset: -3px; +} + +/* Row names mirror the tab typography: 16px / 700. */ +.gs-opt__name { + display: block; + font-size: 16px; + font-weight: 700; +} +/* Rail group segments ("self-hosted" over dstack + Factory, "hosted by us" over Sky): thin + full-width solid bars; the rows beneath carry a thick left spine showing what each group + includes. */ +.gs-rail__group { + background: var(--cs-text); + color: var(--cs-bg); + font-size: 11px; + font-weight: 400; + line-height: 1; + padding: 5px 16px 5px 60.5px; /* left-aligns the label with the row titles */ + border-radius: 8px 8px 0 0; /* same radius as the rows */ + white-space: nowrap; +} +/* The first bar sits inside the rail's border corner: a 12px outer radius with a 1px border has + an 11px inner curve, so the bar needs the nested radius to fill it without a sliver. */ +/* A group bar that follows a row stands off from it, so the groups read as separate blocks. */ +.gs-opt + .gs-rail__group { + margin-top: 10px; +} +.gs-rail__group:first-child { + border-radius: 8px 8px 0 0; /* flush with the borderless rail's 8px clip */ +} +/* Rows keep their full rounding. The bar hugs the first row's top corners with two + inverted-radius notch shapes painted over the corners (nothing black sits behind the row's + antialiased edge, so no hairline outline), and tucks 1px under the row to kill the seam. */ +.gs-rail__group { + margin-bottom: -2px; /* tuck under the next row past any fractional-pixel rounding */ +} +/* A black top border bridges the bar/row boundary in the middle span, so pixel-snapping can + never open a white seam there (the bar's -1px margin tucks it under this border). */ +.gs-rail__group + .gs-opt { + border-top: 1px solid var(--cs-text); +} +.gs-rail__group + .gs-opt::before, +.gs-rail__group + .gs-opt::after { + content: ''; + position: absolute; + top: -1.5px; /* half-pixel deeper into the bar so fractional rounding can't expose a sliver */ + width: 9px; /* 1px overlap + the 8px corner radius */ + height: 9.5px; /* 1.5px overlap + the 8px corner radius */ + pointer-events: none; +} +.gs-rail__group + .gs-opt::before { + left: -1px; + background: radial-gradient(circle at 100% 100%, transparent 8px, var(--cs-text) 8.7px); +} +.gs-rail__group + .gs-opt::after { + right: -1px; + background: radial-gradient(circle at 0 100%, transparent 8px, var(--cs-text) 8.7px); +} +.gs-opt__desc { + display: block; + margin-top: 4px; + font-size: 13px; + line-height: 1.5; + color: color-mix(in srgb, var(--cs-text) 68%, var(--cs-muted)); +} + +/* Open-source product row. */ +.gs-opt--feat { + padding: 13px 14px; + display: flex; + align-items: flex-start; + gap: 14px; +} + +/* dstack Sky / Factory: compact rows with an icon. */ +.gs-opt--row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 13px 14px; +} +.gs-opt__ic { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 34px; + height: 34px; + border-radius: 9px; + border: 1px solid var(--cs-border); + color: inherit; +} +.gs-opt__ic svg { + width: 20px; + height: 20px; +} + +.gs-opt__body { + flex: 1 1 auto; + min-width: 0; +} + +/* Right: the selection's detail. */ +.gs-detail { + min-width: 0; +} +.gs-detail .gs-tabs { + flex-shrink: 0; + overflow-x: auto; +} +.gs-detail .gs-tab { + white-space: nowrap; +} +/* The detail box — shared shell for all three tabs: a bordered container with a fixed height, holding a tab strip, content, and a footer CTA bar. The + height is tuned a touch taller than the rail, giving the content room to breathe and matching the + tabbed code cards higher up the page; all three tabs reuse this shell, so they stay equal height. */ +.gs-box { + height: 360px; /* a touch taller than the rail (~332px); fits the 5-item Governance grid with slack */ + display: flex; + flex-direction: column; + background: var(--cs-bg); + border: 1px solid var(--cs-border); /* matches the landing's cards (.media-card / .figma-card) */ + border-radius: 12px; + overflow: hidden; +} +.gs-box.gs-box--compute, +.gs-box.gs-box--compute-sources { + height: 360px; +} +.gs-box--compute .gs-skybody, +.gs-box--compute-sources .gs-skybody { + overflow-y: auto; +} +.gs-box--offers .gs-skybody { + padding-top: 0; +} +.sky-gpu-prices { + flex: 0 0 auto; + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 13px; +} +.sky-gpu-prices th, +.sky-gpu-prices td { + padding: 6px 0 6px 14px; + text-align: right; + vertical-align: top; +} +.sky-gpu-prices tr > :first-child { + padding-left: 0; + text-align: left; +} +.sky-gpu-prices thead th { + padding-block: 14px; + position: sticky; + top: 0; + z-index: 1; + background: var(--cs-bg); + color: var(--cs-muted); + font-weight: 600; +} +.sky-gpu-prices td { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + white-space: nowrap; +} +.sky-gpu-prices tbody th, +.sky-gpu-prices tbody td { + transition: background-color 0.15s ease; +} +.sky-gpu-prices tbody tr:hover > * { + background: var(--cs-panel); +} +.sky-gpu-prices tbody tr > :first-child { + border-radius: 8px 0 0 8px; +} +.sky-gpu-prices tbody tr > :last-child { + border-radius: 0 8px 8px 0; +} +@media (max-width: 480px) { + .gs-box--offers .gs-skybody { + padding-inline: 16px; + } + .sky-gpu-prices, + .sky-gpu-prices .gs-mkt__g { + font-size: 12px; + } + .sky-gpu-prices th, + .sky-gpu-prices td { + padding-left: 8px; + } +} +.gs-boxfoot { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 14px; + padding: 13px 16px 13px 20px; + border-top: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent); +} +.gs-foot__note { + font-size: 13px; + color: var(--cs-muted); + min-width: 0; /* allow the note to shrink/wrap so it shares the row with the CTA on narrow screens */ + margin-inline-end: auto; /* note left, button(s) right — the footer itself is flex-end */ +} +.gs-foot__actions { + display: flex; + flex: 0 0 auto; + gap: 14px; +} + +@media (max-width: 640px) { + .gs-detail .gs-boxfoot { + flex-wrap: wrap; + } + .gs-detail .gs-foot__note { + flex-basis: 100%; + } + .gs-foot__actions { + flex-wrap: wrap; + gap: 10px; + max-width: 100%; + } +} + +/* The footer CTA(s) keep their width (never shrink/wrap mid-word); the note takes the rest. */ +.gs-boxfoot > [class*='awsui_button'] { + flex: 0 0 auto; +} + +@media (max-width: 480px) { + .gs-box--compute .gs-boxfoot, + .gs-box--compute-sources .gs-boxfoot { + flex-wrap: wrap; + } + .gs-box--compute .gs-foot__note, + .gs-box--compute-sources .gs-foot__note { + flex-basis: 100%; + } +} + +/* Shared tabs for installation methods and Sky compute options. */ +.gs-tabs { + display: flex; + gap: 2px; + padding: 6px 8px 0; + border-bottom: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent); +} +.gs-tab { + padding: 15px 14px; + border: 0; + border-radius: 8px 8px 0 0; + background: transparent; + color: var(--cs-muted); + font-family: inherit; + font-size: 16px; /* match the page's other (Cloudscape) tab groups: 16px / 700 */ + font-weight: 700; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} +.gs-tab:not(.gs-tab--on):hover { + color: var(--cs-text); + background: var(--cs-hover); /* match the Cloudscape tabs' hover tint */ +} +.gs-tab--on { + color: var(--cs-text); + background: var(--cs-panel); +} + +/* Open-source: read-only code fills the body. */ +.gs-box--install .gs-skybody { + flex: 1 1 auto; + min-height: 0; + padding: 10px 12px; + overflow: auto; +} + +/* dstack Sky: two panes (GPU marketplace | bring your own clouds). Source order is header→list, + header→list. Desktop: a 2-row grid with column auto-flow, so the two headers land in row 1 (the + tab strip) and the two lists in row 2 (the content). Mobile (media query): a single column, so + each header sits directly above its own list. The chips reuse the open-source tab look. */ +.gs-sky { + display: grid; + grid-template-rows: auto 1fr; + grid-auto-flow: column; + grid-auto-columns: 1fr; + flex: 1 1 auto; + min-height: 0; +} +.gs-skyhalf { + min-width: 0; + padding: 6px 0 0 8px; /* chip text (chip pad 14) lands at 22px, matching the column content below */ + border-bottom: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent); /* under-tab divider */ +} +.gs-skytab { + display: inline-block; + padding: 15px 14px; + border-radius: 8px 8px 0 0; + background: var(--cs-panel); + color: var(--cs-text); + font-size: 16px; /* match the page's other tab groups: 16px / 700 */ + font-weight: 700; +} +.gs-col { + min-width: 0; + min-height: 0; /* grid item: allow the 1fr row to cap it so the list scrolls instead of overflowing */ + display: flex; + flex-direction: column; + padding: 14px 22px 0; +} +/* Vertical divider between the two lists only (the 2nd list pane), not under the headers. */ +.gs-sky .gs-col ~ .gs-col { + border-left: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent); +} + +/* The explore block's categorized clouds grid: one column per group (hyperscalers, top GPU + neoclouds, smaller GPU clouds), each column an ordered list. Rows match .gs-cloud styling. */ +.gs-cloudcols { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + column-gap: 28px; + flex: 1 1 auto; + min-height: 0; +} +.gs-cloudcols ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 9px; + align-content: start; +} +@media (max-width: 640px) { + /* Too narrow for three columns; the group lists stack as blocks instead. */ + .gs-cloudcols { + grid-template-columns: repeat(2, minmax(0, 1fr)); + row-gap: 9px; + } +} + +/* Sky compute tab content. */ +.gs-skybody { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + padding: 36px 28px 18px; +} +.gs-skybody .gs-caps { + flex: 1 1 auto; + align-content: start; +} + +.gs-mkt__g { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; + font-weight: 400; /* only the GPU name is emphasized (below); VRAM stays regular */ +} +.gs-mkt__name { + font-weight: 600; /* emphasize the GPU name by weight, not size */ +} + +.gs-cloud { + display: flex; + align-items: center; + gap: 10px; + font-size: var(--font-small); + font-weight: 600; /* match the emphasized GPU names in the marketplace tab */ + line-height: 1.3; +} + +/* Factory: the "Extra" capabilities — a 2×2 grid of capability rows (icon + title + one-line + detail), centered in the body. No card borders. */ +.gs-entbody { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + justify-content: center; + padding: 20px 24px; +} +.gs-caps { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 26px; +} + +.gs-cap { + display: flex; + align-items: flex-start; + gap: 11px; +} +.gs-cap, +.gs-cloud { + position: relative; + isolation: isolate; + border-radius: 8px; +} +.docs-article a.gs-cap, +.docs-article a.gs-cloud { + color: var(--cs-text); + text-decoration: none !important; +} +/* Extend the hover fill without changing list spacing or text alignment. */ +.gs-cap::before, +.gs-cloud::before { + content: ''; + position: absolute; + inset: -8px -12px; + z-index: -1; + border-radius: inherit; + background: var(--cs-panel); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; +} +.gs-cap:hover::before, +.gs-cloud:hover::before, +.gs-cap:focus-visible::before, +.gs-cloud:focus-visible::before { + opacity: 1; +} +.docs-article a.gs-cap:focus-visible, +.docs-article a.gs-cloud:focus-visible { + outline: 2px solid var(--cs-text); + outline-offset: 4px; +} +.gs-cap__ic { + flex: 0 0 auto; + margin-top: 1px; + color: #8b2fd0; +} +:root[data-theme='dark'] .gs-cap__ic { + color: #c08bff; +} +.gs-cap__ic svg { + width: 20px; + height: 20px; +} +.gs-cap__b { + display: flex; + flex-direction: column; + min-width: 0; +} +.gs-cap__t { + font-size: var(--font-small); /* same size as the Sky cloud rows */ + font-weight: 600; + line-height: 1.3; +} +.gs-cap__s { + font-size: 12.5px; + color: var(--cs-muted); + margin-top: 2px; + line-height: 1.4; +} + +/* Brand-purple list glyphs (Sky clouds + marketplace GPU chips), matching the capability icons. */ +.gs-li__ic { + display: inline-flex; + flex: 0 0 auto; + color: #8b2fd0; +} +:root[data-theme='dark'] .gs-li__ic { + color: #c08bff; +} +.gs-li__ic svg { + width: 15px; + height: 15px; +} +/* Marketplace row: chip + name group on the left, price on the right. */ +.gs-mkt__left { + display: inline-flex; + align-items: center; + gap: 9px; + min-width: 0; +} + + +@media (max-width: 860px) { + .gs-deploy { grid-template-columns: 1fr; gap: 24px; } + /* Stacked: the box flows naturally to fit its content (no fixed-height scroll). */ + .gs-box { height: auto; } + /* Sky: single column so each header sits directly above its own list (header→list→header→list). */ + .gs-sky { display: flex; flex-direction: column; } + .gs-sky .gs-col ~ .gs-col { border-left: 0; } + /* Separate the second pane (its header follows the first list). */ + .gs-sky .gs-col + .gs-skyhalf { margin-top: 12px; } + .gs-caps { grid-template-columns: 1fr; } + /* Keep footer notes and buttons on one row. */ + .gs-boxfoot { align-items: center; gap: 10px; } + .gs-boxfoot [class*='awsui_button'] { padding-inline: 14px var(--cta-padding-end, 14px) !important; } +} + +.site-footer { + position: relative; + border-top: 0.5px solid var(--cs-border); + background: var(--cs-bg, #ffffff); +} + +/* On home, drop the divider line and white block so the footer blends into the + colored band above (mirrors the transparent, borderless home header). */ +.site-footer--home { + border-top: 0; + background: transparent; +} + +/* Single colored band: rises from the bottom of the page up toward the divider (which + crops it visually), deepening in color toward the footer. Painted behind .home-main so + the content and cards stay clean on top. */ +.site-footer--home::before { + position: absolute; + left: 0; + right: 0; + top: -28rem; + bottom: 0; + z-index: 0; + content: ''; + pointer-events: none; + background: linear-gradient( + 180deg, + rgba(176, 65, 255, 0) 0%, + rgba(176, 65, 255, 0.1) 55%, + rgba(96, 106, 255, 0.22) 100% + ); +} + +.footer-content { + position: relative; + z-index: 1; + padding-top: 48px; + font-size: 13px; +} + +/* Top row: brand block (logo + social) on the left, the link group flush to the + right edge (justify-content on this outer row), so the columns themselves keep a + tight content width + fixed gap rather than being stretched apart. */ +.footer-top { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 48px; + padding-bottom: 32px; +} + +.footer-brand { + display: flex; + flex-direction: column; + gap: 20px; + min-width: 0; +} + +/* Link columns: each sized to its content, separated by a fixed gap. */ +.footer-links { + display: flex; + flex-wrap: wrap; + gap: 32px 72px; +} + +.footer-brand__logo { + display: inline-flex; + align-items: center; + gap: 8px; + width: fit-content; + color: var(--cs-text); + font-size: 20px; + font-weight: 700; + line-height: 1; + text-decoration: none !important; +} + +.footer-brand__logo img { + width: 28px; + height: 28px; +} + +.footer-brand__logo span { + font-family: 'Geist Pixel Square', var(--font-base); + font-weight: 700; + font-size: 25px; +} + +.footer-channels { + display: flex; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.footer-channel-link { + display: inline-flex; + color: var(--cs-text) !important; + text-decoration: none !important; +} + +.footer-channel-link svg { + display: block; +} + +/* Column captions: match the top-menu link size (16px); links sit a step below. */ +.footer-col__heading { + margin: 0 0 14px; + font-size: 16px; + font-weight: 700; + color: var(--cs-text); +} + +/* Bottom bar: theme toggle + copyright, separated from the columns by a divider. */ +.footer-bar { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + min-height: 64px; +} + +/* Theme toggle: a pill switch + crescent moon, matching the dstack control-plane app's toggle + (frontend AppLayout). Light = switch OFF (grey track, white thumb left); [data-theme="dark"] + = switch ON (the app's dark-mode "checked" blue track, dark thumb slid right). Colors are the + Cloudscape control tokens: track #424650, checked #42b4ff, thumb #fff (light) / #0f141a (dark). + The same component renders in the header (large screens) and footer (tablet/mobile); the + --header / --footer variants below toggle which one is visible at the 1024px breakpoint. */ +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; + border: 0; + background: transparent; + color: inherit; /* the moon inherits the surrounding text color */ + cursor: pointer; +} +.theme-toggle__track { + position: relative; + flex: 0 0 auto; + width: 24px; + height: 16px; + border-radius: 8px; + background: #424650; + transition: background-color 0.2s ease; +} +.theme-toggle__thumb { + position: absolute; + top: 2px; + left: 0; + width: 12px; + height: 12px; + border-radius: 50%; + background: #ffffff; + transform: translateX(2px); + transition: transform 0.2s ease, background-color 0.2s ease; +} +.theme-toggle__moon { + flex: 0 0 auto; + display: block; +} +[data-theme='dark'] .theme-toggle__track { + background: #42b4ff; +} +[data-theme='dark'] .theme-toggle__thumb { + background: #0f141a; + transform: translateX(10px); +} + +/* The header toggle shows only on large screens; the footer one takes over at ≤1024px (tablet + + mobile), where the header nav collapses into the burger menu. */ +.theme-toggle--footer { + display: none; +} +.theme-toggle--header { + transform: translateY(3px); /* the nav row centers it a hair high; nudge it down to match */ + margin-right: -5px; /* trim the SpaceBetween gap to GitHub by 5px */ +} +@media (max-width: 1024px) { + .theme-toggle--header { + display: none; + } + .theme-toggle--footer { + display: inline-flex; + } +} + +.footer-copyright { + text-align: left; +} + +.home-stack a, +.docs-article a, +.site-footer a { + text-decoration-line: underline !important; + text-decoration-color: currentColor !important; + text-underline-offset: 0.08em; +} + +.home-stack a:hover, +.docs-article a:hover, +.site-footer a:hover { + text-decoration-thickness: 2px !important; +} + +/* Footer column links: full-strength text, same 16px as the column headings, no underline + until hover. */ +.footer-links a { + color: var(--cs-text) !important; + /* Cloudscape sets 14px via its boosted awsui_font-size-body-m class, so override + with !important to match the 16px column headings. */ + font-size: 16px !important; + text-decoration-line: none !important; +} + +.footer-links a:hover { + text-decoration-line: underline !important; +} + +/* The brand logo and social icons are s inside .site-footer, so the global + footer-link underline rule applies to them — override it (higher specificity). */ +.site-footer .footer-brand__logo, +.site-footer .footer-channel-link { + text-decoration: none !important; +} + +/* On home the divider would sit on the gradient band; soften it so the footer + keeps blending into the colored band above. */ +.site-footer--home .footer-bar { + border-top-color: transparent; +} + +.home-stack p a, +.docs-article p a { + font-size: inherit !important; + line-height: inherit !important; +} + +/* Footer responsive: below the desktop width the columns stack on top, with the brand + (logo + social) below them — the links are the primary footer nav, so they lead. */ +@media (max-width: 1100px) { + .footer-top { + flex-direction: column; + gap: 32px; + } + + /* Links first, brand (logo + social icons) second. */ + .footer-brand { + order: 2; + } + + .footer-links { + width: 100%; + gap: 28px 56px; + } +} + +/* Phones: four columns no longer fit in one row, and three would orphan the fourth, + so drop to a clean two-up grid. */ +@media (max-width: 720px) { + .footer-links { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 28px 32px; + } +} + +@media (max-width: 1100px) { + .site-frame { + width: calc(100vw - 48px); + } + + .home-hero__content { + padding: 64px 0; + } + + .home-hero__art { + opacity: 0.85; + } + + .hero-squircle { + top: 12px; + } + + .doc-card-grid, + .product-grid, + .testimonial-grid { + grid-template-columns: 1fr; + } + + .doc-alternating, + .doc-alternating:not(.image-first) { + grid-template-columns: 1fr; + } + + /* Stacked on mobile, always lead with the text block; the visual follows below + (regardless of the desktop image-first / image-last alternation). */ + .doc-alternating .doc-visual { + order: 2; + } + + /* Tighter gap between blocks once they stack into a single column. */ + .doc-alternating + .doc-alternating { + margin-top: 32px; + } + + .docs-body { + display: flex; + flex-direction: column; + } +} + +/* Switch to the burger nav at ≤1024px: the full menu (Documentation + the three blog links + + theme toggle + GitHub + Get started) is too wide for tablet widths. This matches the theme + toggle's 1024px boundary (header → footer above), so the footer toggle takes over exactly when + the burger appears — no width band is left without a toggle. The hero/content phone layout + stays at ≤640px (below). */ +@media (max-width: 1024px) { + .site-nav { + height: 54px; + } + + .site-nav__inner { + position: relative; + justify-content: space-between; + gap: 0; + width: 100%; + padding: 0 13px; + } + + .site-menu { + display: none; + } + + .site-mobile-trigger, + .site-mobile-spacer { + display: block; + flex: 0 0 28px; + } + + .site-logo { + position: absolute; + left: 50%; + transform: translateX(-50%); + } + + .site-mobile-navigation { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: -1; + max-height: calc(100vh - 58px); + overflow: auto; + padding: 20px; + border-bottom: 0.5px solid var(--cs-border); + background: var(--cs-bg, #ffffff); + } + + /* Below 1024 (where the nav also collapses to a hamburger) the side-by-side hero would + overlap the headline, so stack it: headline + CTAs first, then the squircle below them + in normal flow, centered — on tablet and phone alike. */ + .home-hero { + display: flex; + flex-direction: column; + } + + .home-hero__content { + order: 1; + padding-bottom: 8px; + } + + .home-hero__art { + order: 2; + position: static; + height: auto; + opacity: 1; + padding-bottom: 8px; + } + + .home-hero__art-frame { + height: auto; + } + + .hero-squircle { + position: static; + width: min(100%, 460px); + margin: 8px auto 0; + } +} + +@media (max-width: 640px) { + .home-hero { + display: flex; + flex-direction: column; + } + + .home-hero__content { + order: 1; + /* Tighter than tablet's 64px — the sticky banner + nav already eat a phone's first screen. */ + padding: 40px 0 24px; + } + + /* Hero CTAs share one full-width row on phones — two equal columns spanning the width. */ + .home-hero__actions { + display: grid; + grid-template-columns: 1fr 1fr; + width: 100%; + } + + .home-hero__actions--single { + grid-template-columns: max-content; + } + /* Each button fills its half-column; center the label and trim the desktop side + padding so both fit. !important: Cloudscape sets text-align:start via a boosted rule. */ + .cta-pair > a { + padding-inline: 16px !important; + } + .home-hero__actions [class*='awsui_button'] { + width: 100%; + padding-inline: 16px !important; + text-align: center !important; + } + + .home-hero h1, + .home-hero h2 { + font-size: 32px; + } + + .home-stack { + padding-top: 0; + } + + .figma-card, + .figma-card img { + width: 100%; + } + + .footer-content { + padding-top: 32px; + } + + .footer-top { + padding-bottom: 24px; + } + + .footer-bar { + flex-wrap: wrap; + gap: 12px; + } +} diff --git a/website/src/theme.ts b/website/src/theme.ts new file mode 100644 index 0000000000..589fc870bd --- /dev/null +++ b/website/src/theme.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import { Mode, applyMode } from '@cloudscape-design/global-styles'; + +export type ThemeMode = 'light' | 'dark'; + +const STORAGE_KEY = 'dstack-theme'; + +// Defaults to light; remembers the visitor's explicit choice. +function getInitialTheme(): ThemeMode { + return localStorage.getItem(STORAGE_KEY) === 'dark' ? 'dark' : 'light'; +} + +// Tracks light/dark mode and applies it to the Cloudscape global styles + the document. +export function useTheme() { + const [theme, setTheme] = useState(getInitialTheme); + + useEffect(() => { + const root = document.documentElement; + const body = document.body; + root.dataset.theme = theme; + body.classList.add('awsui-visual-refresh'); + applyMode(theme === 'dark' ? Mode.Dark : Mode.Light); + }, [theme]); + + const toggleTheme = () => + setTheme(current => { + const next = current === 'light' ? 'dark' : 'light'; + localStorage.setItem(STORAGE_KEY, next); + return next; + }); + + return { theme, toggleTheme }; +} diff --git a/website/src/vite-env.d.ts b/website/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/website/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000000..36197f5db2 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2020"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/website/vite.config.ts b/website/vite.config.ts new file mode 100644 index 0000000000..ca0e5f13b3 --- /dev/null +++ b/website/vite.config.ts @@ -0,0 +1,26 @@ +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// The deploy path is injected via BASE_PATH (set by CI from the repo name), so nothing +// in the app hardcodes where it's hosted. Locally (dev/build/preview) it stays at root. +// +// `assetsDir` is namespaced to `website-assets/` (not Vite's default `assets/`) so the +// build can be overlaid onto the MkDocs `site/` output without colliding with MkDocs's +// own `/assets/...` tree. Public files live under `public/static/` for the same reason — +// the HTML entries provide the landing and product routes on static hosting. +export default defineConfig({ + base: process.env.BASE_PATH || '/', + plugins: [react()], + esbuild: { legalComments: 'inline' }, + build: { + assetsDir: 'website-assets', + rollupOptions: { + input: { + home: fileURLToPath(new URL('./index.html', import.meta.url)), + sky: fileURLToPath(new URL('./products/sky/index.html', import.meta.url)), + factory: fileURLToPath(new URL('./products/factory/index.html', import.meta.url)), + }, + }, + }, +});