From 207493188e286072d72a094f5f07a8b47b470561 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Mon, 20 Jul 2026 23:50:20 -0700 Subject: [PATCH 01/38] Update GPU Inference and Model Qualification --- .github/workflows/container-smoke.yml | 16 +- .github/workflows/create-release.yml | 2 +- .github/workflows/dependency-review.yml | 9 +- .github/workflows/gpu-container-image.yml | 152 +- .github/workflows/main-validation.yml | 2 + .github/workflows/validate.yml | 2 +- AGENTS.md | 1 + deploy/carina/bootstrap.sh | 1 - deploy/tests/native_installer_smoke.sh | 24 +- deploy/verify_gpu_compatibility.py | 245 +++ deploy/verify_model_sources.py | 8 +- documentation/architecture/testing.md | 13 +- documentation/models/choose-managed.md | 97 +- documentation/models/run-with-heartwood.md | 4 +- documentation/operate/index.md | 2 +- documentation/platforms/carina.md | 44 +- documentation/platforms/containers.md | 3 +- documentation/platforms/index.md | 3 +- documentation/platforms/terra.md | 20 +- documentation/reference/cli.md | 7 +- documentation/reference/glossary.md | 9 + documentation/reference/gpu-compatibility.md | 69 + documentation/reference/troubleshooting.md | 4 +- images/Dockerfile | 13 +- images/generic/README.md | 4 +- images/generic/local-runtime/snapshots.toml | 177 +- images/generic/scripts/capable_model_e2e.sh | 170 +- images/generic/scripts/coding_agent_e2e.sh | 144 ++ .../scripts/verify_coding_agent_e2e.py | 236 +++ images/gpu/coding_agent_e2e.sh | 164 ++ images/gpu/compatibility.toml | 149 ++ images/gpu/heartwood-vllm | 8 +- images/gpu/heartwood_vllm.py | 209 --- images/gpu/install_runtime.sh | 4 +- images/gpu/qualification_config.py | 50 + images/gpu/sitecustomize.py | 13 - images/gpu/start_vllm.sh | 5 +- images/gpu/verify_runtime.sh | 2 +- images/gpu/verify_vllm.py | 97 ++ images/gpu/vllm-exclusions.txt | 17 + images/gpu/vllm-overrides.txt | 11 +- images/gpu/vllm-requirements.txt | 1504 +++++++++++------ images/gpu/vllm.in | 15 +- images/platforms.toml | 2 +- packages/cli/src/heartwood/cli/__init__.py | 144 +- packages/cli/src/heartwood/cli/_launch.py | 464 ++++- .../tests/test_carina_launch_integration.py | 17 +- packages/cli/tests/test_cli.py | 44 +- packages/cli/tests/test_launch.py | 199 ++- .../tests/test_coding_agent_qualification.py | 178 ++ .../compliance/tests/test_container_assets.py | 164 +- .../tests/test_model_source_verification.py | 25 +- .../gateway/src/heartwood/gateway/__init__.py | 26 +- .../gateway/src/heartwood/gateway/_gateway.py | 191 ++- .../src/heartwood/gateway/_gpu_environment.py | 391 +++++ .../src/heartwood/gateway/_local_import.py | 27 + .../src/heartwood/gateway/_local_models.py | 242 ++- .../src/heartwood/gateway/_model_snapshots.py | 233 ++- .../src/heartwood/gateway/_project_config.py | 187 +- .../gateway/tests/test_gateway_contract.py | 89 +- .../gateway/tests/test_gpu_environment.py | 368 ++++ packages/gateway/tests/test_local_models.py | 40 +- .../gateway/tests/test_model_artifacts.py | 21 +- .../gateway/tests/test_model_snapshots.py | 261 ++- packages/gateway/tests/test_project_config.py | 294 ++++ packages/gateway/tests/test_readiness.py | 3 + packages/webui/src/App.test.tsx | 134 +- packages/webui/src/client.test.ts | 2 +- .../webui/src/components/UtilitySheet.tsx | 266 ++- packages/webui/src/e2e/app.spec.ts | 20 +- packages/webui/src/types.ts | 57 +- zensical.toml | 1 + 72 files changed, 6273 insertions(+), 1546 deletions(-) create mode 100644 deploy/verify_gpu_compatibility.py create mode 100644 documentation/reference/gpu-compatibility.md create mode 100755 images/generic/scripts/coding_agent_e2e.sh create mode 100644 images/generic/scripts/verify_coding_agent_e2e.py create mode 100755 images/gpu/coding_agent_e2e.sh create mode 100644 images/gpu/compatibility.toml delete mode 100644 images/gpu/heartwood_vllm.py create mode 100644 images/gpu/qualification_config.py delete mode 100644 images/gpu/sitecustomize.py create mode 100644 images/gpu/verify_vllm.py create mode 100644 images/gpu/vllm-exclusions.txt create mode 100644 packages/compliance/tests/test_coding_agent_qualification.py create mode 100644 packages/gateway/src/heartwood/gateway/_gpu_environment.py create mode 100644 packages/gateway/tests/test_gpu_environment.py diff --git a/.github/workflows/container-smoke.yml b/.github/workflows/container-smoke.yml index 7a74e3bd..09d7dd47 100644 --- a/.github/workflows/container-smoke.yml +++ b/.github/workflows/container-smoke.yml @@ -93,7 +93,7 @@ jobs: capable-model: name: Capable Model OpenHands Acceptance (linux/amd64) - if: github.event_name == 'workflow_dispatch' && inputs.run_capable_model + if: inputs.run_capable_model runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -139,6 +139,20 @@ jobs: --env HEARTWOOD_LOCAL_MODEL_PATH=/models/qwen25-7b-instruct-q4_k_m/Qwen2.5-7B-Instruct-Q4_K_M.gguf heartwood-capable:local bash /opt/heartwood/images/generic/scripts/capable_model_e2e.sh + - name: Prepare coding-agent qualification evidence + if: always() + run: sudo chown -R "$(id -u):$(id -g)" /tmp/heartwood-project + - name: Upload coding-agent qualification evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: capable-model-qualification-${{ github.sha }} + path: | + /tmp/heartwood-project/heartwood-qualification.json + /tmp/heartwood-project/heartwood-replay.txt + /tmp/heartwood-project/heartwood-transcript.txt + if-no-files-found: warn + retention-days: 14 terra-smoke: name: Terra image smoke test (linux/amd64) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 2b06309b..ab2c50cd 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -67,7 +67,7 @@ jobs: echo "tag already exists: ${RELEASE_VERSION}" >&2 exit 1 fi - - name: Verify recommended model sources + - name: Verify catalog model sources run: python3 deploy/verify_model_sources.py --source-root . - name: Verify release readiness on the exact commit env: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 34906a64..e72fcf76 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -34,11 +34,8 @@ jobs: - name: Review dependencies uses: actions/dependency-review-action@v5.0.0 with: - # The CUDA 11.8 vLLM environment is isolated in the hashed GPU lock. + # The CUDA 12.9 vLLM environment is isolated in the hashed GPU lock. # diskcache and torch have no patched compatible release; Heartwood # does not load caches across trust domains or expose torch.jit.script - # to input. vLLM GHSA-8fr4-5q9j-m8gm is backported by removing the - # vulnerable config registry entry exactly as upstream PR 28126 does, - # with an exploit-regression check at build, install, and launch time. - # Revisit these exact exceptions with every vLLM update. - allow-ghsas: GHSA-w8v5-vhqr-4h9v, GHSA-rrmf-rvhw-rf47, GHSA-8fr4-5q9j-m8gm + # to input. Revisit these exact exceptions with every vLLM update. + allow-ghsas: GHSA-w8v5-vhqr-4h9v, GHSA-rrmf-rvhw-rf47 diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index 6e3d6bfd..1eb1c1c5 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -9,6 +9,29 @@ name: GPU Container Image on: workflow_call: workflow_dispatch: + inputs: + run_gpu_qualification: + description: Run the coding-agent acceptance suite on a protected self-hosted GPU runner + required: true + default: false + type: boolean + configuration_id: + description: Reviewed compatibility-matrix configuration + required: true + default: terra-t4-qwen25-coder-7b-awq + type: choice + options: + - terra-t4-qwen25-coder-7b-awq + - carina-l40s-qwen25-coder-7b-awq + - carina-l40s-qwen3-coder-30b-fp8 + - carina-2xl40s-qwen3-coder-30b-bf16 + - carina-4xl40s-qwen3-coder-next-fp8 + - carina-2xl40s-gpt-oss-120b + qualification_runner: + description: JSON array of protected self-hosted GPU runner labels + required: true + default: '["self-hosted","linux","x64","gpu"]' + type: string concurrency: group: gpu-container-image-${{ github.workflow }}-${{ github.ref }} @@ -44,46 +67,117 @@ jobs: --python-platform x86_64-manylinux_2_28 \ --generate-hashes \ --emit-index-url \ - --exclude-newer 2026-07-15T00:00:00Z \ + --exclude-newer 2026-07-20T00:00:00Z \ --override images/gpu/vllm-overrides.txt \ + --exclude images/gpu/vllm-exclusions.txt \ --output-file images/gpu/vllm-requirements.txt \ images/gpu/vllm.in cmp /tmp/vllm-requirements.expected images/gpu/vllm-requirements.txt - grep --fixed-strings 'vllm-0.10.1.1%2Bcu118' images/gpu/vllm-requirements.txt - grep --fixed-strings 'certifi-2026.6.17-py3-none-any.whl' images/gpu/vllm-requirements.txt - grep --fixed-strings 'ray==2.55.0' images/gpu/vllm-requirements.txt - grep --fixed-strings 'setuptools==78.1.1' images/gpu/vllm-requirements.txt - grep --fixed-strings 'torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt - grep --fixed-strings 'torchaudio-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt - grep --fixed-strings 'torchvision-0.22.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt - grep --fixed-strings 'xformers-0.0.31-cp39-abi3-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt - grep --fixed-strings 'transformers==5.5.0' images/gpu/vllm-requirements.txt - grep --fixed-strings 'requests==2.34.2' images/gpu/vllm-requirements.txt - grep --fixed-strings 'urllib3==2.7.0' images/gpu/vllm-requirements.txt - grep --fixed-strings 'xgrammar==0.1.32' images/gpu/vllm-requirements.txt - grep --fixed-strings 'xgrammar==0.1.32' images/gpu/vllm-overrides.txt - grep --fixed-strings 'idna==3.18' images/gpu/vllm-requirements.txt - grep --fixed-strings 'idna==3.18' images/gpu/vllm-overrides.txt - grep --fixed-strings 'nvidia-cuda-runtime-cu11==11.8.89' images/gpu/vllm-requirements.txt - if grep --fixed-strings --quiet -- '--extra-index-url https://download.pytorch.org/whl/cu118' images/gpu/vllm-requirements.txt; then - echo 'CUDA artifacts must use immutable wheel URLs instead of exposing the CUDA index to general dependency resolution.' >&2 - exit 1 - fi - if grep --fixed-strings --quiet 'nvidia-cuda-runtime-cu13' images/gpu/vllm-requirements.txt; then - echo 'CUDA 13 is incompatible with the NVIDIA driver currently available on Terra.' >&2 - exit 1 - fi - if grep --fixed-strings --quiet 'certifi==2022.12.7' images/gpu/vllm-requirements.txt; then - echo 'The GPU lock must not use the obsolete CA bundle mirrored by the CUDA index.' >&2 - exit 1 - fi + grep --fixed-strings 'vllm-0.25.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt + grep --fixed-strings 'torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt + grep --fixed-strings 'torchaudio-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt + grep --fixed-strings 'torchvision-0.26.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl' images/gpu/vllm-requirements.txt + grep --fixed-strings 'flashinfer-python==0.6.13' images/gpu/vllm-requirements.txt + grep --fixed-strings 'nvidia-cuda-runtime-cu12==12.9.79' images/gpu/vllm-requirements.txt + grep --fixed-strings 'xgrammar==0.2.3' images/gpu/vllm-requirements.txt grep --fixed-strings -- '--hash=sha256:' images/gpu/vllm-requirements.txt + python3 deploy/verify_gpu_compatibility.py + python3 -m py_compile \ + images/gpu/qualification_config.py \ + images/gpu/verify_vllm.py \ + images/generic/scripts/verify_coding_agent_e2e.py \ + deploy/verify_gpu_compatibility.py bash -n images/gpu/start_vllm.sh bash -n images/gpu/heartwood-vllm bash -n images/gpu/install_runtime.sh + bash -n images/gpu/coding_agent_e2e.sh + bash -n images/generic/scripts/coding_agent_e2e.sh test -x images/gpu/start_vllm.sh test -x images/gpu/heartwood-vllm test -x images/gpu/install_runtime.sh + test -x images/gpu/coding_agent_e2e.sh + test -x images/generic/scripts/coding_agent_e2e.sh + + gpu-qualification: + name: GPU Coding-Agent Qualification + if: github.event_name == 'workflow_dispatch' && inputs.run_gpu_qualification + needs: contract + runs-on: ${{ fromJSON(inputs.qualification_runner) }} + timeout-minutes: 180 + environment: gpu-qualification + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + with: + driver: docker + - name: Build the locked GPU runtime + run: >- + docker buildx bake --file docker-bake.hcl --load + --set runtime-gpu-nvidia.platform=linux/amd64 + --set runtime-gpu-nvidia.tags=heartwood-gpu-qualification:local + runtime-gpu-nvidia + - name: Prepare isolated model and project storage + run: | + sudo mkdir -p "${RUNNER_TEMP}/heartwood-gpu-model" "${RUNNER_TEMP}/heartwood-gpu-project" + sudo chown -R 10001:10001 \ + "${RUNNER_TEMP}/heartwood-gpu-model" "${RUNNER_TEMP}/heartwood-gpu-project" + - name: Resolve qualification model + id: model + env: + CONFIGURATION_ID: ${{ inputs.configuration_id }} + run: | + payload="$(python3 images/gpu/qualification_config.py "${CONFIGURATION_ID}")" + echo "snapshot=$(jq -r '.configuration.model_snapshot' <<<"${payload}")" >> "${GITHUB_OUTPUT}" + echo "platform=$(jq -r '.configuration.platform' <<<"${payload}")" >> "${GITHUB_OUTPUT}" + - name: Download and verify the pinned model + env: + SNAPSHOT: ${{ steps.model.outputs.snapshot }} + run: >- + docker run --rm --gpus all + --volume "${RUNNER_TEMP}/heartwood-gpu-model:/workspace" + heartwood-gpu-qualification:local + heartwood models download "${SNAPSHOT}" + - name: Run GPU coding-agent qualification without network + env: + CONFIGURATION_ID: ${{ inputs.configuration_id }} + PLATFORM_ID: ${{ steps.model.outputs.platform }} + SNAPSHOT: ${{ steps.model.outputs.snapshot }} + run: >- + docker run --rm --gpus all --network none --read-only --shm-size=16g + --cap-drop ALL --security-opt no-new-privileges=true --pids-limit 2048 + --tmpfs /tmp:rw,nosuid,nodev,size=8g + --tmpfs /home/heartwood/.cache:rw,nosuid,nodev,size=2g,uid=10001,gid=10001,mode=0700 + --tmpfs /home/heartwood/.openhands:rw,nosuid,nodev,size=512m,uid=10001,gid=10001,mode=0700 + --volume "${RUNNER_TEMP}/heartwood-gpu-model/.heartwood/models/${SNAPSHOT}:/models/${SNAPSHOT}:ro" + --volume "${RUNNER_TEMP}/heartwood-gpu-project:/workspace" + --env HEARTWOOD_CAPABLE_PROJECT=/workspace + --env HEARTWOOD_GPU_CONFIGURATION_ID="${CONFIGURATION_ID}" + --env HEARTWOOD_LOCAL_MODEL_PATH="/models/${SNAPSHOT}" + --env HEARTWOOD_PLATFORM="${PLATFORM_ID}" + heartwood-gpu-qualification:local + bash /opt/heartwood/images/gpu/coding_agent_e2e.sh + - name: Prepare GPU qualification evidence + if: always() + run: >- + sudo chown -R "$(id -u):$(id -g)" + "${RUNNER_TEMP}/heartwood-gpu-project" + - name: Upload GPU qualification evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: gpu-qualification-${{ inputs.configuration_id }}-${{ github.sha }} + path: | + ${{ runner.temp }}/heartwood-gpu-project/heartwood-qualification.json + ${{ runner.temp }}/heartwood-gpu-project/heartwood-replay.txt + ${{ runner.temp }}/heartwood-gpu-project/heartwood-transcript.txt + if-no-files-found: warn + retention-days: 30 pull-request-build: name: Build GPU candidate ${{ matrix.target }} diff --git a/.github/workflows/main-validation.yml b/.github/workflows/main-validation.yml index fd13b9ac..d090085a 100644 --- a/.github/workflows/main-validation.yml +++ b/.github/workflows/main-validation.yml @@ -35,6 +35,8 @@ jobs: container-smoke: name: Container Smoke Tests uses: ./.github/workflows/container-smoke.yml + with: + run_capable_model: ${{ github.event_name != 'pull_request' }} gpu-containers: name: GPU Container Images uses: ./.github/workflows/gpu-container-image.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 20966354..f77f9483 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,7 +21,7 @@ concurrency: jobs: model-sources: - name: Verify Recommended Model Sources + name: Verify Catalog Model Sources runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/AGENTS.md b/AGENTS.md index a6b7b6d8..49ed4e19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,7 @@ When project direction changes, update the relevant architecture or operations p | Browser workflow | [documentation/use/browser.md](documentation/use/browser.md) | | Command reference | [documentation/reference/cli.md](documentation/reference/cli.md) | | Readiness states, stable diagnostics, and recovery steps | [documentation/reference/troubleshooting.md](documentation/reference/troubleshooting.md) | +| Qualified GPU runtime, model, and platform combinations | [documentation/reference/gpu-compatibility.md](documentation/reference/gpu-compatibility.md) | | Product boundaries and durable technical rationale | [documentation/architecture/index.md](documentation/architecture/index.md) | | Project, gateway, adapter, interface, and data-flow architecture | [documentation/architecture/system.md](documentation/architecture/system.md) | | Security and controlled-data responsibilities | [documentation/operate/security.md](documentation/operate/security.md) | diff --git a/deploy/carina/bootstrap.sh b/deploy/carina/bootstrap.sh index 7a562610..6b18fe83 100755 --- a/deploy/carina/bootstrap.sh +++ b/deploy/carina/bootstrap.sh @@ -128,7 +128,6 @@ images/gpu/install_runtime.sh \ export PATH="${root}/bootstrap/bin:${PATH}" export LD_LIBRARY_PATH="${root}/bootstrap/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" -export VLLM_USE_FLASHINFER_SAMPLER=0 printf 'Heartwood: %s\n' "$("${root}/heartwood/bin/heartwood" --version)" HEARTWOOD_VLLM_ROOT="${root}/vllm" \ diff --git a/deploy/tests/native_installer_smoke.sh b/deploy/tests/native_installer_smoke.sh index f3dee188..eb82a195 100755 --- a/deploy/tests/native_installer_smoke.sh +++ b/deploy/tests/native_installer_smoke.sh @@ -115,15 +115,13 @@ venv) mkdir -p "${runtime}/bin" cat >"${runtime}/bin/python" <<'COMMAND' #!/usr/bin/env bash -if [[ "${1:-}" == */heartwood_vllm.py ]]; then - test "${2:-}" = "__heartwood_verify_runtime__" - grep --quiet 'GHSA-7rgv-gqhr-fxg3' "$1" - grep --quiet 'GHSA-65pc-fj4g-8rjx' "$1" - echo "Transformers synthetic integration and GPU security fixes verified" +if [[ "${1:-}" == */verify_vllm.py ]]; then + grep --quiet 'vllm_version' "$1" + grep --quiet 'cuda_13_qualified' "$1" + echo "Heartwood GPU runtime verified: synthetic CUDA 12.9 stack" exit 0 fi -echo "vLLM: synthetic" -echo "PyTorch: synthetic (CUDA 11.8)" +echo "0.25.1+cu129 2.11.0+cu129 12.9" COMMAND cat >"${runtime}/bin/vllm" <<'COMMAND' #!/usr/bin/env bash @@ -323,14 +321,16 @@ test -x "${carina_runtime}/heartwood/bin/heartwood" test -x "${carina_runtime}/vllm/bin/python" test -x "${carina_runtime}/vllm/bin/vllm" test -x "${carina_runtime}/vllm/bin/heartwood-vllm" -test -r "${carina_runtime}/vllm/bin/heartwood_vllm.py" -test -r "${carina_runtime}/vllm/bin/sitecustomize.py" +test -r "${carina_runtime}/vllm/bin/verify_vllm.py" +test -r "${carina_runtime}/vllm/bin/compatibility.toml" +test ! -e "${carina_runtime}/vllm/bin/heartwood_vllm.py" +test ! -e "${carina_runtime}/vllm/bin/sitecustomize.py" test -x "${carina_runtime}/vllm/bin/hf" test "$(file_mode "${carina_runtime}/vllm/bin/heartwood-vllm")" = "555" -test "$(file_mode "${carina_runtime}/vllm/bin/heartwood_vllm.py")" = "444" -test "$(file_mode "${carina_runtime}/vllm/bin/sitecustomize.py")" = "444" +test "$(file_mode "${carina_runtime}/vllm/bin/verify_vllm.py")" = "444" +test "$(file_mode "${carina_runtime}/vllm/bin/compatibility.toml")" = "444" "${carina_runtime}/vllm/bin/heartwood-vllm" __heartwood_verify_runtime__ | \ - grep --quiet 'GPU security fixes verified' + grep --quiet 'synthetic CUDA 12.9 stack' test -L "${carina_installation}/bin/hf" carina_current_target="$(readlink "${carina_installation}/current")" test "$(readlink "${carina_installation}/bin/hf")" = \ diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py new file mode 100644 index 00000000..7d7f2e3d --- /dev/null +++ b/deploy/verify_gpu_compatibility.py @@ -0,0 +1,245 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Verify that GPU runtime, model catalog, and qualification evidence agree.""" + +from __future__ import annotations + +import argparse +import re +import tomllib +from pathlib import Path +from typing import Any + +_SCHEMA = "heartwood.gpu-compatibility.v1" +_CATALOG_SCHEMA = "heartwood.model-snapshot-catalog.v2" +_QUALIFICATION_TEST = "heartwood.coding-agent-e2e.v1" +_CONFIGURATION_FIELDS = { + "configuration_id", + "status", + "platform", + "gpu_model", + "gpu_count", + "minimum_gpu_memory_bytes", + "model_snapshot", + "model_repository", + "model_revision", + "precision", + "context_window", + "tensor_parallel_size", + "tool_call_parser", + "vllm_version", + "pytorch_version", + "cuda_version", + "minimum_driver_version", + "qualification_test", + "startup_seconds_min", + "startup_seconds_max", +} +_FORBIDDEN_CUDA_13 = ( + "cuda-tile==", + "nvidia-cuda-crt==", + "nvidia-cuda-nvcc==", + "nvidia-cuda-runtime==", + "nvidia-cuda-tileiras==", + "nvidia-nvvm==", + "-cu13", +) + + +class CompatibilityError(ValueError): + """Raised when compatibility evidence is incomplete or contradictory.""" + + +def verify_repository(root: Path) -> None: + """Verify the repository's complete GPU compatibility contract.""" + matrix = _toml(root / "images/gpu/compatibility.toml") + catalog = _toml(root / "images/generic/local-runtime/snapshots.toml") + if matrix.get("schema_version") != _SCHEMA: + raise CompatibilityError("unsupported GPU compatibility matrix schema") + if catalog.get("schema_version") != _CATALOG_SCHEMA: + raise CompatibilityError("unsupported model snapshot catalog schema") + runtime = _mapping(matrix, "runtime") + _verify_runtime_lock(root, runtime) + snapshots = _mapping(catalog, "snapshots") + configurations = matrix.get("configurations") + if not isinstance(configurations, list) or not configurations: + raise CompatibilityError("GPU compatibility matrix has no configurations") + + seen_ids: set[str] = set() + covered_snapshots: set[str] = set() + qualified_platforms: dict[str, set[str]] = {} + for configuration in configurations: + if not isinstance(configuration, dict): + raise CompatibilityError("GPU compatibility configurations must be tables") + missing = sorted(_CONFIGURATION_FIELDS - configuration.keys()) + if missing: + raise CompatibilityError( + f"GPU compatibility configuration is missing fields: {', '.join(missing)}" + ) + configuration_id = _string(configuration, "configuration_id") + if configuration_id in seen_ids: + raise CompatibilityError(f"duplicate GPU configuration: {configuration_id}") + seen_ids.add(configuration_id) + status = _string(configuration, "status") + if status not in {"candidate", "qualified"}: + raise CompatibilityError(f"invalid GPU qualification status: {status}") + snapshot_id = _string(configuration, "model_snapshot") + snapshot = snapshots.get(snapshot_id) + if not isinstance(snapshot, dict): + raise CompatibilityError(f"unknown model snapshot in GPU matrix: {snapshot_id}") + covered_snapshots.add(snapshot_id) + _verify_configuration(configuration, snapshot, runtime) + if status == "qualified": + for field in ("validated_at", "validated_driver_version", "evidence"): + _string(configuration, field) + qualified_platforms.setdefault(snapshot_id, set()).add( + _string(configuration, "platform") + ) + + if covered_snapshots != set(snapshots): + missing = sorted(set(snapshots) - covered_snapshots) + raise CompatibilityError( + f"GPU catalog snapshots are absent from the matrix: {', '.join(missing)}" + ) + for snapshot_id, snapshot in snapshots.items(): + if not isinstance(snapshot, dict): + raise CompatibilityError(f"invalid model snapshot: {snapshot_id}") + qualified = snapshot.get("qualification") == "qualified" + platforms = set(_string_list(snapshot, "validated_platforms")) + if qualified != bool(qualified_platforms.get(snapshot_id)): + raise CompatibilityError( + f"model qualification and compatibility evidence disagree: {snapshot_id}" + ) + if platforms != qualified_platforms.get(snapshot_id, set()): + raise CompatibilityError( + f"validated platforms disagree with compatibility evidence: {snapshot_id}" + ) + if snapshot.get("recommended", False) and not qualified: + raise CompatibilityError( + f"only qualified catalog models may be recommended: {snapshot_id}" + ) + + +def _verify_runtime_lock(root: Path, runtime: dict[str, Any]) -> None: + lock = (root / "images/gpu/vllm-requirements.txt").read_text(encoding="utf-8") + expected = ( + f"vllm-{_base_version(_string(runtime, 'vllm_version'))}%2Bcu129", + f"torch-{_base_version(_string(runtime, 'pytorch_version'))}%2Bcu129", + f"torchaudio-{_base_version(_string(runtime, 'torchaudio_version'))}%2Bcu129", + f"torchvision-{_base_version(_string(runtime, 'torchvision_version'))}%2Bcu129", + ) + missing = [item for item in expected if item not in lock] + if missing: + raise CompatibilityError(f"GPU lock is missing runtime wheels: {', '.join(missing)}") + forbidden = [item for item in _FORBIDDEN_CUDA_13 if item in lock.casefold()] + if forbidden: + raise CompatibilityError( + f"GPU lock contains unqualified CUDA 13 dependencies: {', '.join(forbidden)}" + ) + if runtime.get("cuda_version") != "12.9" or runtime.get("cuda_13_qualified") is not False: + raise CompatibilityError("GPU runtime must remain on qualified CUDA 12.9") + + +def _verify_configuration( + configuration: dict[str, Any], + snapshot: dict[str, Any], + runtime: dict[str, Any], +) -> None: + matching_fields = { + "model_repository": "source_repository", + "model_revision": "source_revision", + "precision": "precision", + "context_window": "context_window", + "tensor_parallel_size": "tensor_parallel_size", + "tool_call_parser": "tool_call_parser", + "startup_seconds_min": "startup_seconds_min", + "startup_seconds_max": "startup_seconds_max", + } + for matrix_field, snapshot_field in matching_fields.items(): + if configuration.get(matrix_field) != snapshot.get(snapshot_field): + raise CompatibilityError( + f"GPU matrix field {matrix_field} disagrees with model snapshot" + ) + runtime_fields = { + "vllm_version": "vllm_version", + "pytorch_version": "pytorch_version", + "cuda_version": "cuda_version", + "minimum_driver_version": "minimum_driver_version", + } + for matrix_field, runtime_field in runtime_fields.items(): + if configuration.get(matrix_field) != runtime.get(runtime_field): + raise CompatibilityError( + f"GPU matrix field {matrix_field} disagrees with runtime contract" + ) + if configuration.get("qualification_test") != _QUALIFICATION_TEST: + raise CompatibilityError("GPU model uses an unsupported qualification test") + for field in ( + "gpu_count", + "minimum_gpu_memory_bytes", + "context_window", + "startup_seconds_min", + "startup_seconds_max", + ): + value = configuration.get(field) + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + raise CompatibilityError(f"GPU matrix field {field} must be positive") + if configuration["startup_seconds_min"] > configuration["startup_seconds_max"]: + raise CompatibilityError("GPU startup estimate is invalid") + if configuration["gpu_count"] < snapshot.get("minimum_gpu_count", 0): + raise CompatibilityError("GPU matrix does not satisfy the model GPU count") + if configuration["minimum_gpu_memory_bytes"] < snapshot.get("minimum_gpu_memory_bytes", 0): + raise CompatibilityError("GPU matrix does not satisfy model GPU memory") + revision = _string(configuration, "model_revision") + if re.fullmatch(r"[0-9a-f]{40}", revision) is None: + raise CompatibilityError("GPU model revision must be an immutable commit") + + +def _toml(path: Path) -> dict[str, Any]: + try: + with path.open("rb") as file: + return tomllib.load(file) + except (OSError, tomllib.TOMLDecodeError) as error: + raise CompatibilityError(f"unable to load {path}: {error}") from error + + +def _mapping(data: dict[str, Any], key: str) -> dict[str, Any]: + value = data.get(key) + if not isinstance(value, dict): + raise CompatibilityError(f"{key} must be a table") + return value + + +def _string(data: dict[str, Any], key: str) -> str: + value = data.get(key) + if not isinstance(value, str) or not value: + raise CompatibilityError(f"{key} must be a non-empty string") + return value + + +def _string_list(data: dict[str, Any], key: str) -> list[str]: + value = data.get(key, []) + if not isinstance(value, list) or any(not isinstance(item, str) for item in value): + raise CompatibilityError(f"{key} must be an array of strings") + return value + + +def _base_version(value: str) -> str: + return value.split("+", maxsplit=1)[0] + + +def main() -> int: + """Verify one repository checkout.""" + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, default=Path.cwd()) + args = parser.parse_args() + verify_repository(args.root.resolve()) + print("GPU compatibility matrix verified") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/deploy/verify_model_sources.py b/deploy/verify_model_sources.py index 7d7109ec..a2655b19 100644 --- a/deploy/verify_model_sources.py +++ b/deploy/verify_model_sources.py @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: MIT -"""Verify that recommended Hugging Face model revisions still resolve exactly.""" +"""Verify that catalog Hugging Face model revisions still resolve exactly.""" from __future__ import annotations @@ -26,7 +26,7 @@ class ModelSourceVerificationError(RuntimeError): - """Raised when a recommended model source cannot be verified.""" + """Raised when a catalog model source cannot be verified.""" class ModelSourceUnavailableError(RuntimeError): @@ -35,7 +35,7 @@ class ModelSourceUnavailableError(RuntimeError): @dataclass(frozen=True) class ModelSource: - """One immutable recommended model source.""" + """One immutable catalog model source.""" model_id: str repository: str @@ -53,7 +53,7 @@ def api_url(self) -> str: def load_model_sources(source_root: Path) -> tuple[ModelSource, ...]: - """Load and validate every recommended model pin from the catalog.""" + """Load and validate every model pin from the release catalog.""" catalog_path = source_root / _SNAPSHOT_CATALOG with catalog_path.open("rb") as file: catalog = tomllib.load(file) diff --git a/documentation/architecture/testing.md b/documentation/architecture/testing.md index 1ce9dc72..7610a389 100644 --- a/documentation/architecture/testing.md +++ b/documentation/architecture/testing.md @@ -22,8 +22,17 @@ No single layer establishes every property of a deployment. | Platform-derived CI | Terra Jupyter inheritance, prefixed internal gateway routing, persistence, image media type, CI-only model rejection as an agent profile, and separate real inference | | Live synthetic validation | Exact published artifact in Terra or Carina without protected data | -GPU image CI verifies the locked CUDA-enabled runtime, compatibility guards, launcher, and absence of bundled model weights on standard runners. It does not establish successful GPU initialization or model inference without GPU hardware. Native packaging CI uses deterministic dependency-tool substitutes to verify failure paths and reproducibility, then installs the release archive in an empty Ubuntu 24.04 AMD64 container and runs the real CPU inference and browser paths. Actual GPU model load, inference, and Carina dependency resolution require live synthetic validation in the target environment. -Live platform validation supplements the automated release evidence and should be recorded before a deployment is promoted for operational use; it is not an automated release gate because CI cannot provision institutional workspaces. +GPU image CI verifies the fully hashed CUDA 12.9 environment, exact vLLM and PyTorch versions, compatibility guards, available tool parsers, launcher, and absence of bundled model weights on standard runners. +An optional protected self-hosted GPU job runs the same model qualification used on managed platforms when an eligible runner is configured. +Without GPU hardware, CI does not claim successful CUDA initialization or GPU model loading. + +The shared coding-agent acceptance test performs direct model inference and then drives the real Heartwood gateway and OpenHands adapter through a structured terminal proposal, grouped approval, synthetic file modification, independent file verification, fresh-process replay, and hash-chain-verified audit export. +It emits a machine-readable qualification record containing the exact runtime, model revision, GPU, driver, context, tensor parallelism, and parser. +The CPU capable-model job and GPU qualification wrapper use this same acceptance contract instead of maintaining separate agent scenarios. + +Native packaging CI uses deterministic dependency-tool substitutes to verify failure paths and reproducibility, then installs the release archive in an empty Ubuntu 24.04 AMD64 container and runs the real CPU inference and browser paths. +Actual Terra and Carina qualification still requires the exact published artifact and synthetic task on those platforms because public CI cannot provision their managed workspaces. +That qualification promotes one precise row in the [GPU compatibility matrix](../reference/gpu-compatibility.md); it does not qualify other drivers, model revisions, precisions, parsers, context sizes, or tensor-parallel layouts. ## Synthetic Data Rule diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 560c79dc..1fe7408f 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -6,26 +6,57 @@ SPDX-License-Identifier: MIT # Choose a Heartwood-Managed Model -Heartwood-managed inference runs on the same compute environment as Heartwood rather than sending requests to a separate model provider. -It requires compatible model files, sufficient storage and memory, and an agent-capable chat format. -Heartwood supports a small release-pinned recommendation set and best-effort planning for other public Hugging Face repositories. +Heartwood-managed inference keeps model requests in the environment where Heartwood is running. +The model files are downloaded separately into the current project's `.heartwood/models/` directory; Heartwood images and installers do not contain model weights. -## Recommended Models +Start the guided selection from either interface: -Choose **Run with Heartwood** during setup to see the current recommendations. -Each recommendation includes the canonical Hugging Face repository, immutable revision, artifact format, license metadata, size, context capacity, and minimum and recommended resources. -Heartwood shows the expected download size before any transfer begins. +- run `heartwood` in the terminal and choose **Run with Heartwood**; or +- open **Models** in the Heartwood browser interface. -Recommendations are an onboarding aid, not a closed allowlist and not a guarantee of scientific suitability. -They are selected for pinned runtime compatibility, immutable source metadata, and conservative resource planning. -Release CI validates those contracts without treating a recommendation as evidence of scientific quality or a completed live GPU evaluation. +Both interfaces read the same catalog and project state. +They show the download size, resource guidance, license, and immutable source revision before asking for confirmation. -List them from the terminal: +## Capability Tiers + +Heartwood uses three simple tiers. +The tier describes the intended agent workload, not scientific quality. + +| Tier | Intended Use | +|---|---| +| **Standard** | Bounded edits, focused analysis scripts, and first-time use | +| **Powerful** | Larger repositories, multi-step coding tasks, and longer sessions | +| **Maximum capability** | Broad multi-file work on substantial multi-GPU compute | + +Within each tier, Heartwood automatically recommends only configurations that fit the detected environment and have completed the full coding-agent qualification. +Models still under evaluation are labeled **Evaluation candidate** and are never selected automatically. + +List the complete catalog from the terminal: ```bash heartwood models managed ``` +Technical fields such as precision, parser, context, tensor parallelism, and pinned revision remain available in the detailed model view and the [GPU compatibility matrix](../reference/gpu-compatibility.md). + +## Resource Guide + +Download and startup values below are planning estimates. +The selection screen uses the release catalog as its authoritative source and reports the exact values before making changes. + +| Tier | Model Configuration | Download | GPU Memory | Recommended RAM | Recommended Free Disk | Default Context | Estimated First Start | +|---|---|---:|---:|---:|---:|---:|---:| +| Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | +| Standard candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 32,768 | 2-8 minutes | +| Powerful candidate | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | +| Powerful candidate | Qwen3 Coder 30B BF16 | 56.88 GiB | 2 x 48 GB | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | +| Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | +| Maximum alternative candidate | GPT-OSS 120B MXFP4 | 60.79 GiB | 2 x 48 GB | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | + +Model weights are only part of the memory requirement. +The runtime also needs space for temporary downloads, key/value cache, request handling, and the project itself. +Heartwood therefore uses conservative headroom and may choose a smaller context than the model's advertised maximum. + ## Other Hugging Face Models Choose **Other Hugging Face model** or enter an `owner/model` identifier: @@ -35,35 +66,32 @@ heartwood models inspect unsloth/Qwen2.5-Coder-7B-Instruct-GGUF heartwood models download unsloth/Qwen2.5-Coder-7B-Instruct-GGUF ``` -In the browser, expand **Version options** to request a tag, branch, or commit. In the terminal, pass `--revision REVISION` to `models inspect` or `models download`. Heartwood records the immutable revision resolved by Hugging Face before download. +Heartwood queries Hugging Face, resolves the requested tag or branch to an immutable commit, and selects a supported artifact. +It prefers a balanced single-file GGUF for the portable llama.cpp CPU runtime or a standard safetensors snapshot for the NVIDIA vLLM runtime. +No download begins until the plan is displayed and approved. -Heartwood asks Hugging Face for repository metadata, resolves an immutable revision, and looks for a supported candidate. -It prefers a balanced single-file GGUF for the portable llama.cpp CPU runtime or a safetensors snapshot for vLLM when an NVIDIA runtime is available. -The example above provides a GGUF candidate for the standard CPU image; a repository that contains only safetensors requires a GPU deployment with vLLM. +In the browser, expand **Advanced options** to request a particular tag, branch, or commit. +In the terminal, pass `--revision REVISION` to `models inspect` or `models download`. -The current planner rejects repositories that require executable remote model code, unsupported weight layouts, missing architecture metadata, unresolved revisions, or an unsupported context range. -When a compatible-looking repository cannot be planned, the error explains the reason and links to the [Heartwood issue form](https://github.com/SchmiedmayerLab/heartwood/issues/new/choose). -The message states that the model is not yet supported rather than suggesting an unsafe manual workaround. +The planner stops with a **not yet supported** message when a repository requires executable remote code, has an unsupported weight layout or architecture, lacks a resolvable revision, or cannot fit the available runtime. +Report a compatible-looking rejection through the [Heartwood issue form](https://github.com/SchmiedmayerLab/heartwood/issues/new/choose) rather than bypassing the check. -## What to Evaluate +## Before You Approve a Download -Before downloading, review: +Review these items in the displayed plan: -- **Tool use:** the model needs reliable structured tool calling, not only code completion. -- **License:** confirm that the model and weights are permitted for the intended work. -- **Provenance:** retain the repository and immutable revision with the managed artifact. -- **Format:** use one GGUF file for llama.cpp or a standard safetensors snapshot for vLLM. -- **Disk:** allow space for the model, download staging, runtime files, and project outputs. -- **Memory:** model weights, runtime overhead, and the key/value cache must fit in RAM or GPU memory. -- **Context:** larger windows consume substantially more memory and do not automatically improve task quality. -- **Data policy:** in-environment execution still depends on the security of the compute, storage, and surrounding platform. +- **Tool use:** a coding agent needs reliable structured tool calls, not only code completion. +- **License and provenance:** confirm the license and retain the repository plus immutable revision. +- **Compute:** check GPU count, GPU memory, RAM, disk, and expected allocation cost. +- **Context:** larger windows consume substantially more memory and can increase latency. +- **Data policy:** running in the same environment does not by itself approve the model or platform for controlled data. -Parameter count alone does not predict coding-agent quality or memory use. -Quantization, architecture, context window, concurrency, and runtime all matter. +Parameter count alone does not predict agent quality or resource use. +Architecture, quantization, context, concurrency, runtime, and tool parser all matter. -## Import an Existing Model +## Import Existing Model Files -When model files were transferred through an approved offline process, import them into the project: +For model files transferred through an approved process: ```bash heartwood models import /approved/path/model.gguf \ @@ -72,5 +100,6 @@ heartwood models import /approved/path/model.gguf \ --license apache-2.0 ``` -Heartwood accepts a valid GGUF file or a standard vLLM safetensors directory, rejects symbolic links and executable Python, records provenance, copies the artifact atomically into `.heartwood/models/`, and selects it. -The source path must be visible to the Heartwood process; the browser uses the same server-side import and does not upload multi-gigabyte model files through the page. +Heartwood accepts a valid GGUF file or standard vLLM safetensors directory, rejects symbolic links and executable Python, records provenance, and copies the artifact atomically into `.heartwood/models/`. +The path must be visible to the Heartwood process. +The browser uses this same server-side import and does not upload multi-gigabyte model files through the page. diff --git a/documentation/models/run-with-heartwood.md b/documentation/models/run-with-heartwood.md index 8193826b..2b181386 100644 --- a/documentation/models/run-with-heartwood.md +++ b/documentation/models/run-with-heartwood.md @@ -33,6 +33,7 @@ The restarted browser process verifies the stored model, starts its local server Heartwood reports model-specific minimum and recommended guidance before download. The estimates reserve space for runtime overhead and context, but no static estimate can account for every model architecture, driver, concurrent workload, or platform limit. +For GPU configurations, Heartwood also checks the catalog GPU count, per-device memory, driver, precision, tool parser, tensor-parallel layout, and qualification status against the [GPU compatibility matrix](../reference/gpu-compatibility.md). ## Context Window @@ -55,11 +56,12 @@ Operators can inspect or control runtime allocation separately: ```bash heartwood runtime start --dry-run -heartwood runtime start --partition dev --time 01:00:00 +heartwood runtime start --task-profile powerful --partition dev --time 01:00:00 ``` On Carina, Heartwood prints the complete Slurm request and asks before allocating a GPU. On provisioned Terra compute, it uses the attached resources without submitting a scheduler request. +Model download and scheduler allocation require separate confirmation. ## Stop the Runtime diff --git a/documentation/operate/index.md b/documentation/operate/index.md index dacce886..4823bb16 100644 --- a/documentation/operate/index.md +++ b/documentation/operate/index.md @@ -27,7 +27,7 @@ The browser service must remain on loopback or behind an authenticated platform ## Select an Artifact - Use the multi-platform standard image for generic AMD64/ARM64 deployments. -- Use the NVIDIA image for validated AMD64 vLLM deployments. +- Use the NVIDIA image for AMD64 vLLM deployments and verify the selected configuration against the [GPU compatibility matrix](../reference/gpu-compatibility.md). - Use a Terra-specific single-platform image for Terra Leonardo. - Use the release native installer where containers are not the platform's normal execution mechanism. diff --git a/documentation/platforms/carina.md b/documentation/platforms/carina.md index 1189c39c..3ab61723 100644 --- a/documentation/platforms/carina.md +++ b/documentation/platforms/carina.md @@ -75,13 +75,34 @@ Those current Stanford terms, not Heartwood platform detection, determine data e ### Heartwood-Managed GPU Model -Choose **Run with Heartwood**, select a recommended model or another public Hugging Face repository, and review the download and resource plan. +Choose **Run with Heartwood**, select a catalog model or another public Hugging Face repository, and review the download and resource plan. Model files are stored under the project's `.heartwood/models/`, not the installation directory. -When you start Heartwood with a selected Heartwood-managed model, it discovers GPU-capable Slurm partitions, prints the complete `srun` request, and asks before allocating compute. -The current Carina partitions are `dev`, `normal`, and `long`; all can provide GPUs, with different time limits described in the official [Slurm guide](https://docs.carina.stanford.edu/slurm-carina). +When you start Heartwood with a selected Heartwood-managed model, it inspects the GPU-capable Slurm partitions, available L40S count, GPU memory, CPU and RAM limits, existing model cache, and requested capability tier. +It then prints the strongest compatible qualified model, expected download and startup range, and complete `srun` request. +Heartwood asks separately before downloading weights and before allocating GPUs; it does neither silently. -For a short interactive session, normal `heartwood` startup automatically selects the `dev` partition when it is available: +Carina 2.0 compute nodes provide eight NVIDIA L40S GPUs and 1.5 TB RAM, but the requested allocation should include only the resources the task needs. +The current partitions are `dev`, `normal`, and `long`; all can provide GPUs, with different time limits described in the official [Slurm guide](https://docs.carina.stanford.edu/slurm-carina) and hardware described in [Carina Facts](https://docs.carina.stanford.edu/carina-facts). + +### Choose Carina Resources + +The following configurations are release-pinned evaluation targets. +Heartwood labels one **Recommended** only after its complete tool, approval, edit, replay, and audit qualification passes on Carina. + +| Tier | Model Configuration | GPUs | Recommended RAM | Free Project Storage | Default Context | Estimated First Start | +|---|---|---:|---:|---:|---:|---:| +| Standard fallback | Qwen2.5 Coder 7B AWQ | 1 x L40S | 32 GiB | 16 GiB | 32,768 | 2-8 minutes | +| Powerful | Qwen3 Coder 30B FP8 | 1 x L40S | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | +| Powerful, higher precision | Qwen3 Coder 30B BF16 | 2 x L40S | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | +| Maximum capability | Qwen3 Coder Next FP8 | 4 x L40S | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | +| Maximum alternative | GPT-OSS 120B MXFP4 | 2 x L40S | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | + +Download sizes range from about 5.2 GiB for the Standard GPU model to 74.9 GiB for the largest candidate. +See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for complete sizes and [GPU Compatibility](../reference/gpu-compatibility.md) for exact revisions and runtime settings. + +For a short interactive session, normal `heartwood` startup selects Slurm's default compatible GPU partition. +On the current Carina configuration this is typically `dev` when `sinfo` marks it with `*`: ```bash heartwood @@ -93,7 +114,18 @@ If no default GPU partition can be selected, inspect the plan and specify one th heartwood runtime start --partition dev --time 01:00:00 ``` -Heartwood prepares the compatible runtime libraries, scopes model caches to the project, waits up to ten minutes by default, and reports the current stage and elapsed startup time every 15 seconds. +Preview a particular capability tier without downloading or allocating: + +```bash +heartwood runtime start --task-profile powerful --dry-run +``` + +`auto` prefers **Powerful** on Carina and falls back to the strongest qualified configuration that fits one available allocation. +Use `--task-profile standard`, `powerful`, or `maximum` when the task has a known resource envelope. +The `--gpus` option is an advanced constraint and must match a catalog configuration that was qualified at that tensor-parallel size. + +Heartwood scopes model caches to the project, waits up to ten minutes by default, and reports the current stage and elapsed startup time every 15 seconds. +For scripted deployment, `--yes-download` and `--yes-request-allocation` are separate explicit approvals; normal interactive use should retain both prompts. ## Review, Exit, and Return @@ -107,6 +139,8 @@ The interactive Slurm allocation and supervised vLLM process end with the Heartw - If a command disappears or is killed on a login node, stop and use Slurm for the compute work; Carina documents strict login-node limits. - If a partition is unavailable, run `sinfo --noheader --format='%P|%G|%a'` and choose one of the GPU-capable partitions Heartwood reports. +- If the requested model does not fit the available GPU count or memory, choose the strongest compatible lower tier instead of changing tensor parallelism or precision manually. +- If startup reports a driver or CUDA incompatibility, retain the released CUDA 12.9 environment and report the detected driver; do not install CUDA 13 into the Heartwood runtime. - If model startup fails, inspect `.heartwood/logs/` and the `HW-COMPUTE-*` checks from `heartwood doctor` without sharing project content or secrets. - If an interactive allocation disconnects, the process ends with the terminal; Carina recommends `tmux`, `screen`, or a batch job for work that must survive a connection loss. - Use Carina's [troubleshooting guide](https://docs.carina.stanford.edu/troubleshooting) for platform and Slurm failures. diff --git a/documentation/platforms/containers.md b/documentation/platforms/containers.md index 6ae10781..eb04ee5c 100644 --- a/documentation/platforms/containers.md +++ b/documentation/platforms/containers.md @@ -68,7 +68,8 @@ docker run --rm -it \ ``` Choose **Run with Heartwood** and a vLLM-compatible model. -Heartwood inspects available GPU memory, selects a conservative context tier, and starts the included vLLM runtime after the model is ready. +Heartwood inspects the GPU model, memory, driver, and catalog qualification, selects a conservative context tier, and starts the isolated CUDA 12.9 vLLM runtime after the model is ready. +Review the [GPU compatibility matrix](../reference/gpu-compatibility.md) before using a catalog candidate on hardware that has not completed qualification. ## Image Tags diff --git a/documentation/platforms/index.md b/documentation/platforms/index.md index 4a08ab59..c1106e59 100644 --- a/documentation/platforms/index.md +++ b/documentation/platforms/index.md @@ -20,7 +20,8 @@ The platform determines installation, durable storage, available interfaces, cre | Stanford Carina | Native release installer | Yes | No | No | NVIDIA GPU inference through requested compute | The generic standard image is a multi-platform Linux image for AMD64 and ARM64. -GPU images are AMD64 because the pinned NVIDIA/vLLM stack is validated there. +GPU images are AMD64 because the pinned NVIDIA/vLLM stack is built for that architecture. +Consult the [GPU compatibility matrix](../reference/gpu-compatibility.md) for configuration-specific qualification status. Terra images are AMD64 single-platform Docker manifests because Terra image auto-detection requires that shape. ## Choose the Simplest Route diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index c0a5d885..92b3fa7b 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -34,15 +34,20 @@ Enter one image: |---|---|---| | Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | | Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 52 GB RAM, 75 GB persistent disk | -| Heartwood-managed NVIDIA GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 8 CPUs, 52 GB RAM, one T4-class GPU or better, 100 GB persistent disk | +| Heartwood-managed NVIDIA GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 8 CPUs, 32 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. CPU inference is portable but can be too slow for an interactive coding workflow. These are starting points rather than universal requirements. +The GPU path is designed around a T4 and the release-pinned Qwen2.5 Coder 7B AWQ configuration. +Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. +It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. + Heartwood inspects model size and available memory before launch, chooses a context capacity with response headroom, and warns when the selected compute is below its conservative estimate. -Larger GPU memory can enable context capacities above 32K when the model supports them. +Larger GPU memory can enable context capacities above 32K when the model supports them, but increasing context also increases GPU-memory use and response latency. +See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for download and resource estimates and [GPU Compatibility](../reference/gpu-compatibility.md) for exact runtime combinations. Retain the persistent disk when replacing compute and copy valuable results to workspace storage. See [Starting and Customizing Your Jupyter App](https://support.terra.bio/hc/en-us/articles/5075814468379-Starting-and-customizing-your-Jupyter-app). @@ -93,11 +98,14 @@ The first-use flow confirms the project and asks where the model runs. - Choose OpenAI, Anthropic, or **Other compatible service** only when that endpoint is authorized for the intended data. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. -For managed GPU inference, start with the recommended Qwen2.5 7B AWQ model shown by Heartwood. -You can instead enter another public Hugging Face repository; Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. +For managed GPU inference, choose the **Standard** Qwen2.5 Coder 7B AWQ configuration when Heartwood labels it **Recommended** for the detected T4. +If it is labeled **Evaluation candidate**, use it only for a synthetic qualification task; Heartwood does not automatically recommend unqualified configurations. +You can instead choose **Other Hugging Face model** and enter another public repository. +Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. +The pinned AWQ snapshot downloads about 5.2 GiB; allow at least 16 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. Model download progress appears in the terminal and files persist under `.heartwood/models/`. -The first inference startup can take several minutes while vLLM loads the model and prepares GPU memory. +The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. Heartwood reports the active stage, elapsed time, selected context capacity, and memory assessment while you wait. Use `heartwood --plain` when the full-screen terminal is not rendered correctly. @@ -152,6 +160,8 @@ Deleting the persistent disk removes `.heartwood/` and project files stored only - If `import heartwood` fails in a notebook, switch the notebook kernel to **Python 3 (Heartwood)** and restart the kernel. - If a model download stops, rerun Heartwood from the same project; verified files in `.heartwood/models/` are reused. - If model startup is slow or fails, compare the printed model plan with attached RAM, GPU memory, and persistent-disk space, then inspect `.heartwood/logs/local-model.log` from the same project. +- If Heartwood reports an unsupported P4, P100, or V100, delete and recreate the Cloud Environment with a T4 while retaining the persistent disk; do not replace the released vLLM or PyTorch packages in place. +- If the GPU is not detected, confirm that the GPU image and GPU were selected together, then run `nvidia-smi` and `heartwood doctor` from the project terminal. - If Terra rejects the image during auto-detection, confirm that the tag ends in `-terra` or `-terra-gpu-nvidia`; these tags use the single-platform manifest format required by Terra's Leonardo service. - If `heartwood --version` does not match the requested image tag, replace the Cloud Environment while retaining the persistent disk; resuming an existing environment does not update its image. - Run `heartwood doctor` for stable `HW-TERRA-*` recovery guidance. diff --git a/documentation/reference/cli.md b/documentation/reference/cli.md index 90220315..7fed89d0 100644 --- a/documentation/reference/cli.md +++ b/documentation/reference/cli.md @@ -49,7 +49,7 @@ Provider tokens are not accepted as setup command arguments. | `heartwood models connect CONNECTION MODEL` | Select a discovered model | | `heartwood models validate [PROFILE]` | Evaluate credential and route policy for a profile | | `heartwood models forget CONNECTION` | Remove a saved provider credential from the system credential store | -| `heartwood models managed` | Show recommended and user-selected models Heartwood can run | +| `heartwood models managed` | Show qualified recommendations, evaluation candidates, and user-selected models Heartwood can run | | `heartwood models inspect OWNER/MODEL` | Inspect a public Hugging Face repository without downloading weights | | `heartwood models download MODEL` | Download and select a recommendation or `OWNER/MODEL` repository | | `heartwood models import PATH ...` | Copy and select an existing GGUF or vLLM snapshot with provenance | @@ -90,6 +90,11 @@ Interactive users should use the visible controls or `/allow` and `/reject` with These commands support deployment automation and diagnostics. Researchers should normally use `heartwood` with `--interface` when needed. +`heartwood runtime start --dry-run` prints the model, resource, and scheduler plan without downloading, starting inference, or requesting compute. +On a scheduler-managed GPU platform, `--task-profile standard|powerful|maximum` constrains automatic recommendation to the requested capability tier. +`--gpus` is an advanced constraint and must match a qualified catalog tensor-parallel configuration. +Unattended operation requires separate `--yes-download` and `--yes-request-allocation` approvals; neither is implied by `--non-interactive` or another confirmation flag. + ## Exit Status `0` indicates that the requested command completed successfully or an interactive cancellation changed no files. diff --git a/documentation/reference/glossary.md b/documentation/reference/glossary.md index c8637eb5..3ef65312 100644 --- a/documentation/reference/glossary.md +++ b/documentation/reference/glossary.md @@ -27,6 +27,9 @@ SPDX-License-Identifier: MIT **Context window** : The token budget available to a model for instructions, conversation history, project content, tool results, and output. +**CUDA** +: NVIDIA's software platform and application binary interface for GPU computing. + **Credential binding** : A non-secret identifier that tells Heartwood where an authorized process or platform supplies a provider credential. @@ -60,6 +63,9 @@ SPDX-License-Identifier: MIT **Project state** : Private non-secret configuration, sessions, models, Skills, logs, caches, runtime files, and audit artifacts under `.heartwood/`. +**Quantization** +: A lower-precision model representation, such as AWQ, GPTQ, or FP8, used to reduce memory and storage requirements. + **Research Skill** : A versioned instruction package with declared tools, metadata, and workflow guidance available to the OpenHands agent. @@ -69,5 +75,8 @@ SPDX-License-Identifier: MIT **Terra** : A cloud platform for biomedical research workspaces, data, workflows, and interactive Jupyter compute. +**Tensor parallelism** +: A runtime layout that divides one model across a fixed number of GPUs. + **vLLM** : A GPU-oriented inference server used by supported Heartwood NVIDIA deployments. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md new file mode 100644 index 00000000..ed14d209 --- /dev/null +++ b/documentation/reference/gpu-compatibility.md @@ -0,0 +1,69 @@ + + +# GPU Compatibility + +Heartwood keeps the NVIDIA inference runtime and its model configurations in a release-owned compatibility matrix. +The matrix separates configurations that have completed the full coding-agent qualification from candidates that are still under evaluation. +Heartwood exposes candidates in advanced model selection, but it does not label or automatically select them as recommendations. + +## Runtime + +| Component | Locked Version | +|---|---| +| Python | 3.12 | +| vLLM | `0.25.1+cu129` | +| PyTorch | `2.11.0+cu129` | +| TorchAudio | `2.11.0+cu129` | +| TorchVision | `0.26.0+cu129` | +| CUDA application binary interface | 12.9 | +| Minimum NVIDIA Linux driver | `525.60.13` | + +The vLLM environment is installed separately from Heartwood's application environment and resolved from a fully hashed lock. +Its dependency exclusions prevent a package resolver from replacing the CUDA 12.9 stack with CUDA 13 artifacts. +CUDA 13 is not qualified for Heartwood. + +The minimum driver is CUDA's compatibility floor, not evidence that every driver at or above that version has completed a Heartwood qualification. +The exact driver used in a live qualification is recorded with its machine-readable result. + +## Model Configurations + +| Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Tensor Parallelism | Tool Parser | Status | +|---|---|---|---|---|---:|---:|---|---| +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | 1 | `hermes` | Candidate | +| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | 1 | `hermes` | Candidate | +| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | 1 | `qwen3_coder` | Candidate | +| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | 2 | `qwen3_coder` | Candidate | +| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | 4 | `qwen3_coder` | Candidate | +| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | 2 | `openai` | Candidate | + +All listed model repositories declare the Apache-2.0 license at the pinned revision. +Confirm that a model's license and intended use remain suitable for the project before downloading it. + +## Qualification Requirement + +A configuration becomes **qualified** only after the exact model revision and locked runtime complete one bounded Heartwood task on the named platform. +The acceptance test must establish all of the following: + +1. the model loads and returns a direct inference response; +2. OpenHands receives a native structured tool proposal; +3. Heartwood presents the complete action set for approval; +4. approval executes the proposed operation and modifies only the synthetic project; +5. an independent check verifies the exact file result; +6. a fresh process replays the session; and +7. audit export validates event coverage, hash-chain integrity, and content scrubbing. + +The result records the GPU model, count, memory, driver, runtime versions, model revision, context size, tensor parallelism, and parser. +A candidate remains visible for evaluation but cannot become an automatic recommendation until that result passes. + +## Unsupported Hardware + +The CUDA 12.9 runtime requires an NVIDIA GPU with compute capability 7.5 or newer. +Heartwood therefore stops before model startup on P4, P100, and V100 GPUs. +Choose a T4 or newer GPU, use a hosted model route, or select the portable CPU runtime instead. + +Use `heartwood doctor` for the environment summary and `heartwood runtime start --dry-run` for the complete model and allocation plan. +Do not bypass a compatibility failure by changing vLLM, PyTorch, CUDA, the model revision, tensor parallelism, or parser inside a released environment; that creates a new unqualified configuration. diff --git a/documentation/reference/troubleshooting.md b/documentation/reference/troubleshooting.md index 67cf2676..9cfd4234 100644 --- a/documentation/reference/troubleshooting.md +++ b/documentation/reference/troubleshooting.md @@ -123,6 +123,8 @@ Keep durable project and model state in approved project storage. Choose a hosted model, a GGUF CPU model, or GPU-enabled compute. For containers, verify the NVIDIA Container Toolkit and `--gpus all`; for Terra, select the GPU image and attach an NVIDIA GPU. +The CUDA 12.9 runtime requires compute capability 7.5 or newer, so P4, P100, and V100 GPUs are rejected before model startup. +Compare the detected environment with [GPU Compatibility](gpu-compatibility.md). ## Terra @@ -139,7 +141,7 @@ Do not use `/home/jupyter` itself as the agent boundary. ### `HW-TERRA-002` — Terra GPU Support Is Unavailable Use the `-terra-gpu-nvidia` image and attach supported GPU compute, or choose hosted inference. -Recreate or update the Cloud Environment while retaining the persistent disk. +Delete and recreate the Cloud Environment with a T4 while retaining the persistent disk; Terra does not apply a changed image or GPU selection to an existing environment. ## Environment Fallback diff --git a/images/Dockerfile b/images/Dockerfile index 7f0680bc..cb0f7be4 100644 --- a/images/Dockerfile +++ b/images/Dockerfile @@ -85,10 +85,19 @@ RUN set -eux; \ elif ! id "${HEARTWOOD_RUNTIME_USER}" >/dev/null 2>&1; then \ echo "runtime base does not provide user ${HEARTWOOD_RUNTIME_USER}" >&2; exit 69; \ fi; \ + runtime_group="$(id -gn "${HEARTWOOD_RUNTIME_USER}")"; \ mkdir -p /opt/heartwood /opt/heartwood-vllm /opt/llama.cpp "${HEARTWOOD_WORKDIR}"; \ + install -d --mode=0755 \ + --owner="${HEARTWOOD_RUNTIME_USER}" --group="${runtime_group}" \ + "${HEARTWOOD_RUNTIME_HOME}/.cache" \ + "${HEARTWOOD_RUNTIME_HOME}/.cache/flashinfer" \ + "${HEARTWOOD_RUNTIME_HOME}/.cache/huggingface" \ + "${HEARTWOOD_RUNTIME_HOME}/.cache/vllm"; \ + install -d --mode=0700 \ + --owner="${HEARTWOOD_RUNTIME_USER}" --group="${runtime_group}" \ + "${HEARTWOOD_RUNTIME_HOME}/.openhands"; \ if [ "${HEARTWOOD_CREATE_USER}" = "true" ]; then \ - mkdir -p "${HEARTWOOD_RUNTIME_HOME}/.openhands"; \ - chown -R "${HEARTWOOD_RUNTIME_USER}:${HEARTWOOD_RUNTIME_USER}" \ + chown -R "${HEARTWOOD_RUNTIME_USER}:${runtime_group}" \ "${HEARTWOOD_RUNTIME_HOME}" "${HEARTWOOD_WORKDIR}"; \ fi; \ missing_packages=""; \ diff --git a/images/generic/README.md b/images/generic/README.md index 4013a3bc..ef4e68b5 100644 --- a/images/generic/README.md +++ b/images/generic/README.md @@ -19,7 +19,9 @@ The image contains no model weights, provider credentials, generated model profi `images/generic/compose.yaml` builds the production runtime target, disables container networking, uses a read-only root filesystem and restricted Linux privileges, starts the deterministic loopback model fixture, configures a non-secret managed profile, completes OpenHands SDK conversations in both action-confirmation modes, loads all repository-verified Skills through OpenHands, and exports a scrubbed audit log. The fixture validates integration only and is not a model artifact. -`images/generic/scripts/capable_model_e2e.sh` is the separate resource-qualified inference acceptance path. It consumes an explicitly downloaded, read-only GGUF mount, starts the included llama.cpp runtime, selects **Auto-Approve Low Risk**, and requires a native OpenHands tool proposal, successful terminal execution, exact synthetic file content, route and action-mode records, no error events, and audit export while container networking is disabled. The recommended Qwen2.5 7B Instruct artifact is the default tool-use demonstration model; the Coder variant remains available for coding-output experiments but is not used to claim structured agent-tool compatibility. +`images/generic/scripts/coding_agent_e2e.sh` is the shared coding-agent acceptance contract. It requires direct model inference, a native OpenHands terminal proposal, a displayed grouped approval, successful execution after explicit approval, exact synthetic file content, fresh-process replay, and hash-chain-verified audit export. `images/generic/scripts/capable_model_e2e.sh` applies that contract to an explicitly downloaded, read-only GGUF mount while container networking is disabled. The GPU wrapper applies the same contract to one immutable safetensors snapshot and records the runtime, GPU, driver, context, tensor parallelism, and parser in its qualification report. + +The recommended Qwen2.5 7B Instruct GGUF artifact remains the portable CPU tool-use demonstration. CUDA catalog entries remain evaluation candidates until the complete acceptance contract passes on the named GPU platform. Run `heartwood --interface web --host 0.0.0.0` for the browser interface. Heartwood uses the configured hosted or managed connection directly, or starts the selected downloaded model before opening the same interface. `heartwood runtime start` and `heartwood gateway serve` remain operator commands; the lower-level runtime scripts are test fixtures, not a separate researcher setup contract. diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index e91b62a7..0e480051 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -4,46 +4,157 @@ # # SPDX-License-Identifier: MIT -schema_version = "heartwood.model-snapshot-catalog.v1" +schema_version = "heartwood.model-snapshot-catalog.v2" -[snapshots.qwen25-7b-instruct-awq-vllm] +[download_policies.transformers-safetensors] +allow_patterns = [ + "*.json", + "*.jinja", + "*.model", + "*.safetensors", + "*.tiktoken", + "LICENSE*", + "NOTICE*", + "README*", +] +ignore_patterns = ["*.bin", "*.py", ".git/*", "metal/*", "original/*"] + +[snapshots.qwen25-coder-7b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "GPU-accelerated instruction model validated for synthetic OpenHands tool-use demonstrations on 16 GB GPUs; not reviewed for biomedical use." -source_repository = "Qwen/Qwen2.5-7B-Instruct-AWQ" -source_revision = "b25037543e9394b818fdfca67ab2a00ecc7dd641" -expected_size_bytes = 5580000000 -minimum_free_bytes = 10737418240 -license_posture = "Apache-2.0; retain the model card and source provenance. No biomedical or production-quality claim is made." -model_alias = "Qwen2.5-7B-Instruct-AWQ for vLLM" +purpose = "Standard coding-agent model for a single 16 GB NVIDIA GPU." +source_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" +source_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" +expected_size_bytes = 5582398811 +minimum_free_bytes = 12884901888 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen2.5 Coder 7B" +precision = "AWQ int4" +tier = "standard" +qualification = "candidate" +minimum_gpu_count = 1 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 34359738368 +recommended_disk_bytes = 17179869184 context_window = 32768 -minimum_resource_envelope = "One NVIDIA GPU with at least 16 GB VRAM, 8 CPU cores, 32 GB RAM, and 10 GB free project storage." -recommended_resource_envelope = "One NVIDIA GPU with at least 16 GB VRAM, 8 or more CPU cores, 48 GB RAM, and 16 GB free project storage." -recommended = true +maximum_context_window = 32768 +tool_call_parser = "hermes" +tensor_parallel_size = 1 +startup_seconds_min = 120 +startup_seconds_max = 480 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 CPU cores, 32 GB RAM, and 12 GB free project storage." +recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 or more CPU cores, 32 GB RAM, and 16 GB free project storage." +recommended = false -[snapshots.qwen25-7b-instruct-vllm] +[snapshots.qwen3-coder-30b-a3b-instruct-fp8-vllm] runtime_profile = "vllm-cuda" -purpose = "GPU-accelerated Heartwood-managed model for synthetic tool-use demonstrations; not reviewed for biomedical use." -source_repository = "Qwen/Qwen2.5-7B-Instruct" -source_revision = "a09a35458c702b33eeacc393d103063234e8bc28" -expected_size_bytes = 15200000000 -minimum_free_bytes = 21474836480 -license_posture = "Apache-2.0; retain the model card and source provenance. No biomedical or production-quality claim is made." -model_alias = "Qwen2.5-7B-Instruct for vLLM" +purpose = "Powerful coding-agent model for one 48 GB NVIDIA GPU." +source_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" +source_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" +expected_size_bytes = 31195099643 +minimum_free_bytes = 53687091200 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen3 Coder 30B FP8" +precision = "FP8" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 1 +minimum_gpu_memory_bytes = 42000000000 +recommended_ram_bytes = 103079215104 +recommended_disk_bytes = 68719476736 context_window = 32768 -minimum_resource_envelope = "One NVIDIA GPU with at least 32 GB VRAM, 8 CPU cores, 32 GB RAM, and 20 GB free project storage." -recommended_resource_envelope = "One NVIDIA GPU with at least 32 GB VRAM, 8 or more CPU cores, 64 GB RAM, and 30 GB free project storage." +maximum_context_window = 262144 +tool_call_parser = "qwen3_coder" +tensor_parallel_size = 1 +startup_seconds_min = 180 +startup_seconds_max = 600 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 12 CPU cores, 64 GB RAM, and 50 GB free project storage." +recommended_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 16 CPU cores, 96 GB RAM, and 64 GB free project storage." recommended = false -[snapshots.qwen3-8b-awq-vllm] +[snapshots.qwen3-coder-30b-a3b-instruct-bf16-vllm] runtime_profile = "vllm-cuda" -purpose = "GPU-accelerated instruction model available for user-selected evaluation on 16 GB GPUs; structured tool-use reliability is not part of the release recommendation." -source_repository = "Qwen/Qwen3-8B-AWQ" -source_revision = "4da05a8edb55c6046cce958586c33b61da07bb79" -expected_size_bytes = 6114568235 -minimum_free_bytes = 9171852353 -license_posture = "Apache-2.0; retain the model card and source provenance. No biomedical or production-quality claim is made." -model_alias = "Qwen3-8B-AWQ for vLLM" -context_window = 32768 -minimum_resource_envelope = "One NVIDIA GPU with at least 16 GB VRAM, 8 CPU cores, 32 GB RAM, and 12 GB free project storage." -recommended_resource_envelope = "One NVIDIA GPU with at least 16 GB VRAM, 8 or more CPU cores, 48 GB RAM, and 16 GB free project storage." +purpose = "Powerful higher-precision coding-agent model for two 48 GB NVIDIA GPUs." +source_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct" +source_revision = "b2cff646eb4bb1d68355c01b18ae02e7cf42d120" +expected_size_bytes = 61079793662 +minimum_free_bytes = 85899345920 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen3 Coder 30B" +precision = "BF16" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 2 +minimum_gpu_memory_bytes = 42000000000 +recommended_ram_bytes = 137438953472 +recommended_disk_bytes = 103079215104 +context_window = 65536 +maximum_context_window = 262144 +tool_call_parser = "qwen3_coder" +tensor_parallel_size = 2 +startup_seconds_min = 240 +startup_seconds_max = 720 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Two NVIDIA L40S-class GPUs with 48 GB VRAM each, 16 CPU cores, 96 GB RAM, and 80 GB free project storage." +recommended_resource_envelope = "Two NVIDIA L40S-class GPUs with 48 GB VRAM each, 24 CPU cores, 128 GB RAM, and 96 GB free project storage." +recommended = false + +[snapshots.qwen3-coder-next-fp8-vllm] +runtime_profile = "vllm-cuda" +purpose = "Maximum-capability coding-agent model for four 48 GB NVIDIA GPUs." +source_repository = "Qwen/Qwen3-Coder-Next-FP8" +source_revision = "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c" +expected_size_bytes = 80407731035 +minimum_free_bytes = 118111600640 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen3 Coder Next" +precision = "FP8" +tier = "maximum" +qualification = "candidate" +minimum_gpu_count = 4 +minimum_gpu_memory_bytes = 42000000000 +recommended_ram_bytes = 206158430208 +recommended_disk_bytes = 137438953472 +context_window = 65536 +maximum_context_window = 262144 +tool_call_parser = "qwen3_coder" +tensor_parallel_size = 4 +startup_seconds_min = 300 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Four NVIDIA L40S-class GPUs with 48 GB VRAM each, 24 CPU cores, 160 GB RAM, and 110 GB free project storage." +recommended_resource_envelope = "Four NVIDIA L40S-class GPUs with 48 GB VRAM each, 32 CPU cores, 192 GB RAM, and 128 GB free project storage." +recommended = false + +[snapshots.gpt-oss-120b-vllm] +runtime_profile = "vllm-cuda" +purpose = "Maximum-capability multi-GPU alternative pending Heartwood tool-use qualification." +source_repository = "openai/gpt-oss-120b" +source_revision = "b5c939de8f754692c1647ca79fbf85e8c1e70f8a" +expected_size_bytes = 65276857639 +minimum_free_bytes = 96636764160 +license_id = "Apache-2.0" +license_posture = "Apache-2.0 with the source repository usage policy; retain both with provenance." +model_alias = "GPT-OSS 120B" +precision = "MXFP4" +tier = "maximum" +qualification = "candidate" +minimum_gpu_count = 2 +minimum_gpu_memory_bytes = 42000000000 +recommended_ram_bytes = 171798691840 +recommended_disk_bytes = 120259084288 +context_window = 65536 +maximum_context_window = 131072 +tool_call_parser = "openai" +tensor_parallel_size = 2 +startup_seconds_min = 300 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Two NVIDIA L40S-class GPUs with 48 GB VRAM each, 24 CPU cores, 128 GB RAM, and 90 GB free project storage." +recommended_resource_envelope = "Two or more NVIDIA L40S-class GPUs with 48 GB VRAM each, 32 CPU cores, 160 GB RAM, and 112 GB free project storage." recommended = false diff --git a/images/generic/scripts/capable_model_e2e.sh b/images/generic/scripts/capable_model_e2e.sh index 2f3b943c..0698c292 100755 --- a/images/generic/scripts/capable_model_e2e.sh +++ b/images/generic/scripts/capable_model_e2e.sh @@ -9,53 +9,23 @@ set -euo pipefail model_path="${HEARTWOOD_LOCAL_MODEL_PATH:?HEARTWOOD_LOCAL_MODEL_PATH is required}" project="${HEARTWOOD_CAPABLE_PROJECT:-/tmp/heartwood-capable-project}" -if [[ "${project}" == "/" ]]; then - echo "refusing to use the filesystem root as the capable-model project" >&2 - exit 64 -fi +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +runtime_root="$(cd "${script_dir}/../../.." && pwd)" +runtime_log="${HEARTWOOD_RUNTIME_LOG:-${project}/llama-server.log}" +runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" + if [[ ! -f "${model_path}" ]]; then echo "capable-model artifact is unavailable: ${model_path}" >&2 exit 66 fi mkdir -p "${project}" -project="$(cd "${project}" && pwd -P)" -model_path="$(cd "$(dirname "${model_path}")" && pwd -P)/$(basename "${model_path}")" -case "${model_path}" in - "${project}"/*) - echo "capable-model artifact must be outside the disposable test project" >&2 - exit 64 - ;; -esac +rm -f "${runtime_log}" -state_root="${project}/.heartwood" -workspace="${state_root}/sessions" -session_id="${HEARTWOOD_SESSION_ID:-session-capable-model}" -transcript="${HEARTWOOD_TRANSCRIPT:-${project}/heartwood-transcript.txt}" -runtime_log="${HEARTWOOD_RUNTIME_LOG:-${project}/llama-server.log}" -command_timeout="${HEARTWOOD_COMMAND_TIMEOUT:-900}" -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -runtime_root="$(cd "${script_dir}/../../.." && pwd)" -runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" -cohort_path="${project}/cohort-summary.json" -events_path="${workspace}/${session_id}/events.jsonl" - -export HEARTWOOD_SESSION_ID="${session_id}" export HEARTWOOD_LOCAL_RUNTIME_PROFILE="llama-cpp-cpu" -export HEARTWOOD_LOCAL_MODEL_PATH="${model_path}" -export HEARTWOOD_MANAGED_MODEL_ALIAS="heartwood-managed-runtime" export HEARTWOOD_LOCAL_MODEL_CONTEXT="${HEARTWOOD_LOCAL_MODEL_CONTEXT:-32768}" export HEARTWOOD_LOCAL_MODEL_THREADS="${HEARTWOOD_LOCAL_MODEL_THREADS:-8}" export HEARTWOOD_LOCAL_RUNTIME_PORT="${runtime_port}" export HEARTWOOD_RUNTIME_ROOT="${runtime_root}" -export LITELLM_LOCAL_MODEL_COST_MAP="True" -export OPENHANDS_SUPPRESS_BANNER="1" - -rm -rf "${project}/input" "${state_root}" -mkdir -p "${project}/input" -rm -f "${cohort_path}" "${transcript}" "${runtime_log}" -cp "${runtime_root}/fixtures/synthetic/omop-like/"*.csv \ - "${project}/input/" -cd "${project}" bash "${runtime_root}/images/generic/scripts/start_local_runtime.sh" >"${runtime_log}" 2>&1 & runtime_pid="$!" @@ -84,9 +54,7 @@ deadline = time.time() + 180 last_error = None while time.time() < deadline: try: - with urllib.request.urlopen( - f"http://127.0.0.1:{port}/health", timeout=2 - ) as response: + with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2) as response: if response.status == 200: break except (OSError, urllib.error.URLError) as error: @@ -96,126 +64,4 @@ else: raise SystemExit(f"mounted capable-model runtime did not become ready: {last_error}") PY -run_heartwood() { - timeout "${command_timeout}" heartwood "$@" -} - -run_heartwood models refresh heartwood | tee -a "${transcript}" -run_heartwood models connect heartwood heartwood-managed-runtime \ - | tee -a "${transcript}" -run_heartwood models validate heartwood | tee -a "${transcript}" -run_heartwood actions set auto-approve-low-risk | tee -a "${transcript}" -run_heartwood --session-id "${session_id}" \ - --prompt "Call the terminal tool to execute this exact command: python ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ - | tee -a "${transcript}" - -for _ in 1 2 3 4; do - pending_id="$(python - "${events_path}" <<'PY' -import json -import sys -from pathlib import Path - -events_path = Path(sys.argv[1]) -events = [json.loads(line) for line in events_path.read_text(encoding="utf-8").splitlines()] -resolved = { - event["payload"].get("tool_call_id") - for event in events - if event["kind"] == "confirmation.resolved" -} -pending = [ - event["payload"]["request"]["tool_call_id"] - for event in events - if event["kind"] == "confirmation.requested" - and event["payload"]["request"]["tool_call_id"] not in resolved -] -print(pending[-1] if pending else "") -PY -)" - if [[ -z "${pending_id}" ]]; then - break - fi - run_heartwood --session-id "${session_id}" allow \ - | tee -a "${transcript}" -done - -run_heartwood --session-id "${session_id}" audit export \ - --output "${state_root}/audit-export.jsonl" | tee -a "${transcript}" - -python - "${events_path}" "${cohort_path}" <<'PY' -import json -import sys -from pathlib import Path - -events_path = Path(sys.argv[1]) -cohort_path = Path(sys.argv[2]) -events = [json.loads(line) for line in events_path.read_text(encoding="utf-8").splitlines()] -kinds = {event["kind"] for event in events} -required = { - "model_call.decision.recorded", - "tool.execution.recorded", - "tool_call.proposed", -} -missing = required - kinds -if missing: - raise SystemExit(f"capable-model session is missing events: {sorted(missing)}") -errors = [event["payload"].get("reason", "unknown") for event in events if event["kind"] == "error.recorded"] -if errors: - raise SystemExit(f"capable-model session recorded errors: {errors}") -unresolved = { - event["payload"]["request"]["tool_call_id"] - for event in events - if event["kind"] == "confirmation.requested" -} - { - event["payload"].get("tool_call_id") - for event in events - if event["kind"] == "confirmation.resolved" -} -if unresolved: - raise SystemExit(f"capable-model session has unresolved actions: {sorted(unresolved)}") -tool_executions = [ - event - for event in events - if event["kind"] == "tool.execution.recorded" -] -terminal_executions = [ - event for event in tool_executions if event["payload"].get("tool_name") == "terminal" -] -if not 1 <= len(tool_executions) <= 3 or any( - event["payload"].get("exit_code") != 0 for event in tool_executions -): - raise SystemExit("capable-model session must have one to three successful tool executions") -if not 1 <= len(terminal_executions) <= 3 or any( - event["payload"].get("exit_code") != 0 for event in terminal_executions -): - raise SystemExit("capable-model session must have one to three successful terminal executions") -if not cohort_path.is_file(): - raise SystemExit(f"capable model did not create {cohort_path}") -cohort = json.loads(cohort_path.read_text(encoding="utf-8")) -summary = cohort["summary"] -if summary["source_participant_count"] != 24 or summary["participant_count"] != 20: - raise SystemExit(f"capable model produced an unexpected reference cohort: {summary}") -if summary["source_condition_occurrence_count"] != 39: - raise SystemExit(f"capable model produced an unexpected source condition count: {summary}") -if summary["condition_occurrence_count"] != 35: - raise SystemExit(f"capable model produced an unexpected condition count: {summary}") -if not cohort["export_guard"]["exportable"]: - raise SystemExit("capable model reference cohort unexpectedly failed the count floor") -checks = cohort["quality_checks"] -if checks["aggregate_only_output"] is not True: - raise SystemExit("capable model reference artifact contains row-level output") -decisions = [ - event["payload"]["decision"]["decision"] - for event in events - if event["kind"] == "model_call.decision.recorded" -] -if not decisions or set(decisions) != {"allow"}: - raise SystemExit(f"capable-model route was not consistently allowed: {decisions}") -confirmation_modes = { - event["payload"]["model_profile"]["action_confirmation_mode"] - for event in events - if event["kind"] == "model_call.decision.recorded" -} -if confirmation_modes != {"confirm-risky"}: - raise SystemExit(f"unexpected action confirmation modes: {sorted(confirmation_modes)}") -print("Mounted capable-model OpenHands end-to-end test: ok") -PY +bash "${script_dir}/coding_agent_e2e.sh" diff --git a/images/generic/scripts/coding_agent_e2e.sh b/images/generic/scripts/coding_agent_e2e.sh new file mode 100755 index 00000000..41f331ce --- /dev/null +++ b/images/generic/scripts/coding_agent_e2e.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +set -euo pipefail + +model_path="${HEARTWOOD_LOCAL_MODEL_PATH:?HEARTWOOD_LOCAL_MODEL_PATH is required}" +project="${HEARTWOOD_CAPABLE_PROJECT:-/tmp/heartwood-capable-project}" +if [[ "${project}" == "/" ]]; then + echo "refusing to use the filesystem root as the coding-agent test project" >&2 + exit 64 +fi +if [[ ! -e "${model_path}" ]]; then + echo "coding-agent model is unavailable: ${model_path}" >&2 + exit 66 +fi +mkdir -p "${project}" +project="$(cd "${project}" && pwd -P)" +if [[ -d "${model_path}" ]]; then + model_path="$(cd "${model_path}" && pwd -P)" +else + model_path="$(cd "$(dirname "${model_path}")" && pwd -P)/$(basename "${model_path}")" +fi +case "${model_path}" in + "${project}"/*) + echo "coding-agent model must be outside the disposable test project" >&2 + exit 64 + ;; +esac + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +runtime_root="${HEARTWOOD_RUNTIME_ROOT:-$(cd "${script_dir}/../../.." && pwd)}" +state_root="${project}/.heartwood" +workspace="${state_root}/sessions" +session_id="${HEARTWOOD_SESSION_ID:-session-capable-model}" +transcript="${HEARTWOOD_TRANSCRIPT:-${project}/heartwood-transcript.txt}" +replay="${HEARTWOOD_REPLAY_TRANSCRIPT:-${project}/heartwood-replay.txt}" +report="${HEARTWOOD_QUALIFICATION_REPORT:-${project}/heartwood-qualification.json}" +inference="${state_root}/qualification-inference.json" +command_timeout="${HEARTWOOD_COMMAND_TIMEOUT:-900}" +runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" +cohort_path="${project}/cohort-summary.json" +events_path="${workspace}/${session_id}/events.jsonl" +audit_path="${state_root}/audit-export.jsonl" + +export HEARTWOOD_SESSION_ID="${session_id}" +export HEARTWOOD_LOCAL_MODEL_PATH="${model_path}" +export HEARTWOOD_MANAGED_MODEL_ALIAS="${HEARTWOOD_MANAGED_MODEL_ALIAS:-heartwood-managed-runtime}" +export HEARTWOOD_LOCAL_RUNTIME_PORT="${runtime_port}" +export HEARTWOOD_RUNTIME_ROOT="${runtime_root}" +export LITELLM_LOCAL_MODEL_COST_MAP=True +export OPENHANDS_SUPPRESS_BANNER=1 + +rm -rf "${project}/input" "${state_root}" +mkdir -p "${project}/input" +rm -f "${cohort_path}" "${transcript}" "${replay}" "${report}" +cp "${runtime_root}/fixtures/synthetic/omop-like/"*.csv "${project}/input/" +cd "${project}" + +echo "Checking direct model inference..." +python - "${inference}" <<'PY' +import json +import os +import sys +import urllib.request + +payload = json.dumps( + { + "model": os.environ["HEARTWOOD_MANAGED_MODEL_ALIAS"], + "messages": [{"role": "user", "content": "Reply briefly that inference is ready."}], + "max_tokens": 32, + "temperature": 0, + } +).encode() +request = urllib.request.Request( + f"http://127.0.0.1:{os.environ['HEARTWOOD_LOCAL_RUNTIME_PORT']}/v1/chat/completions", + data=payload, + headers={"Content-Type": "application/json"}, +) +with urllib.request.urlopen(request, timeout=300) as response: + result = json.load(response) +message = result["choices"][0]["message"] +content = message.get("content") or message.get("reasoning_content") +if not isinstance(content, str) or not content.strip(): + raise SystemExit("direct model inference returned no content") +with open(sys.argv[1], "w", encoding="utf-8") as file: + json.dump({"content_nonempty": True, "model": result.get("model")}, file) + file.write("\n") +PY + +run_heartwood() { + timeout "${command_timeout}" heartwood "$@" +} + +run_heartwood models refresh heartwood | tee -a "${transcript}" +run_heartwood models connect heartwood heartwood-managed-runtime | tee -a "${transcript}" +run_heartwood models validate heartwood | tee -a "${transcript}" +run_heartwood actions set ask-every-time | tee -a "${transcript}" +run_heartwood --session-id "${session_id}" \ + --prompt "Call the terminal tool to execute this exact command: python ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ + | tee -a "${transcript}" + +for _ in 1 2 3 4; do + pending_id="$(python - "${events_path}" <<'PY' +import json +import sys +from pathlib import Path + +events = [json.loads(line) for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines()] +resolved = { + event["payload"].get("tool_call_id") + for event in events + if event["kind"] == "confirmation.resolved" +} +pending = [ + event["payload"]["request"]["tool_call_id"] + for event in events + if event["kind"] == "confirmation.requested" + and event["payload"]["request"]["tool_call_id"] not in resolved +] +print(pending[-1] if pending else "") +PY +)" + if [[ -z "${pending_id}" ]]; then + break + fi + run_heartwood --session-id "${session_id}" allow | tee -a "${transcript}" +done + +run_heartwood --session-id "${session_id}" replay | tee "${replay}" +run_heartwood --session-id "${session_id}" audit export \ + --output "${audit_path}" | tee -a "${transcript}" + +python "${script_dir}/verify_coding_agent_e2e.py" \ + --events "${events_path}" \ + --audit "${audit_path}" \ + --artifact "${cohort_path}" \ + --replay "${replay}" \ + --inference "${inference}" \ + --report "${report}" \ + --root "${runtime_root}" diff --git a/images/generic/scripts/verify_coding_agent_e2e.py b/images/generic/scripts/verify_coding_agent_e2e.py new file mode 100644 index 00000000..cfd86ebd --- /dev/null +++ b/images/generic/scripts/verify_coding_agent_e2e.py @@ -0,0 +1,236 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Verify and summarize one real-model Heartwood coding-agent acceptance run.""" + +from __future__ import annotations + +import argparse +import json +import os +import tomllib +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, cast + +from heartwood.audit import AuditLog +from heartwood.session import SessionEvent + +_TEST_ID = "heartwood.coding-agent-e2e.v1" +_REQUIRED_EVENT_KINDS = { + "agent_message.emitted", + "confirmation.requested", + "confirmation.resolved", + "model_call.decision.recorded", + "tool.execution.recorded", + "tool_call.proposed", +} + + +def verify_run( + *, + events_path: Path, + audit_path: Path, + artifact_path: Path, + replay_path: Path, + inference_path: Path, +) -> dict[str, object]: + """Validate the complete acceptance contract and return its evidence summary.""" + events = tuple( + SessionEvent.model_validate_json(line) + for line in events_path.read_text(encoding="utf-8").splitlines() + if line + ) + if not events: + raise ValueError("coding-agent session has no events") + if [event.sequence for event in events] != list(range(len(events))): + raise ValueError("coding-agent session event sequence is not contiguous") + kinds = {str(event.kind) for event in events} + missing = sorted(_REQUIRED_EVENT_KINDS - kinds) + if missing: + raise ValueError(f"coding-agent session is missing events: {', '.join(missing)}") + errors = [ + str(event.payload.get("reason", "unknown")) + for event in events + if event.kind == "error.recorded" + ] + if errors: + raise ValueError(f"coding-agent session recorded errors: {errors}") + + requested = { + str(request["tool_call_id"]) + for event in events + if event.kind == "confirmation.requested" + and (request := _mapping(event.payload.get("request"))) + and "tool_call_id" in request + } + resolved = { + str(event.payload.get("tool_call_id")) + for event in events + if event.kind == "confirmation.resolved" + } + if requested != resolved: + raise ValueError("coding-agent session has unresolved or unmatched approvals") + decisions = { + str(event.payload.get("decision")) + for event in events + if event.kind == "confirmation.resolved" + } + if decisions != {"approved"}: + raise ValueError(f"coding-agent actions were not approved: {sorted(decisions)}") + + tool_executions = [event for event in events if event.kind == "tool.execution.recorded"] + terminal_executions = [ + event for event in tool_executions if event.payload.get("tool_name") == "terminal" + ] + if not 1 <= len(tool_executions) <= 3: + raise ValueError("coding-agent session must have one to three tool executions") + if not 1 <= len(terminal_executions) <= 3: + raise ValueError("coding-agent session must execute the terminal tool") + if any(event.payload.get("exit_code") != 0 for event in tool_executions): + raise ValueError("coding-agent tool execution failed") + + routes = [ + decision.get("decision") + for event in events + if event.kind == "model_call.decision.recorded" + and (decision := _mapping(event.payload.get("decision"))) + ] + if not routes or set(routes) != {"allow"}: + raise ValueError(f"coding-agent model route was not consistently allowed: {routes}") + confirmation_modes = { + profile.get("action_confirmation_mode") + for event in events + if event.kind == "model_call.decision.recorded" + and (profile := _mapping(event.payload.get("model_profile"))) + } + if confirmation_modes != {"always-confirm"}: + raise ValueError( + f"coding-agent session used unexpected confirmation modes: {confirmation_modes}" + ) + + cohort = json.loads(artifact_path.read_text(encoding="utf-8")) + summary = cohort["summary"] + expected_summary = { + "source_participant_count": 24, + "participant_count": 20, + "source_condition_occurrence_count": 39, + "condition_occurrence_count": 35, + } + observed_summary = {key: summary.get(key) for key in expected_summary} + if observed_summary != expected_summary: + raise ValueError(f"coding-agent artifact is incorrect: {observed_summary}") + if cohort["quality_checks"].get("aggregate_only_output") is not True: + raise ValueError("coding-agent artifact contains row-level output") + if cohort["export_guard"].get("exportable") is not True: + raise ValueError("coding-agent artifact unexpectedly failed its count floor") + + inference = json.loads(inference_path.read_text(encoding="utf-8")) + if inference.get("content_nonempty") is not True: + raise ValueError("direct model inference did not return content") + + replay = replay_path.read_text(encoding="utf-8") + if "Tool terminal exit=0" not in replay or "Action set approved" not in replay: + raise ValueError("fresh-process replay is missing the approved tool execution") + + audit = AuditLog(audit_path) + audit_events = audit.read() + audit.verify(audit_events) + if len(audit_events) != len(events): + raise ValueError("audit export and replay event counts disagree") + if [event.event_type for event in audit_events] != [str(event.kind) for event in events]: + raise ValueError("audit export and replay event kinds disagree") + audit_text = audit_path.read_text(encoding="utf-8") + for sensitive_value in ( + str(artifact_path.parent), + "target-condition-concept-id", + "Call the terminal tool", + ): + if sensitive_value in audit_text: + raise ValueError("audit export contains unsanitized task content") + + return { + "event_count": len(events), + "audit_event_count": len(audit_events), + "tool_execution_count": len(tool_executions), + "checks": { + "model_loaded_and_inferred": True, + "tool_call_proposed": True, + "grouped_approval_recorded": True, + "file_modified_and_verified": True, + "fresh_process_replay_verified": True, + "audit_export_verified": True, + }, + } + + +def _mapping(value: object) -> dict[str, Any]: + return cast(dict[str, Any], value) if isinstance(value, dict) else {} + + +def _configuration(root: Path, configuration_id: str) -> dict[str, Any]: + with (root / "images/gpu/compatibility.toml").open("rb") as file: + matrix = tomllib.load(file) + for item in matrix.get("configurations", []): + if isinstance(item, dict) and item.get("configuration_id") == configuration_id: + return item + raise ValueError(f"unknown GPU qualification configuration: {configuration_id}") + + +def _report(summary: dict[str, object], root: Path) -> dict[str, object]: + configuration_id = os.environ.get("HEARTWOOD_GPU_CONFIGURATION_ID") + configuration = _configuration(root, configuration_id) if configuration_id is not None else None + runtime_metadata_path = os.environ.get("HEARTWOOD_QUALIFICATION_RUNTIME_METADATA") + runtime_metadata = ( + json.loads(Path(runtime_metadata_path).read_text(encoding="utf-8")) + if runtime_metadata_path + else {} + ) + return { + "schema_version": "heartwood.coding-agent-qualification.v1", + "qualification_test": _TEST_ID, + "status": "passed", + "recorded_at": datetime.now(UTC).replace(microsecond=0).isoformat(), + "platform": os.environ.get("HEARTWOOD_PLATFORM", "generic"), + "configuration_id": configuration_id, + "configuration": configuration, + "runtime": runtime_metadata, + "model": { + "repository": os.environ.get("HEARTWOOD_QUALIFICATION_MODEL_REPOSITORY"), + "revision": os.environ.get("HEARTWOOD_QUALIFICATION_MODEL_REVISION"), + "runtime_profile": os.environ.get("HEARTWOOD_LOCAL_RUNTIME_PROFILE"), + }, + **summary, + } + + +def main() -> int: + """Run acceptance checks and write a portable qualification record.""" + parser = argparse.ArgumentParser() + parser.add_argument("--events", type=Path, required=True) + parser.add_argument("--audit", type=Path, required=True) + parser.add_argument("--artifact", type=Path, required=True) + parser.add_argument("--replay", type=Path, required=True) + parser.add_argument("--inference", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--root", type=Path, required=True) + args = parser.parse_args() + summary = verify_run( + events_path=args.events, + audit_path=args.audit, + artifact_path=args.artifact, + replay_path=args.replay, + inference_path=args.inference, + ) + report = _report(summary, args.root.resolve()) + args.report.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + args.report.chmod(0o600) + print(f"Heartwood coding-agent qualification passed: {args.report}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh new file mode 100755 index 00000000..878edcd5 --- /dev/null +++ b/images/gpu/coding_agent_e2e.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +set -euo pipefail + +configuration_id="${HEARTWOOD_GPU_CONFIGURATION_ID:?HEARTWOOD_GPU_CONFIGURATION_ID is required}" +model_path="${HEARTWOOD_LOCAL_MODEL_PATH:?HEARTWOOD_LOCAL_MODEL_PATH is required}" +project="${HEARTWOOD_CAPABLE_PROJECT:-/tmp/heartwood-gpu-qualification}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +runtime_root="${HEARTWOOD_RUNTIME_ROOT:-$(cd "${script_dir}/../.." && pwd)}" +runtime_log="${HEARTWOOD_RUNTIME_LOG:-${project}/vllm.log}" +runtime_metadata="${HEARTWOOD_QUALIFICATION_RUNTIME_METADATA:-${project}/gpu-runtime.json}" +runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" +vllm_executable="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" +vllm_python="${HEARTWOOD_VLLM_PYTHON:-/opt/heartwood-vllm/bin/python}" + +if [[ ! -d "${model_path}" ]]; then + echo "vLLM model snapshot is unavailable: ${model_path}" >&2 + exit 66 +fi +mkdir -p "${project}" +project="$(cd "${project}" && pwd -P)" +model_path="$(cd "${model_path}" && pwd -P)" +rm -f "${runtime_log}" "${runtime_metadata}" + +configuration="$(python "${script_dir}/qualification_config.py" "${configuration_id}")" +snapshot_id="$(jq -er '.configuration.model_snapshot' <<<"${configuration}")" +repository="$(jq -er '.configuration.model_repository' <<<"${configuration}")" +revision="$(jq -er '.configuration.model_revision' <<<"${configuration}")" +context="$(jq -er '.configuration.context_window' <<<"${configuration}")" +tensor_parallel="$(jq -er '.configuration.tensor_parallel_size' <<<"${configuration}")" +tool_parser="$(jq -er '.configuration.tool_call_parser' <<<"${configuration}")" +startup_min="$(jq -er '.configuration.startup_seconds_min' <<<"${configuration}")" +startup_max="$(jq -er '.configuration.startup_seconds_max' <<<"${configuration}")" + +echo "Verifying the pinned ${repository}@${revision} snapshot..." +python - "${model_path}" "${snapshot_id}" "${repository}" "${revision}" <<'PY' +import json +import sys +from pathlib import Path + +from heartwood.gateway import verify_model_snapshot + +root = Path(sys.argv[1]) +expected = { + "snapshot_id": sys.argv[2], + "source_repository": sys.argv[3], + "source_revision": sys.argv[4], +} +source = json.loads((root / "HEARTWOOD-SOURCE.json").read_text(encoding="utf-8")) +if any(source.get(key) != value for key, value in expected.items()): + raise SystemExit(f"model snapshot source does not match the qualification configuration: {source}") +verify_model_snapshot(root) +PY + +python - "${configuration}" <<'PY' +import json +import os +import sys + +from heartwood.gateway import inspect_gpu_environment + +payload = json.loads(sys.argv[1]) +configuration = payload["configuration"] +environment = inspect_gpu_environment(os.environ.get("HEARTWOOD_PLATFORM", "generic"), os.environ) +compatible, reason = environment.assess( + gpu_count=configuration["gpu_count"], + gpu_memory_bytes=configuration["minimum_gpu_memory_bytes"], +) +print(reason) +if not compatible or not environment.visible_devices: + raise SystemExit("the requested qualification requires compatible GPUs visible in this process") +PY + +"${script_dir}/verify_runtime.sh" +"${vllm_executable}" --version >/dev/null +"${vllm_python}" - "${runtime_metadata}" <<'PY' +import json +import subprocess +import sys +from importlib.metadata import version + +import torch + +query = subprocess.run( + [ + "nvidia-smi", + "--query-gpu=index,name,memory.total,driver_version,compute_cap", + "--format=csv,noheader,nounits", + ], + check=True, + capture_output=True, + text=True, +) +payload = { + "vllm_version": version("vllm"), + "pytorch_version": version("torch"), + "cuda_version": torch.version.cuda, + "visible_gpus": [line.strip() for line in query.stdout.splitlines() if line.strip()], +} +with open(sys.argv[1], "w", encoding="utf-8") as file: + json.dump(payload, file, indent=2, sort_keys=True) + file.write("\n") +PY + +export HEARTWOOD_LOCAL_RUNTIME_PROFILE="vllm-cuda" +export HEARTWOOD_LOCAL_MODEL_PATH="${model_path}" +export HEARTWOOD_LOCAL_MODEL_CONTEXT="${context}" +export HEARTWOOD_LOCAL_RUNTIME_PORT="${runtime_port}" +export HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE="${tensor_parallel}" +export HEARTWOOD_VLLM_TOOL_PARSER="${tool_parser}" +export HEARTWOOD_RUNTIME_ROOT="${runtime_root}" +export HEARTWOOD_QUALIFICATION_MODEL_REPOSITORY="${repository}" +export HEARTWOOD_QUALIFICATION_MODEL_REVISION="${revision}" +export HEARTWOOD_QUALIFICATION_RUNTIME_METADATA="${runtime_metadata}" + +echo "Loading ${repository}; expected startup is approximately ${startup_min}-${startup_max} seconds." +bash "${script_dir}/start_vllm.sh" >"${runtime_log}" 2>&1 & +runtime_pid="$!" + +cleanup() { + status="$?" + trap - EXIT + kill "${runtime_pid}" >/dev/null 2>&1 || true + wait "${runtime_pid}" >/dev/null 2>&1 || true + if ((status != 0)) && [[ -f "${runtime_log}" ]]; then + echo "vLLM runtime log (last 240 lines):" >&2 + tail -n 240 "${runtime_log}" >&2 + fi + exit "${status}" +} +trap cleanup EXIT + +python - "${runtime_port}" "${startup_max}" <<'PY' +import sys +import time +import urllib.error +import urllib.request + +port = int(sys.argv[1]) +deadline = time.time() + int(sys.argv[2]) + 120 +next_update = time.time() + 30 +last_error = None +while time.time() < deadline: + try: + with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2) as response: + if response.status == 200: + print("vLLM is ready; starting coding-agent acceptance.") + break + except (OSError, urllib.error.URLError) as error: + last_error = error + if time.time() >= next_update: + print("Still loading the model; Heartwood will continue when vLLM is ready.", flush=True) + next_update = time.time() + 30 + time.sleep(1) +else: + raise SystemExit(f"vLLM did not become ready before the qualification timeout: {last_error}") +PY + +bash "${runtime_root}/images/generic/scripts/coding_agent_e2e.sh" diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml new file mode 100644 index 00000000..f46db308 --- /dev/null +++ b/images/gpu/compatibility.toml @@ -0,0 +1,149 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +schema_version = "heartwood.gpu-compatibility.v1" + +[runtime] +python_version = "3.12" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +torchaudio_version = "2.11.0+cu129" +torchvision_version = "0.26.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +cuda_13_qualified = false + +[[configurations]] +configuration_id = "terra-t4-qwen25-coder-7b-awq" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 1 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen25-coder-7b-instruct-awq-vllm" +model_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" +model_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" +precision = "AWQ int4" +context_window = 32768 +tensor_parallel_size = 1 +tool_call_parser = "hermes" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 120 +startup_seconds_max = 480 + +[[configurations]] +configuration_id = "carina-l40s-qwen25-coder-7b-awq" +status = "candidate" +platform = "carina" +gpu_model = "NVIDIA L40S" +gpu_count = 1 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen25-coder-7b-instruct-awq-vllm" +model_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" +model_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" +precision = "AWQ int4" +context_window = 32768 +tensor_parallel_size = 1 +tool_call_parser = "hermes" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 120 +startup_seconds_max = 480 + +[[configurations]] +configuration_id = "carina-l40s-qwen3-coder-30b-fp8" +status = "candidate" +platform = "carina" +gpu_model = "NVIDIA L40S" +gpu_count = 1 +minimum_gpu_memory_bytes = 42000000000 +model_snapshot = "qwen3-coder-30b-a3b-instruct-fp8-vllm" +model_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" +model_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" +precision = "FP8" +context_window = 32768 +tensor_parallel_size = 1 +tool_call_parser = "qwen3_coder" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 180 +startup_seconds_max = 600 + +[[configurations]] +configuration_id = "carina-2xl40s-qwen3-coder-30b-bf16" +status = "candidate" +platform = "carina" +gpu_model = "NVIDIA L40S" +gpu_count = 2 +minimum_gpu_memory_bytes = 42000000000 +model_snapshot = "qwen3-coder-30b-a3b-instruct-bf16-vllm" +model_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct" +model_revision = "b2cff646eb4bb1d68355c01b18ae02e7cf42d120" +precision = "BF16" +context_window = 65536 +tensor_parallel_size = 2 +tool_call_parser = "qwen3_coder" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 240 +startup_seconds_max = 720 + +[[configurations]] +configuration_id = "carina-4xl40s-qwen3-coder-next-fp8" +status = "candidate" +platform = "carina" +gpu_model = "NVIDIA L40S" +gpu_count = 4 +minimum_gpu_memory_bytes = 42000000000 +model_snapshot = "qwen3-coder-next-fp8-vllm" +model_repository = "Qwen/Qwen3-Coder-Next-FP8" +model_revision = "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c" +precision = "FP8" +context_window = 65536 +tensor_parallel_size = 4 +tool_call_parser = "qwen3_coder" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 300 +startup_seconds_max = 900 + +[[configurations]] +configuration_id = "carina-2xl40s-gpt-oss-120b" +status = "candidate" +platform = "carina" +gpu_model = "NVIDIA L40S" +gpu_count = 2 +minimum_gpu_memory_bytes = 42000000000 +model_snapshot = "gpt-oss-120b-vllm" +model_repository = "openai/gpt-oss-120b" +model_revision = "b5c939de8f754692c1647ca79fbf85e8c1e70f8a" +precision = "MXFP4" +context_window = 65536 +tensor_parallel_size = 2 +tool_call_parser = "openai" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 300 +startup_seconds_max = 900 diff --git a/images/gpu/heartwood-vllm b/images/gpu/heartwood-vllm index d6ecf010..463e2461 100755 --- a/images/gpu/heartwood-vllm +++ b/images/gpu/heartwood-vllm @@ -8,5 +8,9 @@ set -euo pipefail runtime_bin="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -export PYTHONPATH="${runtime_bin}" -exec "${runtime_bin}/python" "${runtime_bin}/heartwood_vllm.py" "$@" + +if [[ "$#" -eq 1 && "$1" == "__heartwood_verify_runtime__" ]]; then + exec "${runtime_bin}/python" "${runtime_bin}/verify_vllm.py" +fi + +exec "${runtime_bin}/vllm" "$@" diff --git a/images/gpu/heartwood_vllm.py b/images/gpu/heartwood_vllm.py deleted file mode 100644 index 1d840a68..00000000 --- a/images/gpu/heartwood_vllm.py +++ /dev/null @@ -1,209 +0,0 @@ -# This source file is part of the Heartwood open-source project -# -# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) -# -# SPDX-License-Identifier: MIT - -"""Run the CUDA 11.8 vLLM build with its reviewed Transformers compatibility boundary.""" - -from __future__ import annotations - -import importlib -import json -import os -import subprocess -import sys -import tempfile -from contextlib import suppress -from importlib.metadata import version -from pathlib import Path - -_VULNERABLE_CONFIG_TYPE = "Llama_Nemotron_Nano_VL" -_COMPATIBILITY_MARKER = "_heartwood_compatibility_applied" -_REMOVED_CONFIG_MARKER = "_heartwood_vllm_removed_vulnerable_config" - - -def apply_transformers_compatibility() -> None: - """Keep vLLM's pinned configuration classes compatible with Transformers 5.""" - from transformers.configuration_utils import PreTrainedConfig - from transformers.tokenization_utils_base import PreTrainedTokenizerBase - - if getattr(PreTrainedConfig, _COMPATIBILITY_MARKER, False): - return - original = PreTrainedConfig.__init_subclass__.__func__ - - def compatible_init_subclass( - cls: type[PreTrainedConfig], *args: object, **kwargs: object - ) -> None: - if cls.__module__.startswith("vllm."): - super(PreTrainedConfig, cls).__init_subclass__(*args, **kwargs) - return - original(cls, *args, **kwargs) - - PreTrainedConfig.__init_subclass__ = classmethod(compatible_init_subclass) - setattr(PreTrainedConfig, _COMPATIBILITY_MARKER, True) - - if not hasattr(PreTrainedTokenizerBase, "all_special_tokens_extended"): - - @property - def all_special_tokens_extended( - self: PreTrainedTokenizerBase, - ) -> list[object]: - added_by_content = {str(token): token for token in self.added_tokens_decoder.values()} - return [added_by_content.get(token, token) for token in self.all_special_tokens] - - PreTrainedTokenizerBase.all_special_tokens_extended = ( # type: ignore[attr-defined] - all_special_tokens_extended - ) - - -def _apply_vllm_security_backport() -> type[object]: - from vllm.transformers_utils import config as config_module - - removed = getattr(config_module, _REMOVED_CONFIG_MARKER, None) - if removed is None: - removed = config_module._CONFIG_REGISTRY.pop(_VULNERABLE_CONFIG_TYPE, None) - setattr(config_module, _REMOVED_CONFIG_MARKER, removed) - if removed is None or removed.__name__ != "Nemotron_Nano_VL_Config": - raise RuntimeError("the reviewed vLLM security backport no longer matches the runtime") - return removed - - -def activate_runtime_boundary() -> type[object]: - """Apply the reviewed compatibility and security boundary idempotently.""" - apply_transformers_compatibility() - return _apply_vllm_security_backport() - - -def _configure_child_bootstrap() -> None: - runtime_bin = Path(__file__).resolve().parent - bootstrap = runtime_bin / "sitecustomize.py" - if not bootstrap.is_file(): - raise RuntimeError(f"vLLM child bootstrap is unavailable: {bootstrap}") - os.environ["PYTHONPATH"] = str(runtime_bin) - - -def _verify_runtime(removed_config: type[object]) -> None: - import idna # noqa: F401 - import transformers - import xgrammar # noqa: F401 - from transformers.tokenization_utils_base import PreTrainedTokenizerBase - from vllm.model_executor.models.registry import ModelRegistry - from vllm.transformers_utils import config as config_module - from vllm.transformers_utils.tokenizer import get_tokenizer # noqa: F401 - from vllm.v1.structured_output import backend_xgrammar # noqa: F401 - - if _VULNERABLE_CONFIG_TYPE in config_module._CONFIG_REGISTRY: - raise RuntimeError("the vulnerable vLLM configuration remains registered") - if version("xgrammar") != "0.1.32": - raise RuntimeError("the reviewed xgrammar security override is unavailable") - if version("idna") != "3.18": - raise RuntimeError("the reviewed idna security override is unavailable") - if not hasattr(PreTrainedTokenizerBase, "all_special_tokens_extended"): - raise RuntimeError("the reviewed Transformers tokenizer compatibility is unavailable") - - tokenizer_check = subprocess.run( - [ - sys.executable, - "-c", - """ -from tokenizers import Tokenizer -from tokenizers.models import WordLevel -from transformers import PreTrainedTokenizerFast -from vllm.transformers_utils.tokenizer import get_cached_tokenizer - -backend = Tokenizer(WordLevel({"": 0, "test": 1}, unk_token="")) -tokenizer = PreTrainedTokenizerFast( - tokenizer_object=backend, - unk_token="", - eos_token="", -) -cached = get_cached_tokenizer(tokenizer) -assert [str(token) for token in cached.all_special_tokens_extended] == cached.all_special_tokens -assert cached.encode("test", add_special_tokens=False) == [1] -""", - ], - check=False, - capture_output=True, - text=True, - ) - if tokenizer_check.returncode != 0: - detail = tokenizer_check.stderr.strip() or tokenizer_check.stdout.strip() - raise RuntimeError(f"vLLM tokenizer subprocess compatibility failed: {detail}") - - vulnerable_module = importlib.import_module(removed_config.__module__) - dynamic_loader_called = False - - def reject_dynamic_loader(*_args: object, **_kwargs: object) -> type[object]: - nonlocal dynamic_loader_called - dynamic_loader_called = True - raise RuntimeError("vulnerable dynamic configuration loader reached") - - vulnerable_module.get_class_from_dynamic_module = reject_dynamic_loader - - with tempfile.TemporaryDirectory() as directory: - model = Path(directory) - (model / "config.json").write_text( - json.dumps( - { - "architectures": ["Qwen2ForCausalLM"], - "model_type": "qwen2", - "num_attention_heads": 2, - "num_hidden_layers": 2, - "num_key_value_heads": 2, - "hidden_size": 128, - "intermediate_size": 256, - "vocab_size": 256, - } - ), - encoding="utf-8", - ) - config = config_module.get_config(model, trust_remote_code=False) - if config.model_type != "qwen2": - raise RuntimeError(f"unexpected synthetic model type: {config.model_type}") - - (model / "config.json").write_text( - json.dumps( - { - "model_type": _VULNERABLE_CONFIG_TYPE, - "vision_config": { - "auto_map": {"AutoConfig": "untrusted/repository--configuration.Payload"} - }, - } - ), - encoding="utf-8", - ) - with suppress(KeyError, OSError, RuntimeError, ValueError): - config_module.get_config(model, trust_remote_code=False) - if dynamic_loader_called: - raise RuntimeError("the vulnerable vLLM dynamic configuration loader was called") - - registered_model = ModelRegistry.models.get("Qwen2ForCausalLM") - if registered_model is None: - raise RuntimeError("the Qwen2 architecture is unavailable in the reviewed vLLM runtime") - model_info = registered_model.inspect_model_cls() - if model_info.architecture != "Qwen2ForCausalLM": - raise RuntimeError(f"unexpected inspected model architecture: {model_info.architecture}") - - print( - f"Transformers {transformers.__version__} integration and " - "vLLM GHSA-8fr4-5q9j-m8gm, xgrammar GHSA-7rgv-gqhr-fxg3, and " - "idna GHSA-65pc-fj4g-8rjx fixes verified" - ) - - -def main() -> None: - """Validate or start the vLLM command-line interface.""" - _configure_child_bootstrap() - removed_config = activate_runtime_boundary() - if sys.argv[1:] == ["__heartwood_verify_runtime__"]: - _verify_runtime(removed_config) - return - - from vllm.entrypoints.cli.main import main as vllm_main - - vllm_main() - - -if __name__ == "__main__": - main() diff --git a/images/gpu/install_runtime.sh b/images/gpu/install_runtime.sh index a6ac8c9a..aed16822 100755 --- a/images/gpu/install_runtime.sh +++ b/images/gpu/install_runtime.sh @@ -26,6 +26,6 @@ runtime_sources="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" --require-hashes \ --python "${target}/bin/python" \ "${runtime_sources}/vllm-requirements.txt" -install -m 0444 "${runtime_sources}/heartwood_vllm.py" "${target}/bin/heartwood_vllm.py" -install -m 0444 "${runtime_sources}/sitecustomize.py" "${target}/bin/sitecustomize.py" +install -m 0444 "${runtime_sources}/verify_vllm.py" "${target}/bin/verify_vllm.py" +install -m 0444 "${runtime_sources}/compatibility.toml" "${target}/bin/compatibility.toml" install -m 0555 "${runtime_sources}/heartwood-vllm" "${target}/bin/heartwood-vllm" diff --git a/images/gpu/qualification_config.py b/images/gpu/qualification_config.py new file mode 100644 index 00000000..0c80a198 --- /dev/null +++ b/images/gpu/qualification_config.py @@ -0,0 +1,50 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Resolve one reviewed GPU qualification configuration as JSON.""" + +from __future__ import annotations + +import argparse +import json +import tomllib +from pathlib import Path +from typing import Any + + +def load_configuration(path: Path, configuration_id: str) -> dict[str, Any]: + """Load one compatibility entry together with its runtime contract.""" + with path.open("rb") as file: + matrix = tomllib.load(file) + runtime = matrix.get("runtime") + configurations = matrix.get("configurations") + if not isinstance(runtime, dict) or not isinstance(configurations, list): + raise ValueError("GPU compatibility matrix is malformed") + for configuration in configurations: + if ( + isinstance(configuration, dict) + and configuration.get("configuration_id") == configuration_id + ): + return {"runtime": runtime, "configuration": configuration} + raise ValueError(f"unknown GPU qualification configuration: {configuration_id}") + + +def main() -> int: + """Print one resolved configuration for shell and CI consumers.""" + parser = argparse.ArgumentParser() + parser.add_argument("configuration_id") + parser.add_argument( + "--matrix", + type=Path, + default=Path(__file__).with_name("compatibility.toml"), + ) + args = parser.parse_args() + print(json.dumps(load_configuration(args.matrix, args.configuration_id), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/images/gpu/sitecustomize.py b/images/gpu/sitecustomize.py deleted file mode 100644 index 80dacf04..00000000 --- a/images/gpu/sitecustomize.py +++ /dev/null @@ -1,13 +0,0 @@ -# This source file is part of the Heartwood open-source project -# -# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) -# -# SPDX-License-Identifier: MIT - -"""Apply Heartwood's reviewed vLLM boundary in spawned Python interpreters.""" - -from __future__ import annotations - -from heartwood_vllm import activate_runtime_boundary - -activate_runtime_boundary() diff --git a/images/gpu/start_vllm.sh b/images/gpu/start_vllm.sh index d1388e89..898a1d8c 100755 --- a/images/gpu/start_vllm.sh +++ b/images/gpu/start_vllm.sh @@ -13,8 +13,9 @@ port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" alias="${HEARTWOOD_MANAGED_MODEL_ALIAS:-heartwood-managed-runtime}" tool_parser="${HEARTWOOD_VLLM_TOOL_PARSER:-hermes}" context="${HEARTWOOD_LOCAL_MODEL_CONTEXT:-32768}" +tensor_parallel_size="${HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE:-1}" +gpu_memory_utilization="${HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION:-0.90}" vllm="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" -export VLLM_USE_FLASHINFER_SAMPLER="${VLLM_USE_FLASHINFER_SAMPLER:-0}" if [[ "${host}" != "127.0.0.1" && "${host}" != "localhost" && "${host}" != "::1" ]]; then echo "vLLM must bind to loopback, got ${host}" >&2 @@ -34,5 +35,7 @@ exec "${vllm}" serve "${model_path}" \ --port "${port}" \ --served-model-name "${alias}" \ --max-model-len "${context}" \ + --tensor-parallel-size "${tensor_parallel_size}" \ + --gpu-memory-utilization "${gpu_memory_utilization}" \ --enable-auto-tool-choice \ --tool-call-parser "${tool_parser}" diff --git a/images/gpu/verify_runtime.sh b/images/gpu/verify_runtime.sh index e8cd364b..0418052a 100755 --- a/images/gpu/verify_runtime.sh +++ b/images/gpu/verify_runtime.sh @@ -51,7 +51,7 @@ if [[ ! -x "${vllm_executable}" ]]; then fi "${vllm_python}" -c \ - 'import torch, vllm; from importlib.metadata import version; assert torch.version.cuda == "11.8", f"unexpected CUDA build: {torch.version.cuda}"; print(version("vllm"), torch.__version__, torch.version.cuda)' + 'import torch, vllm; from importlib.metadata import version; assert torch.version.cuda == "12.9", f"unexpected CUDA build: {torch.version.cuda}"; print(version("vllm"), torch.__version__, torch.version.cuda)' "${vllm_executable}" __heartwood_verify_runtime__ if (($#)); then diff --git a/images/gpu/verify_vllm.py b/images/gpu/verify_vllm.py new file mode 100644 index 00000000..019d362f --- /dev/null +++ b/images/gpu/verify_vllm.py @@ -0,0 +1,97 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Verify the isolated, supported Heartwood vLLM runtime contract.""" + +from __future__ import annotations + +import tomllib +from importlib import import_module +from importlib.metadata import distributions, version +from pathlib import Path + +import torch +from packaging.utils import canonicalize_name +from vllm.tool_parsers import ToolParserManager + +_DEPENDENCY_VERSIONS = { + "cuda-bindings": "12.9.7", + "cuda-python": "12.9.7", + "flashinfer-cubin": "0.6.13", + "flashinfer-python": "0.6.13", + "nvidia-cuda-runtime-cu12": "12.9.79", +} +_REQUIRED_TOOL_PARSERS = ("hermes", "openai", "qwen3_coder") +_FORBIDDEN_CUDA_13_PACKAGES = { + "cuda-tile", + "nvidia-cuda-crt", + "nvidia-cuda-nvcc", + "nvidia-cuda-runtime", + "nvidia-cuda-tileiras", + "nvidia-nvvm", +} + + +def main() -> None: + """Reject a mixed or incomplete CUDA runtime before model startup.""" + contract = _runtime_contract() + expected = { + **_DEPENDENCY_VERSIONS, + "torch": contract["pytorch_version"], + "torchaudio": contract["torchaudio_version"], + "torchvision": contract["torchvision_version"], + "vllm": contract["vllm_version"], + } + observed = {package: version(package) for package in expected} + if observed != expected: + raise RuntimeError(f"unexpected GPU runtime versions: {observed}") + if torch.version.cuda != contract["cuda_version"]: + raise RuntimeError(f"unexpected PyTorch CUDA build: {torch.version.cuda}") + if contract.get("cuda_13_qualified") is not False: + raise RuntimeError("Heartwood's CUDA 13 runtime is not qualified") + + installed = { + canonicalize_name(distribution.metadata["Name"]) + for distribution in distributions() + if distribution.metadata.get("Name") + } + cuda_13 = sorted( + name + for name in installed + if name in _FORBIDDEN_CUDA_13_PACKAGES or name.endswith("-cu13") or "-cu13-" in name + ) + if cuda_13: + raise RuntimeError(f"unqualified CUDA 13 packages are installed: {', '.join(cuda_13)}") + + import_module("flashinfer") + + available_parsers = set(ToolParserManager.list_registered()) + missing_parsers = sorted(set(_REQUIRED_TOOL_PARSERS) - available_parsers) + if missing_parsers: + missing = ", ".join(missing_parsers) + raise RuntimeError(f"required vLLM tool parsers are unavailable: {missing}") + + print( + "Heartwood GPU runtime verified: " + f"vLLM {observed['vllm']}, PyTorch {observed['torch']}, CUDA {torch.version.cuda}; " + f"tool parsers {', '.join(_REQUIRED_TOOL_PARSERS)}" + ) + + +def _runtime_contract() -> dict[str, object]: + path = Path(__file__).with_name("compatibility.toml") + with path.open("rb") as file: + payload = tomllib.load(file) + if payload.get("schema_version") != "heartwood.gpu-compatibility.v1": + raise RuntimeError("unsupported GPU compatibility contract") + runtime = payload.get("runtime") + if not isinstance(runtime, dict): + raise RuntimeError("GPU runtime compatibility metadata is unavailable") + return runtime + + +if __name__ == "__main__": + main() diff --git a/images/gpu/vllm-exclusions.txt b/images/gpu/vllm-exclusions.txt new file mode 100644 index 00000000..d3f91efd --- /dev/null +++ b/images/gpu/vllm-exclusions.txt @@ -0,0 +1,17 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +# flashinfer-python 0.6.13 declares cuda-tile[tileiras] for optional generated +# kernels. That compiler path requires CUDA 13.2 and an R580 driver. Heartwood's +# qualified runtime uses the vLLM and PyTorch CUDA 12.9 wheels, so the CUDA 13 +# compiler/runtime packages remain excluded until that complete stack is +# separately qualified. +cuda-tile +nvidia-cuda-crt +nvidia-cuda-nvcc +nvidia-cuda-runtime +nvidia-cuda-tileiras +nvidia-nvvm diff --git a/images/gpu/vllm-overrides.txt b/images/gpu/vllm-overrides.txt index 5dcf45b3..ec17ea35 100644 --- a/images/gpu/vllm-overrides.txt +++ b/images/gpu/vllm-overrides.txt @@ -4,10 +4,7 @@ # # SPDX-License-Identifier: MIT -# vLLM 0.10.1.1 pins vulnerable xgrammar 0.1.21. Version 0.1.32 is the first -# release patched for GHSA-7rgv-gqhr-fxg3 and retains the required API. -xgrammar==0.1.32 - -# vLLM's legacy HTTP dependency graph permits vulnerable idna releases. Version -# 3.18 includes the complete GHSA-jjg7-2v4v-x38h and GHSA-65pc-fj4g-8rjx fixes. -idna==3.18 +# Keep optional kernel packages on their base dependency sets. CUDA 13 extras +# are outside the qualified Heartwood runtime. +nvidia-cutlass-dsl==4.5.2 +humming-kernels==0.1.10 diff --git a/images/gpu/vllm-requirements.txt b/images/gpu/vllm-requirements.txt index 49aeb0f4..6b0e93e2 100644 --- a/images/gpu/vllm-requirements.txt +++ b/images/gpu/vllm-requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --python-version 3.12 --python-platform x86_64-manylinux_2_28 --generate-hashes --emit-index-url --exclude-newer 2026-07-15T00:00:00Z --override images/gpu/vllm-overrides.txt --output-file images/gpu/vllm-requirements.txt images/gpu/vllm.in +# uv pip compile --python-version 3.12 --python-platform x86_64-manylinux_2_28 --generate-hashes --emit-index-url --exclude-newer 2026-07-20T00:00:00Z --override images/gpu/vllm-overrides.txt --exclude images/gpu/vllm-exclusions.txt --output-file images/gpu/vllm-requirements.txt images/gpu/vllm.in --index-url https://pypi.org/simple aiohappyeyeballs==2.7.1 \ @@ -141,14 +141,66 @@ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic +anthropic==0.117.0 \ + --hash=sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec \ + --hash=sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496 + # via vllm anyio==4.14.1 \ --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e # via + # anthropic # httpx + # mcp # openai + # sse-starlette # starlette # watchfiles +apache-tvm-ffi==0.1.9 \ + --hash=sha256:01ebb1308b2666c206aa9a4015eb48f03a5d98ea2e9cfb002bd5e2ca0b9c7ef3 \ + --hash=sha256:076ae2b4bb1dbc5bd3ad8cb5699460fb87c06c0d320659b75a2d651f3f090525 \ + --hash=sha256:0e326ff3ccea0f9b03a7270ab5203ebed9c4cbfc55b6c2a77610bc2fe98ee7b7 \ + --hash=sha256:1401693fc2bd4504878e19c4e87910c46029954f63890a48cc80dfc21f8db31e \ + --hash=sha256:1c8dd4018420c0d14bace688594710909ce198056ff8ac2ad1cd462b30fe1bdd \ + --hash=sha256:21365abd2a2a1a6d3b4e6e4f048309651125becfa795440c3607f3cc27d30ac7 \ + --hash=sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46 \ + --hash=sha256:3ba2b01f7eb2d9bd7e243d48c74fd93387b236fa8ccae27ba14bc01f423e4ce3 \ + --hash=sha256:3c0581dd6bfbce7b017ef85cfda08bbe38891cc4b3afbcfaa8bc2d383728e426 \ + --hash=sha256:429be745bc6a9301cb48f16f97c3f07723e959cf7094b9d571a07a66284ae244 \ + --hash=sha256:44e7967fa0c6493b6f81756688409bc465af620b1731e4940f0ad70d0e8a7ce6 \ + --hash=sha256:49e52350b0470654847de752e65603b604a4d3323e7e9f5e8a982f44acc4c143 \ + --hash=sha256:6845535b79e089743befe1e67c6cf8687a8aa548c23ab3e73228e2c656d0b007 \ + --hash=sha256:6f16d73a82a9e68a439b7d233d48b1b929be17fe92df4bbf1ee2274e573144a3 \ + --hash=sha256:707edfb9c1a50534a14527b341fbd496a6c0f9f3b209e51faafc81fd2aab2923 \ + --hash=sha256:71fe51ca8993baed194d0506b91e6266e6abde5550d65dfff2324f569ee7f37d \ + --hash=sha256:7445d767c8561adfe81c4697e1781269f8e84e001b86e153f916e3c77210d55e \ + --hash=sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a \ + --hash=sha256:7dfa14be2a49347791ef21222a8225ce7f99bfec17104a676cb4f1bf3a107088 \ + --hash=sha256:7f6bc8846d570b8ce38692fc91b530b44cd6ae092c805a844da23970e81b12c0 \ + --hash=sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622 \ + --hash=sha256:932d94e29595a47109f0ef6e0b4209a934451582954ea8b426e758d6b3e307e3 \ + --hash=sha256:9ee710a9fba3d9ff9747870bbd7e2175eb8d5b9c791f17fd645f35f6dab3f8aa \ + --hash=sha256:a42d7ca27dce83efbdce7ec970fe3e773a69c31d928730ee5d9badb1229d106c \ + --hash=sha256:b4133c7c8f31ce7dd0fa8278f17bc9c8e72efd8aee0be4e12312330eb4639236 \ + --hash=sha256:c0449fc3802987c3652bea266ffda2934a6f69c80bba791a3f55b91040656a18 \ + --hash=sha256:c3349f72ddb8ce206472d0380a729f213017a2180707096f8d57114b81097dd1 \ + --hash=sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b \ + --hash=sha256:d1f4d2b7ec7b1213632e9a104e9330bfc3dec48decffa62114c33aa188c9f43a \ + --hash=sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530 \ + --hash=sha256:d911cbbc83bf12a0d9ec03e5315ff1bb92d95702fe912cd7a050393274382e71 \ + --hash=sha256:dd58da01331826fbe6c064d6f0c9bbc2d62883b78df8d15baa8ea21d37507e4d \ + --hash=sha256:e0fc70020d5ae953c04ae279272df7647eaf16fc988c9c5783aa550102942d1d \ + --hash=sha256:e4f01d16ba53fe118e363f7257253f07003797e4abe6fc9567f23b6a930dbff2 \ + --hash=sha256:eefcd17f61bf503ff0f4ad429e03ef6c241c7d13682f58281d883218b854c9bd \ + --hash=sha256:f3ec9149f207a7af3ea3531cad7a0b0d04ded06df4f51a547479d5eb489428dd \ + --hash=sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65 + # via + # flashinfer-python + # quack-kernels + # tilelang + # tokenspeed-mla + # vllm + # xgrammar astor==0.8.1 \ --hash=sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5 \ --hash=sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e @@ -322,10 +374,10 @@ cbor2==6.1.3 \ --hash=sha256:f291a0ae4c1ed96eadb0afa9752568c7424f7d6fa818676d5e33005fcd22ddd9 \ --hash=sha256:fcec149218bacf1f98caf44225b67b4908e54e0440ca446ea488aac1ddc85a3b # via vllm -certifi @ https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl \ +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db # via - # -r images/gpu/vllm.in # httpcore # httpx # requests @@ -431,7 +483,7 @@ cffi==2.1.0 \ --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f - # via soundfile + # via cryptography charset-normalizer==2.1.1 \ --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f # via requests @@ -439,38 +491,106 @@ click==8.4.2 \ --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via + # flashinfer-python # huggingface-hub - # ray # rich-toolkit # uvicorn cloudpickle==3.1.2 \ --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via + # tilelang + # vllm +compressed-tensors==0.17.0 \ + --hash=sha256:15c20d06bdbcf35b51fc99fd125e7b9be1e1855567c33b7a46dfac26ad6fb126 \ + --hash=sha256:4a1b89b508f7efb8ffb4eee8a6e69e0452d9b080cae130146025c64fbe9fa9aa # via vllm -compressed-tensors==0.10.2 \ - --hash=sha256:6de13ac535d7ffdd8890fad3d229444c33076170acaa8fab6bab8ecfa96c1d8f \ - --hash=sha256:e1b4d9bc2006e3fd3a938e59085f318fdb280c5af64688a4792bf1bc263e579d - # via vllm -cupy-cuda12x==13.6.0 \ - --hash=sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447 \ - --hash=sha256:4d2dfd9bb4705d446f542739a3616b4c9eea98d674fce247402cc9bcec89a1e4 \ - --hash=sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a \ - --hash=sha256:6ccd2fc75b0e0e24493531b8f8d8f978efecddb45f8479a48890c40d3805eb87 \ - --hash=sha256:771f3135861b68199c18b49345210180d4fcdce4681b51c28224db389c4aac5d \ - --hash=sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48 \ - --hash=sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1 \ - --hash=sha256:9e37f60f27ff9625dfdccc4688a09852707ec613e32ea9404f425dd22a386d14 \ - --hash=sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af \ - --hash=sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59 \ - --hash=sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44 \ - --hash=sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e \ - --hash=sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c \ - --hash=sha256:e78409ea72f5ac7d6b6f3d33d99426a94005254fa57e10617f430f9fd7c3a0a1 \ - --hash=sha256:f33c9c975782ef7a42c79b6b4fb3d5b043498f9b947126d792592372b432d393 - # via ray -depyf==0.19.0 \ - --hash=sha256:040b35fc0997d49df024b7d094f2a7836f91e9ed02f49982dd37e70aa3285ad5 \ - --hash=sha256:afed0916b32d141cc90fa6220df01885eda442ca43b297d5050eeb90b4a5cb44 +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via pyjwt +cuda-bindings==12.9.7 \ + --hash=sha256:00a833d399b31071fab4cf3de2929840ae462dc4848116eeff033d09219e7116 \ + --hash=sha256:0666d3c082ef8f4b2d670950589373550e9f3bf564d635dd883f24a0b40402ff \ + --hash=sha256:11aeafa2b33995f890086b3fb0f062075176d956e9b6a6fe1a699dddc413f6ad \ + --hash=sha256:16043ef5b15ab88fe9954c5c2061b1d8007591b27f2c916331056de0ebc6187e \ + --hash=sha256:1e2a4f2ec5b67408c04bb4fbed45d214b66de1f00ee2e972865cacb8708d4e1e \ + --hash=sha256:266379e4942051f544a8e7ea1a30ead8d7e8199b6b30fcdc8917cae2bf614e61 \ + --hash=sha256:2dbcd4801954eb3508f4dc2fa0d0c8eb93eb3f45326fd61be2731418c371e7a0 \ + --hash=sha256:3747ea132642416786a8e31bf229032df3a7856911ae5426a7be53d032df183d \ + --hash=sha256:3c089aaf4f5f570ec50244c68f5a2b00a2c9a8e01e04219fd2e36e340be0d88b \ + --hash=sha256:3c47ec1a7a441d91aab32339951df7a1be53451121a12c094bba51467717a35a \ + --hash=sha256:4c7ef48c5e13ae90f3b2ecfb72f8e99ac43c8f4c43e67e1325b8aae331453687 \ + --hash=sha256:59cf4a37b0d662ba15037c9ceebe1a306ebf2c01a8235a09be13cd07094fdb74 \ + --hash=sha256:5bd89dcb78475a6d8a4620ea94b74edf0cbbeacee6d1622d8f94452c1e8d3f15 \ + --hash=sha256:62e245bfa4830d473b038d0d7e5ab9cc1b377a09d2bc9afceaf5c5bab96ab1c6 \ + --hash=sha256:64f7ade7a7a3b69001489753acc21706d9dbda32db8deb68a767a0a0aab30b68 \ + --hash=sha256:775960ac9e530717f3b48e165cc6f68684fa9a4141764fd923e4c1a9820acc73 \ + --hash=sha256:a318075ef3277ca2fdd7df5d4bf671388696b4f2b65e2c4483f1853517692e3b \ + --hash=sha256:c392ffa5010ef4073bfd9dfff4d1ae56032094ed52d3d732014f8e41a73e6b59 \ + --hash=sha256:c6496a88d84b1209d6651b0370c19c26319e157c22f6d018bf9a358cd8049041 \ + --hash=sha256:ece8dfbc22e6de96a26940ab9887eb3cfe1fc1bc3966169391cdb866bb82bb64 \ + --hash=sha256:f95debd2c54c5f087462668511bcb2b1295baa389cd6de9e768212f3cab2bbe2 + # via + # cuda-python + # humming-kernels + # torch +cuda-pathfinder==1.5.6 \ + --hash=sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0 + # via cuda-bindings +cuda-python==12.9.7 \ + --hash=sha256:23a1fc406d491eef7a7e985095725cb7b20a04a7bd9b7a66400e5c86e082e0aa + # via nvidia-cutlass-dsl-libs-base +cuda-toolkit==12.9.1 \ + --hash=sha256:0c8636dfacbecfe9867a949a211864f080a805bc54023ce4a361aa4e1fd8738b + # via torch +depyf==0.20.0 \ + --hash=sha256:d31effad4261cebecb58955d832e448ace88f432328f95f82fd99c30fd9308d4 \ + --hash=sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98 # via vllm detect-installer==0.1.0 \ --hash=sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7 \ @@ -487,15 +607,24 @@ diskcache==5.6.3 \ distro==1.9.0 \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 - # via openai + # via + # anthropic + # openai dnspython==2.8.0 \ --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f # via email-validator +docstring-parser==0.18.0 \ + --hash=sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015 \ + --hash=sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b + # via anthropic einops==0.8.2 \ --hash=sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193 \ --hash=sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827 - # via vllm + # via + # flashinfer-python + # quack-kernels + # vllm email-validator==2.3.0 \ --hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \ --hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426 @@ -505,7 +634,9 @@ email-validator==2.3.0 \ fastapi==0.136.3 \ --hash=sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620 \ --hash=sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab - # via vllm + # via + # model-hosting-container-standards + # vllm fastapi-cli==0.0.29 \ --hash=sha256:05bf08e0e527e3649a50c44bd1e0a2c13575c6cf9a939ff70013f288afc074de \ --hash=sha256:d1140852664a91754da6db4db1e750ace4059f1a21adcf9b161ad4310271a621 @@ -657,85 +788,38 @@ fastar==0.11.0 \ # via # fastapi # fastapi-cloud-cli -fastrlock==0.8.3 \ - --hash=sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40 \ - --hash=sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30 \ - --hash=sha256:05029d7080c0c61a81d5fee78e842c9a1bf22552cd56129451a252655290dcef \ - --hash=sha256:0a9dc6fa73174f974dfb22778d05a44445b611a41d5d3776b0d5daa9e50225c6 \ - --hash=sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62 \ - --hash=sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe \ - --hash=sha256:15e13a8b01a3bbf25f1615a6ac1d6ed40ad3bcb8db134ee5ffa7360214a8bc5c \ - --hash=sha256:1dd7f1520f7424793c812e1a4090570f8ff312725dbaf10a925b688aef7425f1 \ - --hash=sha256:1fced4cb0b3f1616be68092b70a56e9173713a4a943d02e90eb9c7897a7b5e07 \ - --hash=sha256:239e85cbebda16f14be92468ce648d0bc25e2442a3d11818deca59a7c43a4416 \ - --hash=sha256:24522689f4b5311afad0c8f998daec84a3dbe3a70cf821a615a763f843903030 \ - --hash=sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a \ - --hash=sha256:314e787532ce555a7362d3c438f0a680cd88a82c69b655e7181a4dd5e67712f5 \ - --hash=sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd \ - --hash=sha256:350f517a7d22d383f8ef76652b0609dc79de6693880a99bafc8a05c100e8c5e7 \ - --hash=sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4 \ - --hash=sha256:387b2ac642938a20170a50f528817026c561882ea33306c5cbe750ae10d0a7c2 \ - --hash=sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2 \ - --hash=sha256:3e77a3d0ca5b29695d86b7d03ea88029c0ed8905cfee658eb36052df3861855a \ - --hash=sha256:40b328369005a0b32de14b699192aed32f549c2d2b27a5e1f614fb7ac4cec4e9 \ - --hash=sha256:45055702fe9bff719cdc62caa849aa7dbe9e3968306025f639ec62ef03c65e88 \ - --hash=sha256:494fc374afd0b6c7281c87f2ded9607c2731fc0057ec63bd3ba4451e7b7cb642 \ - --hash=sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160 \ - --hash=sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d \ - --hash=sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45 \ - --hash=sha256:558b538221e9c5502bb8725a1f51157ec38467a20498212838e385807e4d1b89 \ - --hash=sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695 \ - --hash=sha256:5a0d31840a28d66573047d2df410eb971135a2461fb952894bf51c9533cbfea5 \ - --hash=sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c \ - --hash=sha256:5eef1d32d7614e0ceb6db198cf53df2a5830685cccbcf141a3e116faca967384 \ - --hash=sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670 \ - --hash=sha256:668fad1c8322badbc8543673892f80ee563f3da9113e60e256ae9ddd5b23daa4 \ - --hash=sha256:6cbfb6f7731b5a280851c93883624424068fa5b22c2f546d8ae6f1fd9311e36d \ - --hash=sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e \ - --hash=sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5 \ - --hash=sha256:7a77ebb0a24535ef4f167da2c5ee35d9be1e96ae192137e9dc3ff75b8dfc08a5 \ - --hash=sha256:80876d9e04e8e35abbdb3e1a81a56558f4d5cf90c8592e428d4d12efce048347 \ - --hash=sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed \ - --hash=sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65 \ - --hash=sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da \ - --hash=sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8 \ - --hash=sha256:924abbf21eba69c1b35c04278f3ca081e8de1ef5933355756e86e05499123238 \ - --hash=sha256:92577ff82ef4a94c5667d6d2841f017820932bc59f31ffd83e4a2c56c1738f90 \ - --hash=sha256:963123bafc41c9fba72e57145917a3f23086b5d631b6cda9cf858c428a606ff9 \ - --hash=sha256:9842b7722e4923fe76b08d8c58a9415a9a50d4c29b80673cffeae4874ea6626a \ - --hash=sha256:9c2c24856d2adc60ab398780f7b7cd8a091e4bd0c0e3bb3e67f12bef2800f377 \ - --hash=sha256:9c4068f21fddc47393a3526ce95b180a2f4e1ac286db8d9e59e56771da50c815 \ - --hash=sha256:a0eadc772353cfa464b34c814b2a97c4f3c0ba0ed7b8e1c2e0ad3ebba84bf8e0 \ - --hash=sha256:a8fd6727c1e0952ba93fdc5975753781039772be6c1a3911a3afc87b53460dc0 \ - --hash=sha256:ac4fcc9b43160f7f64b49bd7ecfd129faf0793c1c8c6f0f56788c3bacae7f54a \ - --hash=sha256:accd897ab2799024bb87b489c0f087d6000b89af1f184a66e996d3d96a025a3b \ - --hash=sha256:b6ac082d670e195ad53ec8d0c5d2e87648f8838b0d48f7d44a6e696b8a9528e2 \ - --hash=sha256:bbbe31cb60ec32672969651bf68333680dacaebe1a1ec7952b8f5e6e23a70aa5 \ - --hash=sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05 \ - --hash=sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f \ - --hash=sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd \ - --hash=sha256:cdee8c02c20a0b17dbc52f54c48ede3bd421985e5d9cef5cd2136b14da967996 \ - --hash=sha256:d3ebb29de71bf9e330c2769c34a6b5e69d560126f02994e6c09635a2784f6de3 \ - --hash=sha256:d51f7fb0db8dab341b7f03a39a3031678cf4a98b18533b176c533c122bfce47d \ - --hash=sha256:d7edaf0071a6a98340fc2ec45b0ba37b7a16ed7761479aab577e41e09b3565e1 \ - --hash=sha256:d7f359bb989c01a5875e8dbde9acab37b9da0943b60ef97ba9887c4598eb3009 \ - --hash=sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c \ - --hash=sha256:da53350b90a67d5431df726816b041f1f96fd558ad6e2fc64948e13be3c7c29a \ - --hash=sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259 \ - --hash=sha256:de8c90c1a23fbe929d8a9628a6c1f0f1d8af6019e786354a682a26fa22ea21be \ - --hash=sha256:e0ceefadde046a5f6a261bfeaf25de9e0eba3ee790a9795b1fa9634111d3220e \ - --hash=sha256:f2b84b2fe858e64946e54e0e918b8a0e77fc7b09ca960ae1e50a130e8fbc9af8 \ - --hash=sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f \ - --hash=sha256:fcb50e195ec981c92d0211a201704aecbd9e4f9451aea3a6f71ac5b1ec2c98cf - # via cupy-cuda12x +fastsafetensors==0.3.3 \ + --hash=sha256:2c2f788a936ffd17938484360339645812e14cf1b2bdf4c18c035c713218e5a9 \ + --hash=sha256:2f15b3d293468f2a6aae54b77781521a5d4f0db49bc6cd655442cca2207b3e78 \ + --hash=sha256:3587bc66b8dec560ad903becf9540889013d4d47f0e10cf45f19bedc7b7bffa7 \ + --hash=sha256:39c252a5528fa8653366f979d2ab8fa81159db4456b7c72cb5c288adb7699079 \ + --hash=sha256:50d8137ce2ca072d1717564c3810d7ccf649eb9f27179f26470258465682bf42 \ + --hash=sha256:7db5f3ee98433cc8e5dcaaef2517f47c34a51ea8229350406b24b2f84af209d3 \ + --hash=sha256:92f8cf8e6617cffe24522023c726c5911c1ca5b7e3249da054abb0fac8d27040 \ + --hash=sha256:94bd5f003bd9a0d7bc0d262c823ed2ccd74ba0396772a8a327fe1c16f86b6e4c \ + --hash=sha256:b61a5f6e0e0b0da591f2790d3fbc6eef7f6e8597087977b946069e7bfbb2ec7a \ + --hash=sha256:ba4fb59be8a6adbc91723848c3c6f57a9a9a5d2247d9768f84511380d01e554c \ + --hash=sha256:bbf9fbc5f74d6a3f333777eb222aa63453795dacd7ae7f559316a5398d4942ef \ + --hash=sha256:be753c669b2e12b744f757303482d95079d48cd7fcce992680b74284b12e6ffa \ + --hash=sha256:db542d1dfa60dc3c610d9750e3d90107e2ef76feed34958ba8b81814969ea168 \ + --hash=sha256:df2f641647b79093c5e3f4bb0b9c7d771f22b61279714c3059d5c71a7cf708da \ + --hash=sha256:e3386834e1595834124cfbb23e05a732cb1b4fb5f98562375c1806f14fe1d677 \ + --hash=sha256:f969b2c818942748ff1bc619e6d575a7a7156a7d488d4c41779fba8c4e1891dc + # via vllm filelock==3.29.0 \ --hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \ --hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 # via # huggingface-hub - # ray # torch # vllm +flashinfer-cubin==0.6.13 \ + --hash=sha256:41e4848c2d09d220e8394489b2fb6cfec6b6ad09f897b5ab8b39fc23055f6c24 + # via vllm +flashinfer-python==0.6.13 \ + --hash=sha256:239e6ddc3cbbaf0bee251861a8c7c69438b1171830d69ddfa133ddea4494850d \ + --hash=sha256:8a6d7d3708c7c87952390ec4e3aabe6e1c356defa8c7211b26bccaa355a61c59 + # via vllm frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ @@ -876,10 +960,65 @@ fsspec==2026.4.0 \ # via # huggingface-hub # torch -gguf==0.19.0 \ - --hash=sha256:70bcd10edfe697fb2dad6e40af2234b9d8ece9a41a99761405121ebda1c3c1cd \ - --hash=sha256:dbadcd6cc7ccd44256f2229fe7c2dff5e8aa5cf0612ab987fd2b1a57e428923f - # via vllm +googleapis-common-protos==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +grpcio==1.82.1 \ + --hash=sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9 \ + --hash=sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438 \ + --hash=sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769 \ + --hash=sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f \ + --hash=sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e \ + --hash=sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379 \ + --hash=sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03 \ + --hash=sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e \ + --hash=sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6 \ + --hash=sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0 \ + --hash=sha256:35f990f7784c8fd2872644f07f96ebb4d9e48e145a190ab80d0280af91a1bfb2 \ + --hash=sha256:38e9dcb5258226fb3282630b31b16a968df52c8c6ad514af540646e0a4578f8a \ + --hash=sha256:3d21f19838dc255ecbb79321b15ae9b98fbddff4c3d4aedb0a81bdd7f4ab572a \ + --hash=sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90 \ + --hash=sha256:3dde70abfc80b3be11de53ba0d601c439e7fb2afd3583ad1788d1146bec92fdc \ + --hash=sha256:46536a4a1f4434df3c851b9254ff6fc7df5705b273681a15ca277d5921c178a0 \ + --hash=sha256:4c816180e31e273caaec6f8bd86a8392499d5bbb26f41da44e3dce48bde69095 \ + --hash=sha256:50fd2fe83426b1b1c6cdc4d72d555223b7dddf8ce07c5bac218b13fc6d684c6f \ + --hash=sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2 \ + --hash=sha256:5e171d5f0d6a0af78ea7512783f170a44f80c165259d8773e3a354a7f991f2b5 \ + --hash=sha256:5e5c4dc0a59b0f8490a6bdfd6fc8395b9d8ad8a8407c7d67ca7b5bba15c0877f \ + --hash=sha256:60b0f2c95337694fc094b77d9f60f50566c84b5677393e342eb98daeee242d98 \ + --hash=sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69 \ + --hash=sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5 \ + --hash=sha256:7d7ede11d747b4e1bd05e3bc0260e155b65a88735a895a10f6521f19b889511e \ + --hash=sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17 \ + --hash=sha256:9d76152d7c31d7210d4a106e5d8b64da5bba5d6abf11be30e2f7b0a0c59bbcbf \ + --hash=sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f \ + --hash=sha256:b064fc444812bdaa9825d33c26f8d732d63ee6a5d78557c1faf92c98687fed27 \ + --hash=sha256:b454a2d97bfab7565683a02345f86bd182ab69fd7c2bdb7414171e7538f266b1 \ + --hash=sha256:b758540a24d5394a9c578bf9f6126389f474b106ac3d9df1d53de56cb14c9fd9 \ + --hash=sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197 \ + --hash=sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560 \ + --hash=sha256:b8e110c66df5204c0506d6c8787b35d48b8b699ef5aa366d6c4d67325c67fe9a \ + --hash=sha256:bd2f45e46fff5b91c10997d0743a987517a7dde67c64c592835c2dcaac66f587 \ + --hash=sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58 \ + --hash=sha256:c40d94ba820329cc191981bc22fa6f6eed0799c6d921f3c6709521d59d4a2fd7 \ + --hash=sha256:c4ba4aac238f685743575d9d700003ac16537cce26e7c774993134f530652464 \ + --hash=sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31 \ + --hash=sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b \ + --hash=sha256:d6650a7c1ebb7921c70e12a385439a8118efb99e669fa9ed31cf25db1843937c \ + --hash=sha256:d6e8a08f7038ba7a77f71e250804e4aba84fe91d22cfc54ff43c07b7529c4728 \ + --hash=sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac \ + --hash=sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4 \ + --hash=sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074 \ + --hash=sha256:e20f1edbb15f99e3128ec86433f9785fd5a451d8f115e74fe0056134f092a9d5 \ + --hash=sha256:e31fd780b261830720cb70b0fd8f0aa51d49e75a66d7464ad2e31d4b765f2580 \ + --hash=sha256:ed6fc621d6f366c88a60f0b971d5afd21d441d9aa561ee688de5b7acdb2cf901 \ + --hash=sha256:f5523099c98c292ea1ae08e617249db760c56a78f8deae879027fe7d1ffbcbf6 \ + --hash=sha256:f853eae07235a51a27bb5d6a9a175a59ca55dc9b99edc6ce2f76f07332d333ae \ + --hash=sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d + # via opentelemetry-exporter-otlp-proto-grpc h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 @@ -973,26 +1112,131 @@ httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via + # anthropic # fastapi # fastapi-cloud-cli # huggingface-hub + # mcp + # model-hosting-container-standards # openai +httpx-sse==0.4.3 \ + --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ + --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via mcp huggingface-hub==1.23.0 \ --hash=sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2 \ --hash=sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88 # via # tokenizers # transformers +humming-kernels==0.1.10 \ + --hash=sha256:4ded0998ff085afeddde70baf93f97c2929969ec3d4a63a52cfec5072bc972b4 \ + --hash=sha256:da3e46fb9fc9eba2a9327c2e8135ead68e390c955acd7449f97ee7c71666c8b1 + # via + # --override images/gpu/vllm-overrides.txt + # vllm idna==3.18 \ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via - # --override images/gpu/vllm-overrides.txt # anyio # email-validator # httpx # requests # yarl +ijson==3.5.1 \ + --hash=sha256:05eba5268a38809ba1c3dbfa44ea67336e2c353fc11768acc9c6442fe0ccac50 \ + --hash=sha256:0663f718c6123899c6bfd9c449ec195cd8c67666b7ea2c7b36fa0cc0dcb13e17 \ + --hash=sha256:077b1b0bcb6a622d460c6674fe6647c7af5a3b06503e1996d1efcf9f78c94512 \ + --hash=sha256:0a682954b60fcd0c23d504df6fb1ebde051305e41c9b350f39a3b8bfb168def7 \ + --hash=sha256:0ade373dd765b057b1dec05d7711bfeb5a36f1e825259466d9f545cfd8ef3ba3 \ + --hash=sha256:0b184180d45f85fd4479659582749b109e49f4a29c21ac700ccc9c2280fe015e \ + --hash=sha256:0d7c5025a820f36f3e0e64f4b0232b338c690664c12b497e205cf64dcc64fc12 \ + --hash=sha256:11c1d7d36a13054b5872ecd5d745dc4009d9abdbcba2312de69e66c2f92a46d2 \ + --hash=sha256:12aa7fcf46f0fdc8e9e7cf37541e1dc20ac3f9243a23f4d346ab5395f72b0fe2 \ + --hash=sha256:1321495807dcdaca002cb45f24033208ce1d9f5ffc0c5a5584c5f466d0dcbbd5 \ + --hash=sha256:1356bca96d015948b601b013defb2d5631e4330e8f5880e4d7c933d472a90c34 \ + --hash=sha256:170cc4c209f57decc9b7ee5fd340f2a1602d54020fa222846482ff1c99e88fdc \ + --hash=sha256:1a38d503ce343952e88edfd9a27296a4ec96af7073a9db58b3df6233367f75fc \ + --hash=sha256:1a680122d0c384381f26ef3b89bdda0154f47c2571eb6e503571630aa2bb143d \ + --hash=sha256:1be3a586c8821ecab9ea8b256f39305c8a0cc33222fe393bcc1fb9221470732b \ + --hash=sha256:1de3de278b0ffb40338374ad2a730e1c56f933e0706b1815ebeb07b82239b1a3 \ + --hash=sha256:21e1a250b254edba2f0dd7272a4c56f0a879aabe328d9e306dd1fc115f560e74 \ + --hash=sha256:2699e838099d056818c5f8e4ba702b345d0304e58847bdc79c5c1616d5d750a5 \ + --hash=sha256:292648aa123904d4b40ae50cac21840123b8c2cf36a2c1d0620859581ceecdd2 \ + --hash=sha256:29eb8f0c77a296a10843a1714ad4a5d561e604cda3c88585e9012cf2c1729b0a \ + --hash=sha256:2aa9d0cf21d4de89fb633e5ec27e9ad02c3f9a4ffa3940d120b23b8aed3acffc \ + --hash=sha256:2f41982c73896acab4a2a14faa14e152e444bd69f37c3139204429fd3fe65a10 \ + --hash=sha256:3060b141ef758be3742315d44476109460c265b88247e3a4e479949f8b134eac \ + --hash=sha256:322c783f3ee0c6b383bbd4db88370b10172168808cc2a0bf811f1253f7435602 \ + --hash=sha256:32f64051be2f990d8ae7b614b5abdf4a7bead510ce3666568d7403c6c46ce4d8 \ + --hash=sha256:3321fede2b638d400de0036889a3a25c3bb689feb8df45e70a393346aad6194f \ + --hash=sha256:350caea815e53151994b597abc80cf669454276b5ac6aadcec69ef6d48f7e90b \ + --hash=sha256:3ab6378d9c19f01f206f27f762837ad3979330cabd7864e1b17934c03de6056c \ + --hash=sha256:3c0556d628443d3e871f414855313b2ae6cd9faa0104de3316bd8db03aab1589 \ + --hash=sha256:40ddd236c80a667dd6a1f6b625d18ddac68b8719ff795761b7542f2e1f78e4a4 \ + --hash=sha256:42bfda7858d99ee9777ec28cb6d347928249eefeb577f9b0a67503c18f7ebb6a \ + --hash=sha256:451901c36e12fa87cbb1cafe661bd25c08c6bd7900cc738279614f71cea07048 \ + --hash=sha256:4b75b6bf4b0dbb0df24947db6722cd5723ce8d6e6b13fddbfc98db312ba82237 \ + --hash=sha256:4e99de6fd49b44a05eeaadc857e443a9235c2a2057c4e66809e8b2dced31d2a4 \ + --hash=sha256:534a6c1a9da92a3755bfa6a1024995e840335ad5994c8f2d1f38623ba54ede4f \ + --hash=sha256:539e8d6cca079bcbb68c390e55148f908e0a943a34f7dd321248637c6272adca \ + --hash=sha256:65974568748678165d7e90e3e7ce2f7c233cfe4de6c37fbb0760941c97e14632 \ + --hash=sha256:69b5eef70240e9734c5a2fb5cc3742cae411fc833a66b9a50722b9eedb1e27de \ + --hash=sha256:69d5b74760cb50588e21bfab710a16d89e5b2f0a8fbd9594ad750fd7773a0a7f \ + --hash=sha256:6d581a071dae8dbee61f8d962e892787707bad6e641e2f6fb30dd89d3e896939 \ + --hash=sha256:6ee1e6d59c800aa819952f6cb5ff08707ecd576b29cc9c3d00e33c2b371a92ce \ + --hash=sha256:70542d4542f079c394e525559188d69e3ccfbfd9bab899acd0bf1dbc7323ddd5 \ + --hash=sha256:77b68e91f95fb16ac2e7819903cd545db6cffa308c28833cc34911e6b21e91dd \ + --hash=sha256:85997568d6b304cfa59d5c3f2b04f95b92e9a8c7f57d312343a7989cf8dfff85 \ + --hash=sha256:882bc0bdd25d41eae90a15695cd50707edde0978b8b72a2532e30442dd8fd04c \ + --hash=sha256:8b4ed62287feee41b90b55ae2800ef56d6bdfd2fbfa02b4fd0634cd4524bc995 \ + --hash=sha256:8cb5db5bc122da64efb24ce358752d5e097ab41d224ce2992536a0f9073fe4fd \ + --hash=sha256:904e8cf9ca69f5de5b6bb405a4a075ce3da3413ad50c11f6813f1201e14a8e45 \ + --hash=sha256:936f28671f018f8ac4d3f003ae9fa01d0467ab4ef4cfd0c97f23beda485b61c6 \ + --hash=sha256:94a95065b1ac67602af0cec852b07505abc37b77e3774d1c801d935d05e48f82 \ + --hash=sha256:94def0c5f9997bdc6c2f923c9fdd15e400c901979156bea3c255622db7a43f8d \ + --hash=sha256:9708c0a3d1f86056049de631933aef8ec57f2008d4cb55ce241790c7ed557428 \ + --hash=sha256:9a0b25c750a6bde14a0b31f1dcbfc86368e50767e3eaa73bb138e54128055edd \ + --hash=sha256:9c077fad5420f52cfdc906a7dffa622cb9d55c21f3bf0b4e756c6354d800598d \ + --hash=sha256:9f8c4c673d00115ced7422b6e67ae5e6ffc46ae53195877fd66932a6197decae \ + --hash=sha256:9fac9284d62c4317d541274e15a6a6ab6f6d22561579f6570967e3a6eaafaebc \ + --hash=sha256:a19413a092d458a57aaa574fec08e265851d3b5c6e018377f426cd5e70b91280 \ + --hash=sha256:a889228d3c287ef273c7b55177395de64abcf4950b637744dee928685bbb5760 \ + --hash=sha256:a96066d8c12a18ce2fa90579f2bbf991377cb71725874932e4a5d855226c162a \ + --hash=sha256:a96ab35d7ce2129dfde49c4c807596443410e260d7f7a4ca8fe4d0035553b589 \ + --hash=sha256:aa7a2c94e43c02e0482088e6ff997e2bd7b9a76e6f1d0fd70891b4b5ff51318f \ + --hash=sha256:abd724af41688035719b9f39a926876b9810808947421999b2dc6db34944a4e6 \ + --hash=sha256:af40bd1a85f55db0b8b30715c858761306bd92d5590148636f75c3309e6e76bd \ + --hash=sha256:af6ddbd10ac9bce87a835f2de3ec61455ec435c54e7e0ba7b17c31c66de6f164 \ + --hash=sha256:affb85eb75fa03a21d1f790bbf26a0e66e5701672062a30dc5c3c6a29c5c0a63 \ + --hash=sha256:b70b5da6b0571da8f601a437c4fba2d35bc27739637d85f3acdc8f88916ce68e \ + --hash=sha256:b9517efbe6604bce16f3e50d49b0cd1bdc58917f98cf2eab026599c5c0422991 \ + --hash=sha256:bad5d55c99c89de8cd0a4cded51f86427ba3353c4dccca37ec2e32e06f26b437 \ + --hash=sha256:bc0ed6a336d11b9311171eebd7a8467077291bc61b03de89ae7249bba5fa70ce \ + --hash=sha256:bc16d618a0a8f7a78735acd14628fd9f66bd4dbe80db3c522a51bee3200eb720 \ + --hash=sha256:bd756f7b22df745ac14b7bc2ab9ed7c190a222e4c8e1bef26ef1162af8e54d0f \ + --hash=sha256:c2b83b24be73f0c7a301807a4c3081939524421c7ae1556eb6eac7cff50ddfa7 \ + --hash=sha256:c2e2509dc7f2fa5a2ac9ba7d15dd901f4093bd36b0784f65e04b681b7956651c \ + --hash=sha256:c388f85cbb9eec022b2bdedd23ffacfe7ab100c1200b1f47bee6e6ea2c3309fa \ + --hash=sha256:c4b9a28e9719d1aebebe93ad8dc2ba87f4e2d9035043b196c1c07ef8530b44cc \ + --hash=sha256:c8a36a19b92cb7172c6448ab94f446033cfa3129dc4894aebe205f96b3fabf42 \ + --hash=sha256:cae04eff4006fc36bf0b030b38e2646a97092d87d933d20cfe7262e26ed32321 \ + --hash=sha256:cd0dfc5a788d0b0c2f1eab258b9dabdeefc631ca8ef87644a999f633b0b2555a \ + --hash=sha256:d78f362f51c8691798758a9e6ac3c9d385ee1228cb82987c91562a2fae235cd3 \ + --hash=sha256:e01f95433725e2df62d682ff88e4a57bb694385ff2362bc364adec961167ae04 \ + --hash=sha256:e035cdfb2a1446b13881f0dfc0eecd1541cbb17a27a938ded2160ae6ce25051b \ + --hash=sha256:e2ac204b59f09e38e16d277f906240e9fd38780e42076599419265af183dc4b4 \ + --hash=sha256:e353891d33a2e6aa5caf72c2a5fbadd7a46f5f9b32dcfd0c84113b2444c255b8 \ + --hash=sha256:e3c5f660658f2ebfba5d4dfe4bafe8cd3a0defcda410ec08d2205fe08c398940 \ + --hash=sha256:e4fcebfe1685bb7ba06a8255a5d428ea6b4b895d7acf979cb637d8bbc9db2f47 \ + --hash=sha256:e6cf9e49902f28af7a2e2f8b35c201195c0f0d5c170a5786e0c0a1b8492a4e37 \ + --hash=sha256:e8dbf71b21e65cb7f0d4d387c07fe73be820168070c3be05a0763a80f424f1c7 \ + --hash=sha256:ea4fd7bec203a600b1cc88a492dfe6b75ce4b1b87488a66adcd5406022213f64 \ + --hash=sha256:ee60c7741012671867678eae71c51872cac938b76f3d4ca40a778e6c361774d2 \ + --hash=sha256:eeb2fb2daa5dd30326f93db465d0855b34aa6b1f52a7c0ff94522aec5ad57dfb \ + --hash=sha256:ffba9bce60be21b496afc67a05ab8e3f431f87f0282fd6ce3c62004c951a1428 + # via vllm interegular==0.3.3 \ --hash=sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c \ --hash=sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600 @@ -1002,6 +1246,7 @@ jinja2==3.1.6 \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via # fastapi + # humming-kernels # torch jiter==0.16.0 \ --hash=sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536 \ @@ -1109,13 +1354,20 @@ jiter==0.16.0 \ --hash=sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a \ --hash=sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e \ --hash=sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de - # via openai + # via + # anthropic + # openai +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 + # via model-hosting-container-standards jsonschema==4.26.0 \ --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via + # mcp # mistral-common - # ray + # vllm jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d @@ -1124,43 +1376,59 @@ lark==1.2.2 \ --hash=sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c \ --hash=sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80 # via vllm -llguidance==0.7.30 \ - --hash=sha256:00a256d532911d2cf5ba4ef63e182944e767dd2402f38d63002016bc37755958 \ - --hash=sha256:05234ecceea7c9c6ff13b9739112043173a3bcb88cae860249b20335a07b3075 \ - --hash=sha256:9edc409b9decd6cffba5f5bf3b4fbd7541f95daa8cbc9510cbf96c6ab1ffc153 \ - --hash=sha256:a0d52b8d1b2d3b0e661e3f953ecccfa16644f302026b3067a4815c1baa2ae643 \ - --hash=sha256:af8741c867e4bc7e42f7cdc68350c076b4edd0ca10ecefbde75f15a9f6bc25d0 \ - --hash=sha256:c80af02c118d2b0526bcecaab389af2ed094537a069b0fc724cd2a2f2ba3990f \ - --hash=sha256:e93bf75f2b6e48afb86a5cee23038746975e1654672bf5ba0ae75f7d4d4a2248 \ - --hash=sha256:f4a327a30dd37d86dd6347861ac8de3521fc1dbef9475296c06744e5b40ffc54 +llguidance==1.7.6 \ + --hash=sha256:0444020249cde1292f13acf786e35c245fd3572d466877d2734824a9026e55aa \ + --hash=sha256:0fda51daa7951217ca164f735e96a1929d9aefb804a0b28ee43b16173e1c7325 \ + --hash=sha256:1158cfce353d331859054aad80a5543167da8b45e01c18f93272027a155df449 \ + --hash=sha256:30be5939340f008b5093286f0bbbb9804f58e292ecca5f8b144823d43ff5068b \ + --hash=sha256:4e4f2a489c1c3943bb1b3c206b45794153cb6954f45cd3de8e02198319ddc6b1 \ + --hash=sha256:7def42f7866239b3b940982ed1dcae6b142c212fbd68b57107c1560d778f94f8 \ + --hash=sha256:9c54c899db8cb4b4fba128a7d844730066576c70d806c95ada92b2bd2d6ab498 \ + --hash=sha256:ace7e81cd31950a87186356ab24bd7f75fbc10a05ca9d9f7f8748f931963f763 \ + --hash=sha256:c88787845b94d301d91c4e9ad27eac9d05c334a1ba2c7ff29cca66f26d5b5c3c \ + --hash=sha256:ceec951d29a74309984e3be0fe7f5f56c1362434cd937abd517b259a60908b1e \ + --hash=sha256:d0e1f5402bbc2688bc790d56995f0263978b55771493fceddc09b805dacc83b6 \ + --hash=sha256:db7febbe412ed2015501904646750071d7e00e6df7f85c4b956ad4f206fd2df7 \ + --hash=sha256:e70fa25ed550c2b50c2fd70baa9e2808b4ecb859d01e453bd5459aff62ba38c3 \ + --hash=sha256:e9f68206e0f3f89aceabb90aa1f8ed570db22fb7cb1fd9ebf96fa7727a65af55 \ + --hash=sha256:eabf4572c8731734c0444c353b9ea06bc5c156986d2ff0a4ec0499159271381f \ + --hash=sha256:ef907a562d91f32e13cb3131ee5e1574b9ba5beac5bceedd795f8316a16d94d6 # via vllm -llvmlite==0.44.0 \ - --hash=sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4 \ - --hash=sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad \ - --hash=sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930 \ - --hash=sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516 \ - --hash=sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408 \ - --hash=sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2 \ - --hash=sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf \ - --hash=sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db \ - --hash=sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8 \ - --hash=sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e \ - --hash=sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614 \ - --hash=sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc \ - --hash=sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427 \ - --hash=sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9 \ - --hash=sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1 \ - --hash=sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791 \ - --hash=sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d \ - --hash=sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955 \ - --hash=sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1 \ - --hash=sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3 \ - --hash=sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610 +llvmlite==0.47.0 \ + --hash=sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077 \ + --hash=sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8 \ + --hash=sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea \ + --hash=sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb \ + --hash=sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f \ + --hash=sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17 \ + --hash=sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd \ + --hash=sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc \ + --hash=sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453 \ + --hash=sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94 \ + --hash=sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40 \ + --hash=sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07 \ + --hash=sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c \ + --hash=sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063 \ + --hash=sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec \ + --hash=sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e \ + --hash=sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327 \ + --hash=sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb \ + --hash=sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca \ + --hash=sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa \ + --hash=sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14 \ + --hash=sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7 \ + --hash=sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8 \ + --hash=sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7 \ + --hash=sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b # via numba -lm-format-enforcer==0.10.12 \ - --hash=sha256:130bd7ce8a6b224f25b6314ba9ae78ee4b48594db1767c74391c9182e2902a6c \ - --hash=sha256:267c2b421c77f7cd51ac2e0e3af8db278a373704d834b49ff55f18a2c05e9800 +lm-format-enforcer==0.11.3 \ + --hash=sha256:cf586350875def1ae7a8fba84fcbbfc8371424b6c9d05c1fcba70aa233fbf06f \ + --hash=sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da # via vllm +loguru==0.7.3 \ + --hash=sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6 \ + --hash=sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c + # via compressed-tensors markdown-it-py==4.2.0 \ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a @@ -1256,6 +1524,10 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 +mcp==1.28.1 \ + --hash=sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df \ + --hash=sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683 + # via vllm mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -1264,78 +1536,55 @@ mistral-common==1.11.5 \ --hash=sha256:7c1b09f43a589027315840bfd6f3528d5abf520eed701f8d8b7a922d6e5b855e \ --hash=sha256:ef8c03ad8359fa1386d66ee08d534d8f4a65a6955c9b24bd5caa2e005066cdec # via vllm +ml-dtypes==0.5.4 \ + --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ + --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ + --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ + --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ + --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ + --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ + --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ + --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ + --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ + --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ + --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ + --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ + --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ + --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ + --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ + --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ + --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ + --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ + --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ + --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ + --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ + --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ + --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ + --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ + --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ + --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ + --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ + --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ + --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ + --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ + --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ + --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ + --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ + --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ + --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ + --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ + --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ + --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ + --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 + # via tilelang +model-hosting-container-standards==0.1.16 \ + --hash=sha256:47f4f65713120bc3a69feb022981a38db9e557aedf88dbd72077f20588caa12b \ + --hash=sha256:d34589633900e53c3ee5f7c78280a7cf7e4f6532c35e763341a262fc85cbe84a + # via vllm mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy -msgpack==1.2.1 \ - --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ - --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ - --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ - --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ - --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ - --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ - --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ - --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ - --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ - --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ - --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ - --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ - --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ - --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ - --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ - --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ - --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ - --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ - --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ - --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ - --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ - --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ - --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ - --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ - --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ - --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ - --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ - --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ - --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ - --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ - --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ - --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ - --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ - --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ - --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ - --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ - --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ - --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ - --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ - --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ - --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ - --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ - --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ - --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ - --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ - --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ - --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ - --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ - --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ - --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ - --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ - --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ - --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ - --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ - --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ - --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ - --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ - --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ - --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ - --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ - --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ - --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ - --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ - --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ - --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ - --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c - # via ray msgspec==0.21.1 \ --hash=sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca \ --hash=sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664 \ @@ -1561,29 +1810,35 @@ ninja==1.13.0 \ --hash=sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1 \ --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e - # via vllm -numba==0.61.2 \ - --hash=sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2 \ - --hash=sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60 \ - --hash=sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154 \ - --hash=sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd \ - --hash=sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b \ - --hash=sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8 \ - --hash=sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7 \ - --hash=sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546 \ - --hash=sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e \ - --hash=sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1 \ - --hash=sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140 \ - --hash=sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d \ - --hash=sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18 \ - --hash=sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9 \ - --hash=sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642 \ - --hash=sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18 \ - --hash=sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2 \ - --hash=sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab \ - --hash=sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a \ - --hash=sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd \ - --hash=sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2 + # via + # flashinfer-python + # vllm +numba==0.65.0 \ + --hash=sha256:032b0b8e879512cd424d79eed6d772a1399c6387ded184c2cf3cc22c08d750a6 \ + --hash=sha256:05c0a9fdf75d85f57dee47b719e8d6415707b80aae45d75f63f9dc1b935c29f7 \ + --hash=sha256:15d159578e59a39df246b83480f78d7794b0fca40153b5684d3849a99c48a0fb \ + --hash=sha256:194a243ba53a9157c8538cbb3166ec015d785a8c5d584d06cdd88bee902233c7 \ + --hash=sha256:28e547d0b18024f19cbaf9de02fc5c145790213d9be8a2c95b43f93ec162b9e4 \ + --hash=sha256:46fd679ae4f68c7a5d5721efbd29ecee0b0f3013211591891d79b51bfdf73113 \ + --hash=sha256:4c894c94afa5ffd627c7e3b693df10cb0d905bd5eb06de3dfc31775140cf4f89 \ + --hash=sha256:5046c63f783ca3eb6195f826a50797465e7c4ce811daa17c9bea47e310c9b964 \ + --hash=sha256:583680e0e8faf124d362df23b4b593f3221a8996341a63d1b664c122401bec2f \ + --hash=sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5 \ + --hash=sha256:6254e73b9c929dc736a1fbd3d6f5680789709a5067cae1fa7198707385129c04 \ + --hash=sha256:71e72e9ca2f619df4768f9c3962bfec60191a5a26fe2b6a8c6a07532b6146169 \ + --hash=sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f \ + --hash=sha256:add297d3e1c08dd884f44100152612fa41e66a51d15fdf91307f9dde31d06830 \ + --hash=sha256:af143d823624033a128b5950c0aaf9ffc2386dfe954eb757119cf0432335534c \ + --hash=sha256:af96c0cba53664efcb361528b8c75e011a6556c859c7e08424c2715201c6cf7a \ + --hash=sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa \ + --hash=sha256:b7325b1aab88f0339057288ee32f39dc660e14f93872a6fda14fa6eb9f95b047 \ + --hash=sha256:b8a9008411615c69d083d1dcf477f75a5aa727b30beb16e139799e2be945cdfd \ + --hash=sha256:c6334094563a456a695c812e6846288376ca02327cf246cdcc83e1bb27862367 \ + --hash=sha256:c7fa502960f7a2f3f5cb025bc7bff888a3551277b92431bfdc5ba2f11a375749 \ + --hash=sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e \ + --hash=sha256:dff9fd5fbc9a35c517359c5823ea705d9b65f01fb46e42e35a2eabe5a52c2e96 \ + --hash=sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b \ + --hash=sha256:ee336b398a6fca51b1f626034de99f50cb1bd87d537a166275158a3cee744b82 # via vllm numpy==2.2.6 \ --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ @@ -1642,55 +1897,179 @@ numpy==2.2.6 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 # via - # cupy-cuda12x - # gguf + # flashinfer-python + # humming-kernels # mistral-common + # ml-dtypes # numba + # nvidia-cutlass-dsl-libs-base # opencv-python-headless - # scipy - # soundfile - # soxr + # tilelang # torchvision # transformers # vllm - # xformers # xgrammar -nvidia-cublas-cu11==11.11.3.6 \ - --hash=sha256:39fb40e8f486dd8a2ddb8fdeefe1d5b28f5b99df01c87ab3676f057a74a5a6f3 +nvidia-cublas-cu12==12.9.1.4 \ + --hash=sha256:1e5fee10662e6e52bd71dec533fbbd4971bb70a5f24f3bc3793e5c2e9dc640bf \ + --hash=sha256:453611eb21a7c1f2c2156ed9f3a45b691deda0440ec550860290dc901af5b4c2 \ + --hash=sha256:7a950dae01add3b415a5a5cdc4ec818fb5858263e9cca59004bb99fdbbd3a5d6 # via - # nvidia-cudnn-cu11 - # nvidia-cusolver-cu11 - # torch -nvidia-cuda-cupti-cu11==11.8.87 \ - --hash=sha256:0e50c707df56c75a2c0703dc6b886f3c97a22f37d6f63839f75b7418ba672a8d + # cuda-toolkit + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 +nvidia-cuda-cupti-cu12==12.9.79 \ + --hash=sha256:096bcf334f13e1984ba36685ad4c1d6347db214de03dbb6eebb237b41d9d934f \ + --hash=sha256:1848a9380067560d5bee10ed240eecc22991713e672c0515f9c3d9396adf93c8 \ + --hash=sha256:791853b030602c6a11d08b5578edfb957cadea06e9d3b26adbf8d036135a4afe + # via cuda-toolkit +nvidia-cuda-nvrtc-cu12==12.9.86 \ + --hash=sha256:096d4de6bda726415dfaf3198d4f5c522b8e70139c97feef5cd2ca6d4cd9cead \ + --hash=sha256:210cf05005a447e29214e9ce50851e83fc5f4358df8b453155d5e1918094dcb4 \ + --hash=sha256:72972ebdcf504d69462d3bcd67e7b81edd25d0fb85a2c46d3ea3517666636349 + # via cuda-toolkit +nvidia-cuda-runtime-cu12==12.9.79 \ + --hash=sha256:25bba2dfb01d48a9b59ca474a1ac43c6ebf7011f1b0b8cc44f54eb6ac48a96c3 \ + --hash=sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4 \ + --hash=sha256:8e018af8fa02363876860388bd10ccb89eb9ab8fb0aa749aaf58430a9f7c4891 + # via cuda-toolkit +nvidia-cudnn-cu12==9.17.1.4 \ + --hash=sha256:0760c843fb109631edf5bd4234f2a260a13a05c18ee2a20783fbb4eb04d56645 \ + --hash=sha256:b5083ced291edb2baf8eab09951c6bc58b79b2023c4ec885657a63acdf51d0bd \ + --hash=sha256:d18d61bdd596fca0dbe459c129f6eb7a24ed2e6de1d7988b0a37ac63184ee05d # via torch -nvidia-cuda-nvrtc-cu11==11.8.89 \ - --hash=sha256:1f27d67b0f72902e9065ae568b4f6268dfe49ba3ed269c9a3da99bb86d1d2008 - # via torch -nvidia-cuda-runtime-cu11==11.8.89 \ - --hash=sha256:f587bd726eb2f7612cf77ce38a2c1e65cf23251ff49437f6161ce0d647f64f7c - # via torch -nvidia-cudnn-cu11==9.1.0.70 \ - --hash=sha256:e6135ac63fe9d5b0b89cfb35c3fc1c1349f2b995becadf2e9dc21bca89d9633d - # via torch -nvidia-cufft-cu11==10.9.0.58 \ - --hash=sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81 - # via torch -nvidia-curand-cu11==10.3.0.86 \ - --hash=sha256:ac439548c88580269a1eb6aeb602a5aed32f0dbb20809a31d9ed7d01d77f6bf5 - # via torch -nvidia-cusolver-cu11==11.4.1.48 \ - --hash=sha256:ca538f545645b7e6629140786d3127fe067b3d5a085bd794cde5bfe877c8926f - # via torch -nvidia-cusparse-cu11==11.7.5.86 \ - --hash=sha256:4ae709fe78d3f23f60acaba8c54b8ad556cf16ca486e0cc1aa92dca7555d2d2b +nvidia-cudnn-frontend==1.26.0 \ + --hash=sha256:067e5bd08a1d25391188eb7206f711d88b916a926b929aec3609af3e43dbae0b \ + --hash=sha256:17f86afebb79710a33f7c1c486597b9fb38a46c97e461e3db058c6680403f716 \ + --hash=sha256:1a1223c4e2e8bbe6f620148d6848f4eb773dd94bef534d5b748e91232b5be618 \ + --hash=sha256:1e4ee11bb4d3f912f9f7751f0b88ee08bb04c171d1edf63618fbabf63ae75271 \ + --hash=sha256:1f88e462c6c2cabb08da9b40a5025cbc86274ad3779c421c2a69a405e900278f \ + --hash=sha256:3e60688901b4d8403f05d81170a0274bdd5b1d5ef7e79976a1a56efc9b27415b \ + --hash=sha256:47057c0dacbc9329550e810c6b938ad161a2c8cff2842b4545d42dee0311d3d9 \ + --hash=sha256:67dd4f7116171567a0e3f1864ecb2933e31997a333bf551ace7ca5e36578387b \ + --hash=sha256:70423bc42a1600fdafd3ce91d2b94ea48cb0233d545fce098ba41755c160aa93 \ + --hash=sha256:719c4e7f2ee2765eeefc144b8a55aac894c306c6e5bf41797373c8f74b30dff6 \ + --hash=sha256:7b15c3149ace627fa6bced7a68265b107416faf41b8c2261c5b53ae5d5228824 \ + --hash=sha256:7d2a4981c3825f484187b8ade0203bbca69087b8b7472d1465c809a884f4b2e8 \ + --hash=sha256:7f104a40cb6f25cf01b7d7e84cb99af7dedd32b1cb8264384c2f363b7a3f7fb6 \ + --hash=sha256:7fee9922c6be2c1b43cb10162e7cd63ce102b0509c0f028e37ac23a959e761b4 \ + --hash=sha256:ad20c54dfc042eed8c9ec557e8c6234f73ab66e56174e65fe13e9e41ebc1043e \ + --hash=sha256:ce41d00f7b80171c7e3705f0b519ace6b40a2dc8b372206e120f3930568a9afe \ + --hash=sha256:cf064832e29d74ab5bafa341b1793650496ebfe0c94292ee00b61008ecf9aac4 \ + --hash=sha256:e2dcc87fbc5c7e38d0fbf73c982963878bd0fa763a63836d25042f5fd28942bf \ + --hash=sha256:ee50df3468f672aa31402fde4c911ad545d08e1d5e133e15bc55c3679d9fd9ca \ + --hash=sha256:f1e78e77bb145ec9162f2e166241f4c82ca1779a8a545bc3fe0870813c55d5cc \ + --hash=sha256:f2ea02bdad5081210830cd577de86bd0ae73836ffeb3485211753621b145e9d3 + # via + # flashinfer-python + # vllm +nvidia-cufft-cu12==11.4.1.4 \ + --hash=sha256:1a28c9b12260a1aa7a8fd12f5ebd82d027963d635ba82ff39a1acfa7c4c0fbcf \ + --hash=sha256:8e5bfaac795e93f80611f807d42844e8e27e340e0cde270dcb6c65386d795b80 \ + --hash=sha256:c67884f2a7d276b4b80eb56a79322a95df592ae5e765cf1243693365ccab4e28 + # via cuda-toolkit +nvidia-cufile-cu12==1.14.1.1 \ + --hash=sha256:8dea77590761e02cb6dd955a57cb6414c58aa3cb1b7adbf9919869a11509cf65 \ + --hash=sha256:9552e2231792e94b1ff17bc99e958cc0e6bbbaa4a9d91fa2dbeed97716628fe6 + # via cuda-toolkit +nvidia-curand-cu12==10.3.10.19 \ + --hash=sha256:49b274db4780d421bd2ccd362e1415c13887c53c214f0d4b761752b8f9f6aa1e \ + --hash=sha256:de663377feb1697e1d30ed587b07d5721fdd6d2015c738d7528a6002a6134d37 \ + --hash=sha256:e8129e6ac40dc123bd948e33d3e11b4aa617d87a583fa2f21b3210e90c743cde + # via cuda-toolkit +nvidia-cusolver-cu12==11.7.5.82 \ + --hash=sha256:15da72d1340d29b5b3cf3fd100e3cd53421dde36002eda6ed93811af63c40d88 \ + --hash=sha256:62efa83e4ace59a4c734d052bb72158e888aa7b770e1a5f601682f16fe5b4fd2 \ + --hash=sha256:77666337237716783c6269a658dea310195cddbd80a5b2919b1ba8735cec8efd + # via cuda-toolkit +nvidia-cusparse-cu12==12.5.10.65 \ + --hash=sha256:221c73e7482dd93eda44e65ce567c031c07e2f93f6fa0ecd3ba876a195023e83 \ + --hash=sha256:73060ce019ac064a057267c585bf1fd5a353734151f87472ff02b2c5c9984e78 \ + --hash=sha256:9e487468a22a1eaf1fbd1d2035936a905feb79c4ce5c2f67626764ee4f90227c + # via + # cuda-toolkit + # nvidia-cusolver-cu12 +nvidia-cusparselt-cu12==0.7.1 \ + --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ + --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ + --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 # via torch -nvidia-nccl-cu11==2.21.5 \ - --hash=sha256:49d8350629c7888701d1fd200934942671cb5c728f49acc5a0b3a768820bed29 +nvidia-cutlass-dsl==4.5.2 \ + --hash=sha256:68ed1b63ca74aae87955012da9dfd7fdaae471329d0028b229b841c7192ccf52 + # via + # --override images/gpu/vllm-overrides.txt + # flashinfer-python + # quack-kernels + # tokenspeed-mla + # vllm +nvidia-cutlass-dsl-libs-base==4.5.2 \ + --hash=sha256:12c29f7c1f1f82851092ba3869264dafafb035228c0d9827a8db08b884fb80ca \ + --hash=sha256:15ef6a59193667e663934ef4873f8ccad37455e9b7c3c419c3072113b8aedf61 \ + --hash=sha256:216eee6aa8107d35569f9451b66b03a3c53167841d1af9b630b966ef8d966e19 \ + --hash=sha256:386e832427e3670479049a1560e4d8d2e565d8c0f37a6852c6d7043d046548f1 \ + --hash=sha256:395bd77cf642aeef311313453e6582f11c9357a4b81fe620ea3daccd1fccab9b \ + --hash=sha256:5aca392063ffbc7da30442a267928b22d4a2d37f9ea1db32e4487aa31b0fcc33 \ + --hash=sha256:9117900cba53d3c21a8dacba6bbf3d6e5f269e427a526c320fb44707a0d57363 \ + --hash=sha256:abab8a0d2f3f5661533c366df78f973052b86a3b52b868d997a95dce5aa8f17b \ + --hash=sha256:b62807bc5ea13bbdef648212893fac407ed943f940cece56b880d44af243e075 \ + --hash=sha256:cbb555a95c7011e4b3ca328be407299c77d289660adbea22ed515d4406e6949c \ + --hash=sha256:d2a3c412287e356fbe48fe9f845d6d33cd35dea5e20d7e4f628c20957967cacd \ + --hash=sha256:e59da7d89e5e4f8514c6530843f910f9d8734d8042dcaa079c9d9c5063eb3514 + # via nvidia-cutlass-dsl +nvidia-ml-py==13.610.43 \ + --hash=sha256:65437eb73d68d0c62c931ca4d45038472faff03bd0b8729abba4b899f70d60f2 \ + --hash=sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8 + # via + # flashinfer-python + # humming-kernels +nvidia-nccl-cu12==2.28.9 \ + --hash=sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab \ + --hash=sha256:50a36e01c4a090b9f9c47d92cec54964de6b9fcb3362d0e19b8ffc6323c21b60 # via torch -nvidia-nvtx-cu11==11.8.86 \ - --hash=sha256:890656d8bd9b4e280231c832e1f0d03459200ba4824ddda3dcb59b1e1989b9f5 +nvidia-nvjitlink-cu12==12.9.86 \ + --hash=sha256:994a05ef08ef4b0b299829cde613a424382aff7efb08a7172c1fa616cc3af2ca \ + --hash=sha256:cc6fcec260ca843c10e34c936921a1c426b351753587fdd638e8cff7b16bb9db \ + --hash=sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9 + # via + # cuda-toolkit + # nvidia-cufft-cu12 + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 +nvidia-nvshmem-cu12==3.4.5 \ + --hash=sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd \ + --hash=sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15 # via torch +nvidia-nvtx-cu12==12.9.79 \ + --hash=sha256:1f504e573b3a955e55aae6c747e2ae561b63fdcafcd591e43d18dae9875504f8 \ + --hash=sha256:d1f258e752294acdb4f61c3d31fee87bd0f60e459f1e2f624376369b524cd15d \ + --hash=sha256:fec150986817f2b4e7eed72ed059f2dcb9ba3856b9a96134e448eac946a6952f + # via cuda-toolkit +nvtx==0.2.15 \ + --hash=sha256:02bca69ee55e0be41eabf908de9dbcdd18e702c7f49f9aa63fd396ce684ff5d5 \ + --hash=sha256:10749686633f880ad53dcdbb2179fad41b45dcf5b7631d4a1070a577577bd386 \ + --hash=sha256:157b80ea9b4db6c8f47f8dbe2fa2e81e7a7f1445bb87f8268f43dec9210b78a1 \ + --hash=sha256:2287d3be05b85661deb386f878d1f536c2e532774aa9ec7a50c434942ed81ae5 \ + --hash=sha256:25813ead4fff4d3a6e04f69a72507b096a6bdbecefa369f1100b0e584767bca8 \ + --hash=sha256:2cc530cd0f1a2c14a3a7e683833db509888ac5ed4ead94e5c9e2c7317c6937a7 \ + --hash=sha256:3741edac4678b92f03d22a3f0a2dfd469f422f85e63db71b038e02525b2404ad \ + --hash=sha256:3c6d0f27d4f8a2f479eb64a6b842c13aee32120348a1715d995b9bb9f75b35cf \ + --hash=sha256:3ca8030a6d197952318013dd1c12c22da1d4b9feb76ba72e0fcd449961183c2c \ + --hash=sha256:58653bf6fd8453947b9e5153da2ad7aeb0ceafa030de7f133efb3eada5da7ca7 \ + --hash=sha256:5f3362f0db4252514719326c9d5662b0f93d254659ba97b9c8dbe556286e0e3e \ + --hash=sha256:70a1e768964e0520b68ccabc4df391cc227537c45936a7eba6507bc65e617e00 \ + --hash=sha256:8be06c3c8c267eba56a0396366b9593092e0b75ea8d3702b303d48c0a1662f0e \ + --hash=sha256:9934fad0b441cfa6e896a848b092498ba23e2ff205c2b9a7b60520ff8367ffef \ + --hash=sha256:9d1d10db4fb4a3b0ffd6ed37bf25f0a966a3b4d34b3c9abb1f6572732959a6e5 \ + --hash=sha256:a4f50832fd90a1b480a9deef6e4cd48015b61869095b54dd1a7afe87b4138c6a \ + --hash=sha256:a6650b029263d12f8427a4dee8bd59cb9c91bccb60543bfcb20bc2b00fdcd672 \ + --hash=sha256:aca61135c76b8107ae3c994325613afa661e1336a991c59cc9c6176829b3b32c \ + --hash=sha256:b5171b8283dd3ea9ae688a86d16901b4c2c142c4eb0a4bdbf6c222f5f67f9524 \ + --hash=sha256:c6c9e76d40eae128b6ad0bd44123df516dfcc26e2d6bb65ddd9336b35caeec7f \ + --hash=sha256:d28660d9c46f8ba750d781572b6aa5a1e6221abba224ab32d7fb32c2d0fd67df \ + --hash=sha256:d2ec75546d37738ae41dea026bbd04ccf61c11a9acbf96b744a9478aa93a8c2f \ + --hash=sha256:d71f934e580d4572f382712b6da464ab69e4c212981506f781f927d5c6d935d6 \ + --hash=sha256:dbe41f78f5a811bd4cdad0a237e5b41a4937d8c2c6c9abdd161091671a598bc0 \ + --hash=sha256:f862e22017b43edf7eaf8379091a034e88d99631bc16f3b6b11abf6d09a1177f + # via vllm openai==2.45.0 \ --hash=sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d \ --hash=sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777 @@ -1723,56 +2102,116 @@ opencv-python-headless==5.0.0.93 \ # via # mistral-common # vllm -outlines-core==0.2.10 \ - --hash=sha256:0a9e4b192ca837a472a1bb1428397509f543db08e1aeeee30252525cec34093a \ - --hash=sha256:202a8682319ac2c3ece876a5910eb90b399d7db3e2e0ea9c371bca61071dc840 \ - --hash=sha256:207309a1d4fcf3100e3bbdc31b4d65f2b4f5d809d600c1509e28b6dca028a892 \ - --hash=sha256:32615f6fe4286d80699e9e6537eecbde387bf73d87751858f7a0693947381cdc \ - --hash=sha256:367277a882aefa6c32438d554e1bc7389fdcaf89a3eb4d8a25cda5f1c1efb750 \ - --hash=sha256:4130170e53942561941d5f212583fb9c97e33d100eaac94f4b00fb3e0c4c06cf \ - --hash=sha256:4231fb008d6282f8c49543d6ae57b173e3ca1d77bbc4ff75472706a4a38cecbf \ - --hash=sha256:434aba95e0e08ef8cb6af2008562df1ad67ab02b68e64f4e725eff00bfcceb29 \ - --hash=sha256:534fafab18e2962b9973cae852f47476307dc217dd0708d53cbf54809d8b304e \ - --hash=sha256:5400dbd98ba9cba1817527510f457655ccfd7e4293a48dacc2115e04af55ae74 \ - --hash=sha256:5b651ae12331326b820df0ae9b255d9ed6cd1b725c33c8eeca5ca8ad655d8bf6 \ - --hash=sha256:63b9f0ef1fb61a5e18697e885b2eaa1f244d2ea021d68fdb2c9a607a769aeaa8 \ - --hash=sha256:65b2dba48d0f98b0145eb50494985f026e3c10df3fde94ced40e9c2aa6ea32ca \ - --hash=sha256:70d99dd37a826b4d85a5dcb39ae3b557e986c9bb1c4566bbb26f589531369a53 \ - --hash=sha256:750e2d5e0b083161208599c9c2b99c8c2b944ac82d22de91546f4b2c14c57895 \ - --hash=sha256:766554bed5afb19bb09f3ad01224e67723973ecc9da3d63b78dec36e3a3bfeb9 \ - --hash=sha256:795b19362798c408113da913a03e31a562a5faf4e2ea45ec0f44435843cc185e \ - --hash=sha256:7a1d44ccfeb029f8a0ae973ba729b59333f56ebab3d6bb765ba1cda685ebb407 \ - --hash=sha256:7b48e4bd776d4b3083d07baa3d722654e0425780772c4217f1df49d4984041b6 \ - --hash=sha256:82c97846e36cd6d7a9605013e07e1b9a481a270ac589b0b81076afd5ba850261 \ - --hash=sha256:8666735ec367a06e26331e164a80a4c2936b349713ac05ab53650e2997a30891 \ - --hash=sha256:8e5c11ad18818233ad2f579675c530873966ea155557ade9464c30a67c8aa95f \ - --hash=sha256:91facff8856f38ac77305dfea13e1c1a9be9152a14b3891a6422028291a1ea85 \ - --hash=sha256:9c5b9a3f7e658949a3dd07b8a28134277a047ed7d73f6e3b4ca8209346bbff54 \ - --hash=sha256:a29e261ab57fd992b236854fd19b46b17ad8c8b7fdc6d95a97ae83480e634cff \ - --hash=sha256:ac23b028da10e6914b762f36a7096e793a0e37b6c03f19963ef7875c05b67890 \ - --hash=sha256:b37e192de974fdbfe20332720a4a9cdda92719536dface60b48dc8eeeda24390 \ - --hash=sha256:b5df420c57fc257a30cf3a6e088b174aeb84a19d516f6818f00b29b626540629 \ - --hash=sha256:b984c932bdf2843e3d5a8e57e09830d52c4237ac394f39542c4e543378b94ffb \ - --hash=sha256:c0de2c683f5ca37211a3fe1c8d8530c3d92fa0ae3297b237369517dcea4b5a77 \ - --hash=sha256:c7210bdd63116682ce0a4f38e93b0ace0adbcd333644b2dddddb0d8db2a2a9a7 \ - --hash=sha256:c9ee7be195ac18dda5acce41d8805c2fb550a4affd525414511662cfa7097dfe \ - --hash=sha256:cd13c80be1052d735b10c84488bf081274c710744c34bf7a9b7233f69ba31537 \ - --hash=sha256:d1437c9b90a8faef2b480c8f0b944e8cc0b050c9a97164a7aacaa868ae08ceb1 \ - --hash=sha256:db3e07c999ee17035114f20263c7200bf5bea0f643d2d026671eb5cfc2a9cf71 \ - --hash=sha256:e39847ab495ec9923dc1a59ccab04ef7888b5e066bc5856b5cb7fe98e9663f3d \ - --hash=sha256:e4b2ce2b96cbe858358e71136511075678bd0e1d6d0c1641525c4dbe4c7b9270 \ - --hash=sha256:e8730816d97e17c31e21c26713f22ecd1899f4635fb7eb10ba10b9de2e1f33a6 \ - --hash=sha256:f543f23b263c0b010860ab5ea760b2be566b604315e6a89499632758ca177a5d \ - --hash=sha256:f895834da0a577120dcb8d979c12c0690fe912095413bf0070a73e9ff363b7bf \ - --hash=sha256:faf5b43181b1d033871364e74e9d348362c6a77b1d054d7af35e09fdfcff5b16 +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # vllm +opentelemetry-exporter-otlp==1.44.0 \ + --hash=sha256:4a498fa8d8fd8be9e8e2d175fe5524a3fe581ccffadd8509db86526a5fb97051 \ + --hash=sha256:af1cde7c33ea8ed624bf04ac49a885730fe44c1f1ad698656e592c38f70ce106 + # via vllm +opentelemetry-exporter-otlp-proto-common==1.44.0 \ + --hash=sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694 \ + --hash=sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.44.0 \ + --hash=sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463 \ + --hash=sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e + # via opentelemetry-exporter-otlp +opentelemetry-exporter-otlp-proto-http==1.44.0 \ + --hash=sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3 \ + --hash=sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8 + # via opentelemetry-exporter-otlp +opentelemetry-proto==1.44.0 \ + --hash=sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56 \ + --hash=sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.44.0 \ + --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \ + --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-semantic-conventions-ai + # vllm +opentelemetry-semantic-conventions==0.65b0 \ + --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \ + --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60 + # via + # opentelemetry-sdk + # opentelemetry-semantic-conventions-ai +opentelemetry-semantic-conventions-ai==0.5.1 \ + --hash=sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c \ + --hash=sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4 + # via vllm +outlines-core==0.2.14 \ + --hash=sha256:056f656ea6e4807338963377afb50b9d936593ba3545a819f1aba56fd6e14920 \ + --hash=sha256:060a0174a6262bfd378763f210e374e52011776849a3a767df9863fb6839c142 \ + --hash=sha256:0cd8ce3ce61df44fd9c5450d9744e2280586c2a6e6e3dfefa0dab1944764b424 \ + --hash=sha256:0e4b5b7c8e50489bea444b095692ddb5d8fb92ea6b949c4a6a3381eca9b691a7 \ + --hash=sha256:0e4c69f0a8565edb56464c4c9b6c291a10805f3a96dff84182980e90ae1a5e2f \ + --hash=sha256:1de34681c7e0e7e1551fc9036e4fa3c57986336c905a10536591ceb6d869c258 \ + --hash=sha256:3bb2060c240c4507f334965a8948dbeeb22007560d797f6debd92346c0b620cb \ + --hash=sha256:3e67fc23b1a3ac9562488fb50f409c171538b76f64aa5f7e25d9b0bf14770204 \ + --hash=sha256:4daa22d677dc6a74c44f9266ec9e3151332dcea4250dd019ea0c75b98ae32938 \ + --hash=sha256:5f8893cf24e4f3e5a7b246b578079ff0dff3228aa9c731bd7fb1f3f55ebee19e \ + --hash=sha256:615566bf8257d2bba8ac192cdfc29d1c4357f57b53672fbd622e821215e4f1bd \ + --hash=sha256:63f53cfd9614e754499ae86dd699f3abcecf42d6a4e58d80fd80347881d85960 \ + --hash=sha256:6453e23f01d98ec48e3a4141d7112792ce77001dfb28d91d6fd89f47009f91ef \ + --hash=sha256:64808deed1591ca3029ff64346ceb974cd5d780c916ea82504951fe83523039e \ + --hash=sha256:66e695b375b180725fb534d9adf298531c152ec3d881e3b9e01c82b5dd269f52 \ + --hash=sha256:69410e5b55bcbaad8c865d94bd01e7bff8a57996dcd2251b7d50dec70d7d9a63 \ + --hash=sha256:6b149452f7387501252f46f2c81847067e0820d7bff8b2b4201084f559d2300d \ + --hash=sha256:6bd166d3b07acef2f60d4ede44592a26d3f7d8712876bfc8e22150045def5857 \ + --hash=sha256:7770b5e0497e6f4548a8923299d4438d7dd61dc17c2f58acfd5df4d3101bb991 \ + --hash=sha256:7a52a56b2b627cec2d824af063a5020bf6ab9090cf9c9b4f20dcf1407eee80fc \ + --hash=sha256:7d14c48649d5df5c488b16c67493385cf8b3fe71da14466843e294c562417f21 \ + --hash=sha256:7deef6df74cb247f2a3a62f03438ba967456504b0555ec7029f8db834e054448 \ + --hash=sha256:7eba2b41dac03d6e6e8d5ea0aecbbc03dacb4c57de3b1fc944d0bafb022941f7 \ + --hash=sha256:813b28813b22025c3d079b3b8a20cf5a28c6d5ba29ec21c5b1093442aa5d4e91 \ + --hash=sha256:81d01cfae29de5671bc5013fd6b2008621157bec3d8be284da7da2dc0672745c \ + --hash=sha256:870e8e038853818cb202ccc8cde92251f300f96805bfcc3be1c883adda7b5297 \ + --hash=sha256:87b42440478764cce1353a87d8560ef82f3b39b9d753bfe93195ea3584f369e3 \ + --hash=sha256:8a5e5f34961fe4d04c389d00f92d624c6318ab3ff00467fbf7c93324458886d9 \ + --hash=sha256:8b3e8d668188282a1f7666732bb8a01958ab134db35bb792e7442a40e55ff1e7 \ + --hash=sha256:95e6476d9702d2fcc4e85370dbbfb6933a46c816e9c90107f6ce36eb68b5d64a \ + --hash=sha256:98d1929a7a0ff43332448c176b92041c2755c4843a22223d341c3b96165c2a37 \ + --hash=sha256:9d45462d7548aa0e17176a691ae73447f3e6bed9658a0cd96fe72eadf7474475 \ + --hash=sha256:a2795dc2047821b229457f941a303639e0c14e4c3c5718797540a27b529a062e \ + --hash=sha256:a646a43a482aa0b4a2b286735efa476b31cecd0032024cba4bb9a5d62623ef45 \ + --hash=sha256:adf96395759d7fdf6efeb8a67d3f36f520c1546bfd4df0752306db8c7cb7d6c5 \ + --hash=sha256:b02bb0fc21c5e23e2ff9b2d1459db2c1c3e813a7646c9d5db091c6931edb9c85 \ + --hash=sha256:b582b5d2f773cff966f37d7a5680d97506792647c93fb2e522283e8a14726e9d \ + --hash=sha256:babf97a54662330c55a79fdcab8994f96faa6dcb71b458d4b18c4fb538f5d461 \ + --hash=sha256:bb008c7ecc034bcfda0ddc10a4d1f2181a4b61ec1643ee56183dd6fa64139c9d \ + --hash=sha256:bb3245cbd7a2e9800f257b45cb5a8d690abd39dbaa8371ea40132f6f1eb2e3d2 \ + --hash=sha256:c76f28feb6ea71b1ff4b0ba5901dc383273a32b156213dc1bc753fc634645a1e \ + --hash=sha256:d1776ae984574461f249fe590314a439992eb9b883f4091b8fa7fc56f29f3717 \ + --hash=sha256:e604925d6525f669253160568397df6d6c8124b2e01f1fde553e3b9f28ce9e21 \ + --hash=sha256:e75395b1cccecdf85d8d8265aba28841ddeb1e8da406f4b1e0135df5a6e9960f \ + --hash=sha256:eb27e92204b296a063ac58f361153be4e78c8103a96e0b1c085b22d4fc3534cf \ + --hash=sha256:f04731a5e29a190e2cc9f692a1f3fb2414a645355ca7d01b83df43439c38bea8 \ + --hash=sha256:f0e5037153b5b3abfb617f6dfdc3ff28b6fab50f0de5936ea6995f5675d23e0b \ + --hash=sha256:f753edd430ac27e6dcde5a614665888db72b78c666aa160c478afd1eb986fb8b \ + --hash=sha256:fd7d432817978ea474de428ed6ca5ed3195812f8cfb787b85fb5ec0a51a440d2 # via vllm packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via + # flashinfer-python # huggingface-hub # lm-format-enforcer - # ray # transformers partial-json-parser==0.2.1.1.post7 \ --hash=sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae \ @@ -2019,7 +2458,8 @@ protobuf==7.35.1 \ --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via - # ray + # googleapis-common-protos + # opentelemetry-proto # vllm psutil==7.2.2 \ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ @@ -2043,7 +2483,9 @@ psutil==7.2.2 \ --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 - # via vllm + # via + # tilelang + # vllm py-cpuinfo==9.0.0 \ --hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \ --hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 @@ -2277,11 +2719,14 @@ pydantic==2.13.4 \ --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via + # anthropic # compressed-tensors # fastapi # fastapi-cloud-cli # lm-format-enforcer + # mcp # mistral-common + # model-hosting-container-standards # openai # openai-harmony # pydantic-extra-types @@ -2419,11 +2864,41 @@ pydantic-extra-types==2.11.1 \ pydantic-settings==2.14.2 \ --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f - # via fastapi + # via + # fastapi + # mcp +pyelftools==0.33 \ + --hash=sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f \ + --hash=sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036 + # via humming-kernels pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via mcp +pynvvideocodec==2.0.4 \ + --hash=sha256:46e2adb82dc6ac333d3535cc76e4e25c7e8d80dd272b1aba0c28702b861d5261 \ + --hash=sha256:4dba42331f6d319087d05359787c7c542483fab22107c07b983cad928c1e3cfe \ + --hash=sha256:51724c6a0e3623c092cccdf93c8b09cced6881f3d0c76653f6ffbec0371f29cd \ + --hash=sha256:7af1d9ab37f7415168c23be00539094e7b21774661af2a0a2b9bb574670bc75e \ + --hash=sha256:834fdbef7f3fc79285b5c2a88d1f1f7cd13543a9a7f2a2786141b181d1daaac9 \ + --hash=sha256:8d6d3d63317c452bdd18167f622abb392bd982ae65293673a169e3f253c1d1ed \ + --hash=sha256:8e704a2b553a35cc2de10543a232e7feddc473f3e5478996595236e3194efc5d \ + --hash=sha256:a0daa28b09705806c8c6b26326df217c45e60c0a12a673ea3ea6ee5e2e7193b0 \ + --hash=sha256:b59cec7a1a3f78fad13fead78cad8b6d9686827f9ff4477080245457675a01d0 \ + --hash=sha256:bad9e25f494abdcfa8f9dffa33a840509eda3ffcdf6e7cf6465d73be307c0c82 \ + --hash=sha256:bd3779ff73ad703393c0a19c3650f269ca25e71902c24efa0719ed4a58cd9390 \ + --hash=sha256:c7a133a8088ff9152f7953ff63c5cbc27801f0313d1e577c5f3e65da67abf570 \ + --hash=sha256:d9ec06f47bca7b20a6e8234afaf596699c57a477be292613d676802e7e808ed2 \ + --hash=sha256:f678e5a6903498f291cffaba5d47b023c67bf18172049186044c4466492ad443 \ + --hash=sha256:f809fb18929ac2af042835f10c7679b1c86db9817776f22bc7467907c5c3d918 \ + --hash=sha256:fbe730967d5402ffca520b12fa21725cbd22a6d2c9fae882ea1d95085a400fd9 \ + --hash=sha256:fc299a14e61832850be91f8441669ada6fc270903ad1c50acde8e9353590fd3a \ + --hash=sha256:fcb06ef6ef24ee33b8f34950696a4e01636f2d8cdb96c407cd692931d049ac3d + # via vllm python-dotenv==1.2.2 \ --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 @@ -2437,7 +2912,9 @@ python-json-logger==4.1.0 \ python-multipart==0.0.32 \ --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 - # via fastapi + # via + # fastapi + # mcp pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -2513,10 +2990,8 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # gguf # huggingface-hub # lm-format-enforcer - # ray # transformers # uvicorn # vllm @@ -2614,28 +3089,10 @@ pyzmq==27.1.0 \ --hash=sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6 \ --hash=sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9 # via vllm -ray==2.55.0 \ - --hash=sha256:1da8b8755b6e4fde03db78b6ce2bbcecfcfbd20d39b93833d246c515daeedf3c \ - --hash=sha256:2426e9c4513cb4842bfaf70aaedf4b9edc302fb01de5c1906f68b9b427a0c243 \ - --hash=sha256:2c19db3faf5b662175aa80c4b117f223fa198996161b6c5a421401227c456b55 \ - --hash=sha256:2cf8c2a3f6cd947934cd75d99cf4b5615228fe6c0b42faa7a6926733a82c2467 \ - --hash=sha256:52249146cfdcf769ecffe54f5b2caf25551bf6ea78d9dd89788c0a669ca769b7 \ - --hash=sha256:5da06d27358d38c30a723a617bf9b7df138f4d90e8046f1fa51d9b8c7473b64a \ - --hash=sha256:66814d57c533f58ec378a55591b2968150fd4c057f0b2b87460438e7353fa062 \ - --hash=sha256:6f0b8dfa3716cc9be5fce3b53e9bfdb73cea36025bfe6f1d27928d0f84cfd695 \ - --hash=sha256:b1fd73427093d1aef1fb7bd3c5430a4e686d5e10fa04089757163a4c2a517deb \ - --hash=sha256:b74390f201f28f05c8f250069dfed54d6d6a0109ffe482425d76c11be820e309 \ - --hash=sha256:b77f406072ac0ce90431ac436828f364c183ab57ba15c3a0e688a74ae3c2d3f3 \ - --hash=sha256:c1bc110d39edfa8f8ed85fbab0086dc7e4a8b8c0d784a74ff39eac5e73580d62 \ - --hash=sha256:c85109c255d3d3e71bda431332beb716ed445041e06fd2c7fa6c2232f3cb9d75 \ - --hash=sha256:cd460bdbf8a8a4bb768a20c38b1c534d84fe63bc0e5f3580c5c0ef7302b986b3 \ - --hash=sha256:ceee87a884602aab34db109415e6839a6e9169f4750cab727b7ea1610df5b91f \ - --hash=sha256:d48bc4533b3b76d59ed3f9eab1e6b7322a53a7cdefb8f657d9b46eebad56dbee \ - --hash=sha256:eb0a6179641bc420a66ee85cc9b382e58f22effbd36297e3683a793e5cdc0898 \ - --hash=sha256:f16dea32e5cc58ed406c0ef0dd4be69d60ce77a075edb5f0380356a48bf85ab3 - # via - # -r images/gpu/vllm.in - # vllm +quack-kernels==0.6.1 \ + --hash=sha256:266705ea82117e9b1c8a9e44d68a458519f2498d966c0efffd6812120c3995ad \ + --hash=sha256:a694f89c91d137478de523c0227365a331ac9cb66790cfb08baa3dbfaafc71e7 + # via vllm referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 @@ -2765,10 +3222,9 @@ requests==2.34.2 \ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # -r images/gpu/vllm.in - # gguf + # flashinfer-python # mistral-common - # ray + # opentelemetry-exporter-otlp-proto-http # tiktoken # vllm rich==15.0.0 \ @@ -3077,50 +3533,10 @@ safetensors==0.8.0 \ --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 - # via transformers -scipy==1.18.0 \ - --hash=sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446 \ - --hash=sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468 \ - --hash=sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b \ - --hash=sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553 \ - --hash=sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0 \ - --hash=sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7 \ - --hash=sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2 \ - --hash=sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b \ - --hash=sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61 \ - --hash=sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8 \ - --hash=sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8 \ - --hash=sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6 \ - --hash=sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690 \ - --hash=sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de \ - --hash=sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578 \ - --hash=sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab \ - --hash=sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce \ - --hash=sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f \ - --hash=sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520 \ - --hash=sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378 \ - --hash=sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197 \ - --hash=sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709 \ - --hash=sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132 \ - --hash=sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867 \ - --hash=sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a \ - --hash=sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677 \ - --hash=sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4 \ - --hash=sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0 \ - --hash=sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58 \ - --hash=sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8 \ - --hash=sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76 \ - --hash=sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9 \ - --hash=sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f \ - --hash=sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11 \ - --hash=sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4 \ - --hash=sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b \ - --hash=sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b \ - --hash=sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76 \ - --hash=sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f \ - --hash=sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d \ - --hash=sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707 - # via vllm + # via + # humming-kernels + # transformers + # vllm sentencepiece==0.2.2 \ --hash=sha256:046b15ea22d8042e2e173561d464ec3b64a9c2081324df70ebce7bf7ebb3e497 \ --hash=sha256:0e2aae42960392d6dcb9a72d8e1e65a97294c965071b43c7b3429a42f350250e \ @@ -3287,9 +3703,8 @@ setuptools==78.1.1 \ --hash=sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561 \ --hash=sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d # via - # -r images/gpu/vllm.in + # model-hosting-container-standards # torch - # triton # vllm shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ @@ -3302,56 +3717,37 @@ six==1.17.0 \ sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via openai -soundfile==0.14.0 \ - --hash=sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849 \ - --hash=sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4 \ - --hash=sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d \ - --hash=sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e \ - --hash=sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8 \ - --hash=sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11 \ - --hash=sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c \ - --hash=sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98 \ - --hash=sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377 - # via mistral-common -soxr==1.1.0 \ - --hash=sha256:1577865e993f98ffb261257c3060fa76ec3db44ed3f181b16464268000424464 \ - --hash=sha256:26925618945f1a44dfbd783cc572874f0685e9ecdf46b96f4000f6b8c9c8b825 \ - --hash=sha256:318925f7281df61dfa7f17fe343952eb10cefd3954f2423a733fabe3a517bab2 \ - --hash=sha256:33525740fb7dbed8b09970bf0cd4219b365538845053987b11cc235b20562e09 \ - --hash=sha256:34cc92208c3c412c046813e69da639c04a792c6a41fbfd7d909d359cd3e97a2d \ - --hash=sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1 \ - --hash=sha256:3da87e3ffa3e41823d873b051c7ecb2acebd8d1b6b46b752f5facf10a0d84ab9 \ - --hash=sha256:474aabb9283f177e899747510d60661730538052fca0ed93a943d4686d6655b1 \ - --hash=sha256:52c9ca84e3dc656d83acc424574770e20ea8e0704dc3842d4e27b0fe9d3ba449 \ - --hash=sha256:588c7de1abafe59e66face9a074514658ac0398c85a774cdbb8efac131192692 \ - --hash=sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4 \ - --hash=sha256:868a24d864c25024f60ca964f851a759f2ada5352608fc194d927b7facc2e28b \ - --hash=sha256:8e11e26f1718b5c2e5b96f2f71b9f00e31d247b065289661e3a6996c758669d9 \ - --hash=sha256:9443e5eb82152d8952422b7285692192cc7dcffa5218bb511b096203018bc273 \ - --hash=sha256:9564d82f7fa6bf548e5f18bb86235dff20eea8bd30727b64d49783c95c34fb8d \ - --hash=sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44 \ - --hash=sha256:a941f5aaa0b8abced24318105c1ea22576afcc1138c19f625716ce4e2f76ad64 \ - --hash=sha256:ae30c48ac795378cf23ba3c7c640b8ff794af714ac388b9fd6b31a40b39e6e86 \ - --hash=sha256:b2e94c713b7d96fb92841947b785bcee6606124bc852273fab70454b51bfe270 \ - --hash=sha256:bd30f7201eac896ebf5db7b09156e6f1a1b82601900d29d9c8449bdad8365b11 \ - --hash=sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06 \ - --hash=sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4 \ - --hash=sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da \ - --hash=sha256:e17d4ef9b0185214b2c0935605ae63f827ea423bc74964be44763d68d2b6c21e \ - --hash=sha256:f4977323ef9c3aa3c2a26ff5fe0191c84b8fd759daf7afb1f25a91a55ad8b730 \ - --hash=sha256:feebcba99ac99adb8009d46c8f4c1956b8c167576b0ae8a6fb47502e9a6f78e7 - # via mistral-common + # via + # anthropic + # openai +sse-starlette==3.4.5 \ + --hash=sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a \ + --hash=sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296 + # via mcp starlette==1.3.1 \ --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via # fastapi + # mcp + # model-hosting-container-standards # prometheus-fastapi-instrumentator + # sse-starlette + # vllm +supervisor==4.3.0 \ + --hash=sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db \ + --hash=sha256:4a2bf149adf42997e1bb44b70c43b613275ec9852c3edacca86a9166b27e945e + # via model-hosting-container-standards sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via torch +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via + # flashinfer-python + # humming-kernels tiktoken==0.13.0 \ --hash=sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4 \ --hash=sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58 \ @@ -3413,6 +3809,12 @@ tiktoken==0.13.0 \ # via # mistral-common # vllm +tilelang==0.1.9 \ + --hash=sha256:00ed594fdeb229c5505b9ffa895c3c5daeb28641c78f783fa1f724cf1e08cecd \ + --hash=sha256:287f727c913bb648fcf6c1968809ba3390e55eeed257a5c6bb9a80bc05966af4 \ + --hash=sha256:4bbccfe9035aed775ffafb6dc25a5994504b24e2c5d95d0f39643edfafa7bf12 \ + --hash=sha256:77ab0ee2f40f66ea015b6b21426d482751e28cbc635ef9d1198cbd6502454a7c + # via vllm tokenizers==0.22.2 \ --hash=sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113 \ --hash=sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37 \ @@ -3441,56 +3843,130 @@ tokenizers==0.22.2 \ # via # transformers # vllm -torch @ https://download-r2.pytorch.org/whl/cu118/torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=91454dcfdb81f181fdf216d6d6d9912fbd8795578b90384b3b8b8132737072bb \ - --hash=sha256:91454dcfdb81f181fdf216d6d6d9912fbd8795578b90384b3b8b8132737072bb +tokenspeed-mla==0.1.2 \ + --hash=sha256:592590f36d85e624ecdc5e357ff35e29e761e6d879900dce8b67a6785c8ce75c \ + --hash=sha256:c9466a351fe039792e56cf49f3e79744c1dc28c7af10306a02e62b8e92fa5985 + # via vllm +tokenspeed-triton==3.8.10.post20260709 \ + --hash=sha256:0245fa65a14487837fca6d91381f17a7d984a2154b01ff873e01ca06a5b743cb \ + --hash=sha256:15040e5514671ceb05a22dbead49030cf87ac3becb47361a67ed2f779433d04d \ + --hash=sha256:8aa9ba218151ee67a1277ac50fafb80dfd0b42165d7f068d66131e0ea6f2418f \ + --hash=sha256:9d8a8eeccbd7744fe47a609dd742c61adcc12906533ea912a777cec35308f841 \ + --hash=sha256:be642b257539387d53176566f2d39e393da61afd9a53342ffc1508061d40b807 \ + --hash=sha256:fb45545a53f5f4017f237bb9189ba66eb5996c91e650fc48554a5eaacdd55174 + # via tokenspeed-mla +torch @ https://download-r2.pytorch.org/whl/cu129/torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=68b83cb7d7d43bc67c2833c8aebaea6a966f2017c3389885affa3361c258b7e3 \ + --hash=sha256:68b83cb7d7d43bc67c2833c8aebaea6a966f2017c3389885affa3361c258b7e3 # via # -r images/gpu/vllm.in # compressed-tensors - # torchaudio + # flashinfer-python + # humming-kernels + # quack-kernels + # tilelang + # tokenspeed-mla + # torch-c-dlpack-ext # torchvision # vllm - # xformers # xgrammar -torchaudio @ https://download-r2.pytorch.org/whl/cu118/torchaudio-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=6b64a313866b2a5bb43d4c6906e8820d3dc844f89a97c00dc5c8b92e6b70fe17 \ - --hash=sha256:6b64a313866b2a5bb43d4c6906e8820d3dc844f89a97c00dc5c8b92e6b70fe17 +torch-c-dlpack-ext==0.1.5 \ + --hash=sha256:30e3eab616dbc81dfdb7492aca557be551a9163ba9b585f97394a42b336b113a \ + --hash=sha256:3448c4f0d64104d0b2e58080a7efa72304a04960c18f338024b80b13cd3eca26 \ + --hash=sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510 \ + --hash=sha256:3a58fdf45fb0bda7bc459632cec891570f31c11636d5851c825cf308ec8b73c2 \ + --hash=sha256:3e43083f8f889cdde2b93d7458795aecc95c7268eab40a640d69f71bff7fbcac \ + --hash=sha256:49f1e99d13c64e22dac0a34a1560e9e5a398a49a9fa81df83053e04fde6ec5bd \ + --hash=sha256:4a8680c42ead771773657dbdb88f6ea07d662640abf5f1a405f1da753b5a0dbc \ + --hash=sha256:4b272c1221c3f4988079ea084456f535a25656aa2da5781def43f5bd90afdfd7 \ + --hash=sha256:56bd25a2af19280bf8a06aa62cff5510106f43235b9327d8561b3e9a659c4d84 \ + --hash=sha256:670fbbab70123cc228bed41693a3720757af57a0ad22669063c9db25321e8f55 \ + --hash=sha256:74676474e0afa9a4216c4755ea7cf05e8158be1d168f6bda669ba91097c263f2 \ + --hash=sha256:74acea2ed395cadda63342845b9e9ee7cd4537846223dacfb4431b4610109265 \ + --hash=sha256:78dd4904bd26170a2dd7c0eab56367756ee0a15672ce9b84146169e68f0c6ddc \ + --hash=sha256:7b985a324c68241cf83a9474b28015524b66775b12a91930dd4c0760aa628d01 \ + --hash=sha256:8244a2a6637f148627fdaec58504633a9751f9e00befafb2229c40b451735589 \ + --hash=sha256:ba3d88f0f7d5e1d9c3d4a3179037fc8e261c3b77ac1fad23edc0d3a9214ef193 \ + --hash=sha256:c0f2bd51fcd99c0e5b50314e1985f2728c4941bfa821f065e6c30951d1f995ca \ + --hash=sha256:c7468df84ec152d930fbc3acf460c44a60b3462b95af3d3a676d133629c7e176 \ + --hash=sha256:cac94a4905d391889e679a8da31e46dc325af5d55d13b7c70c0ce3d71d1ced6d \ + --hash=sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe \ + --hash=sha256:d794e19fa3f330ab7a29987c07e031fc08e4953aec516d35701d0827863e356b \ + --hash=sha256:debe62e5ef93e631065d6b9f6e60d3d39bae6b89fa1b25d9523f40b3efbf8aba \ + --hash=sha256:e0f6c197d5293884898b9ebf13d07501de39cb94799b374ed43f91731087d557 \ + --hash=sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524 \ + --hash=sha256:fba674110e1fab0b176bb5a28223e157db65c90767d4ba74abdbee9f537b0e9d + # via + # quack-kernels + # tilelang +torchaudio @ https://download-r2.pytorch.org/whl/cu129/torchaudio-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=de4105653562f031edd6ddf7dd1485f07db6ac62cdc90d17364648cd3c89eb5a \ + --hash=sha256:de4105653562f031edd6ddf7dd1485f07db6ac62cdc90d17364648cd3c89eb5a # via # -r images/gpu/vllm.in # vllm -torchvision @ https://download-r2.pytorch.org/whl/cu118/torchvision-0.22.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=e4a8696261b8c64297a2683656f4ee64418d7ee391627702fe5c06b72ce81123 \ - --hash=sha256:e4a8696261b8c64297a2683656f4ee64418d7ee391627702fe5c06b72ce81123 +torchcodec==0.15.0 \ + --hash=sha256:0cd15a402189953ae9a68854690b53acb72c7b9ba4d9bacb18ce9b654069cb16 \ + --hash=sha256:0fd9665b76496d5453b7b7d0e832f142bc658b46fa2e7ae5a941f6dbb1752ea6 \ + --hash=sha256:1c4257dc64554f1ed848511b7415b772f7df98217a30610d284e2a0a4df1a427 \ + --hash=sha256:1cf1946806c419fa16e41c114ed783fe9f5115e08a02ed4c609c8e0402c6e70d \ + --hash=sha256:24285627a70a935d07322342ab5d92c30bb2d37bcc9ccebacee4b23ec0e3a3c7 \ + --hash=sha256:24e8e6a1824cc13986fe678f91b07ac199d19dbc5f2af39decc4966d957b42f5 \ + --hash=sha256:28c8008494e47c3828eb64b2e9943dbb86d7183c3901a894eda26ce86e01b1a9 \ + --hash=sha256:3a4b24012f7a7fe962dfee8f06d9c91e9e3fd1f4b6302fdb5b8884a02aca3f37 \ + --hash=sha256:3b81d9d522d74981e97754e43f29c0d1f351e122f16135d390ad00023ff6535d \ + --hash=sha256:53cc91c1406f35055ce7077911f4a6fd43310ffe456d45d739afd37f547919ef \ + --hash=sha256:5896a55374d4c90e4788a8eb7c0c7a26a67db5bbe0b6c73975e2bd22f4d98fda \ + --hash=sha256:589e127778870c691d8977c08311bf57c4fecb9eb56fa52cf29d9671fe78eb72 \ + --hash=sha256:5c62f4257b49c6473b0a1006519274b7daef9ef9c1d66b1a6a025dba9df5daac \ + --hash=sha256:672aea29b5d9c56dc023e366f40ec4168bfd52f2ac02cf1c07430ad6560fdbe7 \ + --hash=sha256:7ce1c120275f80eff56842b856ebd48232e46a87c6351607dd8d60ccf197bbb0 \ + --hash=sha256:7d753c456ff9c9c30f2b8862d08ce47074f96b1789fb60f12787a85f6c2c6a50 \ + --hash=sha256:7d96bc899819d975db3a38164f4253620fd5683a3b0d447a840cdb6cb091332e \ + --hash=sha256:80a918b840c346f4167781a8eab241aeaf7d22979a603960f99c1ed1fc992461 \ + --hash=sha256:a1f089f44e237620a291078f3b039e3262c73bf0e5ff83747e20fd67e55d53d7 \ + --hash=sha256:b8b0b5b293024e0753455fa141522db794a72ae7b8e1d0a750db5756704ca6ea \ + --hash=sha256:db73e361d31bd7717d2861b5a41233ddefb29b3d1671cd61ca2b1eae5c0975c0 \ + --hash=sha256:ed08f3f07f1c68c4123cf85a47a0947f5edd57da8cd1c10d46eefd4e1673a789 \ + --hash=sha256:fa31e33884829332cc55b301aa9d23ba90bf164aa8576a8c68aed6c0061c2d8c + # via vllm +torchvision @ https://download-r2.pytorch.org/whl/cu129/torchvision-0.26.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=086aa9ac91e56cf339ea6c646bc000b970e62159b609d84e0a2d3c9859fe5885 \ + --hash=sha256:086aa9ac91e56cf339ea6c646bc000b970e62159b609d84e0a2d3c9859fe5885 # via # -r images/gpu/vllm.in # vllm tqdm==4.66.5 \ --hash=sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd # via - # gguf + # flashinfer-python # huggingface-hub + # humming-kernels # openai + # tilelang # transformers # vllm -transformers==5.5.0 \ - --hash=sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944 \ - --hash=sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd +transformers==5.14.1 \ + --hash=sha256:60d196c27781eacf8637e2b533f517582907ad6f9ae142046d6b69431a5b2173 \ + --hash=sha256:9db974c4079ede2d1a3ea7ca5a240df33f2cc26fc2b36ba64c5f2a4f43b6e725 # via - # -r images/gpu/vllm.in # compressed-tensors # vllm # xgrammar -triton==3.3.1 \ - --hash=sha256:4b79e49b15ae31dd0959090f37a362eb81c77c5643ba0ec56e529903b06d28cf \ - --hash=sha256:592a96f5a81165c8f4ffaf431a61db76b2b91c74e810a1446b49fbfaf0ad6814 \ - --hash=sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43 \ - --hash=sha256:9b1993c3ba470117dcf2888ff116d82809ed7a4d3cd05074fc40ff39190b2ce8 \ - --hash=sha256:9ee4c2ae140cedfb6f9e69e54457fa288aab6f138fd1ac4b71da92cdfa660901 \ - --hash=sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42 \ - --hash=sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b \ - --hash=sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e \ - --hash=sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240 \ - --hash=sha256:d4e316c29dcb5103dcfcdf98de8fa1b91c0bfb26218ac577eccc30585adfb60d \ - --hash=sha256:ef7099ec6e31a85de6a93e73b560e8f5bf79cf785416401b81db4260906a878d \ - --hash=sha256:f6139aeb04a146b0b8e0fbbd89ad1e65861c57cfed881f21d62d3cb94a36bab7 +triton==3.6.0 \ + --hash=sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d \ + --hash=sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9 \ + --hash=sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6 \ + --hash=sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4 \ + --hash=sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd \ + --hash=sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7 \ + --hash=sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651 \ + --hash=sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781 \ + --hash=sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca \ + --hash=sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803 \ + --hash=sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea \ + --hash=sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f \ + --hash=sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3 \ + --hash=sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43 # via + # humming-kernels # torch # xgrammar typer==0.26.8 \ @@ -3499,24 +3975,35 @@ typer==0.26.8 \ # via # fastapi-cli # fastapi-cloud-cli + # fastsafetensors # transformers typing-extensions==4.15.0 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # aiohttp # aiosignal + # anthropic # anyio + # apache-tvm-ffi # fastapi + # grpcio # huggingface-hub + # mcp # mistral-common + # nvidia-cutlass-dsl-libs-base # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions # pydantic # pydantic-core # pydantic-extra-types # referencing # rich-toolkit - # soundfile # starlette + # tilelang # torch # typing-inspection # vllm @@ -3526,13 +4013,13 @@ typing-inspection==0.4.2 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 # via # fastapi + # mcp # pydantic # pydantic-settings urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via - # -r images/gpu/vllm.in # requests # sentry-sdk uvicorn==0.51.0 \ @@ -3542,6 +4029,7 @@ uvicorn==0.51.0 \ # fastapi # fastapi-cli # fastapi-cloud-cli + # mcp uvloop==0.22.1 \ --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ @@ -3593,8 +4081,8 @@ uvloop==0.22.1 \ --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -vllm @ https://github.com/vllm-project/vllm/releases/download/v0.10.1.1/vllm-0.10.1.1%2Bcu118-cp38-abi3-manylinux1_x86_64.whl \ - --hash=sha256:eed23c5c42563f6dc697d15e1b00e404d0ec9474109653f45d86749c9b305790 +vllm @ https://github.com/vllm-project/vllm/releases/download/v0.25.1/vllm-0.25.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl#sha256=9e206f370c934a2d4b6b1f05d3d09708d344e05d80260189ef19f60755709431 \ + --hash=sha256:9e206f370c934a2d4b6b1f05d3d09708d344e05d80260189ef19f60755709431 # via -r images/gpu/vllm.in watchfiles==1.2.0 \ --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ @@ -3818,50 +4306,43 @@ websockets==16.1 \ --hash=sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9 \ --hash=sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0 # via uvicorn -xformers @ https://files.pythonhosted.org/packages/ff/e3/ee90c62a3235152d4ea8e983a5eb7ac00b10582fee86aaadb11571c1ecba/xformers-0.0.31-cp39-abi3-manylinux_2_28_x86_64.whl#sha256=50aedaea82a38d7d28631f77617d1ed1f6f37c60bdc4bf167a69cbc0e39cee76 \ - --hash=sha256:50aedaea82a38d7d28631f77617d1ed1f6f37c60bdc4bf167a69cbc0e39cee76 - # via - # -r images/gpu/vllm.in - # vllm -xgrammar==0.1.32 \ - --hash=sha256:028f8d6a105d06549faee0afbebfaada90aa1941c081dcc88f3d5ef373dad934 \ - --hash=sha256:0c0150c50eb3a56a35d6f0c0af0bce0f113ec5f84f7918bfd46b49e25ecf7fb5 \ - --hash=sha256:0ddf7ca871c7ec8c9ce8c34b8547344fce70ef3faa33b6e29f6826d098bc7714 \ - --hash=sha256:1d179511a1258b410d1660ed00032da24256072b44d6743f5bd3f49743d90801 \ - --hash=sha256:23eacaf826c3aeebca0d91fc271417d9d96e157af2bacf6f14277297af7917ef \ - --hash=sha256:2691be7077e2eae34cdefce2ef3f88e2f018158ccff0eb4b94bb8725a98e2f27 \ - --hash=sha256:4addb8f5d5699e7df7fca6d299a91b3ef1ad799811c0ab7050d6f96d754c9c21 \ - --hash=sha256:4e6015ad2b941a292562f68b9a2ee1ddae8e28df840dc39232dcc7007fc6f606 \ - --hash=sha256:4f68e591a6e9e121d5f03821ab2c44a7af092dc8bf7c9cde1a776871c6bd4dc5 \ - --hash=sha256:51b41c47785aa198d19f8d056b394f75b4421deab88c415568f9c588b1f7e238 \ - --hash=sha256:5b8ad132d0fcf3a51dc054ecb0dc9808566b302122de6edaac7b4aca460adbec \ - --hash=sha256:5d424d52779ca2d3ccaf72f2289d6519efe308e933d0d3fc3c292c780825bb12 \ - --hash=sha256:6588cfd9754f2c46846276a2e8284a46582a74886d7aaea02cf6ce63ccc397ce \ - --hash=sha256:6925258c8e188fbddcf2962c6a0819f71cf664263d2346545feadee92994685e \ - --hash=sha256:70ddbf7216e1e7ec96134a2474a6b84d2b14439a6f6379e079b7c557131be41d \ - --hash=sha256:7f740ba83b69abb423167a5d5b13a9fcde89747220e191f6a004fae4a834311f \ - --hash=sha256:82b5c3d6b3a6fe01a1957d311473808aee90e946d38bd942c7161f1c4aedce11 \ - --hash=sha256:88f3a00623b197c6b76dea3c0c7f326702209c1066eb9993b4723929a106c8ba \ - --hash=sha256:8e8da3e7fc194e098b760bacb2b60ad2227cac70d7be5d2e4f7025b1c360c43d \ - --hash=sha256:9b78d32265f096e5567ab52c72b681855cf473481a48a1e7e6d97d414ba30b82 \ - --hash=sha256:9c0769c3468bd67495c28a03dc5ce3948d83cddaf0a59c6d992b12fc683a1c3e \ - --hash=sha256:a332c0364f665b410a6cfc2ada155c3a6ede430e385ac431015e31735a64fec3 \ - --hash=sha256:b33d2e8f02ca31b93f3cbfea0ed1df7e706a2bea52d7706793884f92a8c1523a \ - --hash=sha256:b8b1ca6d3f3c2842660458660e494aaf0a6745f1b07ae74e4c2230ab4ff70c11 \ - --hash=sha256:b938a9096bccc06c30abb5304b2b39c272a924ca002e19421cce5e6ee9670f4f \ - --hash=sha256:ba6e08c385cce53eda8e9b3bbfba63f100ba3dcb76fa0692a65921a36b20ad0a \ - --hash=sha256:d4f3d18da40cad18e87881925f82d0aed1418cc2fb05886d516e3c9d548db57f \ - --hash=sha256:d7030192cb1d8579699f1f72fd14d31347a402611aab98a2da6a04c3de07e917 \ - --hash=sha256:da8339b38e96d105868c14b2cb2df4b7c83d7a49f8539c74fd7470d61043e5b1 \ - --hash=sha256:e1072d764705c8e87df6136ce3419f96ab3fd423d85f58c2d81c13a647b78894 \ - --hash=sha256:e7baf71bba03a5e734df435b6378da4406b1c15f5511ab4f5d4af9f72775c756 \ - --hash=sha256:f7d4f0f2041cdc97bacc096b4c2d1c841e91794b6e7a54e7a4853fc0907956dc \ - --hash=sha256:f96c7a4fcbd68e18b13cb3b6ed5d24b5326b256933f476bdaf2cc8e609c228db \ - --hash=sha256:f9a637d4e0c541149e0d409c24f4ec79cd74d87508ee6a17a7e64a9b9c0cf56f \ - --hash=sha256:fe2ee94080d77b84e38cb6643b75a6ca29cf814a3e5d5da8e1176eae4034d662 - # via - # --override images/gpu/vllm-overrides.txt - # vllm +xgrammar==0.2.3 \ + --hash=sha256:00f6ba916fe84552f303b1b576061296ff8bea1e24d065efec49543489dd5217 \ + --hash=sha256:07d4c238c4545741573653fbfa66072ae208a85926c3a3f4d57c99fd67fde886 \ + --hash=sha256:11108010c54c8f12f0b14c239ef5bca217cf107fa3d0bf0db008bf594ab0a1ff \ + --hash=sha256:11255f184971489fc72b948b096e2917f482ba2dca975177f5411562cedb9c6d \ + --hash=sha256:11d5cd45891f311d7c487635fdee637c5ae375d8292601ad55483c89653749e6 \ + --hash=sha256:173e167d43a5cf4171eee2be86097decff8803b0a0853d7baaf446c732a7d3a9 \ + --hash=sha256:1ba69a13ae4cc94b7a3a0b5ac67865ce372e0eac7b6486f7e8ca0c0cbbbc3097 \ + --hash=sha256:29d0fbd4709733b224e6e115d1001fb07124c71fe114a9087a2e3e53ce871517 \ + --hash=sha256:355f21445ba361fa258d34106fa31852be9222f0fd3f48fa3fe089d5201679e7 \ + --hash=sha256:3a718fa2c1bfc06951e1c480a656c28bde6d5f828199308fd577bd264e05b923 \ + --hash=sha256:3fdcee5e375c8cfe83e41c4a396a861cafe7cda381c566c24d26628a970a9ec4 \ + --hash=sha256:48d2c9d2bab9b60653204bf334663e06f6044d8f5c104aca68ee23526afb3161 \ + --hash=sha256:5eec3987abb915b7182587cf8063ae16b9fcb51e8b5f70558d8d2f0bfb8e04c4 \ + --hash=sha256:66501b2f02d5620971a13a085566f4fadd5ce86c6de41dbe3b47b5062644881a \ + --hash=sha256:838f88cd74c00670e4b0797ffd7bec8399b67f90ca0f199c96a68333f70553b4 \ + --hash=sha256:8c262af85340825e6bda407293713582f95169fb098cb107751b55c0b3c309fb \ + --hash=sha256:a1b6dc2657d9a1efbb770e0982ec0f2718c1de198cf24ccc8db2372eba01cb64 \ + --hash=sha256:aa35f24835a59c822e249ecc80912eea4de03fc8b04afb2f82c8b950a56be6ef \ + --hash=sha256:b257973b2878bcb2c02f24057d174c0d9715cdc02e8ce29149a350184e619f86 \ + --hash=sha256:b385f2ce3dac24273325ff0c41e3098a07fba061c265ab68cfc9f7fafa71e5b3 \ + --hash=sha256:ba7051a0cc67f9a2ebb9c564f3cf286ccad532534b3faff8b261fe0909dd18c8 \ + --hash=sha256:bec963362548e48b9a763de8f801d2b6c3f6dfb050cc88de2a83fe5e90dec357 \ + --hash=sha256:c4039c128c1636a6ba4def3c8dc31f1d3942133195a0caccf4efc7fef50f2667 \ + --hash=sha256:d38fb3ad3118b8f08b1da53fb6feb81a206e6eb9df6abb16bda47e2cb272deef \ + --hash=sha256:d5c03cdd7847e1281ef2189e600fe7b4fe39058e1f7b6946e58c7f0308dc11cf \ + --hash=sha256:d7e0fa6f30ae206fca972a8ef5f607be543f32a9155523f445730a427a264679 \ + --hash=sha256:e7787dd8321a04f86116b756aa3dadd622e3607a3559b1e986cc5f77da00d68e \ + --hash=sha256:e983c6521ebac727e8513acd3f3eb76b9bfe9fedb3c528ddb01845c08f098652 \ + --hash=sha256:ea79d53314d614a7beab09570a659ec72e4c79fe61b0a9ad153da95b4ee8fcd7 \ + --hash=sha256:eeb5e46bd7d3230e5d8e6385793c48ec872a4fb377c19d341dbcaedc41f495e9 \ + --hash=sha256:f1270736d74ca3276cfba593457c23b9ca37032bd8051614533c81ed892d2727 \ + --hash=sha256:f26c8bb1845119856b09658bcf2ee525957dc618d954684e5c393d16bcc1f1da \ + --hash=sha256:f76423630ae3ac4e090cb38ce1e30e7bcc69b3dee4d22d94353944386a4c6f18 \ + --hash=sha256:fdf081fab29694302d41d61dcf52fad7d253879a718bc6afc68db0a0dabd7f19 \ + --hash=sha256:fe71c621a18ec1fd0a74e755e72c5e521c4854b75d14ad52f1fc8c325d387124 + # via vllm yarl==1.24.2 \ --hash=sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b \ --hash=sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30 \ @@ -3968,3 +4449,12 @@ yarl==1.24.2 \ --hash=sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056 \ --hash=sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14 # via aiohttp +z3-solver==4.15.4.0 \ + --hash=sha256:00bd10c5a6a5f6112d3a9a810d0799227e52f76caa860dafa5e00966bb47eb13 \ + --hash=sha256:00e35b02632ed085ea8199fb230f6015e6fc40554a6680c097bd5f060e827431 \ + --hash=sha256:407e825cc9211f95ef46bdc8d151bf630e7ab2d62a21d24cd74c09cc5b73f3aa \ + --hash=sha256:62c7e9cbdd711932301f29919ad9158de9b2f58b4d281dd259bbcd0a2f408ba1 \ + --hash=sha256:7e103a6f203f505b8b8b8e5c931cc407c95b61556512d4921c1ddc0b3f41b08e \ + --hash=sha256:928c29b58c4eb62106da51c1914f6a4a55d0441f8f48a81b9da07950434a8946 \ + --hash=sha256:be3bc916545c96ffbf89e00d07104ff14f78336e55db069177a1bfbcc01b269d + # via tilelang diff --git a/images/gpu/vllm.in b/images/gpu/vllm.in index 01847e94..8866d951 100644 --- a/images/gpu/vllm.in +++ b/images/gpu/vllm.in @@ -4,14 +4,7 @@ # # SPDX-License-Identifier: MIT -vllm @ https://github.com/vllm-project/vllm/releases/download/v0.10.1.1/vllm-0.10.1.1%2Bcu118-cp38-abi3-manylinux1_x86_64.whl -certifi @ https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl -ray==2.55.0 -requests==2.34.2 -setuptools==78.1.1 -torch @ https://download-r2.pytorch.org/whl/cu118/torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=91454dcfdb81f181fdf216d6d6d9912fbd8795578b90384b3b8b8132737072bb -torchaudio @ https://download-r2.pytorch.org/whl/cu118/torchaudio-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=6b64a313866b2a5bb43d4c6906e8820d3dc844f89a97c00dc5c8b92e6b70fe17 -torchvision @ https://download-r2.pytorch.org/whl/cu118/torchvision-0.22.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=e4a8696261b8c64297a2683656f4ee64418d7ee391627702fe5c06b72ce81123 -transformers==5.5.0 -urllib3==2.7.0 -xformers @ https://files.pythonhosted.org/packages/ff/e3/ee90c62a3235152d4ea8e983a5eb7ac00b10582fee86aaadb11571c1ecba/xformers-0.0.31-cp39-abi3-manylinux_2_28_x86_64.whl#sha256=50aedaea82a38d7d28631f77617d1ed1f6f37c60bdc4bf167a69cbc0e39cee76 +vllm @ https://github.com/vllm-project/vllm/releases/download/v0.25.1/vllm-0.25.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl#sha256=9e206f370c934a2d4b6b1f05d3d09708d344e05d80260189ef19f60755709431 +torch @ https://download-r2.pytorch.org/whl/cu129/torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=68b83cb7d7d43bc67c2833c8aebaea6a966f2017c3389885affa3361c258b7e3 +torchaudio @ https://download-r2.pytorch.org/whl/cu129/torchaudio-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=de4105653562f031edd6ddf7dd1485f07db6ac62cdc90d17364648cd3c89eb5a +torchvision @ https://download-r2.pytorch.org/whl/cu129/torchvision-0.26.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=086aa9ac91e56cf339ea6c646bc000b970e62159b609d84e0a2d3c9859fe5885 diff --git a/images/platforms.toml b/images/platforms.toml index a546f618..eaefdb90 100644 --- a/images/platforms.toml +++ b/images/platforms.toml @@ -58,7 +58,7 @@ ci_tag = "edge-terra-ci" commit_runtime_tag = "sha--terra" commit_gpu_runtime_tag = "sha--terra-gpu-nvidia" bundles_model_artifact = false -gpu_runtime = "vllm @ https://github.com/vllm-project/vllm/releases/download/v0.10.1.1/vllm-0.10.1.1%2Bcu118-cp38-abi3-manylinux1_x86_64.whl" +gpu_runtime = "vLLM 0.25.1+cu129 with PyTorch 2.11.0+cu129" model_cache = "/.heartwood/models" ci_required = true live_workspace_validation_required = false diff --git a/packages/cli/src/heartwood/cli/__init__.py b/packages/cli/src/heartwood/cli/__init__.py index 7c5062e7..c57fa7cb 100644 --- a/packages/cli/src/heartwood/cli/__init__.py +++ b/packages/cli/src/heartwood/cli/__init__.py @@ -167,6 +167,11 @@ def _build_parser() -> argparse.ArgumentParser: help="Require explicit inputs and do not prompt.", ) setup.add_argument("--yes", action="store_true", help="Confirm the displayed configuration.") + setup.add_argument( + "--yes-download", + action="store_true", + help="Confirm the displayed model download without an interactive prompt.", + ) runtime = subparsers.add_parser( "runtime", help="Advanced Heartwood-managed inference operations." @@ -182,12 +187,17 @@ def _build_parser() -> argparse.ArgumentParser: runtime_start.add_argument( "--gpus", type=int, - default=1, - help="Number of GPUs to request; the current runtime supports one GPU.", + help="Advanced: override the catalog model's qualified GPU count.", ) - runtime_start.add_argument("--cpus", type=int, default=8) - runtime_start.add_argument("--memory", default="64G") + runtime_start.add_argument("--cpus", type=int) + runtime_start.add_argument("--memory") runtime_start.add_argument("--time", dest="time_limit", default="02:00:00") + runtime_start.add_argument( + "--task-profile", + choices=("auto", "standard", "powerful", "maximum"), + default="auto", + help="Capability tier used when Heartwood recommends a model.", + ) runtime_start.add_argument("--startup-timeout", type=int, default=600) runtime_start.add_argument("--dry-run", action="store_true") runtime_start.add_argument("--no-allocate", action="store_true") @@ -196,6 +206,11 @@ def _build_parser() -> argparse.ArgumentParser: action="store_true", help="Confirm the displayed scheduler request without an interactive prompt.", ) + runtime_start.add_argument( + "--yes-download", + action="store_true", + help="Confirm the displayed pinned model download without an interactive prompt.", + ) runtime_start.add_argument("--inside-allocation", action="store_true", help=argparse.SUPPRESS) allow = subparsers.add_parser( @@ -412,9 +427,11 @@ def _main(argv: Sequence[str] | None = None) -> int: if args.command == "setup": return _handle_setup(parser, args, project=project) if args.command == "runtime" and args.runtime_command == "start": - if args.gpus != 1: - parser.error("--gpus must be 1; multi-GPU managed inference is not supported") - if args.cpus < 1 or args.startup_timeout < 1 or args.port < 1: + if args.gpus is not None and args.gpus < 1: + parser.error("--gpus must be positive") + if args.cpus is not None and args.cpus < 1: + parser.error("--cpus must be positive") + if args.startup_timeout < 1 or args.port < 1: parser.error("--cpus, --startup-timeout, and --port must be positive") return run_launch(_launch_options(project, args)) configured_gateway: SessionGateway | None = None @@ -594,13 +611,15 @@ def _launch_options(project: ProjectContext, args: argparse.Namespace) -> Launch project=project, session_id=args.session_id, partition=getattr(args, "partition", None), - gpus=getattr(args, "gpus", 1), - cpus=getattr(args, "cpus", 8), - memory=getattr(args, "memory", "64G"), + gpus=getattr(args, "gpus", None), + cpus=getattr(args, "cpus", None), + memory=getattr(args, "memory", None), time_limit=getattr(args, "time_limit", "02:00:00"), + task_profile=getattr(args, "task_profile", "auto"), dry_run=getattr(args, "dry_run", False), no_allocate=getattr(args, "no_allocate", False), yes_request_allocation=getattr(args, "yes_request_allocation", False), + yes_download=getattr(args, "yes_download", False), inside_allocation=getattr(args, "inside_allocation", False), plain=args.plain, web=args.interface == "web", @@ -832,6 +851,7 @@ def _configure_setup( gateway, model_id=model_id, non_interactive=non_interactive, + yes_download=bool(getattr(args, "yes_download", False)), ) return 0, gateway source_option = next( @@ -947,6 +967,7 @@ def _configure_local_model( *, model_id: str | None, non_interactive: bool, + yes_download: bool, ) -> None: local_catalog, service_models = _run_with_progress( lambda: _available_managed_models(gateway), @@ -969,12 +990,14 @@ def _configure_local_model( recommendation_id = str(item.get("model_id")) label = str(item.get("label")) runtime = "CPU" if item.get("runtime") == "llama-cpp" else "NVIDIA GPU" - source = ( - "Recommended" - if item.get("catalog_source") == "recommended" - else "Previously selected" - ) - choices.append((recommendation_id, f"{label} ({source}, {runtime})")) + if item.get("recommended") is True: + source = "Heartwood recommendation" + elif item.get("catalog_source") == "catalog": + source = "Under evaluation" + else: + source = "Previously selected" + tier = _model_tier_label(item.get("tier")) + choices.append((recommendation_id, f"{tier}: {label} ({source}, {runtime})")) choices.append(("other", "Other Hugging Face model")) choices.extend((model, f"{model} (already running)") for model in service_models) for index, (_value, label) in enumerate(choices, start=1): @@ -1006,8 +1029,18 @@ def _configure_local_model( item = known_local_ids[model_id] print("\nSelected Heartwood-managed model") print(f" {item.get('label')}") + print(f" Hugging Face: {item.get('source_repository')}") + print(f" Pinned revision: {item.get('source_revision')}") + size = item.get("size_bytes") + if isinstance(size, int): + print(f" Download: {size / 1024**3:.2f} GiB") if resources := item.get("recommended_resource_envelope"): print(f" {resources}") + _confirm_model_download( + label=str(item.get("label")), + non_interactive=non_interactive, + yes_download=yes_download, + ) _run_with_progress( lambda: gateway.download_local_model_now(model_id), activity=_MODEL_DOWNLOAD_ACTIVITY, @@ -1021,6 +1054,13 @@ def _configure_local_model( print() print(_format_model_repository(plan)) print() + raw_model = plan.get("model", {}) + label = str(raw_model.get("label")) if isinstance(raw_model, dict) else model_id + _confirm_model_download( + label=label, + non_interactive=non_interactive, + yes_download=yes_download, + ) _run_with_progress( lambda: gateway.download_custom_local_model_now(model_id), activity=_MODEL_DOWNLOAD_ACTIVITY, @@ -1033,6 +1073,27 @@ def _configure_local_model( ) +def _confirm_model_download( + *, + label: str, + non_interactive: bool, + yes_download: bool, +) -> None: + if yes_download: + return + if non_interactive: + raise ModelRepositoryError( + "model weights are downloaded only after explicit approval; review the model plan " + "and rerun setup with --yes-download" + ) + try: + approved = input(f"Download {label} into .heartwood/models? [y/N]: ").strip().lower() + except EOFError as error: + raise ModelRepositoryError("model download approval was cancelled") from error + if approved != "y": + raise ModelRepositoryError("model download was not approved") + + def _available_managed_models( gateway: SessionGateway, ) -> tuple[dict[str, object], list[str]]: @@ -1339,24 +1400,33 @@ def _format_model_artifacts(catalog: dict[str, object]) -> str: lines = ["Models Heartwood can run", ""] models = catalog.get("models", []) if isinstance(models, list): - for item in models: - if not isinstance(item, dict): + for tier in ("standard", "powerful", "maximum"): + tier_models = [ + item for item in models if isinstance(item, dict) and item.get("tier") == tier + ] + if not tier_models: continue - size = item.get("size_bytes") - size_gib = float(size) / (1024**3) if isinstance(size, int | float) else 0 - runtime = "CPU" if item.get("runtime") == "llama-cpp" else "NVIDIA GPU" - review = ( - "Recommended" if item.get("catalog_source") == "recommended" else "User selected" - ) - lines.append(f"{item.get('model_id')} {runtime} {size_gib:.2f} GiB {review}") - lines.append(f" {item.get('label')}: {item.get('purpose')}") - context_window = item.get("context_window") - if isinstance(context_window, int): - lines.append(f" Context capacity: up to {context_window:,} tokens") - lines.append(f" {item.get('availability_reason')}") - resources = item.get("recommended_resource_envelope") - if isinstance(resources, str): - lines.append(f" {resources}") + lines.append(_model_tier_label(tier)) + for item in tier_models: + size = item.get("size_bytes") + size_gib = float(size) / (1024**3) if isinstance(size, int | float) else 0 + runtime = "CPU" if item.get("runtime") == "llama-cpp" else "NVIDIA GPU" + if item.get("recommended") is True: + review = "Recommended" + elif item.get("catalog_source") == "catalog": + review = "Evaluation candidate" + else: + review = "User selected" + lines.append(f" {item.get('model_id')} {runtime} {size_gib:.2f} GiB {review}") + lines.append(f" {item.get('label')}: {item.get('purpose')}") + context_window = item.get("context_window") + if isinstance(context_window, int): + lines.append(f" Context capacity: up to {context_window:,} tokens") + lines.append(f" {item.get('availability_reason')}") + resources = item.get("recommended_resource_envelope") + if isinstance(resources, str): + lines.append(f" {resources}") + lines.append("") lines.extend( ( "", @@ -1368,6 +1438,14 @@ def _format_model_artifacts(catalog: dict[str, object]) -> str: return "\n".join(lines) +def _model_tier_label(value: object) -> str: + if value == "powerful": + return "Powerful" + if value == "maximum": + return "Maximum capability" + return "Standard" + + def _format_model_repository(inspection: dict[str, object]) -> str: model = inspection.get("model", {}) if not isinstance(model, dict): diff --git a/packages/cli/src/heartwood/cli/_launch.py b/packages/cli/src/heartwood/cli/_launch.py index 0bc502c6..4ff638b9 100644 --- a/packages/cli/src/heartwood/cli/_launch.py +++ b/packages/cli/src/heartwood/cli/_launch.py @@ -20,7 +20,7 @@ from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, replace from pathlib import Path -from typing import Literal +from typing import Literal, cast from packaging.version import InvalidVersion, Version @@ -28,10 +28,14 @@ from heartwood.cli._model_snapshot import verify_snapshot from heartwood.gateway import ( LocalContextPlan, + ModelSnapshot, ProjectConfig, ProjectConfigStore, ProjectContext, + SessionGateway, + discover_slurm_gpu_partitions, estimate_local_runtime_memory, + inspect_gpu_environment, managed_model_token_budgets, plan_local_context_window, verify_model_artifact, @@ -52,7 +56,6 @@ "TERM", "LD_LIBRARY_PATH", "PYTHONPATH", - "VLLM_USE_FLASHINFER_SAMPLER", ) @@ -67,13 +70,15 @@ class LaunchOptions: project: ProjectContext session_id: str partition: str | None - gpus: int - cpus: int - memory: str + gpus: int | None + cpus: int | None + memory: str | None time_limit: str + task_profile: Literal["auto", "standard", "powerful", "maximum"] dry_run: bool no_allocate: bool yes_request_allocation: bool + yes_download: bool inside_allocation: bool plain: bool web: bool @@ -96,8 +101,20 @@ class LaunchPlan: project_root: Path runtime: str | None model_id: str | None + artifact_id: str | None context_window: int | None partition: str | None = None + gpus: int | None = None + cpus: int | None = None + memory: str | None = None + model_size_bytes: int | None = None + model_tier: str | None = None + model_precision: str | None = None + model_qualification: str | None = None + startup_seconds_min: int | None = None + startup_seconds_max: int | None = None + environment_notes: tuple[str, ...] = () + download_required: bool = False def format(self) -> str: """Render the launch proposal without secrets.""" @@ -108,6 +125,10 @@ def format(self) -> str: f"Platform: {self.platform_id}", f"Compute: {compute}", f"Model: {self.model_root if self.model_root is not None else 'not selected'}", + ( + "Catalog model: " + f"{self.artifact_id if self.artifact_id is not None else 'not selected'}" + ), f"Runtime: {self.runtime if self.runtime is not None else 'not selected'}", ( f"Context capacity: up to {self.context_window:,} tokens" @@ -117,10 +138,34 @@ def format(self) -> str: f"State: {self.state_root}", f"Project: {self.project_root}", ] + if self.model_tier is not None: + lines.append(f"Capability: {self.model_tier.title()}") + if self.model_precision is not None: + lines.append(f"Precision: {self.model_precision}") + if self.model_qualification is not None: + label = ( + "Qualified" if self.model_qualification == "qualified" else "Evaluation candidate" + ) + lines.append(f"Qualification: {label}") + if self.model_size_bytes is not None: + lines.append(f"Model download: {_format_bytes(self.model_size_bytes)}") + if self.startup_seconds_min is not None and self.startup_seconds_max is not None: + lines.append( + "Expected startup: " + f"{_format_duration(self.startup_seconds_min)} to " + f"{_format_duration(self.startup_seconds_max)}" + ) if self.partition is not None: lines.append(f"GPU partition: {self.partition}") + if self.gpus is not None: + lines.append(f"GPU allocation: {self.gpus}") + if self.cpus is not None: + lines.append(f"CPU allocation: {self.cpus}") + if self.memory is not None: + lines.append(f"RAM allocation: {self.memory}") if self.allocation_command: lines.append(f"Request: {shlex.join(self.allocation_command)}") + lines.extend(f"Resource check: {note}" for note in self.environment_notes) return "\n".join(lines) @@ -135,28 +180,166 @@ class LocalRuntimeSelection: size_bytes: int | None artifact_sha256: str | None context_window: int + maximum_context_window: int + tier: Literal["standard", "powerful", "maximum"] + precision: str + qualification: Literal["candidate", "qualified"] + minimum_gpu_count: int + minimum_gpu_memory_bytes: int + recommended_ram_bytes: int | None + recommended_disk_bytes: int | None + tool_call_parser: Literal["hermes", "openai", "qwen3_coder"] | None + tensor_parallel_size: int + startup_seconds_min: int + startup_seconds_max: int + catalog_source: Literal["catalog", "user-selected"] + + +def _allocation_resources( + options: LaunchOptions, + selection: LocalRuntimeSelection | None, +) -> tuple[int, int, str]: + if selection is None or selection.runtime == "llama-cpp": + gpus = options.gpus or 1 + else: + gpus = options.gpus or selection.tensor_parallel_size + if gpus != selection.tensor_parallel_size: + raise LaunchConfigurationError( + f"{selection.model_id} was qualified with " + f"{selection.tensor_parallel_size} GPU(s); choose a catalog model configured " + f"for {gpus} GPU(s) instead" + ) + if gpus < selection.minimum_gpu_count: + raise LaunchConfigurationError( + f"{selection.model_id} requires at least {selection.minimum_gpu_count} GPU(s)" + ) + cpus = options.cpus or max(8, gpus * 8) + recommended_memory = selection.recommended_ram_bytes if selection is not None else None + memory = options.memory or ( + f"{_ceil_gib(recommended_memory)}G" if recommended_memory is not None else "64G" + ) + return gpus, cpus, memory + + +def _validate_gpu_environment( + platform_id: str, + selection: LocalRuntimeSelection | None, + env: Mapping[str, str], + *, + allocation_required: bool, +) -> tuple[str, ...]: + if selection is None or selection.runtime != "vllm": + return () + environment = inspect_gpu_environment(platform_id, env) + available, reason = environment.assess( + gpu_count=selection.tensor_parallel_size, + gpu_memory_bytes=selection.minimum_gpu_memory_bytes, + ) + if not available: + raise LaunchConfigurationError(reason) + notes = [reason] + if allocation_required: + notes.append("The exact devices and NVIDIA driver will be checked inside the allocation.") + elif environment.visible_devices: + drivers = ", ".join( + sorted({device.driver_version for device in environment.visible_devices}) + ) + notes.append(f"NVIDIA driver: {drivers}; Heartwood runtime ABI: CUDA 12.9") + return tuple(notes) + + +def _recommend_model( + options: LaunchOptions, + env: Mapping[str, str], + *, + platform_id: str, +) -> ModelSnapshot | None: + task_profile = options.task_profile + if task_profile == "auto": + task_profile = "powerful" if platform_id == "carina" else "standard" + gateway = SessionGateway(project=options.project, env=env) + try: + gpu_environment = gateway.gpu_environment() + maximum_gpu_count = max( + (capacity.gpu_count for capacity in gpu_environment.capacities), + default=0, + ) + if options.gpus is not None and options.gpus > maximum_gpu_count: + raise LaunchConfigurationError( + f"{options.gpus} GPU(s) were requested, but only {maximum_gpu_count} compatible " + "GPU(s) were detected" + ) + return gateway.recommend_managed_model( + maximum_tier=task_profile, + requested_gpus=options.gpus, + gpu_environment=gpu_environment, + ) + finally: + gateway.stop() + + +def _selection_from_snapshot( + snapshot: ModelSnapshot, + project: ProjectContext, +) -> LocalRuntimeSelection: + return LocalRuntimeSelection( + artifact_id=snapshot.snapshot_id, + model_root=project.models_dir / snapshot.snapshot_id, + runtime="vllm", + model_id="heartwood-managed-model", + size_bytes=snapshot.expected_size_bytes, + artifact_sha256=None, + context_window=snapshot.context_window, + maximum_context_window=snapshot.maximum_context_window, + tier=snapshot.tier, + precision=snapshot.precision, + qualification=snapshot.qualification, + minimum_gpu_count=snapshot.minimum_gpu_count, + minimum_gpu_memory_bytes=snapshot.minimum_gpu_memory_bytes, + recommended_ram_bytes=snapshot.recommended_ram_bytes, + recommended_disk_bytes=snapshot.recommended_disk_bytes, + tool_call_parser=snapshot.tool_call_parser, + tensor_parallel_size=snapshot.tensor_parallel_size, + startup_seconds_min=snapshot.startup_seconds_min, + startup_seconds_max=snapshot.startup_seconds_max, + catalog_source="catalog", + ) def build_launch_plan(options: LaunchOptions, env: Mapping[str, str]) -> LaunchPlan: """Build a platform-specific launch plan without changing external state.""" - if options.gpus != 1: - raise LaunchConfigurationError( - "Heartwood currently supports exactly one GPU per managed vLLM runtime" - ) platform_id = select_platform_adapter(env).adapter_id selection = _local_model_selection(options.project, env) + if selection is None: + recommendation = _recommend_model(options, env, platform_id=platform_id) + if recommendation is not None: + selection = _selection_from_snapshot(recommendation, options.project) + gpus, cpus, memory = _allocation_resources(options, selection) allocation_required = platform_id == "carina" and not env.get("SLURM_JOB_ID") + environment_notes = _validate_gpu_environment( + platform_id, + selection, + env, + allocation_required=allocation_required, + ) command: tuple[str, ...] = () partition: str | None = None if allocation_required: - partition = _resolve_slurm_partition(options.partition, env) + partition = _resolve_slurm_partition( + options.partition, + env, + required_gpus=gpus, + required_gpu_memory_bytes=( + selection.minimum_gpu_memory_bytes if selection is not None else 0 + ), + ) command = ( "srun", "--pty", f"--partition={partition}", - f"--gres=gpu:{options.gpus}", - f"--cpus-per-task={options.cpus}", - f"--mem={options.memory}", + f"--gres=gpu:{gpus}", + f"--cpus-per-task={cpus}", + f"--mem={memory}", f"--time={options.time_limit}", f"--chdir={options.project.root}", _slurm_export_argument(env), @@ -171,8 +354,24 @@ def build_launch_plan(options: LaunchOptions, env: Mapping[str, str]) -> LaunchP options.project.root, selection.runtime if selection is not None else None, selection.model_id if selection is not None else None, + selection.artifact_id if selection is not None else None, selection.context_window if selection is not None else None, partition=partition, + gpus=gpus if allocation_required else None, + cpus=cpus if allocation_required else None, + memory=memory if allocation_required else None, + model_size_bytes=selection.size_bytes if selection is not None else None, + model_tier=selection.tier if selection is not None else None, + model_precision=selection.precision if selection is not None else None, + model_qualification=(selection.qualification if selection is not None else None), + startup_seconds_min=(selection.startup_seconds_min if selection is not None else None), + startup_seconds_max=(selection.startup_seconds_max if selection is not None else None), + environment_notes=environment_notes, + download_required=( + selection is not None + and selection.catalog_source == "catalog" + and not selection.model_root.exists() + ), ) @@ -206,6 +405,45 @@ def run_launch( print(plan.format()) if active_options.dry_run: return 0 + if plan.artifact_id is None: + print( + "\nNo qualified Heartwood-managed model matches the detected resources. " + "Run `heartwood models managed` for lower-resource and advanced options." + ) + return 64 + if plan.download_required: + if not active_options.yes_download: + try: + approved = ( + input_fn("\nDownload this pinned model into .heartwood/models? [y/N]: ") + .strip() + .lower() + == "y" + ) + except EOFError: + approved = False + if not approved: + print("Model download cancelled; no allocation was requested.") + return 1 + print("\nDownloading the reviewed model snapshot. This can take several minutes.") + gateway = SessionGateway(project=active_options.project, env=active_env) + try: + gateway.download_local_model_now( + plan.artifact_id, + progress_callback=_progress_reporter("Model download"), + ) + except (OSError, ValueError) as error: + print(f"Model download failed: {error}") + return 74 + finally: + gateway.stop() + try: + plan = build_launch_plan(active_options, active_env) + except LaunchConfigurationError as error: + print(f"Launch configuration error after model download: {error}") + return 64 + print("\nModel ready. Updated launch plan:\n") + print(plan.format()) if plan.allocation_required: if active_options.no_allocate: print("\nA GPU allocation is required; rerun without --no-allocate.") @@ -373,11 +611,9 @@ def _run_runtime(options: LaunchOptions, env: Mapping[str, str]) -> int: ) runtime = subprocess.Popen( _runtime_command( - runtime_kind, runtime_executable, staged_source, - model_id, - selection.context_window, + selection, ), stdout=log_file, stderr=subprocess.STDOUT, @@ -500,13 +736,11 @@ def _resolve_runtime_executable(runtime: str) -> Path: def _runtime_command( - runtime: str, executable: Path, model: Path, - model_id: str, - context_window: int, + selection: LocalRuntimeSelection, ) -> tuple[str, ...]: - if runtime == "llama-cpp": + if selection.runtime == "llama-cpp": model_file = _gguf_file(model) return ( str(executable), @@ -514,14 +748,16 @@ def _runtime_command( str(model_file), "--jinja", "--alias", - model_id, + selection.model_id, "--host", "127.0.0.1", "--port", "8765", "--ctx-size", - str(context_window), + str(selection.context_window), ) + if selection.tool_call_parser is None: # pragma: no cover - persisted invariant + raise LaunchConfigurationError("the selected vLLM model has no tool-call parser") return ( str(executable), "serve", @@ -531,12 +767,14 @@ def _runtime_command( "--port", "8765", "--served-model-name", - model_id, + selection.model_id, "--max-model-len", - str(context_window), + str(selection.context_window), + "--tensor-parallel-size", + str(selection.tensor_parallel_size), "--enable-auto-tool-choice", "--tool-call-parser", - "hermes", + selection.tool_call_parser, ) @@ -574,6 +812,25 @@ def _local_model_selection( size_bytes=selection.size_bytes, artifact_sha256=selection.artifact_sha256, context_window=selection.context_window, + maximum_context_window=selection.maximum_context_window, + tier=cast(Literal["standard", "powerful", "maximum"], selection.tier), + precision=selection.precision or "Unspecified", + qualification=cast(Literal["candidate", "qualified"], selection.qualification), + minimum_gpu_count=selection.minimum_gpu_count, + minimum_gpu_memory_bytes=selection.minimum_gpu_memory_bytes, + recommended_ram_bytes=selection.recommended_ram_bytes, + recommended_disk_bytes=selection.recommended_disk_bytes, + tool_call_parser=cast( + Literal["hermes", "openai", "qwen3_coder"] | None, + selection.tool_call_parser, + ), + tensor_parallel_size=selection.tensor_parallel_size, + startup_seconds_min=selection.startup_seconds_min, + startup_seconds_max=selection.startup_seconds_max, + catalog_source=cast( + Literal["catalog", "user-selected"], + selection.catalog_source, + ), ) @@ -614,7 +871,10 @@ def _print_resource_assessment( model_size_bytes=model_bytes, runtime="vllm", ) - gpu_available = _available_gpu_memory_bytes(env) + gpu_available = _available_gpu_memory_bytes( + env, + count=selection.tensor_parallel_size, + ) _print_memory_result("GPU memory", gpu_required, gpu_available) @@ -626,7 +886,7 @@ def _context_plan( if model_bytes is None and selection.model_root.exists(): model_bytes = _model_size(selection.model_root) available = ( - _available_gpu_memory_bytes(env) + _available_gpu_memory_bytes(env, count=selection.tensor_parallel_size) if selection.runtime == "vllm" else _available_system_memory_bytes() ) @@ -691,7 +951,11 @@ def _available_system_memory_bytes() -> int | None: return min(candidates) if candidates else None -def _available_gpu_memory_bytes(env: Mapping[str, str]) -> int | None: +def _available_gpu_memory_bytes( + env: Mapping[str, str], + *, + count: int = 1, +) -> int | None: executable = shutil.which("nvidia-smi", path=env.get("PATH")) if executable is None: return None @@ -718,7 +982,9 @@ def _available_gpu_memory_bytes(env: Mapping[str, str]) -> int | None: ] except ValueError: return None - return min(values) if values else None + if len(values) < count: + return sum(values) if values else None + return sum(sorted(values, reverse=True)[:count]) def _verify_local_model( @@ -819,11 +1085,6 @@ def _runtime_environment( "XDG_CACHE_HOME": str(project.cache_dir), } ) - result.update( - { - "VLLM_USE_FLASHINFER_SAMPLER": env.get("VLLM_USE_FLASHINFER_SAMPLER", "0"), - } - ) return result @@ -952,63 +1213,64 @@ def update(config: ProjectConfig) -> ProjectConfig: store.update(update) -def _resolve_slurm_partition(requested: str | None, env: Mapping[str, str]) -> str: - partitions = _discover_slurm_gpu_partitions(env) +def _resolve_slurm_partition( + requested: str | None, + env: Mapping[str, str], + *, + required_gpus: int = 1, + required_gpu_memory_bytes: int = 0, +) -> str: + partitions = discover_slurm_gpu_partitions(env) + eligible = tuple( + partition + for partition in partitions + if partition.gpu_count >= required_gpus + and ( + partition.gpu_memory_bytes is None + or partition.gpu_memory_bytes >= required_gpu_memory_bytes + ) + ) if requested: - if partitions and requested not in {name for name, _is_default in partitions}: - available = ", ".join(name for name, _is_default in partitions) + requested_partitions = tuple( + partition for partition in partitions if partition.name == requested + ) + if partitions and not requested_partitions: + available = ", ".join(sorted({partition.name for partition in partitions})) raise LaunchConfigurationError( f"GPU partition {requested!r} is unavailable; choose one of: {available}" ) + if requested_partitions and not any( + partition.gpu_count >= required_gpus + and ( + partition.gpu_memory_bytes is None + or partition.gpu_memory_bytes >= required_gpu_memory_bytes + ) + for partition in requested_partitions + ): + available_count = max(partition.gpu_count for partition in requested_partitions) + raise LaunchConfigurationError( + f"GPU partition {requested!r} exposes at most {available_count} GPU(s) or " + "insufficient per-device memory for the selected model" + ) return requested - for name, is_default in partitions: - if is_default: - return name - if len(partitions) == 1: - return partitions[0][0] - if partitions: - available = ", ".join(name for name, _is_default in partitions) + for partition in eligible: + if partition.is_default: + return partition.name + eligible_names = tuple(dict.fromkeys(partition.name for partition in eligible)) + if len(eligible_names) == 1: + return eligible_names[0] + if eligible: + available = ", ".join(eligible_names) raise LaunchConfigurationError( f"no default GPU partition was detected; choose one of: {available}" ) - raise LaunchConfigurationError("no available GPU partition was detected; pass --partition") - - -def _discover_slurm_gpu_partitions(env: Mapping[str, str]) -> tuple[tuple[str, bool], ...]: - try: - completed = subprocess.run( - ("sinfo", "--noheader", "--format=%P|%G|%a"), - check=False, - capture_output=True, - text=True, - timeout=15, - env=_scheduler_environment(env), + if partitions: + largest = max(partition.gpu_count for partition in partitions) + raise LaunchConfigurationError( + f"available GPU partitions expose at most {largest} GPU(s); " + f"the selected model requires {required_gpus}" ) - except (OSError, subprocess.TimeoutExpired): - return () - if completed.returncode != 0: - return () - partitions: list[tuple[str, bool]] = [] - for line in completed.stdout.splitlines(): - fields = line.strip().split("|") - if len(fields) != 3: - continue - raw_name, resources, state = fields - if "gpu" not in resources.lower() or state.lower() not in {"up", "idle", "mix", "alloc"}: - continue - name = raw_name.rstrip("*") - entry = (name, raw_name.endswith("*")) - if name and entry not in partitions: - partitions.append(entry) - return tuple(partitions) - - -def _scheduler_environment(env: Mapping[str, str]) -> dict[str, str]: - return { - name: env[name] - for name in ("PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE") - if name in env - } + raise LaunchConfigurationError("no available GPU partition was detected; pass --partition") def _slurm_export_argument(env: Mapping[str, str]) -> str: @@ -1147,6 +1409,44 @@ def _format_bytes(value: int) -> str: return f"{amount:.1f} TiB" +def _ceil_gib(value: int) -> int: + return max(1, (value + 1024**3 - 1) // 1024**3) + + +def _format_duration(seconds: int) -> str: + if seconds < 60: + return f"{seconds} seconds" + minutes = (seconds + 59) // 60 + return f"{minutes} minute{'s' if minutes != 1 else ''}" + + +def _progress_reporter(label: str) -> Callable[[int, int], None]: + started = time.monotonic() + last_report = -5.0 + + def report(completed: int, total: int) -> None: + nonlocal last_report + now = time.monotonic() + if completed < total and now - last_report < 5: + return + last_report = now + elapsed = max(now - started, 0.001) + percent = completed / total * 100 if total > 0 else 0.0 + rate = completed / elapsed + remaining = (total - completed) / rate if rate > 0 and total > completed else 0 + timing = ( + f", about {_format_duration(max(1, int(remaining)))} remaining" + if remaining > 0 and elapsed >= 5 + else "" + ) + print( + f"{label}: {percent:5.1f}% " + f"({_format_bytes(completed)} of {_format_bytes(total)}{timing})" + ) + + return report + + def _print_runtime_failure(log_path: Path, return_code: int | None) -> None: if return_code is not None: print(f"vLLM exited with status {return_code}.") diff --git a/packages/cli/tests/test_carina_launch_integration.py b/packages/cli/tests/test_carina_launch_integration.py index 1299d1fc..4fc51fee 100644 --- a/packages/cli/tests/test_carina_launch_integration.py +++ b/packages/cli/tests/test_carina_launch_integration.py @@ -70,6 +70,9 @@ def test_carina_launch_handoff_setup_and_cleanup(tmp_path: Path) -> None: path=model_root, runtime="vllm", model_id="test-model", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + tool_call_parser="hermes", ) _write_executable( @@ -77,7 +80,7 @@ def test_carina_launch_handoff_setup_and_cleanup(tmp_path: Path) -> None: """ #!/usr/bin/env bash set -euo pipefail - printf 'dev*|gpu:nvidia_l40s:8|up\n' + printf 'dev*|gpu:nvidia_l40s:8|up|512000|128\n' """, ) _write_executable( @@ -126,6 +129,14 @@ def test_carina_launch_handoff_setup_and_cleanup(tmp_path: Path) -> None: exec env -i "${clean_environment[@]}" "${command[@]}" """, ) + _write_executable( + scheduler_bin / "nvidia-smi", + """ + #!/usr/bin/env bash + set -euo pipefail + printf '0, NVIDIA L40S, 46068, 45000, 570.148.08, 8.9\n' + """, + ) heartwood_python = runtime_root / "heartwood" / "bin" / "python" heartwood_python.parent.mkdir(parents=True) heartwood_python.symlink_to(sys.executable) @@ -134,7 +145,7 @@ def test_carina_launch_handoff_setup_and_cleanup(tmp_path: Path) -> None: """ #!/usr/bin/env bash set -euo pipefail - echo '0.10.1.1 2.7.1 11.8' + echo '0.25.1+cu129 2.11.0+cu129 12.9' """, ) _write_python_executable( @@ -243,7 +254,7 @@ def stop(_signum, _frame): assert runtime_environment["ld_library_path"].startswith( str(runtime_root / "bootstrap" / "lib") ) - assert runtime_environment["sampler"] == "0" + assert runtime_environment["sampler"] is None assert not runtime_environment["secret_present"] assert runtime_environment["path"].startswith(str(runtime_root / "bootstrap" / "bin")) config = tomllib.loads(project.config_path.read_text(encoding="utf-8")) diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index 1a1d2d7d..f3cdddd1 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -285,13 +285,21 @@ def test_non_interactive_local_setup_accepts_one_hugging_face_identifier( artifact_sha256=hashlib.sha256(b"content").hexdigest(), minimum_resource_envelope="Estimated minimum: 4 CPU cores.", recommended_resource_envelope="Recommended: 8 CPU cores.", + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=21, ) class Repository: def plan(self, *_args: object, **_kwargs: object) -> LocalModelDownloadPlan: return LocalModelDownloadPlan(choice, "Selected a balanced GGUF model.") - def download(artifact: ModelArtifact, *, cache_dir: Path) -> Path: + def download( + artifact: ModelArtifact, + *, + cache_dir: Path, + progress_callback: object = None, + ) -> Path: + del progress_callback destination = cache_dir / artifact.artifact_id / artifact.source_path destination.parent.mkdir(parents=True) destination.write_bytes(b"content") @@ -304,22 +312,18 @@ def download(artifact: ModelArtifact, *, cache_dir: Path) -> Path: model_repository=Repository(), ) - assert ( - _run( - project, - monkeypatch, - [ - "setup", - "--model-source", - "heartwood", - "--model-id", - "example/research-model-gguf", - "--non-interactive", - "--yes", - ], - ) - == 0 - ) + setup = [ + "setup", + "--model-source", + "heartwood", + "--model-id", + "example/research-model-gguf", + "--non-interactive", + "--yes", + ] + assert _run(project, monkeypatch, setup) == 1 + assert "rerun setup with --yes-download" in capsys.readouterr().out + assert _run(project, monkeypatch, [*setup, "--yes-download"]) == 0 config = RealSessionGateway(project=ProjectContext(project), env={}).config_store.load() assert config.local_model is not None @@ -860,7 +864,7 @@ def test_invalid_session_and_launch_resources_are_argument_errors( with pytest.raises(SystemExit) as invalid_resources: _run(tmp_path / "launch", monkeypatch, ["runtime", "start", "--gpus", "0"]) assert invalid_resources.value.code == 2 - assert "--gpus must be 1" in capsys.readouterr().err + assert "--gpus must be positive" in capsys.readouterr().err @pytest.mark.parametrize( @@ -1478,8 +1482,9 @@ def test_cli_formatters_fail_closed_on_malformed_projection_data( { "model_id": "gguf", "runtime": "llama-cpp", + "tier": "standard", "size_bytes": "unknown", - "catalog_source": "recommended", + "catalog_source": "catalog", "label": "GGUF", "purpose": "Synthetic", "availability_reason": "Available", @@ -1487,6 +1492,7 @@ def test_cli_formatters_fail_closed_on_malformed_projection_data( { "model_id": "vllm", "runtime": "vllm", + "tier": "powerful", "size_bytes": 1024, "catalog_source": "user-selected", "label": "vLLM", diff --git a/packages/cli/tests/test_launch.py b/packages/cli/tests/test_launch.py index a6ddcd95..cda77296 100644 --- a/packages/cli/tests/test_launch.py +++ b/packages/cli/tests/test_launch.py @@ -13,7 +13,7 @@ from collections.abc import Sequence from pathlib import Path from types import SimpleNamespace -from typing import cast +from typing import Literal, cast import pytest @@ -25,7 +25,6 @@ _available_gpu_memory_bytes, _available_system_memory_bytes, _catalog_contains_model, - _discover_slurm_gpu_partitions, _ensure_setup, _format_bytes, _gguf_file, @@ -51,11 +50,13 @@ ) from heartwood.cli._model_snapshot import verify_snapshot from heartwood.gateway import ( + GpuDevice, ModelSettings, ProjectConfig, ProjectConfigStore, ProjectContext, SessionGateway, + SlurmGpuPartition, model_profile_from_preset, ) @@ -66,6 +67,7 @@ def _options( selected: bool = True, runtime: str = "vllm", context_window: int = 32_768, + qualification: Literal["candidate", "qualified"] = "qualified", **overrides: object, ) -> LaunchOptions: tmp_path.mkdir(parents=True, exist_ok=True) @@ -85,7 +87,25 @@ def _options( path=project.models_dir / ("model.gguf" if runtime == "llama-cpp" else "model"), runtime=runtime, model_id="test-model", + display_name="Synthetic test model", + source_repository="example/test-model", + source_revision="1" * 40, + size_bytes=1024, + minimum_free_bytes=2048, + license_posture="Synthetic test model.", + license_id="Apache-2.0", context_window=context_window, + maximum_context_window=context_window, + minimum_resource_envelope="Synthetic minimum resources.", + recommended_resource_envelope="Synthetic recommended resources.", + precision="Synthetic", + qualification=qualification, + minimum_gpu_count=0 if runtime == "llama-cpp" else 1, + minimum_gpu_memory_bytes=0 if runtime == "llama-cpp" else 1, + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=32 * 1024**3, + tool_call_parser=None if runtime == "llama-cpp" else "hermes", + catalog_source="user-selected", ) values: dict[str, object] = { "project": project, @@ -95,9 +115,11 @@ def _options( "cpus": 8, "memory": "64G", "time_limit": "02:00:00", + "task_profile": "auto", "dry_run": False, "no_allocate": False, "yes_request_allocation": False, + "yes_download": False, "inside_allocation": False, "plain": True, "web": False, @@ -111,6 +133,41 @@ def _options( return LaunchOptions(**values) # type: ignore[arg-type] +def _partition( + name: str = "gpu", + *, + default: bool = True, + gpu_model: str = "nvidia_l40s", + gpu_count: int = 8, +) -> SlurmGpuPartition: + return SlurmGpuPartition( + name=name, + is_default=default, + gpu_model=gpu_model, + gpu_count=gpu_count, + node_memory_bytes=512 * 1024**3, + node_cpu_count=64, + state="up", + ) + + +@pytest.fixture(autouse=True) +def _synthetic_visible_gpu(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_visible_gpus", + lambda _env: ( + GpuDevice( + index=0, + name="Tesla T4", + total_memory_bytes=16_000_000_000, + free_memory_bytes=15_000_000_000, + driver_version="570.86.15", + compute_capability=(7, 5), + ), + ), + ) + + def _snapshot(root: Path) -> None: root.mkdir(parents=True, exist_ok=True) weights = root / "weights.safetensors" @@ -123,7 +180,7 @@ def test_carina_plan_preserves_project_and_exports_no_credentials( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr("heartwood.cli._launch._discover_slurm_gpu_partitions", lambda _env: ()) + monkeypatch.setattr("heartwood.cli._launch.discover_slurm_gpu_partitions", lambda _env: ()) options = _options(tmp_path) plan = build_launch_plan( options, @@ -139,6 +196,7 @@ def test_carina_plan_preserves_project_and_exports_no_credentials( assert plan.allocation_required assert plan.context_window == 32_768 assert "Context capacity: up to 32,768 tokens" in plan.format() + assert "Qualification: Qualified" in plan.format() assert plan.allocation_command[:3] == ("srun", "--pty", "--partition=gpu") assert f"--chdir={tmp_path}" in plan.allocation_command export = next(item for item in plan.allocation_command if item.startswith("--export=")) @@ -148,22 +206,31 @@ def test_carina_plan_preserves_project_and_exports_no_credentials( assert "--model-root" not in plan.allocation_command -def test_launch_plan_rejects_unimplemented_multi_gpu_runtime(tmp_path: Path) -> None: - with pytest.raises(LaunchConfigurationError, match="exactly one GPU"): +def test_launch_plan_rejects_gpu_count_outside_the_model_qualification(tmp_path: Path) -> None: + with pytest.raises(LaunchConfigurationError, match="qualified with 1 GPU"): build_launch_plan( _options(tmp_path, gpus=2), {"HEARTWOOD_PLATFORM": "carina"}, ) +def test_launch_plan_labels_evaluation_candidate(tmp_path: Path) -> None: + plan = build_launch_plan( + _options(tmp_path, qualification="candidate"), + {"HEARTWOOD_PLATFORM": "terra", "PATH": "/usr/bin"}, + ) + + assert "Qualification: Evaluation candidate" in plan.format() + + def test_launch_requires_consent_and_honors_dry_run_and_no_allocate( tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr( - "heartwood.cli._launch._discover_slurm_gpu_partitions", - lambda _env: (("gpu", True),), + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: (_partition(),), ) called = False @@ -207,8 +274,8 @@ def test_launch_handles_closed_consent_and_submits_approved_plan( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr( - "heartwood.cli._launch._discover_slurm_gpu_partitions", - lambda _env: (("gpu", True),), + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: (_partition(),), ) submitted: list[tuple[str, ...]] = [] @@ -254,7 +321,7 @@ def test_launch_reports_missing_selection_artifact_and_runtime( ) -> None: env = {"HEARTWOOD_PLATFORM": "generic"} assert run_launch(_options(tmp_path, selected=False), env=env) == 64 - assert "No Heartwood-managed model is selected" in capsys.readouterr().out + assert "No qualified Heartwood-managed model" in capsys.readouterr().out assert run_launch(_options(tmp_path), env=env) == 66 _snapshot(tmp_path / ".heartwood" / "models" / "model") monkeypatch.setattr( @@ -513,7 +580,7 @@ def completed( monkeypatch.setattr("heartwood.cli._launch._preflight_vllm", lambda *_args: None) monkeypatch.setattr( "heartwood.cli._launch._available_gpu_memory_bytes", - lambda _env: 48 * 1024**3, + lambda _env, **_kwargs: 48 * 1024**3, ) monkeypatch.setattr("heartwood.cli._launch.subprocess.Popen", FakeProcess) monkeypatch.setattr("heartwood.cli._launch._wait_for_runtime", lambda *_args, **_kwargs: True) @@ -626,12 +693,32 @@ def terminate(self) -> None: def test_llama_cpp_command_uses_the_selected_gguf(tmp_path: Path) -> None: model = tmp_path / "model.gguf" model.write_bytes(b"synthetic") + selection = LocalRuntimeSelection( + artifact_id="test-model", + model_root=model, + runtime="llama-cpp", + model_id="heartwood-managed-model", + size_bytes=model.stat().st_size, + artifact_sha256=hashlib.sha256(model.read_bytes()).hexdigest(), + context_window=32_768, + maximum_context_window=32_768, + tier="standard", + precision="GGUF synthetic", + qualification="qualified", + minimum_gpu_count=0, + minimum_gpu_memory_bytes=0, + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=32 * 1024**3, + tool_call_parser=None, + tensor_parallel_size=1, + startup_seconds_min=1, + startup_seconds_max=2, + catalog_source="user-selected", + ) command = _runtime_command( - "llama-cpp", Path("/opt/llama.cpp/llama-server"), model, - "heartwood-managed-model", - 32_768, + selection, ) assert command[:3] == ( "/opt/llama.cpp/llama-server", @@ -657,6 +744,19 @@ def test_resource_assessment_reports_context_and_memory_status( size_bytes=10 * 1024**3, artifact_sha256=None, context_window=32_768, + maximum_context_window=32_768, + tier="standard", + precision="Synthetic", + qualification="qualified", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=32 * 1024**3, + tool_call_parser="hermes", + tensor_parallel_size=1, + startup_seconds_min=1, + startup_seconds_max=2, + catalog_source="user-selected", ) monkeypatch.setattr( "heartwood.cli._launch._available_system_memory_bytes", @@ -664,7 +764,7 @@ def test_resource_assessment_reports_context_and_memory_status( ) monkeypatch.setattr( "heartwood.cli._launch._available_gpu_memory_bytes", - lambda _env: 24 * 1024**3, + lambda _env, **_kwargs: 24 * 1024**3, ) _print_resource_assessment(selection, {"PATH": "/usr/bin"}) @@ -712,7 +812,7 @@ def test_available_gpu_memory_uses_least_available_visible_device( ), ) - assert _available_gpu_memory_bytes({"PATH": "/usr/bin"}) == 8 * 1024**3 + assert _available_gpu_memory_bytes({"PATH": "/usr/bin"}) == 16 * 1024**3 def test_runtime_resolution_and_gguf_directory_contract( @@ -789,7 +889,7 @@ def test_runtime_environment_scrubs_credentials_and_legacy_path_controls() -> No assert runtime_env["CLUSTER_NAME"] == "saturn-runtime" assert runtime_env["HEARTWOOD_GPU_RUNTIME"] == "vllm" assert runtime_env["HEARTWOOD_PLATFORM_HOME"] == "/home/jupyter" - assert runtime_env["VLLM_USE_FLASHINFER_SAMPLER"] == "0" + assert "VLLM_USE_FLASHINFER_SAMPLER" not in runtime_env assert not any("API_KEY" in name for name in runtime_env) assert "HEARTWOOD_HOME" not in runtime_env assert "HEARTWOOD_WORKSPACE" not in runtime_env @@ -824,8 +924,8 @@ def run_successfully(command: Sequence[str]) -> int: return 0 monkeypatch.setattr( - "heartwood.cli._launch._discover_slurm_gpu_partitions", - lambda _env: (("gpu", True),), + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: (_partition(),), ) assert ( @@ -1046,15 +1146,12 @@ def test_partition_discovery_and_selection( capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.setattr( - "heartwood.cli._launch.subprocess.run", - lambda *_args, **_kwargs: subprocess.CompletedProcess( - (), 0, stdout="dev*|gpu:nvidia_l40s:8|up\nlong|gpu:nvidia_l40s:8|up\n" + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: ( + _partition("dev", default=True), + _partition("long", default=False), ), ) - assert _discover_slurm_gpu_partitions({"PATH": "/usr/bin"}) == ( - ("dev", True), - ("long", False), - ) plan = build_launch_plan( _options(tmp_path, partition=None), {"HEARTWOOD_PLATFORM": "carina"}, @@ -1062,8 +1159,11 @@ def test_partition_discovery_and_selection( assert "--partition=dev" in plan.allocation_command monkeypatch.setattr( - "heartwood.cli._launch._discover_slurm_gpu_partitions", - lambda _env: (("dev", True), ("normal", False)), + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: ( + _partition("dev", default=True), + _partition("normal", default=False), + ), ) assert ( run_launch( @@ -1087,8 +1187,11 @@ def test_partition_discovery_handles_ambiguous_and_unavailable_schedulers( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr( - "heartwood.cli._launch._discover_slurm_gpu_partitions", - lambda _env: (("dev", False), ("normal", False)), + "heartwood.cli._launch.discover_slurm_gpu_partitions", + lambda _env: ( + _partition("dev", default=False), + _partition("normal", default=False), + ), ) assert ( run_launch( @@ -1099,17 +1202,6 @@ def test_partition_discovery_handles_ambiguous_and_unavailable_schedulers( ) assert "no default GPU partition" in capsys.readouterr().out - monkeypatch.setattr( - "heartwood.cli._launch.subprocess.run", - lambda *_args, **_kwargs: subprocess.CompletedProcess((), 1, stdout=""), - ) - assert _discover_slurm_gpu_partitions({}) == () - monkeypatch.setattr( - "heartwood.cli._launch.subprocess.run", - lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("missing sinfo")), - ) - assert _discover_slurm_gpu_partitions({}) == () - def test_vllm_preflight_and_output_helpers( tmp_path: Path, @@ -1135,7 +1227,7 @@ def completed(command: Sequence[str], **_kwargs: object) -> subprocess.Completed "xgrammar GHSA-7rgv-gqhr-fxg3 fixes verified\n" ), ) - return subprocess.CompletedProcess(command, 0, stdout="0.10.1.1 2.7.1 11.8\n") + return subprocess.CompletedProcess(command, 0, stdout="0.25.1+cu129 2.11.0+cu129 12.9\n") monkeypatch.setattr("heartwood.cli._launch.subprocess.run", completed) assert _preflight_vllm(executable, {"PATH": "/usr/bin"}) is None @@ -1194,12 +1286,12 @@ def test_vllm_preflight_reports_missing_and_failed_runtime( stdout="", stderr=( "Traceback (most recent call last):\n" - "AssertionError: CUDA is unavailable to PyTorch 2.7.1 (built for CUDA 11.8)\n" + "AssertionError: CUDA is unavailable to PyTorch 2.11.0 (built for CUDA 12.9)\n" ), ), ) assert _preflight_vllm(executable, {}) == ( - "AssertionError: CUDA is unavailable to PyTorch 2.7.1 (built for CUDA 11.8)" + "AssertionError: CUDA is unavailable to PyTorch 2.11.0 (built for CUDA 12.9)" ) monkeypatch.setattr( @@ -1238,7 +1330,7 @@ def failed_compatibility( stdout="", stderr="first line\nincompatible model configuration\n", ) - return subprocess.CompletedProcess(command, 0, stdout="0.10.1.1 2.7.1 11.8\n") + return subprocess.CompletedProcess(command, 0, stdout="0.25.1+cu129 2.11.0+cu129 12.9\n") monkeypatch.setattr("heartwood.cli._launch.subprocess.run", failed_compatibility) assert _preflight_vllm(wrapper, {}) == "incompatible model configuration" @@ -1248,7 +1340,7 @@ def timed_out_compatibility( ) -> subprocess.CompletedProcess[str]: if command == (str(wrapper), "__heartwood_verify_runtime__"): raise subprocess.TimeoutExpired(command, 60) - return subprocess.CompletedProcess(command, 0, stdout="0.10.1.1 2.7.1 11.8\n") + return subprocess.CompletedProcess(command, 0, stdout="0.25.1+cu129 2.11.0+cu129 12.9\n") monkeypatch.setattr("heartwood.cli._launch.subprocess.run", timed_out_compatibility) assert "timed out after 60 seconds" in str(_preflight_vllm(wrapper, {})) @@ -1265,6 +1357,19 @@ def test_model_staging_helpers_cover_files_directories_and_sizes(tmp_path: Path) size_bytes=4, artifact_sha256=hashlib.sha256(b"1234").hexdigest(), context_window=32_768, + maximum_context_window=32_768, + tier="standard", + precision="GGUF synthetic", + qualification="qualified", + minimum_gpu_count=0, + minimum_gpu_memory_bytes=0, + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=32 * 1024**3, + tool_call_parser=None, + tensor_parallel_size=1, + startup_seconds_min=1, + startup_seconds_max=2, + catalog_source="user-selected", ) _verify_local_model(selection) file_destination = tmp_path / "file-stage" @@ -1300,7 +1405,7 @@ def test_imported_vllm_snapshot_passes_the_launch_integrity_gate(tmp_path: Path) gateway.import_local_model( snapshot, - source_repository="example/research-model", + source_repository="Qwen/Qwen2.5-Coder-7B-Instruct", source_revision="1" * 40, license_posture="Apache-2.0", context_window=32_768, diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py new file mode 100644 index 00000000..2de70a9e --- /dev/null +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -0,0 +1,178 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Tests for the portable real-model coding-agent qualification contract.""" + +from __future__ import annotations + +import importlib.util +import json +from collections.abc import Callable +from pathlib import Path +from types import ModuleType +from typing import Any, cast + +import pytest + +from heartwood.audit import AuditLog +from heartwood.session import SessionEvent + + +def _root() -> Path: + return Path(__file__).resolve().parents[3] + + +def _module(name: str, path: Path) -> ModuleType: + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _event(sequence: int, kind: str, payload: dict[str, Any]) -> SessionEvent: + return SessionEvent( + event_id=f"qualification-event-{sequence:06d}", + session_id="qualification", + sequence=sequence, + kind=cast(Any, kind), + occurred_at="2026-07-20T00:00:00Z", + payload=payload, + ) + + +def _acceptance_files(tmp_path: Path) -> tuple[Path, Path, Path, Path, Path]: + events = ( + _event( + 0, + "model_call.decision.recorded", + { + "decision": {"decision": "allow"}, + "model_profile": {"action_confirmation_mode": "always-confirm"}, + }, + ), + _event( + 1, + "tool_call.proposed", + {"tool_call_id": "tool-1", "tool_name": "terminal"}, + ), + _event( + 2, + "confirmation.requested", + {"request": {"tool_call_id": "tool-1"}}, + ), + _event( + 3, + "confirmation.resolved", + {"tool_call_id": "tool-1", "decision": "approved"}, + ), + _event( + 4, + "tool.execution.recorded", + {"tool_name": "terminal", "exit_code": 0}, + ), + _event(5, "agent_message.emitted", {"content": "Complete"}), + _event(6, "audit.export.recorded", {"scrubbed": True}), + ) + events_path = tmp_path / "events.jsonl" + events_path.write_text( + "".join(event.model_dump_json() + "\n" for event in events), + encoding="utf-8", + ) + audit_path = tmp_path / "audit-export.jsonl" + audit = AuditLog(audit_path) + for event in events: + audit.append( + session_id=event.session_id, + event_type=str(event.kind), + occurred_at=event.occurred_at, + payload={"safe": True}, + ) + artifact_path = tmp_path / "cohort-summary.json" + artifact_path.write_text( + json.dumps( + { + "summary": { + "source_participant_count": 24, + "participant_count": 20, + "source_condition_occurrence_count": 39, + "condition_occurrence_count": 35, + }, + "quality_checks": {"aggregate_only_output": True}, + "export_guard": {"exportable": True}, + } + ), + encoding="utf-8", + ) + replay_path = tmp_path / "replay.txt" + replay_path.write_text( + "Action set approved (1 action)\nTool terminal exit=0\n", + encoding="utf-8", + ) + inference_path = tmp_path / "inference.json" + inference_path.write_text('{"content_nonempty": true}\n', encoding="utf-8") + return events_path, audit_path, artifact_path, replay_path, inference_path + + +def test_coding_agent_qualification_verifies_complete_acceptance_evidence( + tmp_path: Path, +) -> None: + module = _module( + "verify_coding_agent_e2e", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + + summary = verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) + + assert summary["tool_execution_count"] == 1 + assert cast(dict[str, bool], summary["checks"])["audit_export_verified"] is True + + +def test_coding_agent_qualification_rejects_incomplete_replay(tmp_path: Path) -> None: + module = _module( + "verify_coding_agent_e2e_incomplete", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + replay.write_text("Action set approved (1 action)\n", encoding="utf-8") + + with pytest.raises(ValueError, match="fresh-process replay"): + verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) + + +def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: + module = _module( + "gpu_qualification_config", + _root() / "images/gpu/qualification_config.py", + ) + load = cast(Callable[[Path, str], dict[str, Any]], module.load_configuration) + + resolved = load( + _root() / "images/gpu/compatibility.toml", + "terra-t4-qwen25-coder-7b-awq", + ) + + assert resolved["runtime"]["cuda_version"] == "12.9" + assert resolved["configuration"]["tool_call_parser"] == "hermes" + assert resolved["configuration"]["model_revision"] == ( + "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" + ) diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index bd5d9075..602ff62f 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -59,7 +59,12 @@ def test_generic_image_packages_one_no_weight_runtime() -> None: assert digest in llama_installer assert "/etc/ld.so.conf.d/heartwood-llama.conf" in dockerfile assert "ldconfig" in dockerfile - assert 'chown -R "${HEARTWOOD_RUNTIME_USER}:${HEARTWOOD_RUNTIME_USER}"' in dockerfile + assert 'runtime_group="$(id -gn "${HEARTWOOD_RUNTIME_USER}")"' in dockerfile + assert '--owner="${HEARTWOOD_RUNTIME_USER}" --group="${runtime_group}"' in dockerfile + assert '"${HEARTWOOD_RUNTIME_HOME}/.cache/flashinfer"' in dockerfile + assert '"${HEARTWOOD_RUNTIME_HOME}/.cache/huggingface"' in dockerfile + assert '"${HEARTWOOD_RUNTIME_HOME}/.cache/vllm"' in dockerfile + assert 'chown -R "${HEARTWOOD_RUNTIME_USER}:${runtime_group}"' in dockerfile assert "COPY --chown=heartwood:heartwood" not in dockerfile assert dockerfile.index("uv sync --locked --no-dev --all-extras") < dockerfile.index( "USER ${HEARTWOOD_RUNTIME_USER}" @@ -138,6 +143,8 @@ def test_platform_image_adds_heartwood_without_replacing_terra_runtime() -> None assert "HEARTWOOD_IMAGE_FLAVOR=${HEARTWOOD_IMAGE_FLAVOR}" in dockerfile assert "HEARTWOOD_PLATFORM=${HEARTWOOD_PLATFORM}" in dockerfile assert "HEARTWOOD_PLATFORM_HOME=${HEARTWOOD_RUNTIME_HOME}" in dockerfile + assert "install -d --mode=0755 \\" in dockerfile + assert '"${HEARTWOOD_RUNTIME_HOME}/.cache/flashinfer"' in dockerfile assert 'dockerfile = "images/Dockerfile"' in bake assert 'target = "runtime-image"' in bake assert 'target = "platform-runtime-image"' in bake @@ -167,10 +174,7 @@ def test_platform_image_adds_heartwood_without_replacing_terra_runtime() -> None assert terra["gpu_runtime_tag"] == "edge-terra-gpu-nvidia" assert terra["commit_runtime_tag"] == "sha--terra" assert terra["commit_gpu_runtime_tag"] == "sha--terra-gpu-nvidia" - assert terra["gpu_runtime"] == ( - "vllm @ https://github.com/vllm-project/vllm/releases/download/v0.10.1.1/" - "vllm-0.10.1.1%2Bcu118-cp38-abi3-manylinux1_x86_64.whl" - ) + assert terra["gpu_runtime"] == "vLLM 0.25.1+cu129 with PyTorch 2.11.0+cu129" assert terra["bundles_model_artifact"] is False assert terra["supported_platforms"] == ["linux/amd64"] assert terra["manifest_media_type"] == "application/vnd.docker.distribution.manifest.v2+json" @@ -274,11 +278,11 @@ def test_gpu_runtime_is_isolated_pinned_and_no_weight() -> None: installer = _read("images/gpu/install_runtime.sh") launcher = _read("images/gpu/start_vllm.sh") verifier = _read("images/gpu/verify_runtime.sh") - compatibility = _read("images/gpu/heartwood_vllm.py") - sitecustomize = _read("images/gpu/sitecustomize.py") + runtime_contract = _toml("images/gpu/compatibility.toml") + runtime_verifier = _read("images/gpu/verify_vllm.py") executable = _read("images/gpu/heartwood-vllm") lock = _read("images/gpu/vllm-requirements.txt") - overrides = _read("images/gpu/vllm-overrides.txt") + exclusions = _read("images/gpu/vllm-exclusions.txt") assert "images/gpu/install_runtime.sh" in dockerfile assert "--target /opt/heartwood-vllm --python 3.12" in dockerfile @@ -296,75 +300,58 @@ def test_gpu_runtime_is_isolated_pinned_and_no_weight() -> None: assert '"${uv}" venv "${target}"' in installer assert '"${uv}" pip sync' in installer assert '"${runtime_sources}/vllm-requirements.txt"' in installer - assert '"${runtime_sources}/heartwood_vllm.py"' in installer - assert '"${runtime_sources}/sitecustomize.py"' in installer + assert '"${runtime_sources}/verify_vllm.py"' in installer + assert '"${runtime_sources}/compatibility.toml"' in installer assert '"${runtime_sources}/heartwood-vllm"' in installer - assert "vllm-0.10.1.1%2Bcu118" in lock - assert "certifi-2026.6.17-py3-none-any.whl" in lock - assert "certifi==2022.12.7" not in lock - assert "ray==2.55.0" in lock - assert "setuptools==78.1.1" in lock - assert "torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl" in lock - assert "torchaudio-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl" in lock - assert "torchvision-0.22.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl" in lock - assert "xformers-0.0.31-cp39-abi3-manylinux_2_28_x86_64.whl" in lock - assert "transformers==5.5.0" in lock - assert "requests==2.34.2" in lock - assert "urllib3==2.7.0" in lock - assert "xgrammar==0.1.32" in lock - assert "GHSA-7rgv-gqhr-fxg3" in overrides - assert "xgrammar==0.1.32" in overrides - assert "idna==3.18" in lock - assert "GHSA-65pc-fj4g-8rjx" in overrides - assert "idna==3.18" in overrides - assert "nvidia-cuda-runtime-cu11==11.8.89" in lock - assert "--extra-index-url https://download.pytorch.org/whl/cu118" not in lock - assert "nvidia-cuda-runtime-cu13" not in lock + assert "vllm-0.25.1%2Bcu129-cp38-abi3-manylinux_2_28_x86_64.whl" in lock + assert "torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" in lock + assert "torchaudio-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" in lock + assert "torchvision-0.26.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" in lock + assert "nvidia-cuda-runtime-cu12==12.9.79" in lock + assert "flashinfer-python==0.6.13" in lock + assert "xgrammar==0.2.3" in lock + assert "--extra-index-url" not in lock + for package in ( + "cuda-tile==", + "nvidia-cuda-crt==", + "nvidia-cuda-nvcc==", + "nvidia-cuda-runtime==", + "nvidia-cuda-tileiras==", + "nvidia-nvvm==", + ): + assert package not in lock + assert package.removesuffix("==") in exclusions assert "--hash=sha256:" in lock assert 'host="${HEARTWOOD_LOCAL_RUNTIME_HOST:-127.0.0.1}"' in launcher assert "--enable-auto-tool-choice" in launcher assert 'tool_parser="${HEARTWOOD_VLLM_TOOL_PARSER:-hermes}"' in launcher - assert "VLLM_USE_FLASHINFER_SAMPLER" in launcher + assert "VLLM_USE_FLASHINFER_SAMPLER" not in launcher assert "huggingface.co" not in launcher assert "/opt/heartwood-vllm/bin/heartwood-vllm" in launcher assert "/opt/heartwood-vllm/bin/python" in verifier assert "__heartwood_verify_runtime__" in verifier - assert 'torch.version.cuda == "11.8"' in verifier + assert 'torch.version.cuda == "12.9"' in verifier assert "-name '*.gguf' -o -name '*.safetensors'" in verifier assert "-name '*.bin' -size +10M" in verifier assert "compressed_tensors/transform/utils/hadamards.safetensors" in verifier assert "verify_no_model_artifacts /opt /home" in verifier assert "GPU runtime image contains a model artifact" in verifier - assert 'cls.__module__.startswith("vllm.")' in compatibility - assert "PreTrainedConfig.__init_subclass__ = classmethod" in compatibility - assert "_heartwood_compatibility_applied" in compatibility - assert 'hasattr(PreTrainedTokenizerBase, "all_special_tokens_extended")' in compatibility - assert "self.added_tokens_decoder.values()" in compatibility - assert 'ModelRegistry.models.get("Qwen2ForCausalLM")' in compatibility - assert "registered_model.inspect_model_cls()" in compatibility - assert "get_cached_tokenizer" in compatibility - assert "tokenizer_check = subprocess.run" in compatibility - assert "activate_runtime_boundary" in sitecustomize - assert 'export PYTHONPATH="${runtime_bin}"' in executable - assert "${PYTHONPATH" not in executable - assert '_VULNERABLE_CONFIG_TYPE = "Llama_Nemotron_Nano_VL"' in compatibility - assert "_CONFIG_REGISTRY.pop(_VULNERABLE_CONFIG_TYPE, None)" in compatibility - assert ( - "vulnerable_module.get_class_from_dynamic_module = reject_dynamic_loader" in compatibility - ) - assert "GHSA-8fr4-5q9j-m8gm" in compatibility - assert "GHSA-7rgv-gqhr-fxg3" in compatibility - assert 'version("xgrammar") != "0.1.32"' in compatibility - assert "GHSA-65pc-fj4g-8rjx" in compatibility - assert 'version("idna") != "3.18"' in compatibility - assert "vllm.v1.structured_output import backend_xgrammar" in compatibility - assert "from vllm.transformers_utils import config as config_module" in compatibility - assert "vllm.transformers_utils.tokenizer" in compatibility - assert "vllm.model_executor.models.registry import ModelRegistry" in compatibility - assert 'os.environ["PYTHONPATH"] = str(runtime_bin)' in compatibility - assert "activate_runtime_boundary()" in sitecustomize - assert '"model_type": "qwen2"' in compatibility - assert "trust_remote_code=False" in compatibility + assert "PYTHONPATH" not in executable + assert runtime_contract["runtime"] == { + "python_version": "3.12", + "vllm_version": "0.25.1+cu129", + "pytorch_version": "2.11.0+cu129", + "torchaudio_version": "2.11.0+cu129", + "torchvision_version": "0.26.0+cu129", + "cuda_version": "12.9", + "minimum_driver_version": "525.60.13", + "cuda_13_qualified": False, + } + assert "ToolParserManager.list_registered" in runtime_verifier + assert 'import_module("flashinfer")' in runtime_verifier + assert "_FORBIDDEN_CUDA_13_PACKAGES" in runtime_verifier + assert not (_repo_root() / "images/gpu/heartwood_vllm.py").exists() + assert not (_repo_root() / "images/gpu/sitecustomize.py").exists() assert os.access(_repo_root() / "images/gpu/verify_runtime.sh", os.X_OK) assert os.access(_repo_root() / "images/gpu/heartwood-vllm", os.X_OK) assert os.access(_repo_root() / "images/gpu/install_runtime.sh", os.X_OK) @@ -481,7 +468,7 @@ def test_carina_native_launch_requires_verified_synthetic_allocation() -> None: assert "images/gpu/vllm-requirements.txt" not in bootstrap assert '"${root}/vllm/bin/python"' in bootstrap assert "import torch, vllm" in runtime_verifier - assert "VLLM_USE_FLASHINFER_SAMPLER=0" in bootstrap + assert "VLLM_USE_FLASHINFER_SAMPLER" not in bootstrap assert "ffmpeg" not in bootstrap_environment assert " - tmux" in bootstrap_environment assert "SLURM_JOB_ID" in launch_runtime @@ -497,7 +484,8 @@ def test_carina_native_launch_requires_verified_synthetic_allocation() -> None: assert '"--model-source"' in launch_runtime assert '"heartwood"' in launch_runtime assert "127.0.0.1:8765/v1/models" in launch_runtime - assert '"sinfo", "--noheader", "--format=%P|%G|%a"' in launch_runtime + gpu_environment = _read("packages/gateway/src/heartwood/gateway/_gpu_environment.py") + assert '"sinfo", "--noheader", "--format=%P|%G|%a|%m|%c"' in gpu_environment assert "_SLURM_EXPORTED_ENVIRONMENT" in launch_runtime assert "--export=ALL" not in launch_runtime assert environment["flavors"]["runtime_gpu_nvidia"]["public_default"] is False @@ -664,10 +652,8 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: assert "immutable GPU commit tag does not match" in workflow assert "refusing to move GPU channel tags from a stale main workflow" in workflow assert "promoted ${channel} digest does not match" in workflow - assert "allow-ghsas: GHSA-w8v5-vhqr-4h9v" in dependency_review - assert "GHSA-rrmf-rvhw-rf47" in dependency_review - assert "GHSA-8fr4-5q9j-m8gm" in dependency_review - assert "upstream PR 28126" in dependency_review + assert "allow-ghsas: GHSA-w8v5-vhqr-4h9v, GHSA-rrmf-rvhw-rf47" in dependency_review + assert "GHSA-8fr4-5q9j-m8gm" not in dependency_review def test_vllm_advisory_exceptions_remain_isolated_to_gpu_dependencies() -> None: @@ -693,11 +679,11 @@ def test_vllm_advisory_exceptions_remain_isolated_to_gpu_dependencies() -> None: assert unexpected == [] text = lock.read_text(encoding="utf-8") assert "diskcache==5.6.3" in text - assert "vllm-0.10.1.1%2Bcu118" in text - assert "torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl" in text - assert "vllm-0.10.1.1%2Bcu118" in input_file.read_text(encoding="utf-8") - assert "torch-2.7.1%2Bcu118-cp312-cp312-manylinux_2_28_x86_64.whl" in input_file.read_text( - encoding="utf-8" + assert "vllm-0.25.1%2Bcu129" in text + assert "torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" in text + assert "vllm-0.25.1%2Bcu129" in input_file.read_text(encoding="utf-8") + assert "torch-2.11.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" in ( + input_file.read_text(encoding="utf-8") ) @@ -706,6 +692,7 @@ def test_isolated_smoke_uses_real_openhands_sdk_without_weights() -> None: workflow = _read(".github/workflows/container-smoke.yml") smoke = _read("images/generic/scripts/offline_stack_smoke.sh") capable = _read("images/generic/scripts/capable_model_e2e.sh") + coding_agent = _read("images/generic/scripts/coding_agent_e2e.sh") model_stub = _read("images/generic/scripts/local_model_stub.py") assert "network_mode: none" in compose @@ -719,9 +706,11 @@ def test_isolated_smoke_uses_real_openhands_sdk_without_weights() -> None: assert "HEARTWOOD_LOCAL_RUNTIME_PROFILE=stub-loopback" in smoke assert "HEARTWOOD_SMOKE_PROJECT:-/tmp/heartwood-offline-project" in smoke assert "HEARTWOOD_CAPABLE_PROJECT:-/tmp/heartwood-capable-project" in capable - assert "capable-model artifact must be outside the disposable test project" in capable + assert "coding-agent model must be outside the disposable test project" in coding_agent assert 'workspace = Path.cwd() / ".heartwood" / "sessions"' in smoke - assert 'cohort_path="${project}/cohort-summary.json"' in capable + assert 'cohort_path="${project}/cohort-summary.json"' in coding_agent + assert "Checking direct model inference" in coding_agent + assert "verify_coding_agent_e2e.py" in coding_agent assert "/tmp/heartwood-model-cache/.heartwood/models:/models:ro" in workflow assert "models refresh heartwood" in smoke assert "models connect heartwood heartwood-managed-runtime" in smoke @@ -775,10 +764,12 @@ def test_local_model_stub_preserves_explicit_action_risk() -> None: def test_launch_scripts_are_valid_and_require_explicit_local_artifact() -> None: scripts = ( "images/generic/scripts/capable_model_e2e.sh", + "images/generic/scripts/coding_agent_e2e.sh", "images/generic/scripts/offline_stack_smoke.sh", "images/generic/scripts/container_persistence_smoke.sh", "images/generic/scripts/local_inference_smoke.sh", "images/generic/scripts/start_local_runtime.sh", + "images/gpu/coding_agent_e2e.sh", "images/platform/scripts/terra_image_smoke.sh", "images/platform/scripts/terra_jupyter_contract_smoke.sh", "images/platform/scripts/terra_jupyter_launch_smoke.sh", @@ -853,6 +844,8 @@ def test_publish_workflow_uses_digest_merge_and_clean_public_tags() -> None: smoke = _read(".github/workflows/container-smoke.yml") compose = _read("images/generic/compose.yaml") capable_model = _read("images/generic/scripts/capable_model_e2e.sh") + coding_agent = _read("images/generic/scripts/coding_agent_e2e.sh") + qualification = _read("images/generic/scripts/verify_coding_agent_e2e.py") assert "packages: write" in publish assert "push-by-digest=true" in publish @@ -949,22 +942,25 @@ def test_publish_workflow_uses_digest_merge_and_clean_public_tags() -> None: "images/generic/scripts/local_inference_smoke.sh" ) assert "run_capable_model" in smoke - assert "github.event_name == 'workflow_dispatch'" in smoke + assert "if: inputs.run_capable_model" in smoke + assert "run_capable_model: ${{ github.event_name != 'pull_request' }}" in _read( + ".github/workflows/main-validation.yml" + ) assert "qwen25-7b-instruct-q4_k_m" in smoke assert "capable_model_e2e.sh" in smoke assert "--network none --read-only" in smoke assert smoke.count("uid=10001,gid=10001,mode=0700") == 2 assert compose.count("uid=10001,gid=10001,mode=0700") == 2 - assert "not 1 <= len(terminal_executions) <= 3" in capable_model - assert "not 1 <= len(tool_executions) <= 3" in capable_model - assert "&& cat cohort-summary.json" in capable_model + assert "not 1 <= len(terminal_executions) <= 3" in qualification + assert "not 1 <= len(tool_executions) <= 3" in qualification + assert "&& cat cohort-summary.json" in coding_agent assert 'f"http://127.0.0.1:{port}/health"' in capable_model assert "llama.cpp runtime log (last 200 lines)" in capable_model - assert "cohort_path.is_file()" in capable_model + assert "artifact_path.read_text" in qualification assert "--jinja" in _read("images/generic/scripts/start_local_runtime.sh") - assert 'summary["source_participant_count"] != 24' in capable_model - assert 'summary["participant_count"] != 20' in capable_model - assert 'checks["aggregate_only_output"] is not True' in capable_model + assert '"source_participant_count": 24' in qualification + assert '"participant_count": 20' in qualification + assert 'cohort["quality_checks"].get("aggregate_only_output") is not True' in qualification def test_buildx_metadata_reader_handles_runtime_target_names(tmp_path: Path) -> None: diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index da1cbcd5..6a4fafb5 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -31,10 +31,27 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non verifier = _verifier() sources = verifier.load_model_sources(Path.cwd()) - assert {source.model_id for source in sources} == { - "qwen25-7b-instruct-awq-vllm", - "qwen25-7b-instruct-vllm", - "qwen3-8b-awq-vllm", + assert {source.model_id: (source.repository, source.revision) for source in sources} == { + "gpt-oss-120b-vllm": ( + "openai/gpt-oss-120b", + "b5c939de8f754692c1647ca79fbf85e8c1e70f8a", + ), + "qwen25-coder-7b-instruct-awq-vllm": ( + "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ", + "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a", + ), + "qwen3-coder-30b-a3b-instruct-bf16-vllm": ( + "Qwen/Qwen3-Coder-30B-A3B-Instruct", + "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", + ), + "qwen3-coder-30b-a3b-instruct-fp8-vllm": ( + "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", + "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", + ), + "qwen3-coder-next-fp8-vllm": ( + "Qwen/Qwen3-Coder-Next-FP8", + "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c", + ), } assert all(len(source.revision) == 40 for source in sources) assert all(f"/revision/{source.revision}" in source.api_url for source in sources) diff --git a/packages/gateway/src/heartwood/gateway/__init__.py b/packages/gateway/src/heartwood/gateway/__init__.py index 095b07d0..b94850a6 100644 --- a/packages/gateway/src/heartwood/gateway/__init__.py +++ b/packages/gateway/src/heartwood/gateway/__init__.py @@ -28,6 +28,15 @@ diagnostic_for, ) from heartwood.gateway._gateway import SessionGateway +from heartwood.gateway._gpu_environment import ( + GpuCapacity, + GpuDevice, + GpuEnvironment, + SlurmGpuPartition, + discover_slurm_gpu_partitions, + discover_visible_gpus, + inspect_gpu_environment, +) from heartwood.gateway._local_import import LocalModelImport, import_local_model from heartwood.gateway._local_model_contract import ( LocalContextPlan, @@ -43,7 +52,7 @@ LocalModelRuntime, ModelRepositoryError, ModelRepositoryInspection, - recommended_model_choices, + catalog_model_choices, ) from heartwood.gateway._model_artifacts import ( LocalModelDownloadManager, @@ -79,9 +88,12 @@ model_settings_from_mapping, ) from heartwood.gateway._model_snapshots import ( + ModelQualification, ModelSnapshot, ModelSnapshotCatalog, ModelSnapshotError, + ModelTier, + ToolCallParser, download_model_snapshot, load_model_snapshot_catalog, verify_model_snapshot, @@ -136,6 +148,9 @@ "DiagnosticDefinition", "GatewayAsgiApp", "GatewayEventStream", + "GpuCapacity", + "GpuDevice", + "GpuEnvironment", "HuggingFaceModelRepository", "InterfaceKind", "LocalContextPlan", @@ -156,6 +171,7 @@ "ModelDownload", "ModelPreset", "ModelProfile", + "ModelQualification", "ModelRepositoryError", "ModelRepositoryInspection", "ModelSettings", @@ -164,6 +180,7 @@ "ModelSnapshotCatalog", "ModelSnapshotError", "ModelSourceOption", + "ModelTier", "OpenHandsSdkBackend", "OpenHandsSdkError", "ProjectConfig", @@ -185,17 +202,23 @@ "SkillManager", "SkillSettingsError", "SkillSummary", + "SlurmGpuPartition", "StartupPlan", + "ToolCallParser", "action_settings_from_mapping", + "catalog_model_choices", "custom_model_connection", "custom_model_connection_requires_token", "diagnostic_catalog", "diagnostic_for", + "discover_slurm_gpu_partitions", + "discover_visible_gpus", "download_model_artifact", "download_model_snapshot", "estimate_local_runtime_memory", "import_local_model", "inspect_deployment", + "inspect_gpu_environment", "load_model_artifact_catalog", "load_model_connections", "load_model_snapshot_catalog", @@ -209,7 +232,6 @@ "persist_deployment_profile", "plan_local_context_window", "plan_startup", - "recommended_model_choices", "verify_model_artifact", "verify_model_snapshot", ] diff --git a/packages/gateway/src/heartwood/gateway/_gateway.py b/packages/gateway/src/heartwood/gateway/_gateway.py index 32ce32b4..4784aa4c 100644 --- a/packages/gateway/src/heartwood/gateway/_gateway.py +++ b/packages/gateway/src/heartwood/gateway/_gateway.py @@ -16,7 +16,7 @@ from functools import wraps from pathlib import Path from threading import RLock -from typing import Concatenate, Protocol, cast +from typing import Any, Concatenate, Protocol, cast from heartwood.adapters.platform import select_platform_adapter from heartwood.core_adapter import ( @@ -35,6 +35,7 @@ ActionSettingsError, ) from heartwood.gateway._credentials import CredentialStore, CredentialStoreError +from heartwood.gateway._gpu_environment import GpuEnvironment, inspect_gpu_environment from heartwood.gateway._local_import import import_local_model from heartwood.gateway._local_model_contract import ( MINIMUM_AGENT_RUNTIME_CONTEXT_WINDOW, @@ -46,7 +47,7 @@ LocalModelChoice, LocalModelRuntime, ModelRepositoryError, - recommended_model_choices, + catalog_model_choices, ) from heartwood.gateway._model_artifacts import ( LocalModelDownloadManager, @@ -77,8 +78,10 @@ model_profile_from_preset, ) from heartwood.gateway._model_snapshots import ( + ModelSnapshot, ModelSnapshotCatalog, ModelSnapshotError, + ModelTier, download_model_snapshot, load_model_snapshot_catalog, ) @@ -299,19 +302,24 @@ def __init__( self.snapshot_catalog = snapshot_catalog or load_model_snapshot_catalog( snapshot_catalog_path ) - downloadable_choices = recommended_model_choices( + downloadable_choices = catalog_model_choices( self.artifact_catalog.artifacts, self.snapshot_catalog.snapshots, recommended_only=False, ) - choices = recommended_model_choices( - self.artifact_catalog.artifacts, - self.snapshot_catalog.snapshots, - ) self._downloadable_local_model_choices = { choice.model_id: choice for choice in downloadable_choices } - self._local_model_choices = {choice.model_id: choice for choice in choices} + self._local_model_choices = dict(self._downloadable_local_model_choices) + self._recommended_local_model_ids = { + artifact.artifact_id + for artifact in self.artifact_catalog.artifacts + if artifact.recommended + } | { + snapshot.snapshot_id + for snapshot in self.snapshot_catalog.snapshots + if snapshot.recommended + } selected_local_model = self.config_store.load().local_model if selected_local_model is not None: selected_choice = self._downloadable_local_model_choices.get( @@ -763,6 +771,7 @@ def model_artifacts(self) -> dict[str, object]: bytes_total=size, path=str(path), ) + gpu_environment = self.gpu_environment() preferred_runtime = self._preferred_local_runtime() local_choices = list(self._local_model_choices.values()) local_choices.sort( @@ -772,14 +781,24 @@ def model_artifacts(self) -> dict[str, object]: choice.runtime != preferred_runtime, ) ) - preferred_id = next( - ( - choice.model_id - for choice in local_choices - if self._local_runtime_available(choice.runtime) - and choice.runtime == preferred_runtime - ), - None, + recommendation = self.recommend_managed_model( + maximum_tier=("powerful" if gpu_environment.platform_id == "carina" else "standard"), + gpu_environment=gpu_environment, + ) + preferred_id = ( + recommendation.snapshot_id + if recommendation is not None + else next( + ( + choice.model_id + for choice in local_choices + if choice.model_id in self._recommended_local_model_ids + and choice.qualification_for(gpu_environment.platform_id) == "qualified" + and self._local_runtime_available(choice.runtime) + and choice.runtime == preferred_runtime + ), + None, + ) ) choices = [ self._local_model_choice_dict( @@ -795,6 +814,7 @@ def model_artifacts(self) -> dict[str, object]: else None ) ), + gpu_environment=gpu_environment, ) for choice in local_choices ] @@ -804,8 +824,45 @@ def model_artifacts(self) -> dict[str, object]: "snapshots": snapshot_catalog["snapshots"], "models": choices, "downloads": [status.safe_dict() for status in statuses.values()], + "gpu_environment": { + "platform_id": gpu_environment.platform_id, + "capacities": [ + { + "label": capacity.label, + "gpu_model": capacity.gpu_model, + "gpu_count": capacity.gpu_count, + "gpu_memory_bytes": capacity.gpu_memory_bytes, + "allocation_required": capacity.allocation_required, + "partition": capacity.partition, + } + for capacity in gpu_environment.capacities + ], + }, } + def gpu_environment(self) -> GpuEnvironment: + """Return the shared GPU and scheduler inventory for this deployment.""" + return inspect_gpu_environment(self.config_store.load().platform_id, self.env) + + def recommend_managed_model( + self, + *, + maximum_tier: ModelTier, + requested_gpus: int | None = None, + gpu_environment: GpuEnvironment | None = None, + ) -> ModelSnapshot | None: + """Choose one qualified catalog model for the detected resource envelopes.""" + environment = gpu_environment or self.gpu_environment() + return self.snapshot_catalog.recommend_for_capacities( + platform_id=environment.platform_id, + capacities=tuple( + (capacity.gpu_count, capacity.gpu_memory_bytes) + for capacity in environment.capacities + ), + maximum_tier=maximum_tier, + requested_gpus=requested_gpus, + ) + def _verify_selected_local_artifact( self, selected: LocalModelSelection, @@ -876,14 +933,24 @@ def download_custom_local_model( def download_local_model_now( self, model_id: str, + *, + progress_callback: Callable[[int, int], None] | None = None, ) -> Path: """Download and verify a known model, selecting it when agent-compatible.""" self.project.initialize() model = self._require_local_model_runtime(model_id).download_model() if isinstance(model, ModelArtifact): - path = download_artifact(model, cache_dir=self.model_cache_dir) + path = download_artifact( + model, + cache_dir=self.model_cache_dir, + progress_callback=progress_callback, + ) else: - path = download_model_snapshot(model, cache_dir=self.model_cache_dir) + path = download_model_snapshot( + model, + cache_dir=self.model_cache_dir, + progress_callback=progress_callback, + ) runtime_profile = model.runtime_profile self._select_downloaded_local_model(model_id, path, runtime_profile) return path @@ -893,6 +960,7 @@ def download_custom_local_model_now( repository: str, *, revision: str | None = None, + progress_callback: Callable[[int, int], None] | None = None, ) -> Path: """Resolve, download, verify, and select one user-selected model.""" self.project.initialize() @@ -902,9 +970,17 @@ def download_custom_local_model_now( ) model = choice.download_model() if isinstance(model, ModelArtifact): - path = download_artifact(model, cache_dir=self.model_cache_dir) + path = download_artifact( + model, + cache_dir=self.model_cache_dir, + progress_callback=progress_callback, + ) else: - path = download_model_snapshot(model, cache_dir=self.model_cache_dir) + path = download_model_snapshot( + model, + cache_dir=self.model_cache_dir, + progress_callback=progress_callback, + ) self._select_downloaded_local_model(choice.model_id, path, model.runtime_profile) return path @@ -1249,6 +1325,7 @@ def _select_downloaded_local_model( .model_settings.with_profile(profile) .selecting(profile.profile_id) ) + platform_id = self.config_store.load().platform_id self.config_store.select_local_model( artifact_id=model_id, path=path, @@ -1262,10 +1339,28 @@ def _select_downloaded_local_model( size_bytes=choice.size_bytes, minimum_free_bytes=choice.minimum_free_bytes, license_posture=choice.license_posture, + license_id=choice.license_id, artifact_sha256=choice.artifact_sha256, context_window=choice.context_window, + maximum_context_window=choice.maximum_context_window, minimum_resource_envelope=choice.minimum_resource_envelope, recommended_resource_envelope=choice.recommended_resource_envelope, + precision=choice.precision, + tier=choice.tier, + qualification=choice.qualification_for(platform_id), + minimum_gpu_count=choice.minimum_gpu_count, + minimum_gpu_memory_bytes=choice.minimum_gpu_memory_bytes, + recommended_ram_bytes=choice.recommended_ram_bytes, + recommended_disk_bytes=choice.recommended_disk_bytes, + tool_call_parser=choice.tool_call_parser, + tensor_parallel_size=choice.tensor_parallel_size, + startup_seconds_min=choice.startup_seconds_min, + startup_seconds_max=choice.startup_seconds_max, + download_policy=choice.download_policy, + allow_patterns=choice.allow_patterns, + ignore_patterns=choice.ignore_patterns, + validated_platforms=choice.validated_platforms, + qualification_test=choice.qualification_test, catalog_source=choice.catalog_source, settings=settings, ) @@ -1300,19 +1395,52 @@ def _local_model_choice_dict( active: bool = False, selected: bool = False, recommendation: str | None = None, + gpu_environment: GpuEnvironment | None = None, ) -> dict[str, object]: - available = self._local_runtime_available(choice.runtime) + platform_id = ( + gpu_environment.platform_id + if gpu_environment is not None + else self.config_store.load().platform_id + ) + qualification = choice.qualification_for(platform_id) + runtime_available = self._local_runtime_available(choice.runtime) + resource_reason: str | None = None + available = runtime_available + if choice.runtime == "vllm" and runtime_available: + environment = gpu_environment or self.gpu_environment() + available, resource_reason = environment.assess( + gpu_count=choice.tensor_parallel_size, + gpu_memory_bytes=choice.minimum_gpu_memory_bytes, + ) + if qualification == "candidate": + candidate_reason = "Evaluation candidate; not yet a recommended model" + recommendation = ( + f"{recommendation}; {candidate_reason.lower()}" + if recommendation + else candidate_reason + ) + unavailable_reason = resource_reason if resource_reason and not available else None + if resource_reason and available: + recommendation = ( + f"{recommendation}; {resource_reason}" if recommendation else resource_reason + ) reason = self._local_model_availability_reason( choice.runtime, available=available, recommendation=recommendation, + unavailable_reason=unavailable_reason, ) return { **choice.safe_dict(), + "qualification": qualification, "active": active, "available": available, "selected": selected, "availability_reason": reason, + "recommended": ( + qualification == "qualified" + and choice.model_id in self._recommended_local_model_ids + ), } @staticmethod @@ -1321,10 +1449,11 @@ def _local_model_availability_reason( *, available: bool, recommendation: str | None, + unavailable_reason: str | None = None, ) -> str: if available: return recommendation or "Available on this deployment" - unavailable = ( + unavailable = unavailable_reason or ( "Requires a Heartwood NVIDIA GPU runtime" if runtime == "vllm" else "The portable CPU runtime is not available on this deployment" @@ -1427,6 +1556,24 @@ def _selected_local_model_choice(selection: LocalModelSelection) -> LocalModelCh context_window=selection.context_window, minimum_resource_envelope=selection.minimum_resource_envelope, recommended_resource_envelope=selection.recommended_resource_envelope, + license_id=selection.license_id or "Unspecified", + precision=selection.precision or "Unspecified", + tier=cast(Any, selection.tier), + qualification=cast(Any, selection.qualification), + minimum_gpu_count=selection.minimum_gpu_count, + minimum_gpu_memory_bytes=selection.minimum_gpu_memory_bytes, + recommended_ram_bytes=selection.recommended_ram_bytes or selection.minimum_free_bytes, + recommended_disk_bytes=selection.recommended_disk_bytes or selection.minimum_free_bytes, + maximum_context_window=selection.maximum_context_window, + tool_call_parser=cast(Any, selection.tool_call_parser), + tensor_parallel_size=selection.tensor_parallel_size, + startup_seconds_min=selection.startup_seconds_min, + startup_seconds_max=selection.startup_seconds_max, + download_policy=selection.download_policy, + allow_patterns=selection.allow_patterns, + ignore_patterns=selection.ignore_patterns, + validated_platforms=selection.validated_platforms, + qualification_test=selection.qualification_test, ) choice.validate() return choice diff --git a/packages/gateway/src/heartwood/gateway/_gpu_environment.py b/packages/gateway/src/heartwood/gateway/_gpu_environment.py new file mode 100644 index 00000000..40247b74 --- /dev/null +++ b/packages/gateway/src/heartwood/gateway/_gpu_environment.py @@ -0,0 +1,391 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +"""Shared NVIDIA and Slurm resource discovery for managed model planning.""" + +from __future__ import annotations + +import csv +import io +import re +import shutil +import subprocess +from collections.abc import Mapping +from dataclasses import dataclass + +from packaging.version import InvalidVersion, Version + +_CUDA_12_MINIMUM_DRIVER = Version("525.60.13") +_KNOWN_GPU_MEMORY_BYTES = { + "nvidia_l40s": 48_000_000_000, + "l40s": 48_000_000_000, + "tesla_t4": 16_000_000_000, + "t4": 16_000_000_000, +} +_KNOWN_COMPUTE_CAPABILITIES = { + "l40s": (8, 9), + "p4": (6, 1), + "p100": (6, 0), + "t4": (7, 5), + "v100": (7, 0), +} + + +@dataclass(frozen=True, slots=True) +class GpuDevice: + """One NVIDIA device visible to the current process.""" + + index: int + name: str + total_memory_bytes: int + free_memory_bytes: int + driver_version: str + compute_capability: tuple[int, int] | None + + @property + def modern_vllm_issue(self) -> str | None: + """Return why the CUDA 12.9 runtime must not start on this device.""" + capability = self.compute_capability or _capability_from_name(self.name) + if capability is not None and capability < (7, 5): + return ( + f"{self.name} has compute capability {capability[0]}.{capability[1]}; " + "the vLLM CUDA 12.9 runtime requires 7.5 or newer" + ) + try: + driver = Version(self.driver_version) + except InvalidVersion: + return f"NVIDIA driver version could not be interpreted: {self.driver_version}" + if driver < _CUDA_12_MINIMUM_DRIVER: + return ( + f"NVIDIA driver {self.driver_version} is older than the CUDA 12.x " + f"compatibility minimum {_CUDA_12_MINIMUM_DRIVER}" + ) + return None + + +@dataclass(frozen=True, slots=True) +class SlurmGpuPartition: + """GPU capacity reported for one available Slurm partition.""" + + name: str + is_default: bool + gpu_model: str | None + gpu_count: int + node_memory_bytes: int | None + node_cpu_count: int | None + state: str + + @property + def gpu_memory_bytes(self) -> int | None: + """Return reviewed per-device memory for known scheduler GPU names.""" + if self.gpu_model is None: + return None + normalized = re.sub(r"[^a-z0-9]+", "_", self.gpu_model.casefold()).strip("_") + return _KNOWN_GPU_MEMORY_BYTES.get(normalized) + + +@dataclass(frozen=True, slots=True) +class GpuCapacity: + """One visible or schedulable GPU resource envelope.""" + + label: str + gpu_model: str + gpu_count: int + gpu_memory_bytes: int + allocation_required: bool + partition: str | None = None + + +@dataclass(frozen=True, slots=True) +class GpuEnvironment: + """GPU inventory used by every Heartwood interaction surface.""" + + platform_id: str + visible_devices: tuple[GpuDevice, ...] + slurm_partitions: tuple[SlurmGpuPartition, ...] + capacities: tuple[GpuCapacity, ...] + + def assess( + self, + *, + gpu_count: int, + gpu_memory_bytes: int, + ) -> tuple[bool, str]: + """Explain whether the inventory can run one catalog configuration.""" + eligible = tuple( + capacity + for capacity in self.capacities + if capacity.gpu_count >= gpu_count and capacity.gpu_memory_bytes >= gpu_memory_bytes + ) + if eligible: + capacity = min( + eligible, + key=lambda item: ( + item.allocation_required, + item.gpu_count, + item.gpu_memory_bytes, + item.label, + ), + ) + if capacity.allocation_required: + return ( + True, + f"Available through Slurm partition {capacity.partition} with " + f"{capacity.gpu_count} {capacity.gpu_model} GPU(s); allocation approval " + "is required before startup.", + ) + return ( + True, + f"Compatible with {capacity.gpu_count} visible {capacity.gpu_model} GPU(s).", + ) + issues = tuple( + dict.fromkeys( + issue + for device in self.visible_devices + if (issue := device.modern_vllm_issue) is not None + ) + ) + if issues: + return False, "; ".join(issues) + if not self.capacities: + if self.platform_id == "terra": + return ( + False, + "No compatible NVIDIA GPU is visible. Recreate the Terra cloud environment " + "with an NVIDIA T4 and the Heartwood GPU image.", + ) + if self.platform_id == "carina": + return False, "No compatible Slurm GPU capacity was reported by Carina." + return False, "No compatible NVIDIA GPU is visible to Heartwood." + largest_count = max(capacity.gpu_count for capacity in self.capacities) + largest_memory = max(capacity.gpu_memory_bytes for capacity in self.capacities) + return ( + False, + f"Requires {gpu_count} GPU(s) with {_format_bytes(gpu_memory_bytes)} each; " + f"the detected capacity provides at most {largest_count} GPU(s) with " + f"{_format_bytes(largest_memory)} each.", + ) + + +def inspect_gpu_environment(platform_id: str, env: Mapping[str, str]) -> GpuEnvironment: + """Inspect visible devices or, on Carina login nodes, schedulable devices.""" + visible = discover_visible_gpus(env) + inside_slurm = bool(env.get("SLURM_JOB_ID")) + partitions = ( + discover_slurm_gpu_partitions(env) if platform_id == "carina" and not inside_slurm else () + ) + capacities = _visible_capacities(visible) if visible else _slurm_capacities(partitions) + return GpuEnvironment( + platform_id=platform_id, + visible_devices=visible, + slurm_partitions=partitions, + capacities=capacities, + ) + + +def discover_visible_gpus(env: Mapping[str, str]) -> tuple[GpuDevice, ...]: + """Inspect visible NVIDIA devices without initializing CUDA.""" + executable = shutil.which("nvidia-smi", path=env.get("PATH")) + if executable is None: + return () + fields = "index,name,memory.total,memory.free,driver_version,compute_cap" + completed = _run_nvidia_smi(executable, fields, env) + has_capability = completed is not None and completed.returncode == 0 + if not has_capability: + fields = "index,name,memory.total,memory.free,driver_version" + completed = _run_nvidia_smi(executable, fields, env) + if completed is None or completed.returncode != 0: + return () + devices: list[GpuDevice] = [] + for row in csv.reader(io.StringIO(completed.stdout), skipinitialspace=True): + if len(row) != (6 if has_capability else 5): + continue + try: + capability = _parse_compute_capability(row[5]) if has_capability else None + devices.append( + GpuDevice( + index=int(row[0].strip()), + name=row[1].strip(), + total_memory_bytes=int(row[2].strip()) * 1024**2, + free_memory_bytes=int(row[3].strip()) * 1024**2, + driver_version=row[4].strip(), + compute_capability=capability, + ) + ) + except ValueError: + continue + return tuple(sorted(devices, key=lambda device: device.index)) + + +def discover_slurm_gpu_partitions(env: Mapping[str, str]) -> tuple[SlurmGpuPartition, ...]: + """Return available GPU partitions and their node-level capacity.""" + try: + completed = subprocess.run( + ("sinfo", "--noheader", "--format=%P|%G|%a|%m|%c"), + check=False, + capture_output=True, + text=True, + timeout=15, + env=_scheduler_environment(env), + ) + except (OSError, subprocess.TimeoutExpired): + return () + if completed.returncode != 0: + return () + partitions: dict[tuple[str, str | None], SlurmGpuPartition] = {} + for line in completed.stdout.splitlines(): + fields = line.strip().split("|") + if len(fields) != 5: + continue + raw_name, resources, state, memory, cpus = fields + gpu_model, gpu_count = _parse_gres(resources) + if gpu_count < 1 or state.casefold() not in {"alloc", "idle", "mix", "up"}: + continue + name = raw_name.rstrip("*") + if not name: + continue + candidate = SlurmGpuPartition( + name=name, + is_default=raw_name.endswith("*"), + gpu_model=gpu_model, + gpu_count=gpu_count, + node_memory_bytes=_parse_slurm_memory(memory), + node_cpu_count=_parse_positive_int(cpus), + state=state.casefold(), + ) + key = (candidate.name, candidate.gpu_model) + previous = partitions.get(key) + if previous is None or candidate.gpu_count > previous.gpu_count: + partitions[key] = candidate + return tuple(partitions.values()) + + +def _visible_capacities(devices: tuple[GpuDevice, ...]) -> tuple[GpuCapacity, ...]: + compatible = tuple(device for device in devices if device.modern_vllm_issue is None) + thresholds = sorted({device.total_memory_bytes for device in compatible}, reverse=True) + capacities: list[GpuCapacity] = [] + for threshold in thresholds: + eligible = tuple(device for device in compatible if device.total_memory_bytes >= threshold) + names = ", ".join(sorted({device.name for device in eligible})) + capacities.append( + GpuCapacity( + label=f"{len(eligible)} visible {names} GPU(s)", + gpu_model=names, + gpu_count=len(eligible), + gpu_memory_bytes=threshold, + allocation_required=False, + ) + ) + return tuple(capacities) + + +def _slurm_capacities( + partitions: tuple[SlurmGpuPartition, ...], +) -> tuple[GpuCapacity, ...]: + return tuple( + GpuCapacity( + label=f"Slurm partition {partition.name}", + gpu_model=partition.gpu_model or "NVIDIA", + gpu_count=partition.gpu_count, + gpu_memory_bytes=partition.gpu_memory_bytes, + allocation_required=True, + partition=partition.name, + ) + for partition in partitions + if partition.gpu_memory_bytes is not None + ) + + +def _format_bytes(value: int) -> str: + return f"{value / 1024**3:.1f} GiB" + + +def _run_nvidia_smi( + executable: str, + fields: str, + env: Mapping[str, str], +) -> subprocess.CompletedProcess[str] | None: + try: + return subprocess.run( + ( + executable, + f"--query-gpu={fields}", + "--format=csv,noheader,nounits", + ), + check=False, + capture_output=True, + text=True, + timeout=10, + env=dict(env), + ) + except (OSError, subprocess.TimeoutExpired): + return None + + +def _parse_compute_capability(value: str) -> tuple[int, int] | None: + match = re.fullmatch(r"\s*(\d+)\.(\d+)\s*", value) + return (int(match.group(1)), int(match.group(2))) if match is not None else None + + +def _capability_from_name(name: str) -> tuple[int, int] | None: + normalized = name.casefold() + return next( + ( + capability + for marker, capability in _KNOWN_COMPUTE_CAPABILITIES.items() + if marker in normalized + ), + None, + ) + + +def _parse_gres(value: str) -> tuple[str | None, int]: + best_model: str | None = None + best_count = 0 + for resource in value.split(","): + fields = resource.strip().split("(", maxsplit=1)[0].split(":") + if not fields or fields[0].casefold() != "gpu": + continue + if len(fields) == 2: + model = None + raw_count = fields[1] + elif len(fields) >= 3: + model = fields[1] + raw_count = fields[2] + else: + continue + count_match = re.match(r"(\d+)", raw_count) + if count_match is None: + continue + count = int(count_match.group(1)) + if count > best_count: + best_model = model + best_count = count + return best_model, best_count + + +def _parse_slurm_memory(value: str) -> int | None: + match = re.fullmatch(r"\s*(\d+)([KMGT]?)\+?\s*", value, re.IGNORECASE) + if match is None: + return None + amount = int(match.group(1)) + unit = match.group(2).upper() + multiplier = {"": 1024**2, "K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4}[unit] + return amount * multiplier + + +def _parse_positive_int(value: str) -> int | None: + match = re.match(r"\s*(\d+)", value) + parsed = int(match.group(1)) if match is not None else 0 + return parsed if parsed > 0 else None + + +def _scheduler_environment(env: Mapping[str, str]) -> dict[str, str]: + return { + name: env[name] + for name in ("PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE") + if name in env + } diff --git a/packages/gateway/src/heartwood/gateway/_local_import.py b/packages/gateway/src/heartwood/gateway/_local_import.py index 34dc82ad..32b0d9ce 100644 --- a/packages/gateway/src/heartwood/gateway/_local_import.py +++ b/packages/gateway/src/heartwood/gateway/_local_import.py @@ -24,6 +24,8 @@ LocalModelRuntime, ModelRepositoryError, infer_model_type, + infer_tool_call_parser, + safe_snapshot_download_policy, ) from heartwood.gateway._model_identity import ( is_hugging_face_model_id, @@ -114,6 +116,14 @@ def import_local_model( if destination.exists() or destination.is_symlink(): raise ModelRepositoryError(f"this model is already imported: {model_id}") checksum = _sha256(source) if runtime == "llama-cpp" else None + download_policy, allow_patterns, ignore_patterns = safe_snapshot_download_policy() + tool_call_parser = ( + infer_tool_call_parser(source_repository, model_type) if runtime == "vllm" else None + ) + if runtime == "vllm" and tool_call_parser is None: + raise ModelRepositoryError( + "Heartwood cannot infer a supported tool-call parser for this imported model" + ) choice = LocalModelChoice( model_id=model_id, label=source_repository.rsplit("/", maxsplit=1)[-1], @@ -134,6 +144,23 @@ def import_local_model( artifact_sha256=checksum, minimum_resource_envelope=_minimum_resource_envelope(runtime, size_bytes), recommended_resource_envelope=_recommended_resource_envelope(runtime, size_bytes), + license_id="Unspecified", + precision="Repository-defined safetensors" if runtime == "vllm" else "GGUF quantized", + minimum_gpu_count=1 if runtime == "vllm" else 0, + minimum_gpu_memory_bytes=( + max(16_000_000_000, int(size_bytes * 1.25)) if runtime == "vllm" else 0 + ), + recommended_ram_bytes=( + max(32 * 1024**3, size_bytes * 2) + if runtime == "vllm" + else max(16 * 1024**3, size_bytes * 4) + ), + recommended_disk_bytes=max(minimum_free_bytes, size_bytes * 2), + maximum_context_window=context_window, + tool_call_parser=tool_call_parser, + download_policy=download_policy if runtime == "vllm" else None, + allow_patterns=allow_patterns if runtime == "vllm" else (), + ignore_patterns=ignore_patterns if runtime == "vllm" else (), ) choice.validate() temporary = Path(tempfile.mkdtemp(prefix=f".{model_id}.", dir=models_dir)) diff --git a/packages/gateway/src/heartwood/gateway/_local_models.py b/packages/gateway/src/heartwood/gateway/_local_models.py index b1dea6bb..752704ff 100644 --- a/packages/gateway/src/heartwood/gateway/_local_models.py +++ b/packages/gateway/src/heartwood/gateway/_local_models.py @@ -11,6 +11,7 @@ import hashlib import re from dataclasses import asdict, dataclass +from fnmatch import fnmatchcase from importlib import import_module from pathlib import PurePosixPath from typing import Literal, Protocol, cast @@ -27,24 +28,36 @@ is_immutable_revision, is_resolved_revision, ) -from heartwood.gateway._model_snapshots import ModelSnapshot +from heartwood.gateway._model_snapshots import ( + ModelQualification, + ModelSnapshot, + ModelTier, + ToolCallParser, +) type LocalModelRuntime = Literal["llama-cpp", "vllm"] -type LocalModelCatalogSource = Literal["recommended", "user-selected"] +type LocalModelCatalogSource = Literal["catalog", "user-selected"] _SPLIT_GGUF = re.compile(r"-\d{5}-of-\d{5}\.gguf$", re.IGNORECASE) _SAFETENSORS_WEIGHTS = re.compile( r"^model(?:-\d{5}-of-\d{5})?\.safetensors(?:\.index\.json)?$", re.IGNORECASE, ) -_PYTORCH_WEIGHTS = re.compile( - r"^pytorch_model(?:-\d{5}-of-\d{5})?\.bin(?:\.index\.json)?$", - re.IGNORECASE, -) _ISSUE_URL = "https://github.com/SchmiedmayerLab/heartwood/issues/new/choose" _GGUF_PREFERENCE = ("q4_k_m", "q5_k_m", "q4_k_s", "q5_k_s", "q8_0") -_HERMES_MODEL_TYPES = {"qwen2", "qwen3"} +_HERMES_MODEL_TYPES = {"qwen2", "qwen3", "qwen3_moe", "qwen3_next"} _TEXT_GENERATION_PIPELINES = {"conversational", "text-generation"} +_SNAPSHOT_ALLOW_PATTERNS = ( + "*.json", + "*.jinja", + "*.model", + "*.safetensors", + "*.tiktoken", + "LICENSE*", + "NOTICE*", + "README*", +) +_SNAPSHOT_IGNORE_PATTERNS = ("*.bin", "*.py", ".git/*", "metal/*", "original/*") _USER_SELECTED_PURPOSE = ( "User-selected Hugging Face model; Heartwood has not reviewed its capabilities, " "license, or suitability." @@ -89,6 +102,24 @@ class LocalModelChoice: artifact_sha256: str | None = None minimum_resource_envelope: str | None = None recommended_resource_envelope: str | None = None + license_id: str = "Unspecified" + precision: str = "Unspecified" + tier: ModelTier = "standard" + qualification: ModelQualification = "candidate" + minimum_gpu_count: int = 0 + minimum_gpu_memory_bytes: int = 0 + recommended_ram_bytes: int = 0 + recommended_disk_bytes: int = 0 + maximum_context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW + tool_call_parser: ToolCallParser | None = None + tensor_parallel_size: int = 1 + startup_seconds_min: int = 30 + startup_seconds_max: int = 600 + download_policy: str | None = None + allow_patterns: tuple[str, ...] = () + ignore_patterns: tuple[str, ...] = () + validated_platforms: tuple[str, ...] = () + qualification_test: str | None = None def validate(self) -> None: """Validate source provenance and runtime-specific integrity metadata.""" @@ -109,6 +140,16 @@ def validate(self) -> None: raise ModelRepositoryError("managed model storage metadata is invalid") if not self.license_posture.strip(): raise ModelRepositoryError("managed model license posture must not be empty") + if not self.license_id.strip() or not self.precision.strip(): + raise ModelRepositoryError("managed model license and precision must not be empty") + if self.tier not in {"standard", "powerful", "maximum"}: + raise ModelRepositoryError(f"unsupported managed model tier: {self.tier}") + if self.qualification not in {"candidate", "qualified"}: + raise ModelRepositoryError( + f"unsupported managed model qualification: {self.qualification}" + ) + if self.startup_seconds_min <= 0 or self.startup_seconds_max < self.startup_seconds_min: + raise ModelRepositoryError("managed model startup estimate is invalid") if self.model_type is not None and re.fullmatch(r"[a-z0-9_-]+", self.model_type) is None: raise ModelRepositoryError("managed model type must be a normalized identifier") if self.context_window < 2048: @@ -118,6 +159,10 @@ def validate(self) -> None: "managed model context window must be at most " f"{MAXIMUM_LOCAL_CONTEXT_WINDOW} tokens" ) + if not self.context_window <= self.maximum_context_window <= MAXIMUM_LOCAL_CONTEXT_WINDOW: + raise ModelRepositoryError("managed model maximum context capacity is invalid") + if self.recommended_ram_bytes <= 0 or self.recommended_disk_bytes < self.minimum_free_bytes: + raise ModelRepositoryError("managed model RAM or disk recommendation is invalid") if self.runtime == "llama-cpp": if self.source_path is None or not self.source_path.casefold().endswith(".gguf"): raise ModelRepositoryError("CPU models require one GGUF file") @@ -129,14 +174,33 @@ def validate(self) -> None: or re.fullmatch(r"[0-9a-f]{64}", self.artifact_sha256) is None ): raise ModelRepositoryError("GGUF models require a source SHA-256 digest") - elif self.source_path is not None or self.artifact_sha256 is not None: - raise ModelRepositoryError("GPU snapshots must not select one repository file") + if self.minimum_gpu_count != 0 or self.minimum_gpu_memory_bytes != 0: + raise ModelRepositoryError("CPU models must not require GPU resources") + if self.tool_call_parser is not None or self.download_policy is not None: + raise ModelRepositoryError("CPU models must not declare vLLM settings") + else: + if self.source_path is not None or self.artifact_sha256 is not None: + raise ModelRepositoryError("GPU snapshots must not select one repository file") + if self.minimum_gpu_count <= 0 or self.minimum_gpu_memory_bytes <= 0: + raise ModelRepositoryError("GPU models require a positive GPU resource envelope") + if self.tensor_parallel_size < self.minimum_gpu_count: + raise ModelRepositoryError("tensor parallelism must cover the minimum GPU count") + if self.tool_call_parser not in {"hermes", "openai", "qwen3_coder"}: + raise ModelRepositoryError("GPU models require a supported tool-call parser") + if self.download_policy is None or not self.allow_patterns: + raise ModelRepositoryError("GPU models require a reviewed download policy") def safe_dict(self) -> dict[str, object]: """Return non-secret model metadata for every interaction surface.""" self.validate() return asdict(self) + def qualification_for(self, platform_id: str) -> ModelQualification: + """Return qualification for one exact managed-platform configuration.""" + if self.qualification == "qualified" and platform_id in self.validated_platforms: + return "qualified" + return "candidate" + def download_model(self) -> ModelArtifact | ModelSnapshot: """Translate the normalized choice to the existing download implementation.""" self.validate() @@ -170,8 +234,26 @@ def download_model(self) -> ModelArtifact | ModelSnapshot: source_revision=self.source_revision, expected_size_bytes=self.size_bytes, minimum_free_bytes=self.minimum_free_bytes, + license_id=self.license_id, license_posture=self.license_posture, model_alias=self.label, + precision=self.precision, + tier=self.tier, + qualification=self.qualification, + minimum_gpu_count=self.minimum_gpu_count, + minimum_gpu_memory_bytes=self.minimum_gpu_memory_bytes, + recommended_ram_bytes=self.recommended_ram_bytes, + recommended_disk_bytes=self.recommended_disk_bytes, + maximum_context_window=self.maximum_context_window, + tool_call_parser=cast(ToolCallParser, self.tool_call_parser), + tensor_parallel_size=self.tensor_parallel_size, + startup_seconds_min=self.startup_seconds_min, + startup_seconds_max=self.startup_seconds_max, + download_policy=cast(str, self.download_policy), + allow_patterns=self.allow_patterns, + ignore_patterns=self.ignore_patterns, + validated_platforms=self.validated_platforms, + qualification_test=self.qualification_test, context_window=self.context_window, minimum_resource_envelope=self.minimum_resource_envelope, recommended_resource_envelope=self.recommended_resource_envelope, @@ -323,7 +405,7 @@ def inspect( else: metadata_complete = False files = tuple(inspected_files) - license_posture = _license_posture(getattr(info, "card_data", None)) + license_id, license_posture = _license_metadata(getattr(info, "card_data", None)) context_window = _context_window(info) if context_window < MINIMUM_AGENT_RUNTIME_CONTEXT_WINDOW: raise ModelRepositoryError( @@ -343,6 +425,7 @@ def inspect( license_posture, context_window=context_window, model_type=model_type, + license_id=license_id, ) ) is not None @@ -353,9 +436,10 @@ def inspect( files, license_posture, metadata_complete=metadata_complete, - supports_tool_calls=_supports_hermes_tool_calls(info), + tool_call_parser=_tool_call_parser(source_repository, info), context_window=context_window, model_type=model_type, + license_id=license_id, ) if snapshot is not None: candidates.append(snapshot) @@ -395,13 +479,13 @@ class _RepositoryFile: sha256: str | None -def recommended_model_choices( +def catalog_model_choices( artifacts: tuple[ModelArtifact, ...], snapshots: tuple[ModelSnapshot, ...], *, recommended_only: bool = True, ) -> tuple[LocalModelChoice, ...]: - """Normalize the centrally configured recommendation catalogs into one ordered list.""" + """Normalize the centrally configured model catalogs into one ordered list.""" choices = [ LocalModelChoice( model_id=artifact.artifact_id, @@ -414,12 +498,24 @@ def recommended_model_choices( size_bytes=artifact.artifact_size_bytes, minimum_free_bytes=artifact.minimum_free_bytes, license_posture=artifact.license_posture, - catalog_source="recommended", + catalog_source="catalog", model_type=infer_model_type(artifact.source_repository), context_window=artifact.context_window, artifact_sha256=artifact.artifact_sha256, minimum_resource_envelope=artifact.minimum_resource_envelope, recommended_resource_envelope=artifact.recommended_resource_envelope, + license_id=_license_id_from_posture(artifact.license_posture), + precision=_gguf_precision(artifact.source_path), + tier="standard", + qualification="qualified", + recommended_ram_bytes=max(16 * 1024**3, artifact.artifact_size_bytes * 4), + recommended_disk_bytes=max( + artifact.minimum_free_bytes, + artifact.artifact_size_bytes * 3, + ), + maximum_context_window=artifact.context_window, + validated_platforms=("generic", "terra"), + qualification_test="heartwood.coding-agent-e2e.v1", ) for artifact in artifacts if artifact.recommended or not recommended_only @@ -436,11 +532,29 @@ def recommended_model_choices( size_bytes=snapshot.expected_size_bytes, minimum_free_bytes=snapshot.minimum_free_bytes, license_posture=snapshot.license_posture, - catalog_source="recommended", + catalog_source="catalog", model_type=infer_model_type(snapshot.source_repository), context_window=snapshot.context_window, minimum_resource_envelope=snapshot.minimum_resource_envelope, recommended_resource_envelope=snapshot.recommended_resource_envelope, + license_id=snapshot.license_id, + precision=snapshot.precision, + tier=snapshot.tier, + qualification=snapshot.qualification, + minimum_gpu_count=snapshot.minimum_gpu_count, + minimum_gpu_memory_bytes=snapshot.minimum_gpu_memory_bytes, + recommended_ram_bytes=snapshot.recommended_ram_bytes, + recommended_disk_bytes=snapshot.recommended_disk_bytes, + maximum_context_window=snapshot.maximum_context_window, + tool_call_parser=snapshot.tool_call_parser, + tensor_parallel_size=snapshot.tensor_parallel_size, + startup_seconds_min=snapshot.startup_seconds_min, + startup_seconds_max=snapshot.startup_seconds_max, + download_policy=snapshot.download_policy, + allow_patterns=snapshot.allow_patterns, + ignore_patterns=snapshot.ignore_patterns, + validated_platforms=snapshot.validated_platforms, + qualification_test=snapshot.qualification_test, ) for snapshot in snapshots if snapshot.recommended or not recommended_only @@ -470,6 +584,7 @@ def _gguf_candidate( *, context_window: int, model_type: str | None, + license_id: str, ) -> LocalModelChoice | None: if ( not file.path.casefold().endswith(".gguf") @@ -497,6 +612,11 @@ def _gguf_candidate( artifact_sha256=file.sha256, minimum_resource_envelope=_cpu_resources(file.size, recommended=False), recommended_resource_envelope=_cpu_resources(file.size, recommended=True), + license_id=license_id, + precision=_gguf_precision(file.path), + recommended_ram_bytes=max(16 * 1024**3, file.size * 4), + recommended_disk_bytes=max((file.size * 3 + 1) // 2, file.size * 3), + maximum_context_window=context_window, ) @@ -507,21 +627,19 @@ def _snapshot_candidate( license_posture: str, *, metadata_complete: bool, - supports_tool_calls: bool, + tool_call_parser: ToolCallParser | None, context_window: int, model_type: str | None, + license_id: str, ) -> LocalModelChoice | None: - if not metadata_complete or not supports_tool_calls: + if not metadata_complete or tool_call_parser is None: return None - paths = {file.path for file in files} - has_weights = any( - _SAFETENSORS_WEIGHTS.fullmatch(path) is not None - or _PYTORCH_WEIGHTS.fullmatch(path) is not None - for path in paths - ) - if "config.json" not in paths or not has_weights or len(files) == 0: + included_files = tuple(file for file in files if _included_snapshot_file(file.path)) + paths = {file.path for file in included_files} + has_weights = any(_SAFETENSORS_WEIGHTS.fullmatch(path) is not None for path in paths) + if "config.json" not in paths or not has_weights or len(included_files) == 0: return None - size = sum(file.size for file in files) + size = sum(file.size for file in included_files) if size <= 0: return None label = repository.rsplit("/", maxsplit=1)[-1] @@ -541,6 +659,18 @@ def _snapshot_candidate( context_window=context_window, minimum_resource_envelope=_gpu_resources(size, recommended=False), recommended_resource_envelope=_gpu_resources(size, recommended=True), + license_id=license_id, + precision="Repository-defined safetensors", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=max(16_000_000_000, int(size * 1.25)), + recommended_ram_bytes=max(32 * 1024**3, size * 2), + recommended_disk_bytes=max((size * 3 + 1) // 2, size * 2), + maximum_context_window=context_window, + tool_call_parser=tool_call_parser, + tensor_parallel_size=1, + download_policy="transformers-safetensors", + allow_patterns=_SNAPSHOT_ALLOW_PATTERNS, + ignore_patterns=_SNAPSHOT_IGNORE_PATTERNS, ) @@ -557,7 +687,7 @@ def _model_id( return f"hf-{slug}-{digest}" -def _license_posture(card_data: object) -> str: +def _license_metadata(card_data: object) -> tuple[str, str]: license_id = getattr(card_data, "license", None) if not isinstance(license_id, str) and card_data is not None: to_dict = getattr(card_data, "to_dict", None) @@ -568,8 +698,34 @@ def _license_posture(card_data: object) -> str: if isinstance(value, str): license_id = value if isinstance(license_id, str) and license_id.strip(): - return f"Source model card reports {license_id.strip()}; review its terms before use." - return "No machine-readable license was reported; review the source repository before use." + normalized = license_id.strip() + return ( + normalized, + f"Source model card reports {normalized}; review its terms before use.", + ) + return ( + "Unspecified", + "No machine-readable license was reported; review the source repository before use.", + ) + + +def _license_id_from_posture(posture: str) -> str: + for license_id in ("Apache-2.0", "MIT", "BSD-3-Clause", "BSD-2-Clause"): + if license_id.casefold() in posture.casefold(): + return license_id + return "Unspecified" + + +def _gguf_precision(path: str) -> str: + filename = PurePosixPath(path).stem.upper() + match = re.search(r"(?:^|[-_.])(Q\d+(?:_[A-Z0-9]+)+)(?:$|[-_.])", filename) + return f"GGUF {match.group(1)}" if match is not None else "GGUF quantized" + + +def _included_snapshot_file(path: str) -> bool: + if any(fnmatchcase(path, pattern) for pattern in _SNAPSHOT_IGNORE_PATTERNS): + return False + return any(fnmatchcase(path, pattern) for pattern in _SNAPSHOT_ALLOW_PATTERNS) def _requires_custom_code(info: object) -> bool: @@ -582,12 +738,34 @@ def _requires_custom_code(info: object) -> bool: return isinstance(config, dict) and bool(config.get("auto_map")) -def _supports_hermes_tool_calls(info: object) -> bool: +def _tool_call_parser(repository: str, info: object) -> ToolCallParser | None: model_type = _model_type(info) - if model_type not in _HERMES_MODEL_TYPES: - return False pipeline_tag = getattr(info, "pipeline_tag", None) - return not isinstance(pipeline_tag, str) or pipeline_tag in _TEXT_GENERATION_PIPELINES + if isinstance(pipeline_tag, str) and pipeline_tag not in _TEXT_GENERATION_PIPELINES: + return None + return infer_tool_call_parser(repository, model_type) + + +def infer_tool_call_parser( + repository: str, + model_type: str | None, +) -> ToolCallParser | None: + """Choose a supported vLLM parser from reviewed model-family metadata.""" + normalized_repository = repository.casefold().replace("_", "-") + if "qwen3-coder" in normalized_repository: + return "qwen3_coder" + if model_type == "gpt_oss" or normalized_repository.startswith("openai/gpt-oss-"): + return "openai" + return "hermes" if model_type in _HERMES_MODEL_TYPES else None + + +def safe_snapshot_download_policy() -> tuple[str, tuple[str, ...], tuple[str, ...]]: + """Return the shared no-custom-code safetensors download policy.""" + return ( + "transformers-safetensors", + _SNAPSHOT_ALLOW_PATTERNS, + _SNAPSHOT_IGNORE_PATTERNS, + ) def _model_type(info: object) -> str | None: diff --git a/packages/gateway/src/heartwood/gateway/_model_snapshots.py b/packages/gateway/src/heartwood/gateway/_model_snapshots.py index 8f1be057..482d8db2 100644 --- a/packages/gateway/src/heartwood/gateway/_model_snapshots.py +++ b/packages/gateway/src/heartwood/gateway/_model_snapshots.py @@ -20,7 +20,7 @@ from dataclasses import asdict, dataclass from importlib import import_module from pathlib import Path, PurePosixPath -from typing import Any, Protocol, cast +from typing import Any, Literal, Protocol, cast from filelock import FileLock @@ -36,9 +36,19 @@ _ENTRY = re.compile(r"^([0-9a-fA-F]{64}) [ *](.+)$") _SNAPSHOT_ID = re.compile(r"^[a-z0-9][a-z0-9._-]*$") +_SAFE_PATTERN = re.compile(r"^[A-Za-z0-9._*?/[\]-]+$") _SIZE_TOLERANCE = 0.20 type ProgressCallback = Callable[[int, int], None] +type ModelTier = Literal["standard", "powerful", "maximum"] +type ModelQualification = Literal["candidate", "qualified"] +type ToolCallParser = Literal["hermes", "openai", "qwen3_coder"] + +_MODEL_TIERS = {"standard", "powerful", "maximum"} +_MODEL_TIER_RANK: dict[str, int] = {"standard": 0, "powerful": 1, "maximum": 2} +_MODEL_QUALIFICATIONS = {"candidate", "qualified"} +_TOOL_CALL_PARSERS = {"hermes", "openai", "qwen3_coder"} +_VALIDATED_PLATFORMS = {"carina", "generic", "terra"} class SnapshotDownloader(Protocol): @@ -52,6 +62,8 @@ def __call__( local_dir: Path, cache_dir: Path, token: bool, + allow_patterns: tuple[str, ...], + ignore_patterns: tuple[str, ...], ) -> str: ... @@ -70,8 +82,26 @@ class ModelSnapshot: source_revision: str expected_size_bytes: int minimum_free_bytes: int + license_id: str license_posture: str model_alias: str + precision: str + tier: ModelTier + qualification: ModelQualification + minimum_gpu_count: int + minimum_gpu_memory_bytes: int + recommended_ram_bytes: int + recommended_disk_bytes: int + maximum_context_window: int + tool_call_parser: ToolCallParser + tensor_parallel_size: int + startup_seconds_min: int + startup_seconds_max: int + download_policy: str + allow_patterns: tuple[str, ...] + ignore_patterns: tuple[str, ...] + validated_platforms: tuple[str, ...] = () + qualification_test: str | None = None context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW minimum_resource_envelope: str | None = None recommended_resource_envelope: str | None = None @@ -88,17 +118,62 @@ def validate(self) -> None: for name, value in ( ("runtime_profile", self.runtime_profile), ("purpose", self.purpose), + ("license_id", self.license_id), ("license_posture", self.license_posture), ("model_alias", self.model_alias), + ("precision", self.precision), + ("download_policy", self.download_policy), ): if not value: raise ModelSnapshotError(f"{name} must be a non-empty string") if self.expected_size_bytes <= 0 or self.minimum_free_bytes < self.expected_size_bytes: raise ModelSnapshotError("snapshot storage metadata is invalid") + if self.recommended_disk_bytes < self.minimum_free_bytes: + raise ModelSnapshotError("recommended_disk_bytes must cover minimum_free_bytes") + if self.recommended_ram_bytes <= 0: + raise ModelSnapshotError("recommended_ram_bytes must be positive") + if self.minimum_gpu_count <= 0 or self.minimum_gpu_memory_bytes <= 0: + raise ModelSnapshotError("GPU resource metadata must be positive") + if self.tensor_parallel_size < self.minimum_gpu_count: + raise ModelSnapshotError("tensor_parallel_size must cover the minimum GPU count") + if self.tier not in _MODEL_TIERS: + raise ModelSnapshotError(f"unsupported model tier: {self.tier}") + if self.qualification not in _MODEL_QUALIFICATIONS: + raise ModelSnapshotError(f"unsupported model qualification: {self.qualification}") + if self.tool_call_parser not in _TOOL_CALL_PARSERS: + raise ModelSnapshotError(f"unsupported vLLM tool-call parser: {self.tool_call_parser}") + if self.startup_seconds_min <= 0 or self.startup_seconds_max < self.startup_seconds_min: + raise ModelSnapshotError("snapshot startup estimate is invalid") if not MINIMUM_LOCAL_CONTEXT_WINDOW <= self.context_window <= MAXIMUM_LOCAL_CONTEXT_WINDOW: raise ModelSnapshotError( f"context_window must be between 2048 and {MAXIMUM_LOCAL_CONTEXT_WINDOW} tokens" ) + if not self.context_window <= self.maximum_context_window <= MAXIMUM_LOCAL_CONTEXT_WINDOW: + raise ModelSnapshotError( + "maximum_context_window must cover the default context window and remain bounded" + ) + if not self.allow_patterns: + raise ModelSnapshotError("allow_patterns must select reviewed snapshot files") + for name, patterns in ( + ("allow_patterns", self.allow_patterns), + ("ignore_patterns", self.ignore_patterns), + ): + if len(patterns) != len(set(patterns)): + raise ModelSnapshotError(f"{name} must not contain duplicates") + if any(not _safe_pattern(pattern) for pattern in patterns): + raise ModelSnapshotError(f"{name} contains an unsafe repository pattern") + if len(self.validated_platforms) != len(set(self.validated_platforms)): + raise ModelSnapshotError("validated_platforms must not contain duplicates") + if any(platform not in _VALIDATED_PLATFORMS for platform in self.validated_platforms): + raise ModelSnapshotError("validated_platforms contains an unsupported platform") + if self.qualification == "qualified" and ( + not self.validated_platforms or self.qualification_test is None + ): + raise ModelSnapshotError( + "qualified models require validated platforms and a qualification test" + ) + if self.qualification == "candidate" and self.recommended: + raise ModelSnapshotError("candidate models cannot be recommended") def safe_dict(self) -> dict[str, object]: """Return non-secret catalog metadata.""" @@ -126,6 +201,76 @@ def safe_dict(self) -> dict[str, object]: "snapshots": [snapshot.safe_dict() for snapshot in self.snapshots], } + def recommend( + self, + *, + platform_id: str, + gpu_count: int, + gpu_memory_bytes: int, + maximum_tier: ModelTier, + requested_gpus: int | None = None, + ) -> ModelSnapshot | None: + """Return the strongest qualified recommendation within reviewed resources.""" + maximum_rank = _MODEL_TIER_RANK[maximum_tier] + candidates = [ + (index, snapshot) + for index, snapshot in enumerate(self.snapshots) + if snapshot.recommended + and snapshot.qualification == "qualified" + and platform_id in snapshot.validated_platforms + and _MODEL_TIER_RANK[snapshot.tier] <= maximum_rank + and snapshot.minimum_gpu_count <= gpu_count + and snapshot.minimum_gpu_memory_bytes <= gpu_memory_bytes + and (requested_gpus is None or snapshot.tensor_parallel_size == requested_gpus) + ] + if not candidates: + return None + return max( + candidates, + key=lambda item: ( + _MODEL_TIER_RANK[item[1].tier], + item[1].tensor_parallel_size, + -item[0], + ), + )[1] + + def recommend_for_capacities( + self, + *, + platform_id: str, + capacities: tuple[tuple[int, int], ...], + maximum_tier: ModelTier, + requested_gpus: int | None = None, + ) -> ModelSnapshot | None: + """Return the strongest recommendation from distinct resource envelopes.""" + candidates = tuple( + dict.fromkeys( + recommendation + for gpu_count, gpu_memory_bytes in capacities + if ( + recommendation := self.recommend( + platform_id=platform_id, + gpu_count=gpu_count, + gpu_memory_bytes=gpu_memory_bytes, + maximum_tier=maximum_tier, + requested_gpus=requested_gpus, + ) + ) + is not None + ) + ) + if not candidates: + return None + positions = {snapshot.snapshot_id: index for index, snapshot in enumerate(self.snapshots)} + return max( + candidates, + key=lambda snapshot: ( + _MODEL_TIER_RANK[snapshot.tier], + snapshot.tensor_parallel_size, + -positions[snapshot.snapshot_id], + ), + ) + def load_model_snapshot_catalog(path: Path) -> ModelSnapshotCatalog: """Load recommended snapshot metadata from the repository catalog.""" @@ -136,15 +281,33 @@ def load_model_snapshot_catalog(path: Path) -> ModelSnapshotCatalog: f"unable to load model snapshot catalog {path}: {error}" ) from error schema_version = _string(data, "schema_version") - if schema_version != "heartwood.model-snapshot-catalog.v1": + if schema_version != "heartwood.model-snapshot-catalog.v2": raise ModelSnapshotError(f"unsupported model snapshot catalog schema: {schema_version}") raw_snapshots = data.get("snapshots") if not isinstance(raw_snapshots, dict): raise ModelSnapshotError("model snapshot catalog must include a snapshots table") + raw_policies = data.get("download_policies") + if not isinstance(raw_policies, dict) or not raw_policies: + raise ModelSnapshotError("model snapshot catalog must include download policies") + policies: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {} + for policy_id, policy in raw_policies.items(): + if not isinstance(policy_id, str) or not isinstance(policy, dict): + raise ModelSnapshotError("download policy entries must be tables") + policies[policy_id] = ( + _string_tuple(policy, "allow_patterns", required=True), + _string_tuple(policy, "ignore_patterns"), + ) snapshots: list[ModelSnapshot] = [] for snapshot_id, item in raw_snapshots.items(): if not isinstance(snapshot_id, str) or not isinstance(item, dict): raise ModelSnapshotError("model snapshot entries must be tables") + download_policy = _string(item, "download_policy") + try: + allow_patterns, ignore_patterns = policies[download_policy] + except KeyError as error: + raise ModelSnapshotError( + f"unknown snapshot download policy: {download_policy}" + ) from error snapshot = ModelSnapshot( snapshot_id=snapshot_id, runtime_profile=_string(item, "runtime_profile"), @@ -153,8 +316,32 @@ def load_model_snapshot_catalog(path: Path) -> ModelSnapshotCatalog: source_revision=_string(item, "source_revision"), expected_size_bytes=_positive_int(item, "expected_size_bytes"), minimum_free_bytes=_positive_int(item, "minimum_free_bytes"), + license_id=_string(item, "license_id"), license_posture=_string(item, "license_posture"), model_alias=_string(item, "model_alias"), + precision=_string(item, "precision"), + tier=cast(ModelTier, _enum_string(item, "tier", _MODEL_TIERS)), + qualification=cast( + ModelQualification, + _enum_string(item, "qualification", _MODEL_QUALIFICATIONS), + ), + minimum_gpu_count=_positive_int(item, "minimum_gpu_count"), + minimum_gpu_memory_bytes=_positive_int(item, "minimum_gpu_memory_bytes"), + recommended_ram_bytes=_positive_int(item, "recommended_ram_bytes"), + recommended_disk_bytes=_positive_int(item, "recommended_disk_bytes"), + maximum_context_window=_positive_int(item, "maximum_context_window"), + tool_call_parser=cast( + ToolCallParser, + _enum_string(item, "tool_call_parser", _TOOL_CALL_PARSERS), + ), + tensor_parallel_size=_positive_int(item, "tensor_parallel_size"), + startup_seconds_min=_positive_int(item, "startup_seconds_min"), + startup_seconds_max=_positive_int(item, "startup_seconds_max"), + download_policy=download_policy, + allow_patterns=allow_patterns, + ignore_patterns=ignore_patterns, + validated_platforms=_string_tuple(item, "validated_platforms"), + qualification_test=_optional_string(item, "qualification_test"), context_window=_positive_int(item, "context_window"), minimum_resource_envelope=_optional_string(item, "minimum_resource_envelope"), recommended_resource_envelope=_optional_string(item, "recommended_resource_envelope"), @@ -229,6 +416,8 @@ def download_model_snapshot( local_dir=staging, cache_dir=staging / ".cache" / "huggingface", token=False, + allow_patterns=snapshot.allow_patterns, + ignore_patterns=snapshot.ignore_patterns, ) finally: progress_stop.set() @@ -242,10 +431,13 @@ def download_model_snapshot( shutil.rmtree(staging / ".cache", ignore_errors=True) _verify_download_size(staging, snapshot) source_record = { - "schema_version": "heartwood.model-snapshot-source.v1", + "schema_version": "heartwood.model-snapshot-source.v2", "snapshot_id": snapshot.snapshot_id, "source_repository": snapshot.source_repository, "source_revision": snapshot.source_revision, + "download_policy": snapshot.download_policy, + "allow_patterns": list(snapshot.allow_patterns), + "ignore_patterns": list(snapshot.ignore_patterns), } (staging / "HEARTWOOD-SOURCE.json").write_text( json.dumps(source_record, indent=2, sort_keys=True) + "\n", @@ -368,10 +560,13 @@ def _verify_source_record(root: Path, snapshot: ModelSnapshot) -> None: except (OSError, UnicodeError, json.JSONDecodeError) as error: raise ModelSnapshotError("model snapshot source record is unavailable") from error expected = { - "schema_version": "heartwood.model-snapshot-source.v1", + "schema_version": "heartwood.model-snapshot-source.v2", "snapshot_id": snapshot.snapshot_id, "source_repository": snapshot.source_repository, "source_revision": snapshot.source_revision, + "download_policy": snapshot.download_policy, + "allow_patterns": list(snapshot.allow_patterns), + "ignore_patterns": list(snapshot.ignore_patterns), } if source != expected: raise ModelSnapshotError("model snapshot source record does not match the reviewed source") @@ -403,3 +598,33 @@ def _optional_bool(data: dict[str, Any], key: str, *, default: bool) -> bool: if not isinstance(value, bool): raise ModelSnapshotError(f"{key} must be a boolean") return value + + +def _enum_string(data: dict[str, Any], key: str, allowed: set[str]) -> str: + value = _string(data, key) + if value not in allowed: + raise ModelSnapshotError(f"unsupported {key}: {value}") + return value + + +def _string_tuple( + data: dict[str, Any], + key: str, + *, + required: bool = False, +) -> tuple[str, ...]: + value = data.get(key, []) + if not isinstance(value, list) or any(not isinstance(item, str) or not item for item in value): + raise ModelSnapshotError(f"{key} must be an array of non-empty strings") + if required and not value: + raise ModelSnapshotError(f"{key} must not be empty") + return tuple(value) + + +def _safe_pattern(value: str) -> bool: + path = PurePosixPath(value) + return ( + _SAFE_PATTERN.fullmatch(value) is not None + and not path.is_absolute() + and ".." not in path.parts + ) diff --git a/packages/gateway/src/heartwood/gateway/_project_config.py b/packages/gateway/src/heartwood/gateway/_project_config.py index 8f857cb8..493f2738 100644 --- a/packages/gateway/src/heartwood/gateway/_project_config.py +++ b/packages/gateway/src/heartwood/gateway/_project_config.py @@ -84,11 +84,29 @@ class LocalModelSelection: size_bytes: int | None = None minimum_free_bytes: int | None = None license_posture: str | None = None + license_id: str | None = None artifact_sha256: str | None = None context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW + maximum_context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW minimum_resource_envelope: str | None = None recommended_resource_envelope: str | None = None - catalog_source: str = "recommended" + precision: str | None = None + tier: str = "standard" + qualification: str = "candidate" + minimum_gpu_count: int = 0 + minimum_gpu_memory_bytes: int = 0 + recommended_ram_bytes: int | None = None + recommended_disk_bytes: int | None = None + tool_call_parser: str | None = None + tensor_parallel_size: int = 1 + startup_seconds_min: int = 30 + startup_seconds_max: int = 600 + download_policy: str | None = None + allow_patterns: tuple[str, ...] = () + ignore_patterns: tuple[str, ...] = () + validated_platforms: tuple[str, ...] = () + qualification_test: str | None = None + catalog_source: str = "catalog" def validate(self, project: ProjectContext) -> None: """Validate identifiers and keep the selected artifact under the model root.""" @@ -96,8 +114,12 @@ def validate(self, project: ProjectContext) -> None: raise ProjectConfigError("Heartwood-managed model identifiers must not be empty") if self.runtime not in {"auto", "llama-cpp", "vllm"}: raise ProjectConfigError(f"unsupported Heartwood-managed model runtime: {self.runtime}") - if self.catalog_source not in {"recommended", "user-selected"}: + if self.catalog_source not in {"catalog", "user-selected"}: raise ProjectConfigError("unsupported Heartwood-managed model catalog source") + if self.tier not in {"standard", "powerful", "maximum"}: + raise ProjectConfigError("unsupported Heartwood-managed model tier") + if self.qualification not in {"candidate", "qualified"}: + raise ProjectConfigError("unsupported Heartwood-managed model qualification") if self.display_name is not None and not self.display_name.strip(): raise ProjectConfigError("Heartwood-managed model display_name must not be empty") if self.source_repository is not None and not is_hugging_face_model_id( @@ -137,13 +159,51 @@ def validate(self, project: ProjectContext) -> None: f"Heartwood-managed model context_window must be between 2048 and " f"{MAXIMUM_LOCAL_CONTEXT_WINDOW} tokens" ) + if not self.context_window <= self.maximum_context_window <= MAXIMUM_LOCAL_CONTEXT_WINDOW: + raise ProjectConfigError("Heartwood-managed maximum context window is invalid") + if self.minimum_gpu_count < 0 or self.minimum_gpu_memory_bytes < 0: + raise ProjectConfigError("Heartwood-managed GPU requirements cannot be negative") + if self.tensor_parallel_size < 1: + raise ProjectConfigError("Heartwood-managed tensor parallelism must be positive") + if self.startup_seconds_min <= 0 or self.startup_seconds_max < self.startup_seconds_min: + raise ProjectConfigError("Heartwood-managed startup estimate is invalid") for field_name, value in ( + ("recommended_ram_bytes", self.recommended_ram_bytes), + ("recommended_disk_bytes", self.recommended_disk_bytes), + ): + if value is not None and value <= 0: + raise ProjectConfigError(f"Heartwood-managed {field_name} must be positive") + if self.tool_call_parser is not None and self.tool_call_parser not in { + "hermes", + "openai", + "qwen3_coder", + }: + raise ProjectConfigError("unsupported Heartwood-managed tool-call parser") + if self.runtime == "vllm" and ( + self.minimum_gpu_count < 1 + or self.minimum_gpu_memory_bytes < 1 + or self.tool_call_parser is None + ): + raise ProjectConfigError("vLLM model runtime metadata is incomplete") + if self.runtime == "llama-cpp" and ( + self.minimum_gpu_count != 0 + or self.minimum_gpu_memory_bytes != 0 + or self.tool_call_parser is not None + ): + raise ProjectConfigError("llama.cpp models cannot declare vLLM GPU settings") + for metadata_name, metadata_value in ( ("license_posture", self.license_posture), + ("license_id", self.license_id), + ("precision", self.precision), + ("download_policy", self.download_policy), + ("qualification_test", self.qualification_test), ("minimum_resource_envelope", self.minimum_resource_envelope), ("recommended_resource_envelope", self.recommended_resource_envelope), ): - if value is not None and not value.strip(): - raise ProjectConfigError(f"Heartwood-managed model {field_name} must not be empty") + if metadata_value is not None and not metadata_value.strip(): + raise ProjectConfigError( + f"Heartwood-managed model {metadata_name} must not be empty" + ) if ( self.artifact_sha256 is not None and re.fullmatch(r"[0-9a-f]{64}", self.artifact_sha256) is None @@ -340,11 +400,29 @@ def select_local_model( size_bytes: int | None = None, minimum_free_bytes: int | None = None, license_posture: str | None = None, + license_id: str | None = None, artifact_sha256: str | None = None, context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW, + maximum_context_window: int = DEFAULT_LOCAL_CONTEXT_WINDOW, minimum_resource_envelope: str | None = None, recommended_resource_envelope: str | None = None, - catalog_source: str = "recommended", + precision: str | None = None, + tier: str = "standard", + qualification: str = "candidate", + minimum_gpu_count: int = 0, + minimum_gpu_memory_bytes: int = 0, + recommended_ram_bytes: int | None = None, + recommended_disk_bytes: int | None = None, + tool_call_parser: str | None = None, + tensor_parallel_size: int = 1, + startup_seconds_min: int = 30, + startup_seconds_max: int = 600, + download_policy: str | None = None, + allow_patterns: tuple[str, ...] = (), + ignore_patterns: tuple[str, ...] = (), + validated_platforms: tuple[str, ...] = (), + qualification_test: str | None = None, + catalog_source: str = "catalog", settings: ModelSettings | None = None, ) -> ProjectConfig: """Persist one verified Heartwood-managed model and optional active profile.""" @@ -364,10 +442,28 @@ def select_local_model( size_bytes=size_bytes, minimum_free_bytes=minimum_free_bytes, license_posture=license_posture, + license_id=license_id, artifact_sha256=artifact_sha256, context_window=context_window, + maximum_context_window=maximum_context_window, minimum_resource_envelope=minimum_resource_envelope, recommended_resource_envelope=recommended_resource_envelope, + precision=precision, + tier=tier, + qualification=qualification, + minimum_gpu_count=minimum_gpu_count, + minimum_gpu_memory_bytes=minimum_gpu_memory_bytes, + recommended_ram_bytes=recommended_ram_bytes, + recommended_disk_bytes=recommended_disk_bytes, + tool_call_parser=tool_call_parser, + tensor_parallel_size=tensor_parallel_size, + startup_seconds_min=startup_seconds_min, + startup_seconds_max=startup_seconds_max, + download_policy=download_policy, + allow_patterns=allow_patterns, + ignore_patterns=ignore_patterns, + validated_platforms=validated_platforms, + qualification_test=qualification_test, catalog_source=catalog_source, ) @@ -514,12 +610,24 @@ def _local_model_from_mapping(value: object) -> LocalModelSelection: "artifact_id", "artifact_sha256", "display_name", + "download_policy", + "allow_patterns", + "ignore_patterns", "license_posture", + "license_id", "minimum_free_bytes", + "minimum_gpu_count", + "minimum_gpu_memory_bytes", "minimum_resource_envelope", "model_id", "model_type", + "maximum_context_window", "path", + "precision", + "qualification", + "qualification_test", + "recommended_disk_bytes", + "recommended_ram_bytes", "recommended_resource_envelope", "catalog_source", "context_window", @@ -528,10 +636,20 @@ def _local_model_from_mapping(value: object) -> LocalModelSelection: "source_path", "source_repository", "source_revision", + "startup_seconds_max", + "startup_seconds_min", + "tensor_parallel_size", + "tier", + "tool_call_parser", + "validated_platforms", } ) if unknown: raise ProjectConfigError(f"local_model contains unsupported fields: {', '.join(unknown)}") + context_window = ( + _optional_positive_int(value.get("context_window"), "context_window") + or DEFAULT_LOCAL_CONTEXT_WINDOW + ) return LocalModelSelection( artifact_id=_required_string(value, "artifact_id"), path=_required_string(value, "path"), @@ -547,15 +665,54 @@ def _local_model_from_mapping(value: object) -> LocalModelSelection: value.get("minimum_free_bytes"), "minimum_free_bytes" ), license_posture=_optional_string(value.get("license_posture"), "license_posture"), + license_id=_optional_string(value.get("license_id"), "license_id"), artifact_sha256=_optional_string(value.get("artifact_sha256"), "artifact_sha256"), - context_window=_optional_positive_int(value.get("context_window"), "context_window") - or DEFAULT_LOCAL_CONTEXT_WINDOW, + context_window=context_window, + maximum_context_window=_optional_positive_int( + value.get("maximum_context_window"), "maximum_context_window" + ) + or context_window, minimum_resource_envelope=_optional_string( value.get("minimum_resource_envelope"), "minimum_resource_envelope" ), recommended_resource_envelope=_optional_string( value.get("recommended_resource_envelope"), "recommended_resource_envelope" ), + precision=_optional_string(value.get("precision"), "precision"), + tier=_optional_string(value.get("tier"), "tier") or "standard", + qualification=_optional_string(value.get("qualification"), "qualification") or "candidate", + minimum_gpu_count=_optional_nonnegative_int( + value.get("minimum_gpu_count"), "minimum_gpu_count" + ), + minimum_gpu_memory_bytes=_optional_nonnegative_int( + value.get("minimum_gpu_memory_bytes"), "minimum_gpu_memory_bytes" + ), + recommended_ram_bytes=_optional_positive_int( + value.get("recommended_ram_bytes"), "recommended_ram_bytes" + ), + recommended_disk_bytes=_optional_positive_int( + value.get("recommended_disk_bytes"), "recommended_disk_bytes" + ), + tool_call_parser=_optional_string(value.get("tool_call_parser"), "tool_call_parser"), + tensor_parallel_size=_optional_positive_int( + value.get("tensor_parallel_size"), "tensor_parallel_size" + ) + or 1, + startup_seconds_min=_optional_positive_int( + value.get("startup_seconds_min"), "startup_seconds_min" + ) + or 30, + startup_seconds_max=_optional_positive_int( + value.get("startup_seconds_max"), "startup_seconds_max" + ) + or 600, + download_policy=_optional_string(value.get("download_policy"), "download_policy"), + allow_patterns=_optional_string_tuple(value.get("allow_patterns"), "allow_patterns"), + ignore_patterns=_optional_string_tuple(value.get("ignore_patterns"), "ignore_patterns"), + validated_platforms=_optional_string_tuple( + value.get("validated_platforms"), "validated_platforms" + ), + qualification_test=_optional_string(value.get("qualification_test"), "qualification_test"), catalog_source=_optional_string(value.get("catalog_source"), "catalog_source") or "recommended", ) @@ -586,3 +743,19 @@ def _optional_positive_int(value: object, name: str) -> int | None: if not isinstance(value, int) or isinstance(value, bool) or value <= 0: raise ProjectConfigError(f"{name} must be a positive integer when provided") return value + + +def _optional_nonnegative_int(value: object, name: str) -> int: + if value is None: + return 0 + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise ProjectConfigError(f"{name} must be a nonnegative integer when provided") + return value + + +def _optional_string_tuple(value: object, name: str) -> tuple[str, ...]: + if value is None: + return () + if not isinstance(value, list) or any(not isinstance(item, str) or not item for item in value): + raise ProjectConfigError(f"{name} must be an array of non-empty strings") + return tuple(value) diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index 7be04edc..8f6f9524 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -21,6 +21,8 @@ from heartwood.core_adapter import SessionResult from heartwood.gateway import ( + GpuCapacity, + GpuEnvironment, LocalModelChoice, LocalModelDownloadPlan, ModelArtifact, @@ -680,7 +682,7 @@ def test_rest_manages_model_profiles_and_artifact_metadata(tmp_path: Path) -> No assert validated.status_code == 200 assert artifacts.status_code == 200 assert artifacts.body["schema_version"] == "heartwood.local-model-catalog.v1" - assert artifacts.body["snapshot_schema_version"] == "heartwood.model-snapshot-catalog.v1" + assert artifacts.body["snapshot_schema_version"] == "heartwood.model-snapshot-catalog.v2" assert artifacts.body["snapshots"] assert removed.body["active_profile"] is None artifact_ids = { @@ -774,11 +776,40 @@ def without_packaged_runtimes(path: Path) -> bool: "heartwood.gateway._gateway.shutil.which", lambda executable, **_kwargs: f"/runtime/{executable}", ) - gateway.env["CUDA_VISIBLE_DEVICES"] = "0" + gateway.env["CUDA_VISIBLE_DEVICES"] = "0,1,2,3" + monkeypatch.setattr( + gateway, + "gpu_environment", + lambda: GpuEnvironment( + platform_id="generic", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + label="4 visible NVIDIA L40S GPUs", + gpu_model="NVIDIA L40S", + gpu_count=4, + gpu_memory_bytes=48_000_000_000, + allocation_required=False, + ), + ), + ), + ) fully_available = cast(list[dict[str, JsonValue]], gateway.model_artifacts()["models"]) assert all(model["available"] for model in fully_available) - assert fully_available[0]["model_id"] == "qwen25-7b-instruct-awq-vllm" - assert fully_available[0]["availability_reason"] == "Recommended for this deployment" + expected_runtime = ( + "vllm" if any(model["runtime"] == "vllm" for model in fully_available) else "llama-cpp" + ) + assert fully_available[0]["runtime"] == expected_runtime + if fully_available[0]["runtime"] == "vllm": + assert str(fully_available[0]["availability_reason"]).startswith( + "Evaluation candidate; not yet a recommended model" + ) + assert "Compatible with 4 visible NVIDIA L40S GPU(s)" in str( + fully_available[0]["availability_reason"] + ) + else: + assert fully_available[0]["availability_reason"] == "Available on this deployment" def test_inaccessible_packaged_runtime_is_reported_as_unavailable( @@ -1054,6 +1085,8 @@ def test_user_selected_model_plan_persists_across_gateway_restart( artifact_sha256=hashlib.sha256(b"content").hexdigest(), minimum_resource_envelope="Estimated minimum resources", recommended_resource_envelope="Recommended resources", + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=21, ) @dataclass @@ -1072,7 +1105,13 @@ def plan(self, *_args: object, **_kwargs: object) -> LocalModelDownloadPlan: model_repository=cast(Any, repository), ) - def download(artifact: ModelArtifact, *, cache_dir: Path) -> Path: + def download( + artifact: ModelArtifact, + *, + cache_dir: Path, + progress_callback: object = None, + ) -> Path: + del progress_callback destination = cache_dir / artifact.artifact_id / artifact.source_path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(b"content") @@ -1154,14 +1193,26 @@ def test_gateway_downloads_recommended_artifacts_and_snapshots_through_one_inter monkeypatch.setattr(gateway, "_local_runtime_available", lambda _runtime: True) observed: list[tuple[str, str, Path]] = [] - def artifact_download(artifact: ModelArtifact, *, cache_dir: Path) -> Path: + def artifact_download( + artifact: ModelArtifact, + *, + cache_dir: Path, + progress_callback: object = None, + ) -> Path: + del progress_callback artifact_id = artifact.artifact_id observed.append(("artifact", artifact_id, cache_dir)) path = cache_dir / artifact_id path.mkdir(parents=True) return path - def snapshot_download(snapshot: ModelSnapshot, *, cache_dir: Path) -> Path: + def snapshot_download( + snapshot: ModelSnapshot, + *, + cache_dir: Path, + progress_callback: object = None, + ) -> Path: + del progress_callback snapshot_id = snapshot.snapshot_id observed.append(("snapshot", snapshot_id, cache_dir)) path = cache_dir / snapshot_id @@ -1173,19 +1224,20 @@ def snapshot_download(snapshot: ModelSnapshot, *, cache_dir: Path) -> Path: artifact = gateway.download_local_model_now("llama-cpp-stories260k-ci") assert gateway.config_store.load().local_model is None - snapshot = gateway.download_local_model_now("qwen25-7b-instruct-vllm") + snapshot_id = "qwen25-coder-7b-instruct-awq-vllm" + snapshot = gateway.download_local_model_now(snapshot_id) model_cache = tmp_path / ".heartwood" / "models" assert artifact == model_cache / "llama-cpp-stories260k-ci" - assert snapshot == model_cache / "qwen25-7b-instruct-vllm" + assert snapshot == model_cache / snapshot_id assert observed == [ ("artifact", "llama-cpp-stories260k-ci", model_cache), - ("snapshot", "qwen25-7b-instruct-vllm", model_cache), + ("snapshot", snapshot_id, model_cache), ] config = gateway.config_store.load() assert config.model_source == "heartwood" assert config.local_model is not None - assert config.local_model.artifact_id == "qwen25-7b-instruct-vllm" + assert config.local_model.artifact_id == snapshot_id assert config.model_settings.active_profile == "heartwood" assert config.model_settings.profile().model == "openai/heartwood-managed-model" assert config.model_settings.profile().max_input_tokens == 28_672 @@ -1195,7 +1247,7 @@ def snapshot_download(snapshot: ModelSnapshot, *, cache_dir: Path) -> Path: assert restarted.project_readiness()["state"] == "compute-required" restored_models = cast(list[dict[str, object]], restarted.model_artifacts()["models"]) restored_selection = next( - model for model in restored_models if model["model_id"] == "qwen25-7b-instruct-vllm" + model for model in restored_models if model["model_id"] == snapshot_id ) assert restored_selection["selected"] is True with pytest.raises(ModelRepositoryError, match="unknown Heartwood-managed model: missing"): @@ -1208,12 +1260,19 @@ def test_gateway_download_uses_the_normalized_model_catalog( ) -> None: gateway = _gateway(tmp_path) monkeypatch.setattr(gateway, "_local_runtime_available", lambda _runtime: True) - destination = tmp_path / ".heartwood" / "models" / "qwen25-7b-instruct-vllm" + snapshot_id = "qwen25-coder-7b-instruct-awq-vllm" + destination = tmp_path / ".heartwood" / "models" / snapshot_id def fail_lookup(_catalog: ModelArtifactCatalog, _model_id: str) -> ModelArtifact: raise ValueError("artifact catalog validation failed") - def snapshot_download(_snapshot: ModelSnapshot, *, cache_dir: Path) -> Path: + def snapshot_download( + _snapshot: ModelSnapshot, + *, + cache_dir: Path, + progress_callback: object = None, + ) -> Path: + del progress_callback assert cache_dir == tmp_path / ".heartwood" / "models" destination.mkdir(parents=True) return destination @@ -1221,7 +1280,7 @@ def snapshot_download(_snapshot: ModelSnapshot, *, cache_dir: Path) -> Path: monkeypatch.setattr(ModelArtifactCatalog, "artifact", fail_lookup) monkeypatch.setattr("heartwood.gateway._gateway.download_model_snapshot", snapshot_download) - assert gateway.download_local_model_now("qwen25-7b-instruct-vllm") == destination + assert gateway.download_local_model_now(snapshot_id) == destination def test_rest_model_settings_routes_report_invalid_requests(tmp_path: Path) -> None: diff --git a/packages/gateway/tests/test_gpu_environment.py b/packages/gateway/tests/test_gpu_environment.py new file mode 100644 index 00000000..854dede6 --- /dev/null +++ b/packages/gateway/tests/test_gpu_environment.py @@ -0,0 +1,368 @@ +# This source file is part of the Heartwood open-source project +# +# SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import subprocess + +import pytest + +from heartwood.gateway._gpu_environment import ( + GpuCapacity, + GpuDevice, + GpuEnvironment, + SlurmGpuPartition, + discover_slurm_gpu_partitions, + discover_visible_gpus, + inspect_gpu_environment, +) + + +def test_visible_gpu_discovery_reports_t4_and_l40s_resources( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.shutil.which", + lambda *_args, **_kwargs: "/usr/bin/nvidia-smi", + ) + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, + 0, + stdout=( + "0, Tesla T4, 15109, 14000, 570.86.15, 7.5\n" + "1, NVIDIA L40S, 46068, 45000, 570.86.15, 8.9\n" + ), + ), + ) + + devices = discover_visible_gpus({"PATH": "/usr/bin"}) + + assert [device.name for device in devices] == ["Tesla T4", "NVIDIA L40S"] + assert devices[0].total_memory_bytes == 15_109 * 1024**2 + assert devices[1].free_memory_bytes == 45_000 * 1024**2 + assert all(device.modern_vllm_issue is None for device in devices) + + +@pytest.mark.parametrize( + ("device", "message"), + [ + ( + GpuDevice(0, "Tesla P4", 8_000_000_000, 8_000_000_000, "570.86.15", (6, 1)), + "requires 7.5 or newer", + ), + ( + GpuDevice(0, "Tesla V100", 16_000_000_000, 16_000_000_000, "570.86.15", None), + "requires 7.5 or newer", + ), + ( + GpuDevice(0, "Tesla T4", 16_000_000_000, 16_000_000_000, "510.47.03", None), + "older than the CUDA 12.x compatibility minimum", + ), + ], +) +def test_modern_vllm_compatibility_rejects_unsupported_devices( + device: GpuDevice, + message: str, +) -> None: + assert message in str(device.modern_vllm_issue) + + +def test_modern_vllm_compatibility_rejects_unparseable_driver() -> None: + device = GpuDevice(0, "NVIDIA T4", 16_000_000_000, 15_000_000_000, "unknown", None) + + assert device.modern_vllm_issue == "NVIDIA driver version could not be interpreted: unknown" + + +def test_visible_gpu_discovery_falls_back_when_compute_capability_is_unavailable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.shutil.which", + lambda *_args, **_kwargs: "/usr/bin/nvidia-smi", + ) + calls = 0 + + def run(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + nonlocal calls + calls += 1 + if calls == 1: + return subprocess.CompletedProcess(command, 1, stdout="") + return subprocess.CompletedProcess( + command, + 0, + stdout="0, Tesla T4, 15109, 14000, 570.86.15\n", + ) + + monkeypatch.setattr("heartwood.gateway._gpu_environment.subprocess.run", run) + + devices = discover_visible_gpus({"PATH": "/usr/bin"}) + + assert len(devices) == 1 + assert devices[0].compute_capability is None + assert devices[0].modern_vllm_issue is None + + +def test_visible_gpu_discovery_rejects_p100_without_reported_compute_capability( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.shutil.which", + lambda *_args, **_kwargs: "/usr/bin/nvidia-smi", + ) + calls = 0 + + def run(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + nonlocal calls + calls += 1 + if calls == 1: + return subprocess.CompletedProcess(command, 1, stdout="") + return subprocess.CompletedProcess( + command, + 0, + stdout="0, Tesla P100-PCIE-16GB, 16280, 16000, 575.57.08\n", + ) + + monkeypatch.setattr("heartwood.gateway._gpu_environment.subprocess.run", run) + + devices = discover_visible_gpus({"PATH": "/usr/bin"}) + + assert len(devices) == 1 + assert devices[0].compute_capability is None + assert "requires 7.5 or newer" in str(devices[0].modern_vllm_issue) + + +def test_slurm_discovery_reports_partition_capacity( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, + 0, + stdout=( + "dev*|gpu:nvidia_l40s:8(S:0-7)|up|515000|64\n" + "long|gpu:nvidia_l40s:4|up|256G|32\n" + "cpu|(null)|up|128G|16\n" + "offline|gpu:nvidia_l40s:8|down|515000|64\n" + ), + ), + ) + + partitions = discover_slurm_gpu_partitions({"PATH": "/usr/bin"}) + + assert [partition.name for partition in partitions] == ["dev", "long"] + assert partitions[0].is_default is True + assert partitions[0].gpu_model == "nvidia_l40s" + assert partitions[0].gpu_count == 8 + assert partitions[0].gpu_memory_bytes == 48_000_000_000 + assert partitions[0].node_memory_bytes == 515_000 * 1024**2 + assert partitions[1].node_memory_bytes == 256 * 1024**3 + + +def test_gpu_environment_assesses_visible_scheduler_and_insufficient_capacity() -> None: + visible = GpuEnvironment( + platform_id="terra", + visible_devices=(), + slurm_partitions=(), + capacities=(GpuCapacity("one T4", "NVIDIA T4", 1, 16_000_000_000, False),), + ) + compatible, reason = visible.assess(gpu_count=1, gpu_memory_bytes=15_000_000_000) + assert compatible + assert reason == "Compatible with 1 visible NVIDIA T4 GPU(s)." + + insufficient, reason = visible.assess(gpu_count=2, gpu_memory_bytes=42_000_000_000) + assert not insufficient + assert "Requires 2 GPU(s) with 39.1 GiB each" in reason + assert "at most 1 GPU(s) with 14.9 GiB each" in reason + + scheduled = GpuEnvironment( + platform_id="carina", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + "Slurm partition dev", + "NVIDIA L40S", + 8, + 48_000_000_000, + True, + "dev", + ), + ), + ) + compatible, reason = scheduled.assess(gpu_count=4, gpu_memory_bytes=42_000_000_000) + assert compatible + assert "Slurm partition dev" in reason + assert "allocation approval is required" in reason + + +@pytest.mark.parametrize( + ("platform_id", "message"), + [ + ("terra", "Recreate the Terra cloud environment with an NVIDIA T4"), + ("carina", "No compatible Slurm GPU capacity was reported by Carina"), + ("generic", "No compatible NVIDIA GPU is visible to Heartwood"), + ], +) +def test_gpu_environment_explains_missing_capacity(platform_id: str, message: str) -> None: + environment = GpuEnvironment(platform_id, (), (), ()) + + compatible, reason = environment.assess(gpu_count=1, gpu_memory_bytes=1) + + assert not compatible + assert message in reason + + +def test_gpu_environment_surfaces_incompatible_visible_devices() -> None: + p4 = GpuDevice(0, "Tesla P4", 8_000_000_000, 8_000_000_000, "570.86.15", (6, 1)) + environment = GpuEnvironment("terra", (p4,), (), ()) + + compatible, reason = environment.assess(gpu_count=1, gpu_memory_bytes=1) + + assert not compatible + assert "Tesla P4 has compute capability 6.1" in reason + + +def test_environment_inspection_prefers_visible_devices_and_skips_slurm_in_allocation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + t4 = GpuDevice(0, "NVIDIA T4", 16_000_000_000, 15_000_000_000, "570.86.15", (7, 5)) + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_visible_gpus", + lambda _env: (t4,), + ) + discovered_slurm = False + + def discover(_env: object) -> tuple[SlurmGpuPartition, ...]: + nonlocal discovered_slurm + discovered_slurm = True + return () + + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_slurm_gpu_partitions", + discover, + ) + + environment = inspect_gpu_environment("carina", {"SLURM_JOB_ID": "123"}) + + assert not discovered_slurm + assert environment.capacities[0].gpu_count == 1 + assert environment.capacities[0].gpu_memory_bytes == 16_000_000_000 + + +def test_environment_inspection_builds_distinct_mixed_gpu_capacities( + monkeypatch: pytest.MonkeyPatch, +) -> None: + devices = ( + GpuDevice(0, "NVIDIA L40S", 48_000_000_000, 47_000_000_000, "570.86.15", (8, 9)), + GpuDevice(1, "NVIDIA T4", 16_000_000_000, 15_000_000_000, "570.86.15", (7, 5)), + ) + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_visible_gpus", + lambda _env: devices, + ) + + environment = inspect_gpu_environment("generic", {}) + + assert [(item.gpu_count, item.gpu_memory_bytes) for item in environment.capacities] == [ + (1, 48_000_000_000), + (2, 16_000_000_000), + ] + + +def test_environment_inspection_builds_known_slurm_capacity( + monkeypatch: pytest.MonkeyPatch, +) -> None: + partition = SlurmGpuPartition("dev", True, "nvidia_l40s", 8, None, None, "up") + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_visible_gpus", + lambda _env: (), + ) + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_slurm_gpu_partitions", + lambda _env: (partition, SlurmGpuPartition("other", False, None, 2, None, None, "up")), + ) + + environment = inspect_gpu_environment("carina", {}) + + assert environment.capacities == ( + GpuCapacity("Slurm partition dev", "nvidia_l40s", 8, 48_000_000_000, True, "dev"), + ) + assert environment.slurm_partitions[1].gpu_memory_bytes is None + + +def test_visible_gpu_discovery_handles_missing_malformed_and_failed_queries( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("heartwood.gateway._gpu_environment.shutil.which", lambda *_a, **_k: None) + assert discover_visible_gpus({}) == () + + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.shutil.which", + lambda *_args, **_kwargs: "/usr/bin/nvidia-smi", + ) + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, + 0, + stdout="malformed\nnot-an-index, T4, x, y, driver, bad\n", + ), + ) + assert discover_visible_gpus({}) == () + + def fail(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[str]: + raise subprocess.TimeoutExpired("nvidia-smi", 10) + + monkeypatch.setattr("heartwood.gateway._gpu_environment.subprocess.run", fail) + assert discover_visible_gpus({}) == () + + +def test_slurm_discovery_ignores_malformed_rows_and_keeps_largest_capacity( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, + 0, + stdout=( + "malformed\n" + "*|gpu:nvidia_l40s:8|up|bad|bad\n" + "dev|gpu|up|1T|0\n" + "dev|gpu:nvidia_l40s:x|up|1T|32\n" + "dev|gpu:nvidia_l40s:2,gpu:nvidia_l40s:4|idle|1T+|32\n" + "dev|gpu:nvidia_l40s:2|alloc|512G|16\n" + ), + ), + ) + + partitions = discover_slurm_gpu_partitions( + {"PATH": "/usr/bin", "HOME": "/home/test", "SECRET": "excluded"} + ) + + assert len(partitions) == 1 + assert partitions[0].gpu_count == 4 + assert partitions[0].node_memory_bytes == 1024**4 + assert partitions[0].node_cpu_count == 32 + + +@pytest.mark.parametrize( + "failure", + [OSError("missing sinfo"), subprocess.TimeoutExpired("sinfo", 15)], +) +def test_slurm_discovery_handles_unavailable_scheduler( + monkeypatch: pytest.MonkeyPatch, + failure: BaseException, +) -> None: + def fail(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[str]: + raise failure + + monkeypatch.setattr("heartwood.gateway._gpu_environment.subprocess.run", fail) + + assert discover_slurm_gpu_partitions({}) == () diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index 73589f82..5cdae0fc 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -20,12 +20,12 @@ ModelArtifact, ModelRepositoryError, ModelSnapshot, + catalog_model_choices, load_model_artifact_catalog, load_model_snapshot_catalog, managed_model_request_body, managed_model_token_budgets, plan_local_context_window, - recommended_model_choices, ) @@ -435,6 +435,11 @@ def test_local_model_choice_reuses_existing_download_contracts() -> None: runtime="vllm", source_path=None, artifact_sha256=None, + minimum_gpu_count=1, + minimum_gpu_memory_bytes=16 * 1024**3, + tool_call_parser="hermes", + download_policy="synthetic", + allow_patterns=("*.json", "*.safetensors"), ) gpu_download = gpu.download_model() @@ -454,8 +459,8 @@ def test_central_catalog_exposes_only_recommended_models() -> None: root / "images" / "generic" / "local-runtime" / "snapshots.toml" ) - choices = recommended_model_choices(artifacts.artifacts, snapshots.snapshots) - downloadable = recommended_model_choices( + choices = catalog_model_choices(artifacts.artifacts, snapshots.snapshots) + downloadable = catalog_model_choices( artifacts.artifacts, snapshots.snapshots, recommended_only=False, @@ -463,12 +468,37 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert {choice.model_id for choice in choices} == { "qwen25-7b-instruct-q4_k_m", - "qwen25-7b-instruct-awq-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) assert all(choice.context_window == 32_768 for choice in choices) assert "llama-cpp-stories260k-ci" in {choice.model_id for choice in downloadable} assert "qwen25-coder-7b-instruct-q4_k_m" in {choice.model_id for choice in downloadable} + assert { + "qwen25-coder-7b-instruct-awq-vllm", + "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-bf16-vllm", + "qwen3-coder-next-fp8-vllm", + "gpt-oss-120b-vllm", + } <= {choice.model_id for choice in downloadable} + assert all(choice.catalog_source == "catalog" for choice in downloadable) + assert all( + choice.qualification == "candidate" for choice in downloadable if choice.runtime == "vllm" + ) + + +def test_catalog_qualification_is_scoped_to_the_validated_platform() -> None: + root = Path(__file__).resolve().parents[3] + artifacts = load_model_artifact_catalog( + root / "images" / "generic" / "local-runtime" / "model-catalog.toml" + ) + snapshots = load_model_snapshot_catalog( + root / "images" / "generic" / "local-runtime" / "snapshots.toml" + ) + cpu = catalog_model_choices(artifacts.artifacts, snapshots.snapshots)[0] + + assert cpu.qualification_for("generic") == "qualified" + assert cpu.qualification_for("terra") == "qualified" + assert cpu.qualification_for("carina") == "candidate" def _repository( @@ -515,4 +545,6 @@ def _cpu_choice() -> LocalModelChoice: license_posture="Source model card reports apache-2.0.", catalog_source="user-selected", artifact_sha256="a" * 64, + recommended_ram_bytes=16 * 1024**3, + recommended_disk_bytes=3072, ) diff --git a/packages/gateway/tests/test_model_artifacts.py b/packages/gateway/tests/test_model_artifacts.py index 49e75ceb..fe9dc6b8 100644 --- a/packages/gateway/tests/test_model_artifacts.py +++ b/packages/gateway/tests/test_model_artifacts.py @@ -308,8 +308,25 @@ def test_background_manager_downloads_and_selects_a_snapshot( source_revision="a" * 40, expected_size_bytes=7, minimum_free_bytes=7, + license_id="Apache-2.0", license_posture="Synthetic", model_alias="Test snapshot", + precision="Synthetic", + tier="standard", + qualification="candidate", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + recommended_ram_bytes=1, + recommended_disk_bytes=7, + maximum_context_window=32_768, + tool_call_parser="hermes", + tensor_parallel_size=1, + startup_seconds_min=1, + startup_seconds_max=2, + download_policy="synthetic", + allow_patterns=("*.json", "*.safetensors"), + ignore_patterns=("*.bin",), + context_window=32_768, ) installed = tmp_path / "models" / snapshot.snapshot_id @@ -332,7 +349,7 @@ def download( artifacts=(), ), snapshot_catalog=ModelSnapshotCatalog( - schema_version="heartwood.model-snapshot-catalog.v1", + schema_version="heartwood.model-snapshot-catalog.v2", snapshots=(snapshot,), ), cache_dir=tmp_path / "models", @@ -547,7 +564,7 @@ def _artifact(content: bytes) -> ModelArtifact: def _empty_snapshot_catalog() -> ModelSnapshotCatalog: return ModelSnapshotCatalog( - schema_version="heartwood.model-snapshot-catalog.v1", + schema_version="heartwood.model-snapshot-catalog.v2", snapshots=(), ) diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 55ed83eb..d4942b6c 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -18,6 +18,7 @@ from heartwood.gateway import ( ModelSnapshot, + ModelSnapshotCatalog, ModelSnapshotError, download_model_snapshot, load_model_snapshot_catalog, @@ -26,41 +27,90 @@ ) -def test_repository_snapshot_catalog_pins_the_carina_demo_model() -> None: +@pytest.mark.parametrize( + ( + "snapshot_id", + "repository", + "revision", + "tier", + "gpu_count", + "tool_parser", + ), + [ + ( + "qwen25-coder-7b-instruct-awq-vllm", + "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ", + "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a", + "standard", + 1, + "hermes", + ), + ( + "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", + "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", + "powerful", + 1, + "qwen3_coder", + ), + ( + "qwen3-coder-30b-a3b-instruct-bf16-vllm", + "Qwen/Qwen3-Coder-30B-A3B-Instruct", + "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", + "powerful", + 2, + "qwen3_coder", + ), + ( + "qwen3-coder-next-fp8-vllm", + "Qwen/Qwen3-Coder-Next-FP8", + "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c", + "maximum", + 4, + "qwen3_coder", + ), + ( + "gpt-oss-120b-vllm", + "openai/gpt-oss-120b", + "b5c939de8f754692c1647ca79fbf85e8c1e70f8a", + "maximum", + 2, + "openai", + ), + ], +) +def test_repository_snapshot_catalog_pins_gpu_candidates( + snapshot_id: str, + repository: str, + revision: str, + tier: str, + gpu_count: int, + tool_parser: str, +) -> None: catalog = load_model_snapshot_catalog( _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" ) - snapshot = catalog.snapshot("qwen25-7b-instruct-vllm") + snapshot = catalog.snapshot(snapshot_id) assert snapshot.runtime_profile == "vllm-cuda" - assert snapshot.source_repository == "Qwen/Qwen2.5-7B-Instruct" - assert snapshot.source_revision == "a09a35458c702b33eeacc393d103063234e8bc28" + assert snapshot.source_repository == repository + assert snapshot.source_revision == revision + assert snapshot.tier == tier + assert snapshot.tensor_parallel_size == gpu_count + assert snapshot.tool_call_parser == tool_parser assert snapshot.minimum_free_bytes >= snapshot.expected_size_bytes - assert snapshot.context_window == 32_768 - - terra_snapshot = catalog.snapshot("qwen25-7b-instruct-awq-vllm") - assert terra_snapshot.runtime_profile == "vllm-cuda" - assert terra_snapshot.source_repository == "Qwen/Qwen2.5-7B-Instruct-AWQ" - assert terra_snapshot.source_revision == "b25037543e9394b818fdfca67ab2a00ecc7dd641" - assert terra_snapshot.minimum_free_bytes >= terra_snapshot.expected_size_bytes - assert terra_snapshot.context_window == 32_768 - - recommended_snapshot = catalog.snapshot("qwen25-7b-instruct-awq-vllm") - assert recommended_snapshot.source_repository == "Qwen/Qwen2.5-7B-Instruct-AWQ" - assert recommended_snapshot.recommended is True - - qwen3_snapshot = catalog.snapshot("qwen3-8b-awq-vllm") - assert qwen3_snapshot.source_repository == "Qwen/Qwen3-8B-AWQ" - assert qwen3_snapshot.source_revision == "4da05a8edb55c6046cce958586c33b61da07bb79" - assert qwen3_snapshot.recommended is False + assert snapshot.recommended_disk_bytes >= snapshot.minimum_free_bytes + assert snapshot.context_window <= snapshot.maximum_context_window + assert snapshot.qualification == "candidate" + assert snapshot.validated_platforms == () + assert snapshot.qualification_test is None + assert snapshot.recommended is False @pytest.mark.parametrize( ("snapshot_id", "minimum_vram_gib"), [ - ("qwen25-7b-instruct-awq-vllm", 16), - ("qwen25-7b-instruct-vllm", 32), - ("qwen3-8b-awq-vllm", 16), + ("qwen25-coder-7b-instruct-awq-vllm", 16), ], ) def test_snapshot_minimum_vram_supports_its_advertised_context( @@ -80,7 +130,111 @@ def test_snapshot_minimum_vram_supports_its_advertised_context( ) assert plan.effective_window == snapshot.context_window - assert f"at least {minimum_vram_gib} GB VRAM" in str(snapshot.minimum_resource_envelope) + assert f"{minimum_vram_gib} GB VRAM" in str(snapshot.minimum_resource_envelope) + + +def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> None: + source = load_model_snapshot_catalog( + _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" + ) + standard = replace( + source.snapshot("qwen25-coder-7b-instruct-awq-vllm"), + qualification="qualified", + validated_platforms=("terra",), + qualification_test="heartwood.coding-agent-e2e.v1", + recommended=True, + ) + powerful = replace( + source.snapshot("qwen3-coder-30b-a3b-instruct-fp8-vllm"), + qualification="qualified", + validated_platforms=("carina",), + qualification_test="heartwood.coding-agent-e2e.v1", + recommended=True, + ) + catalog = ModelSnapshotCatalog(source.schema_version, (standard, powerful)) + + assert ( + catalog.recommend( + platform_id="terra", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + maximum_tier="maximum", + ) + == standard + ) + assert ( + catalog.recommend( + platform_id="carina", + gpu_count=1, + gpu_memory_bytes=48_000_000_000, + maximum_tier="powerful", + ) + == powerful + ) + assert ( + catalog.recommend( + platform_id="carina", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + maximum_tier="powerful", + ) + is None + ) + assert ( + source.recommend( + platform_id="terra", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + maximum_tier="maximum", + ) + is None + ) + + assert ( + catalog.recommend_for_capacities( + platform_id="terra", + capacities=((1, 16_000_000_000), (1, 48_000_000_000)), + maximum_tier="maximum", + ) + == standard + ) + assert ( + catalog.recommend_for_capacities( + platform_id="generic", + capacities=((4, 48_000_000_000),), + maximum_tier="maximum", + ) + is None + ) + + +def test_catalog_capacity_recommendation_prefers_more_parallelism_within_tier() -> None: + source = load_model_snapshot_catalog( + _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" + ) + single = replace( + source.snapshot("qwen3-coder-30b-a3b-instruct-fp8-vllm"), + qualification="qualified", + validated_platforms=("carina",), + qualification_test="heartwood.coding-agent-e2e.v1", + recommended=True, + ) + dual = replace( + source.snapshot("qwen3-coder-30b-a3b-instruct-bf16-vllm"), + qualification="qualified", + validated_platforms=("carina",), + qualification_test="heartwood.coding-agent-e2e.v1", + recommended=True, + ) + catalog = ModelSnapshotCatalog(source.schema_version, (single, dual)) + + recommendation = catalog.recommend_for_capacities( + platform_id="carina", + capacities=((1, 48_000_000_000), (2, 48_000_000_000)), + maximum_tier="powerful", + ) + + assert recommendation == dual def test_snapshot_download_is_atomic_and_creates_exact_provenance(tmp_path: Path) -> None: @@ -216,7 +370,33 @@ def test_snapshot_metadata_rejects_floating_revisions() -> None: ({"purpose": ""}, "purpose must be"), ({"expected_size_bytes": 0}, "storage metadata"), ({"minimum_free_bytes": 1}, "storage metadata"), + ({"recommended_disk_bytes": 19}, "must cover minimum_free_bytes"), + ({"recommended_ram_bytes": 0}, "recommended_ram_bytes must be positive"), + ({"minimum_gpu_count": 0}, "GPU resource metadata must be positive"), + ({"minimum_gpu_memory_bytes": 0}, "GPU resource metadata must be positive"), + ({"tensor_parallel_size": 0}, "must cover the minimum GPU count"), + ({"tier": "unknown"}, "unsupported model tier"), + ({"qualification": "unknown"}, "unsupported model qualification"), + ({"tool_call_parser": "unknown"}, "unsupported vLLM tool-call parser"), + ({"startup_seconds_min": 0}, "startup estimate is invalid"), + ({"startup_seconds_max": 0}, "startup estimate is invalid"), + ({"context_window": 2047}, "between 2048 and 1048576"), ({"context_window": 1_048_577}, "between 2048 and 1048576"), + ({"maximum_context_window": 1024}, "must cover the default context window"), + ({"maximum_context_window": 1_048_577}, "must cover the default context window"), + ({"allow_patterns": ()}, "must select reviewed snapshot files"), + ({"allow_patterns": ("*.json", "*.json")}, "must not contain duplicates"), + ({"ignore_patterns": ("*.bin", "*.bin")}, "must not contain duplicates"), + ({"allow_patterns": ("../*.json",)}, "unsafe repository pattern"), + ({"ignore_patterns": ("/tmp/*",)}, "unsafe repository pattern"), + ({"validated_platforms": ("terra", "terra")}, "must not contain duplicates"), + ({"validated_platforms": ("unknown",)}, "unsupported platform"), + ({"qualification": "qualified"}, "require validated platforms"), + ( + {"qualification": "qualified", "validated_platforms": ("terra",)}, + "require validated platforms", + ), + ({"recommended": True}, "candidate models cannot be recommended"), ], ) def test_snapshot_metadata_rejects_unsafe_values(changes: dict[str, object], message: str) -> None: @@ -242,7 +422,7 @@ def test_snapshot_catalog_reports_unknown_ids_and_invalid_documents(tmp_path: Pa missing_snapshots = tmp_path / "missing-snapshots.toml" missing_snapshots.write_text( - 'schema_version = "heartwood.model-snapshot-catalog.v1"\n', + 'schema_version = "heartwood.model-snapshot-catalog.v2"\n', encoding="utf-8", ) with pytest.raises(ModelSnapshotError, match="snapshots table"): @@ -250,7 +430,11 @@ def test_snapshot_catalog_reports_unknown_ids_and_invalid_documents(tmp_path: Pa invalid_entry = tmp_path / "entry.toml" invalid_entry.write_text( - 'schema_version = "heartwood.model-snapshot-catalog.v1"\n[snapshots]\ninvalid = "value"\n', + 'schema_version = "heartwood.model-snapshot-catalog.v2"\n' + "[download_policies.safe]\n" + 'allow_patterns = ["*.json"]\n' + "[snapshots]\n" + 'invalid = "value"\n', encoding="utf-8", ) with pytest.raises(ModelSnapshotError, match="entries must be tables"): @@ -258,12 +442,14 @@ def test_snapshot_catalog_reports_unknown_ids_and_invalid_documents(tmp_path: Pa invalid_fields = tmp_path / "fields.toml" invalid_fields.write_text( - 'schema_version = "heartwood.model-snapshot-catalog.v1"\n' + 'schema_version = "heartwood.model-snapshot-catalog.v2"\n' + "[download_policies.safe]\n" + 'allow_patterns = ["*.json"]\n' "[snapshots.invalid]\n" 'runtime_profile = ""\n', encoding="utf-8", ) - with pytest.raises(ModelSnapshotError, match="runtime_profile"): + with pytest.raises(ModelSnapshotError, match="download_policy"): load_model_snapshot_catalog(invalid_fields) @@ -344,8 +530,25 @@ def _snapshot() -> ModelSnapshot: source_revision="0123456789abcdef0123456789abcdef01234567", expected_size_bytes=20, minimum_free_bytes=20, + license_id="Apache-2.0", license_posture="Synthetic test content only.", model_alias="Synthetic vLLM", + precision="Synthetic", + tier="standard", + qualification="candidate", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + recommended_ram_bytes=1, + recommended_disk_bytes=20, + maximum_context_window=32_768, + tool_call_parser="hermes", + tensor_parallel_size=1, + startup_seconds_min=1, + startup_seconds_max=2, + download_policy="synthetic", + allow_patterns=("*.json", "*.safetensors"), + ignore_patterns=("*.bin",), + context_window=32_768, ) diff --git a/packages/gateway/tests/test_project_config.py b/packages/gateway/tests/test_project_config.py index 1c61dcf1..eae58e59 100644 --- a/packages/gateway/tests/test_project_config.py +++ b/packages/gateway/tests/test_project_config.py @@ -8,6 +8,7 @@ import tomllib from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy from dataclasses import asdict, replace from pathlib import Path from threading import Event @@ -272,6 +273,104 @@ def test_project_config_rejects_symlink(tmp_path: Path) -> None: ), "unsupported Heartwood-managed model runtime", ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + catalog_source="unknown", + ), + "unsupported Heartwood-managed model catalog source", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + tier="unknown", + ), + "unsupported Heartwood-managed model tier", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + qualification="unknown", + ), + "unsupported Heartwood-managed model qualification", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + display_name=" ", + ), + "display_name must not be empty", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + source_repository="invalid", + ), + "source_repository must use owner/model format", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + source_revision="main", + ), + "source_revision must be immutable", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + catalog_source="user-selected", + source_revision="abcdef0", + ), + "source_revision must be a resolved commit", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + source_path="../model.gguf", + ), + "source_path must be repository-relative", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + model_type="Not Normalized", + ), + "model_type must be normalized", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + size_bytes=0, + ), + "size_bytes must be positive", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + minimum_free_bytes=1, + ), + "minimum_free_bytes must cover its size", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + size_bytes=2, + minimum_free_bytes=1, + ), + "minimum_free_bytes must cover its size", + ), ( LocalModelSelection(artifact_id="model", path=".heartwood/models"), r"under \.heartwood/models", @@ -288,6 +387,139 @@ def test_project_config_rejects_symlink(tmp_path: Path) -> None: ), "between 2048 and 1048576", ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + maximum_context_window=2047, + ), + "maximum context window is invalid", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + minimum_gpu_count=-1, + ), + "GPU requirements cannot be negative", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + minimum_gpu_memory_bytes=-1, + ), + "GPU requirements cannot be negative", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + tensor_parallel_size=0, + ), + "tensor parallelism must be positive", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + startup_seconds_min=0, + ), + "startup estimate is invalid", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + startup_seconds_min=10, + startup_seconds_max=9, + ), + "startup estimate is invalid", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + recommended_ram_bytes=0, + ), + "recommended_ram_bytes must be positive", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + recommended_disk_bytes=0, + ), + "recommended_disk_bytes must be positive", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + tool_call_parser="unknown", + ), + "unsupported Heartwood-managed tool-call parser", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + runtime="vllm", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + ), + "vLLM model runtime metadata is incomplete", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + runtime="llama-cpp", + minimum_gpu_count=1, + ), + "llama.cpp models cannot declare vLLM GPU settings", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + license_id=" ", + ), + "license_id must not be empty", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + artifact_sha256="invalid", + ), + "artifact_sha256 must be a SHA-256 digest", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + catalog_source="user-selected", + ), + "provenance is incomplete", + ), + ( + LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + catalog_source="user-selected", + display_name="Model", + source_repository="example/model", + source_revision="a" * 40, + source_path="model.gguf", + size_bytes=1, + minimum_free_bytes=1, + license_posture="Apache-2.0", + minimum_resource_envelope="Minimum", + recommended_resource_envelope="Recommended", + ), + "integrity metadata is incomplete", + ), ], ) def test_local_model_selection_rejects_invalid_metadata( @@ -373,6 +605,68 @@ def test_project_config_parser_rejects_unsupported_structure(tmp_path: Path) -> project_config_from_mapping({**valid, "local_model": []}, project=project) +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("unknown", "value", "local_model contains unsupported fields"), + ("artifact_id", "", "artifact_id must be a non-empty string"), + ("display_name", [], "display_name must be a non-empty string"), + ("size_bytes", True, "size_bytes must be a positive integer"), + ("minimum_gpu_count", True, "minimum_gpu_count must be a nonnegative integer"), + ("allow_patterns", [""], "allow_patterns must be an array"), + ], +) +def test_project_config_parser_rejects_invalid_local_model_fields( + tmp_path: Path, + field: str, + value: object, + message: str, +) -> None: + project = ProjectContext(tmp_path) + config = replace( + _default_config(project), + local_model=LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + ), + ) + mapping = deepcopy(_config_mapping(config)) + local_model = mapping["local_model"] + assert isinstance(local_model, dict) + local_model[field] = value + + with pytest.raises(ProjectConfigError, match=message): + project_config_from_mapping(mapping, project=project) + + +def test_project_config_parser_defaults_optional_local_model_collections(tmp_path: Path) -> None: + project = ProjectContext(tmp_path) + config = replace( + _default_config(project), + local_model=LocalModelSelection( + artifact_id="model", + path=".heartwood/models/model", + ), + ) + mapping = deepcopy(_config_mapping(config)) + local_model = mapping["local_model"] + assert isinstance(local_model, dict) + for field in ( + "minimum_gpu_count", + "minimum_gpu_memory_bytes", + "allow_patterns", + "ignore_patterns", + "validated_platforms", + ): + local_model.pop(field, None) + + parsed = project_config_from_mapping(mapping, project=project) + + assert parsed.local_model is not None + assert parsed.local_model.minimum_gpu_count == 0 + assert parsed.local_model.allow_patterns == () + + def test_project_config_parser_rejects_non_platform_connection(tmp_path: Path) -> None: project = ProjectContext(tmp_path) mapping = _config_mapping(_default_config(project)) diff --git a/packages/gateway/tests/test_readiness.py b/packages/gateway/tests/test_readiness.py index 131452ec..bdd082a3 100644 --- a/packages/gateway/tests/test_readiness.py +++ b/packages/gateway/tests/test_readiness.py @@ -118,6 +118,9 @@ def _configure_model( artifact_id="synthetic-model", path=model_path, runtime="vllm", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=1, + tool_call_parser="hermes", ) config = store.load() else: diff --git a/packages/webui/src/App.test.tsx b/packages/webui/src/App.test.tsx index 3999b34c..7762da47 100644 --- a/packages/webui/src/App.test.tsx +++ b/packages/webui/src/App.test.tsx @@ -124,6 +124,50 @@ const actions = (): ActionSettings => ({ ], }); +const localModelChoice = ( + overrides: Partial = {}, +): LocalModelChoice => ({ + model_id: "stories260k", + label: "Stories 260K", + purpose: "Synthetic smoke-test model.", + runtime: "llama-cpp", + source_repository: "example/stories260k", + source_revision: "0".repeat(40), + source_path: "model.gguf", + size_bytes: 256 * 1024 * 1024, + minimum_free_bytes: 256 * 1024 * 1024, + license_id: "test-fixture", + license_posture: "Test fixture", + catalog_source: "catalog", + context_window: 32_768, + maximum_context_window: 32_768, + precision: "GGUF Q4_K_M", + tier: "standard", + qualification: "qualified", + minimum_gpu_count: 0, + minimum_gpu_memory_bytes: 0, + recommended_ram_bytes: 16 * 1024 * 1024 * 1024, + recommended_disk_bytes: 1024 * 1024 * 1024, + tool_call_parser: null, + tensor_parallel_size: 1, + startup_seconds_min: 5, + startup_seconds_max: 30, + download_policy: null, + allow_patterns: [], + ignore_patterns: [], + validated_platforms: ["ci"], + qualification_test: "synthetic-browser-e2e-v1", + artifact_sha256: "a".repeat(64), + minimum_resource_envelope: "Minimum: 4 CPU cores and 8 GB RAM.", + recommended_resource_envelope: "Recommended: 8 CPU cores and 16 GB RAM.", + active: false, + available: true, + selected: false, + availability_reason: "Available on this deployment", + recommended: true, + ...overrides, +}); + class FakeClient implements HeartwoodClient { commands: SessionCommand[] = []; auditExportCalls = 0; @@ -267,27 +311,21 @@ class FakeClient implements HeartwoodClient { ): Promise { this.localImportRequest = request; return Promise.resolve({ - model: { + model: localModelChoice({ model_id: "imported-model", label: "Imported model", purpose: "User-imported model", - runtime: "llama-cpp", source_repository: request.repository, source_revision: request.revision, - source_path: "model.gguf", size_bytes: 1024, minimum_free_bytes: 2048, + license_id: request.license, license_posture: request.license, catalog_source: "user-selected", - context_window: 32_768, - artifact_sha256: "a".repeat(64), minimum_resource_envelope: "4 GiB RAM", recommended_resource_envelope: "8 GiB RAM", - active: false, - available: true, selected: true, - availability_reason: "Available on this deployment", - }, + }), path: "/project/.heartwood/models/imported/model.gguf", status: "ready", }); @@ -460,7 +498,7 @@ class FakeClient implements HeartwoodClient { } return Promise.resolve({ schema_version: "heartwood.local-model-catalog.v1", - snapshot_schema_version: "heartwood.model-snapshot-catalog.v1", + snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [ { artifact_id: "stories260k", @@ -482,31 +520,17 @@ class FakeClient implements HeartwoodClient { ], snapshots: [], models: [ - { - model_id: "stories260k", - label: "Stories 260K", - purpose: "Synthetic smoke-test model.", - runtime: "llama-cpp", - source_repository: "example/stories260k", - source_revision: "0".repeat(40), - source_path: "model.gguf", - size_bytes: 256 * 1024 * 1024, - minimum_free_bytes: 256 * 1024 * 1024, - license_posture: "Test fixture", - catalog_source: "recommended", - context_window: 32_768, - artifact_sha256: "a".repeat(64), - minimum_resource_envelope: "Minimum: 4 CPU cores and 8 GB RAM.", - recommended_resource_envelope: - "Recommended: 8 CPU cores and 16 GB RAM.", + localModelChoice({ active: this.activeManagedModel, - available: true, selected: this.currentSettings.model_source === "heartwood", - availability_reason: "Available on this deployment", - }, + }), ...(this.customModel === null ? [] : [this.customModel]), ], downloads: this.currentDownloads, + gpu_environment: { + platform_id: "generic", + capacities: [], + }, }); } @@ -515,28 +539,23 @@ class FakeClient implements HeartwoodClient { ): Promise { this.inspectedRepository = request; if (this.repositoryError) return Promise.reject(this.repositoryError); - const candidate: LocalModelChoice = { + const candidate = localModelChoice({ model_id: "hf-research-model-123456789abc", label: "Research Model Q4_K_M", purpose: "User-selected Hugging Face model.", - runtime: "llama-cpp", source_repository: request.repository, source_revision: "1".repeat(40), source_path: "research-model-q4_k_m.gguf", size_bytes: 4 * 1024 * 1024 * 1024, minimum_free_bytes: 4 * 1024 * 1024 * 1024, + license_id: "Apache-2.0", license_posture: "Source model card reports apache-2.0.", catalog_source: "user-selected", - context_window: 32_768, artifact_sha256: "b".repeat(64), minimum_resource_envelope: "Estimated minimum: 4 CPU cores and 12 GB RAM.", recommended_resource_envelope: "Recommended: 8 CPU cores and 16 GB RAM.", - active: false, - available: true, - selected: false, - availability_reason: "Available on this deployment", - }; + }); return Promise.resolve({ model: candidate, selection_reason: "Selected a balanced GGUF model for the CPU runtime.", @@ -562,33 +581,29 @@ class FakeClient implements HeartwoodClient { request: CustomLocalModelDownloadRequest, ): Promise { this.customDownloadRequest = request; - this.customModel = { + const customModel = localModelChoice({ model_id: "hf-research-model-123456789abc", label: "Research Model Q4_K_M", purpose: "User-selected Hugging Face model.", - runtime: "llama-cpp", source_repository: request.repository, source_revision: request.revision ?? "1".repeat(40), source_path: "research-model-q4_k_m.gguf", size_bytes: 4 * 1024 * 1024 * 1024, minimum_free_bytes: 4 * 1024 * 1024 * 1024, + license_id: "Apache-2.0", license_posture: "Source model card reports apache-2.0.", catalog_source: "user-selected", - context_window: 32_768, artifact_sha256: "b".repeat(64), minimum_resource_envelope: "Estimated minimum: 4 CPU cores and 12 GB RAM.", recommended_resource_envelope: "Recommended: 8 CPU cores and 16 GB RAM.", - active: false, - available: true, - selected: false, - availability_reason: "Available on this deployment", - }; + }); + this.customModel = customModel; const download: ModelDownload = { - model_id: this.customModel.model_id, + model_id: customModel.model_id, status: "downloading", bytes_downloaded: 0, - bytes_total: this.customModel.size_bytes, + bytes_total: customModel.size_bytes, path: null, error: null, }; @@ -1036,6 +1051,14 @@ describe("App", () => { expect(await screen.findByText("Authorized")).toBeInTheDocument(); expect(screen.getByText("Allowed by this environment")).toBeInTheDocument(); fireEvent.click(screen.getByLabelText("Download Stories 260K")); + expect(client.downloadedArtifact).toBeNull(); + fireEvent.click( + within( + screen.getByRole("dialog", { name: "Download Stories 260K?" }), + ).getByRole("button", { + name: "Download model", + }), + ); await waitFor(() => expect(client.downloadedArtifact).toBe("stories260k")); const progress = await screen.findByRole("progressbar", { name: "Download progress for Stories 260K", @@ -1178,6 +1201,14 @@ describe("App", () => { within(modelPlan as HTMLElement).getByText(`Revision: ${"1".repeat(40)}`), ).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Download model" })); + expect(client.customDownloadRequest).toBeNull(); + fireEvent.click( + within( + screen.getByRole("dialog", { name: "Download Research Model Q4_K_M?" }), + ).getByRole("button", { + name: "Download model", + }), + ); await waitFor(() => expect(client.customDownloadRequest).toEqual({ @@ -1240,6 +1271,13 @@ describe("App", () => { client.artifactFailures = 1; fireEvent.click(screen.getByLabelText("Download Stories 260K")); + fireEvent.click( + within( + screen.getByRole("dialog", { name: "Download Stories 260K?" }), + ).getByRole("button", { + name: "Download model", + }), + ); expect( await screen.findByText("temporary model status failure"), diff --git a/packages/webui/src/client.test.ts b/packages/webui/src/client.test.ts index 19be747b..6ac77848 100644 --- a/packages/webui/src/client.test.ts +++ b/packages/webui/src/client.test.ts @@ -397,7 +397,7 @@ describe("GatewayClient", () => { it("lists and starts recommended model downloads", async () => { const artifacts = { schema_version: "heartwood.local-model-catalog.v1", - snapshot_schema_version: "heartwood.model-snapshot-catalog.v1", + snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [], snapshots: [], models: [], diff --git a/packages/webui/src/components/UtilitySheet.tsx b/packages/webui/src/components/UtilitySheet.tsx index e051af8c..3e7b9237 100644 --- a/packages/webui/src/components/UtilitySheet.tsx +++ b/packages/webui/src/components/UtilitySheet.tsx @@ -9,6 +9,14 @@ import { Badge } from "@stanfordspezi/spezi-web-design-system/components/Badge"; import { Button } from "@stanfordspezi/spezi-web-design-system/components/Button"; import { Checkbox } from "@stanfordspezi/spezi-web-design-system/components/Checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@stanfordspezi/spezi-web-design-system/components/Dialog"; import { Input } from "@stanfordspezi/spezi-web-design-system/components/Input"; import { Progress } from "@stanfordspezi/spezi-web-design-system/components/Progress"; import { @@ -312,7 +320,13 @@ const SettingsContent = (props: UtilitySheetProps) => { const [settingsView, setSettingsView] = useState<"models" | "approvals">( "models", ); + const [downloadConfirmation, setDownloadConfirmation] = + useState(null); const localModels = artifacts ? localModelOptions(artifacts) : []; + const recommendedModels = localModels.filter((model) => model.recommended); + const evaluationModels = localModels.filter( + (model) => model.catalog_source === "catalog" && !model.recommended, + ); const applyPreset = (presetId: string) => { const preset = settings?.presets.find( (item) => item.preset_id === presetId, @@ -432,65 +446,129 @@ const SettingsContent = (props: UtilitySheetProps) => {

Models Heartwood can run

- {artifacts && localModels.length ? - localModels.map((model) => { - const download = artifacts.downloads.find( - (item) => item.model_id === model.model_id, + {artifacts && recommendedModels.length ? + (["standard", "powerful", "maximum"] as const).map((tier) => { + const tierModels = recommendedModels.filter( + (model) => model.tier === tier, ); - return ( + return tierModels.length ? + +

{localTierLabel(tier)}

+ {tierModels.map((model) => { + const download = artifacts.downloads.find( + (item) => item.model_id === model.model_id, + ); + return ( +
+
+ {model.label} + + {localComputeLabel(model.runtime)} ·{" "} + {formatBytes(model.size_bytes)} ·{" "} + {model.context_window.toLocaleString()} tokens + + {model.purpose} + {model.availability_reason} + {model.recommended_resource_envelope ? + + {model.recommended_resource_envelope} + + : null} +
+ Technical details +
+ Precision: {model.precision} + License: {model.license_id} + + Maximum context:{" "} + {model.maximum_context_window.toLocaleString()}{" "} + tokens + + {model.tool_call_parser ? + + Tool parser: {model.tool_call_parser}; + tensor parallelism:{" "} + {model.tensor_parallel_size} + + : null} + + Revision: {model.source_revision} + +
+
+ +
+ + + +
+ ); + })} +
+ : null; + }) + :

No recommended models available

} +
+ + {evaluationModels.length ? +
+ Models under evaluation +
+

+ These pinned configurations are available for qualification + but are not Heartwood recommendations yet. +

+ {evaluationModels.map((model) => (
- - {model.label} - {model.catalog_source === "recommended" ? - Recommended - : null} - + {model.label} - {localComputeLabel(model.runtime)} ·{" "} - {formatBytes(model.size_bytes)} · Up to{" "} - {model.context_window.toLocaleString()} tokens + {localTierLabel(model.tier)} ·{" "} + {formatBytes(model.size_bytes)} - {model.purpose} {model.availability_reason} - {model.recommended_resource_envelope ? - {model.recommended_resource_envelope} - : null} -
- +
- ); - }) - :

No recommended models available

} - + ))} +
+
+ : null} { onForgetCredential={onForgetCredential} /> + setDownloadConfirmation(null)} + onConfirm={() => { + if (downloadConfirmation) { + onDownload(downloadConfirmation.model_id); + setDownloadConfirmation(null); + } + }} + /> +
More options
@@ -671,6 +760,82 @@ const localModelOptions = (catalog: ModelArtifacts): LocalModelChoice[] => const localComputeLabel = (runtime: LocalModelChoice["runtime"]): string => runtime === "vllm" ? "Requires an NVIDIA GPU" : "Runs on CPU"; +const localTierLabel = (tier: LocalModelChoice["tier"]): string => { + if (tier === "maximum") return "Maximum capability"; + return tier.charAt(0).toUpperCase() + tier.slice(1); +}; + +const ModelDownloadConfirmation = ({ + model, + onCancel, + onConfirm, +}: { + model: LocalModelChoice | null; + onCancel: () => void; + onConfirm: () => void; +}) => ( + !open && onCancel()}> + + + Download {model?.label ?? "model"}? + + Model weights are stored in this project and may take several minutes + to download and start. + + + {model ? +
+ {model.qualification === "candidate" ? + Evaluation candidate + : null} + {formatBytes(model.size_bytes)} download + + Allow up to {formatBytes(model.recommended_disk_bytes)} of + persistent storage. + + {model.recommended_resource_envelope} + + Expected startup:{" "} + {formatDurationRange( + model.startup_seconds_min, + model.startup_seconds_max, + )} + +
+ Source and runtime details +
+ Repository: {model.source_repository} + Revision: {model.source_revision} + + {model.precision} · {model.context_window.toLocaleString()}{" "} + token default + +
+
+
+ : null} + + + + +
+
+); + +const formatDurationRange = (minimum: number, maximum: number): string => + `${formatDuration(minimum)} to ${formatDuration(maximum)}`; + +const formatDuration = (seconds: number): string => { + if (seconds < 60) return `${seconds} seconds`; + const minutes = Math.ceil(seconds / 60); + return `${minutes} minute${minutes === 1 ? "" : "s"}`; +}; + const CustomLocalModelSetup = ({ downloads, onDownload, @@ -694,6 +859,7 @@ const CustomLocalModelSetup = ({ const [importRevision, setImportRevision] = useState(""); const [importLicense, setImportLicense] = useState(""); const [importComplete, setImportComplete] = useState(false); + const [confirmDownload, setConfirmDownload] = useState(false); const modelDownload = plan === null ? undefined : ( downloads.find((item) => item.model_id === plan.model.model_id) @@ -846,7 +1012,7 @@ const CustomLocalModelSetup = ({ modelDownload?.status === "ready" } isPending={pending || modelDownload?.status === "downloading"} - onClick={() => void download()} + onClick={() => setConfirmDownload(true)} > {modelDownload?.status === "ready" ? @@ -855,6 +1021,14 @@ const CustomLocalModelSetup = ({ "Downloaded" : "Download model"} + setConfirmDownload(false)} + onConfirm={() => { + setConfirmDownload(false); + void download(); + }} + />
: null}
diff --git a/packages/webui/src/e2e/app.spec.ts b/packages/webui/src/e2e/app.spec.ts index 6095cd65..145ce5b8 100644 --- a/packages/webui/src/e2e/app.spec.ts +++ b/packages/webui/src/e2e/app.spec.ts @@ -482,7 +482,7 @@ const installGatewayRoutes = async (page: Page): Promise => { await page.route("**/settings/models/artifacts", (route) => json(route, { schema_version: "heartwood.local-model-catalog.v1", - snapshot_schema_version: "heartwood.model-snapshot-catalog.v1", + snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [], snapshots: [], models: [], @@ -515,9 +515,27 @@ const installGatewayRoutes = async (page: Page): Promise => { source_path: "research-model-q4_k_m.gguf", size_bytes: 4 * 1024 * 1024 * 1024, minimum_free_bytes: 4 * 1024 * 1024 * 1024, + license_id: "Apache-2.0", license_posture: "Source model card reports apache-2.0.", catalog_source: "user-selected", context_window: 32_768, + maximum_context_window: 32_768, + precision: "GGUF Q4_K_M", + tier: "standard", + qualification: "qualified", + minimum_gpu_count: 0, + minimum_gpu_memory_bytes: 0, + recommended_ram_bytes: 16 * 1024 * 1024 * 1024, + recommended_disk_bytes: 8 * 1024 * 1024 * 1024, + tool_call_parser: null, + tensor_parallel_size: 1, + startup_seconds_min: 5, + startup_seconds_max: 30, + download_policy: null, + allow_patterns: [], + ignore_patterns: [], + validated_platforms: ["ci"], + qualification_test: "synthetic-browser-e2e-v1", artifact_sha256: "a".repeat(64), minimum_resource_envelope: "Estimated minimum: 4 CPU cores and 12 GB RAM.", diff --git a/packages/webui/src/types.ts b/packages/webui/src/types.ts index 4c1dd787..76de395d 100644 --- a/packages/webui/src/types.ts +++ b/packages/webui/src/types.ts @@ -375,6 +375,9 @@ export interface ModelDownload { } export type LocalModelRuntime = "llama-cpp" | "vllm"; +export type LocalModelTier = "standard" | "powerful" | "maximum"; +export type LocalModelQualification = "candidate" | "qualified"; +export type ToolCallParser = "hermes" | "openai" | "qwen3_coder"; export interface LocalModelChoice { model_id: string; @@ -386,9 +389,27 @@ export interface LocalModelChoice { source_path: string | null; size_bytes: number; minimum_free_bytes: number; + license_id: string; license_posture: string; - catalog_source: "recommended" | "user-selected"; + catalog_source: "catalog" | "user-selected"; context_window: number; + maximum_context_window: number; + precision: string; + tier: LocalModelTier; + qualification: LocalModelQualification; + minimum_gpu_count: number; + minimum_gpu_memory_bytes: number; + recommended_ram_bytes: number; + recommended_disk_bytes: number; + tool_call_parser: ToolCallParser | null; + tensor_parallel_size: number; + startup_seconds_min: number; + startup_seconds_max: number; + download_policy: string | null; + allow_patterns: string[]; + ignore_patterns: string[]; + validated_platforms: string[]; + qualification_test: string | null; artifact_sha256: string | null; minimum_resource_envelope: string | null; recommended_resource_envelope: string | null; @@ -396,6 +417,16 @@ export interface LocalModelChoice { available: boolean; selected: boolean; availability_reason: string; + recommended: boolean; +} + +export interface GpuCapacity { + label: string; + gpu_model: string; + gpu_count: number; + gpu_memory_bytes: number; + allocation_required: boolean; + partition: string | null; } export interface ModelRepositoryPlan { @@ -435,9 +466,27 @@ export interface ModelSnapshot { source_revision: string; expected_size_bytes: number; minimum_free_bytes: number; + license_id: string; license_posture: string; model_alias: string; + precision: string; + tier: LocalModelTier; + qualification: LocalModelQualification; + minimum_gpu_count: number; + minimum_gpu_memory_bytes: number; + recommended_ram_bytes: number; + recommended_disk_bytes: number; context_window: number; + maximum_context_window: number; + tool_call_parser: ToolCallParser; + tensor_parallel_size: number; + startup_seconds_min: number; + startup_seconds_max: number; + download_policy: string; + allow_patterns: string[]; + ignore_patterns: string[]; + validated_platforms: string[]; + qualification_test: string | null; minimum_resource_envelope: string | null; recommended_resource_envelope: string | null; recommended: boolean; @@ -445,11 +494,15 @@ export interface ModelSnapshot { export interface ModelArtifacts { schema_version: "heartwood.local-model-catalog.v1"; - snapshot_schema_version: "heartwood.model-snapshot-catalog.v1"; + snapshot_schema_version: "heartwood.model-snapshot-catalog.v2"; artifacts: ModelArtifact[]; snapshots: ModelSnapshot[]; models: LocalModelChoice[]; downloads: ModelDownload[]; + gpu_environment: { + platform_id: string; + capacities: GpuCapacity[]; + }; } export interface SkillSummary { diff --git a/zensical.toml b/zensical.toml index c2869f11..527c19fc 100644 --- a/zensical.toml +++ b/zensical.toml @@ -58,6 +58,7 @@ nav = [ { "Command Reference" = "reference/cli.md" }, { "Configuration and State" = "reference/configuration.md" }, { "Platform Capabilities" = "reference/capabilities.md" }, + { "GPU Compatibility" = "reference/gpu-compatibility.md" }, { "Diagnostics and Troubleshooting" = "reference/troubleshooting.md" }, { "Glossary" = "reference/glossary.md" }, ] }, From f54e45c82368e3611b156834240267577d9f8dcb Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 01:57:55 -0700 Subject: [PATCH 02/38] Address Pull Request Review Feedback --- .../scripts/verify_coding_agent_e2e.py | 6 ++- packages/cli/src/heartwood/cli/__init__.py | 2 +- packages/cli/tests/test_cli.py | 9 ++++ .../tests/test_coding_agent_qualification.py | 52 +++++++++++++++++++ .../src/heartwood/gateway/_project_config.py | 3 +- packages/gateway/tests/test_project_config.py | 2 + 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/images/generic/scripts/verify_coding_agent_e2e.py b/images/generic/scripts/verify_coding_agent_e2e.py index cfd86ebd..06a299e4 100644 --- a/images/generic/scripts/verify_coding_agent_e2e.py +++ b/images/generic/scripts/verify_coding_agent_e2e.py @@ -90,7 +90,9 @@ def verify_run( raise ValueError("coding-agent session must have one to three tool executions") if not 1 <= len(terminal_executions) <= 3: raise ValueError("coding-agent session must execute the terminal tool") - if any(event.payload.get("exit_code") != 0 for event in tool_executions): + if any(not isinstance(event.payload.get("exit_code"), int) for event in tool_executions): + raise ValueError("coding-agent tool execution has no valid exit code") + if any(event.payload["exit_code"] != 0 for event in tool_executions): raise ValueError("coding-agent tool execution failed") routes = [ @@ -145,7 +147,7 @@ def verify_run( raise ValueError("audit export and replay event kinds disagree") audit_text = audit_path.read_text(encoding="utf-8") for sensitive_value in ( - str(artifact_path.parent), + str(artifact_path.resolve().parent), "target-condition-concept-id", "Call the terminal tool", ): diff --git a/packages/cli/src/heartwood/cli/__init__.py b/packages/cli/src/heartwood/cli/__init__.py index c57fa7cb..0d05efde 100644 --- a/packages/cli/src/heartwood/cli/__init__.py +++ b/packages/cli/src/heartwood/cli/__init__.py @@ -432,7 +432,7 @@ def _main(argv: Sequence[str] | None = None) -> int: if args.cpus is not None and args.cpus < 1: parser.error("--cpus must be positive") if args.startup_timeout < 1 or args.port < 1: - parser.error("--cpus, --startup-timeout, and --port must be positive") + parser.error("--startup-timeout and --port must be positive") return run_launch(_launch_options(project, args)) configured_gateway: SessionGateway | None = None if args.command is None: diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index f3cdddd1..a4287467 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -866,6 +866,15 @@ def test_invalid_session_and_launch_resources_are_argument_errors( assert invalid_resources.value.code == 2 assert "--gpus must be positive" in capsys.readouterr().err + with pytest.raises(SystemExit) as invalid_timeout: + _run( + tmp_path / "timeout", + monkeypatch, + ["runtime", "start", "--startup-timeout", "0"], + ) + assert invalid_timeout.value.code == 2 + assert "--startup-timeout and --port must be positive" in capsys.readouterr().err + @pytest.mark.parametrize( "args", diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 2de70a9e..79564fde 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -159,6 +159,58 @@ def test_coding_agent_qualification_rejects_incomplete_replay(tmp_path: Path) -> ) +def test_coding_agent_qualification_requires_explicit_tool_exit_code( + tmp_path: Path, +) -> None: + module = _module( + "verify_coding_agent_e2e_exit_code", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + payloads = [json.loads(line) for line in events.read_text(encoding="utf-8").splitlines()] + execution = next( + payload for payload in payloads if payload["kind"] == "tool.execution.recorded" + ) + execution["payload"].pop("exit_code") + events.write_text( + "".join(json.dumps(payload) + "\n" for payload in payloads), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="no valid exit code"): + verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) + + +def test_coding_agent_qualification_accepts_relative_artifact_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + module = _module( + "verify_coding_agent_e2e_relative_artifact", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + monkeypatch.chdir(tmp_path) + + summary = verify( + events_path=events, + audit_path=audit, + artifact_path=Path(artifact.name), + replay_path=replay, + inference_path=inference, + ) + + assert summary["tool_execution_count"] == 1 + + def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: module = _module( "gpu_qualification_config", diff --git a/packages/gateway/src/heartwood/gateway/_project_config.py b/packages/gateway/src/heartwood/gateway/_project_config.py index 493f2738..44d1231a 100644 --- a/packages/gateway/src/heartwood/gateway/_project_config.py +++ b/packages/gateway/src/heartwood/gateway/_project_config.py @@ -713,8 +713,7 @@ def _local_model_from_mapping(value: object) -> LocalModelSelection: value.get("validated_platforms"), "validated_platforms" ), qualification_test=_optional_string(value.get("qualification_test"), "qualification_test"), - catalog_source=_optional_string(value.get("catalog_source"), "catalog_source") - or "recommended", + catalog_source=_optional_string(value.get("catalog_source"), "catalog_source") or "catalog", ) diff --git a/packages/gateway/tests/test_project_config.py b/packages/gateway/tests/test_project_config.py index eae58e59..b02d8649 100644 --- a/packages/gateway/tests/test_project_config.py +++ b/packages/gateway/tests/test_project_config.py @@ -657,6 +657,7 @@ def test_project_config_parser_defaults_optional_local_model_collections(tmp_pat "allow_patterns", "ignore_patterns", "validated_platforms", + "catalog_source", ): local_model.pop(field, None) @@ -665,6 +666,7 @@ def test_project_config_parser_defaults_optional_local_model_collections(tmp_pat assert parsed.local_model is not None assert parsed.local_model.minimum_gpu_count == 0 assert parsed.local_model.allow_patterns == () + assert parsed.local_model.catalog_source == "catalog" def test_project_config_parser_rejects_non_platform_connection(tmp_path: Path) -> None: From fd28aa52209f883d82fcbf86a5643efbf1c32c1d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 08:22:32 -0700 Subject: [PATCH 03/38] Enable Commit-Scoped GPU Candidate Testing --- .github/workflows/gpu-container-image.yml | 9 ++++++++- packages/compliance/tests/test_container_assets.py | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index 1eb1c1c5..bf130a50 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -10,6 +10,11 @@ on: workflow_call: workflow_dispatch: inputs: + publish_commit_candidate: + description: Publish validated immutable GPU tags for this commit without moving channel tags + required: true + default: false + type: boolean run_gpu_qualification: description: Run the coding-agent acceptance suite on a protected self-hosted GPU runner required: true @@ -220,7 +225,9 @@ jobs: build: name: Build And Validate ${{ matrix.target }} - if: github.ref == 'refs/heads/main' + if: >- + github.ref == 'refs/heads/main' + || (github.event_name == 'workflow_dispatch' && inputs.publish_commit_candidate) needs: contract runs-on: ubuntu-24.04 permissions: diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 602ff62f..0881fdc4 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -636,6 +636,11 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: assert "attest=type=sbom,disabled=true" in pull_request_build assert "attest=type=provenance,disabled=true" in pull_request_build assert "Promote GPU Channel Tags" in workflow + assert "publish_commit_candidate:" in workflow + assert "Publish validated immutable GPU tags for this commit" in workflow + assert "github.event_name == 'workflow_dispatch' && inputs.publish_commit_candidate" in ( + main_build + ) assert "if: github.ref == 'refs/heads/main'" in workflow assert "push-by-digest=true" in workflow assert 'BUILDX_NO_DEFAULT_ATTESTATIONS: "1"' in workflow @@ -650,6 +655,9 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: assert "terra_image_smoke.sh" in main_build assert "offline_stack_smoke.sh" in main_build assert "immutable GPU commit tag does not match" in workflow + assert "sha-${GIT_SHA}-${COMMIT_SUFFIX}" in main_build + assert "edge-gpu-nvidia" not in main_build + assert "edge-terra-gpu-nvidia" not in main_build assert "refusing to move GPU channel tags from a stale main workflow" in workflow assert "promoted ${channel} digest does not match" in workflow assert "allow-ghsas: GHSA-w8v5-vhqr-4h9v, GHSA-rrmf-rvhw-rf47" in dependency_review From 1ede85e5a6e21ab6582f2c08f4dca219528468ec Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 10:38:09 -0700 Subject: [PATCH 04/38] Make T4 GPU Inference Memory-Safe --- .github/workflows/gpu-container-image.yml | 12 ++--- documentation/platforms/terra.md | 10 +++- documentation/reference/gpu-compatibility.md | 16 +++--- images/gpu/coding_agent_e2e.sh | 2 + images/gpu/compatibility.toml | 3 +- images/gpu/start_vllm.sh | 27 +++++++--- packages/cli/src/heartwood/cli/_launch.py | 26 ++++++++- packages/cli/tests/test_launch.py | 54 +++++++++++++++++++ .../tests/test_coding_agent_qualification.py | 2 + .../compliance/tests/test_container_assets.py | 18 +++++++ 10 files changed, 144 insertions(+), 26 deletions(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index bf130a50..9e2cdcd1 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -20,9 +20,9 @@ on: required: true default: false type: boolean - configuration_id: - description: Reviewed compatibility-matrix configuration - required: true + qualification_configuration: + description: Optional compatibility-matrix row used only when GPU qualification is enabled + required: false default: terra-t4-qwen25-coder-7b-awq type: choice options: @@ -135,7 +135,7 @@ jobs: - name: Resolve qualification model id: model env: - CONFIGURATION_ID: ${{ inputs.configuration_id }} + CONFIGURATION_ID: ${{ inputs.qualification_configuration }} run: | payload="$(python3 images/gpu/qualification_config.py "${CONFIGURATION_ID}")" echo "snapshot=$(jq -r '.configuration.model_snapshot' <<<"${payload}")" >> "${GITHUB_OUTPUT}" @@ -150,7 +150,7 @@ jobs: heartwood models download "${SNAPSHOT}" - name: Run GPU coding-agent qualification without network env: - CONFIGURATION_ID: ${{ inputs.configuration_id }} + CONFIGURATION_ID: ${{ inputs.qualification_configuration }} PLATFORM_ID: ${{ steps.model.outputs.platform }} SNAPSHOT: ${{ steps.model.outputs.snapshot }} run: >- @@ -176,7 +176,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: gpu-qualification-${{ inputs.configuration_id }}-${{ github.sha }} + name: gpu-qualification-${{ inputs.qualification_configuration }}-${{ github.sha }} path: | ${{ runner.temp }}/heartwood-gpu-project/heartwood-qualification.json ${{ runner.temp }}/heartwood-gpu-project/heartwood-replay.txt diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 92b3fa7b..07ae4d60 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -33,19 +33,24 @@ Enter one image: | Model Route | Image | Practical Starting Point | |---|---|---| | Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | -| Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 52 GB RAM, 75 GB persistent disk | -| Heartwood-managed NVIDIA GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 8 CPUs, 32 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | +| Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | +| Heartwood-managed NVIDIA GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. CPU inference is portable but can be too slow for an interactive coding workflow. These are starting points rather than universal requirements. +Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. +The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. The GPU path is designed around a T4 and the release-pinned Qwen2.5 Coder 7B AWQ configuration. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. +For the first model download and startup, set auto-pause to at least 120 minutes; image creation, model verification, and inference startup can each take several minutes without terminal output from the model itself. +After setup is complete, shorten auto-pause to match the normal research workflow. Heartwood inspects model size and available memory before launch, chooses a context capacity with response headroom, and warns when the selected compute is below its conservative estimate. +On a 16 GB T4, Heartwood uses eager vLLM execution to avoid the additional GPU-memory peak from CUDA graph capture. Larger GPU memory can enable context capacities above 32K when the model supports them, but increasing context also increases GPU-memory use and response latency. See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for download and resource estimates and [GPU Compatibility](../reference/gpu-compatibility.md) for exact runtime combinations. @@ -105,6 +110,7 @@ Heartwood inspects its metadata and reports a clear unsupported-model error when The pinned AWQ snapshot downloads about 5.2 GiB; allow at least 16 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. Model download progress appears in the terminal and files persist under `.heartwood/models/`. +Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. Heartwood reports the active stage, elapsed time, selected context capacity, and memory assessment while you wait. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index ed14d209..4c936937 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -31,14 +31,14 @@ The exact driver used in a live qualification is recorded with its machine-reada ## Model Configurations -| Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Tensor Parallelism | Tool Parser | Status | -|---|---|---|---|---|---:|---:|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | 1 | `hermes` | Candidate | -| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | 1 | `hermes` | Candidate | -| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | 1 | `qwen3_coder` | Candidate | -| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | 2 | `qwen3_coder` | Candidate | -| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | 4 | `qwen3_coder` | Candidate | -| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | 2 | `openai` | Candidate | +| Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Tool Parser | Status | +|---|---|---|---|---|---:|---|---:|---|---| +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | Candidate | +| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | Candidate | +| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | Candidate | +| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | Candidate | +| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | Candidate | +| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | Candidate | All listed model repositories declare the Apache-2.0 license at the pinned revision. Confirm that a model's license and intended use remain suitable for the project before downloading it. diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh index 878edcd5..236b7e3c 100755 --- a/images/gpu/coding_agent_e2e.sh +++ b/images/gpu/coding_agent_e2e.sh @@ -34,6 +34,7 @@ revision="$(jq -er '.configuration.model_revision' <<<"${configuration}")" context="$(jq -er '.configuration.context_window' <<<"${configuration}")" tensor_parallel="$(jq -er '.configuration.tensor_parallel_size' <<<"${configuration}")" tool_parser="$(jq -er '.configuration.tool_call_parser' <<<"${configuration}")" +enforce_eager="$(jq -er 'if .configuration.enforce_eager then "1" else "0" end' <<<"${configuration}")" startup_min="$(jq -er '.configuration.startup_seconds_min' <<<"${configuration}")" startup_max="$(jq -er '.configuration.startup_seconds_max' <<<"${configuration}")" @@ -113,6 +114,7 @@ export HEARTWOOD_LOCAL_MODEL_CONTEXT="${context}" export HEARTWOOD_LOCAL_RUNTIME_PORT="${runtime_port}" export HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE="${tensor_parallel}" export HEARTWOOD_VLLM_TOOL_PARSER="${tool_parser}" +export HEARTWOOD_VLLM_ENFORCE_EAGER="${enforce_eager}" export HEARTWOOD_RUNTIME_ROOT="${runtime_root}" export HEARTWOOD_QUALIFICATION_MODEL_REPOSITORY="${repository}" export HEARTWOOD_QUALIFICATION_MODEL_REVISION="${revision}" diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index f46db308..6fd50ae5 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -27,9 +27,10 @@ model_snapshot = "qwen25-coder-7b-instruct-awq-vllm" model_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" model_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" precision = "AWQ int4" -context_window = 32768 +context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" +enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" diff --git a/images/gpu/start_vllm.sh b/images/gpu/start_vllm.sh index 898a1d8c..15e59bfe 100755 --- a/images/gpu/start_vllm.sh +++ b/images/gpu/start_vllm.sh @@ -15,6 +15,7 @@ tool_parser="${HEARTWOOD_VLLM_TOOL_PARSER:-hermes}" context="${HEARTWOOD_LOCAL_MODEL_CONTEXT:-32768}" tensor_parallel_size="${HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE:-1}" gpu_memory_utilization="${HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION:-0.90}" +enforce_eager="${HEARTWOOD_VLLM_ENFORCE_EAGER:-0}" vllm="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" if [[ "${host}" != "127.0.0.1" && "${host}" != "localhost" && "${host}" != "::1" ]]; then @@ -30,12 +31,22 @@ if [[ ! -x "${vllm}" ]]; then exit 69 fi -exec "${vllm}" serve "${model_path}" \ - --host "${host}" \ - --port "${port}" \ - --served-model-name "${alias}" \ - --max-model-len "${context}" \ - --tensor-parallel-size "${tensor_parallel_size}" \ - --gpu-memory-utilization "${gpu_memory_utilization}" \ - --enable-auto-tool-choice \ +arguments=( + serve "${model_path}" + --host "${host}" + --port "${port}" + --served-model-name "${alias}" + --max-model-len "${context}" + --tensor-parallel-size "${tensor_parallel_size}" + --gpu-memory-utilization "${gpu_memory_utilization}" + --enable-auto-tool-choice --tool-call-parser "${tool_parser}" +) +if [[ "${enforce_eager}" == "1" ]]; then + arguments+=(--enforce-eager) +elif [[ "${enforce_eager}" != "0" ]]; then + echo "HEARTWOOD_VLLM_ENFORCE_EAGER must be 0 or 1" >&2 + exit 64 +fi + +exec "${vllm}" "${arguments[@]}" diff --git a/packages/cli/src/heartwood/cli/_launch.py b/packages/cli/src/heartwood/cli/_launch.py index 4ff638b9..767209e6 100644 --- a/packages/cli/src/heartwood/cli/_launch.py +++ b/packages/cli/src/heartwood/cli/_launch.py @@ -44,6 +44,7 @@ InputFunction = Callable[[str], str] RunFunction = Callable[[Sequence[str]], int] +_VLLM_EAGER_MEMORY_PER_GPU = 20 * 1024**3 _SLURM_EXPORTED_ENVIRONMENT = ( "PATH", "HOME", @@ -614,6 +615,7 @@ def _run_runtime(options: LaunchOptions, env: Mapping[str, str]) -> int: runtime_executable, staged_source, selection, + enforce_eager=_use_eager_vllm(selection, runtime_env), ), stdout=log_file, stderr=subprocess.STDOUT, @@ -739,6 +741,8 @@ def _runtime_command( executable: Path, model: Path, selection: LocalRuntimeSelection, + *, + enforce_eager: bool = False, ) -> tuple[str, ...]: if selection.runtime == "llama-cpp": model_file = _gguf_file(model) @@ -758,7 +762,7 @@ def _runtime_command( ) if selection.tool_call_parser is None: # pragma: no cover - persisted invariant raise LaunchConfigurationError("the selected vLLM model has no tool-call parser") - return ( + command = ( str(executable), "serve", str(model), @@ -776,6 +780,21 @@ def _runtime_command( "--tool-call-parser", selection.tool_call_parser, ) + return (*command, "--enforce-eager") if enforce_eager else command + + +def _use_eager_vllm( + selection: LocalRuntimeSelection, + env: Mapping[str, str], +) -> bool: + """Avoid CUDA graph capture overhead on constrained NVIDIA GPUs.""" + if selection.runtime != "vllm": + return False + available = _available_gpu_memory_bytes(env, count=selection.tensor_parallel_size) + if available is None: + return False + per_device_available = available // selection.tensor_parallel_size + return per_device_available <= _VLLM_EAGER_MEMORY_PER_GPU def _local_model_selection( @@ -876,6 +895,11 @@ def _print_resource_assessment( count=selection.tensor_parallel_size, ) _print_memory_result("GPU memory", gpu_required, gpu_available) + if _use_eager_vllm(selection, env): + print( + "Runtime mode: eager execution selected to avoid CUDA graph capture " + "overhead on the available GPU memory." + ) def _context_plan( diff --git a/packages/cli/tests/test_launch.py b/packages/cli/tests/test_launch.py index cda77296..b39c5f09 100644 --- a/packages/cli/tests/test_launch.py +++ b/packages/cli/tests/test_launch.py @@ -43,6 +43,7 @@ _runtime_command, _runtime_environment, _stage_model, + _use_eager_vllm, _verify_local_model, _wait_for_runtime, build_launch_plan, @@ -774,6 +775,59 @@ def test_resource_assessment_reports_context_and_memory_status( assert "model capacity: 32,768" in output assert "Warning: RAM may be insufficient" in output assert "GPU memory available; estimated minimum" in output + assert "Runtime mode: eager execution selected" not in output + + +def test_constrained_vllm_gpu_uses_eager_execution( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + selection = LocalRuntimeSelection( + artifact_id="test-model", + model_root=tmp_path / "model", + runtime="vllm", + model_id="test-model", + size_bytes=5 * 1024**3, + artifact_sha256=None, + context_window=18_432, + maximum_context_window=32_768, + tier="standard", + precision="AWQ int4", + qualification="candidate", + minimum_gpu_count=1, + minimum_gpu_memory_bytes=15_000_000_000, + recommended_ram_bytes=32 * 1024**3, + recommended_disk_bytes=16 * 1024**3, + tool_call_parser="hermes", + tensor_parallel_size=1, + startup_seconds_min=120, + startup_seconds_max=480, + catalog_source="catalog", + ) + monkeypatch.setattr( + "heartwood.cli._launch._available_gpu_memory_bytes", + lambda _env, **_kwargs: 16 * 1024**3, + ) + + assert _use_eager_vllm(selection, {}) + _print_resource_assessment(selection, {}) + assert "Runtime mode: eager execution selected" in capsys.readouterr().out + assert ( + _runtime_command( + Path("/opt/heartwood-vllm/bin/heartwood-vllm"), + selection.model_root, + selection, + enforce_eager=True, + )[-1] + == "--enforce-eager" + ) + + monkeypatch.setattr( + "heartwood.cli._launch._available_gpu_memory_bytes", + lambda _env, **_kwargs: 48 * 1024**3, + ) + assert not _use_eager_vllm(selection, {}) def test_available_system_memory_honors_cgroup_v1_limit( diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 79564fde..16520a73 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -225,6 +225,8 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "hermes" + assert resolved["configuration"]["context_window"] == 18_432 + assert resolved["configuration"]["enforce_eager"] is True assert resolved["configuration"]["model_revision"] == ( "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" ) diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 0881fdc4..95630659 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -437,6 +437,16 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert values[values.index("--served-model-name") + 1] == "test-model" assert values[values.index("--tool-call-parser") + 1] == "hermes" + env["HEARTWOOD_VLLM_ENFORCE_EAGER"] = "1" + eager = subprocess.run(["bash", str(script)], env=env, check=False) + assert eager.returncode == 0 + assert arguments.read_text(encoding="utf-8").splitlines()[-1] == "--enforce-eager" + + env["HEARTWOOD_VLLM_ENFORCE_EAGER"] = "invalid" + invalid = subprocess.run(["bash", str(script)], env=env, check=False) + assert invalid.returncode == 64 + + env.pop("HEARTWOOD_VLLM_ENFORCE_EAGER") env["HEARTWOOD_LOCAL_RUNTIME_HOST"] = "0.0.0.0" denied = subprocess.run(["bash", str(script)], env=env, check=False) assert denied.returncode == 64 @@ -622,6 +632,9 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: pull_request_build = workflow.split(" pull-request-build:\n", maxsplit=1)[1].split( "\n build:\n", maxsplit=1 )[0] + qualification = workflow.split(" gpu-qualification:\n", maxsplit=1)[1].split( + "\n pull-request-build:\n", maxsplit=1 + )[0] main_build = workflow.split(" build:\n", maxsplit=1)[1].split("\n promote:\n", maxsplit=1)[0] assert "runtime-gpu-nvidia" in workflow @@ -638,6 +651,11 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: assert "Promote GPU Channel Tags" in workflow assert "publish_commit_candidate:" in workflow assert "Publish validated immutable GPU tags for this commit" in workflow + assert "qualification_configuration:" in workflow + assert "used only when GPU qualification is enabled" in workflow + assert "inputs.qualification_configuration" in qualification + assert "inputs.qualification_configuration" not in pull_request_build + assert "inputs.qualification_configuration" not in main_build assert "github.event_name == 'workflow_dispatch' && inputs.publish_commit_candidate" in ( main_build ) From 027b712aee95b8e15c00258293780b3a997a6f8f Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 10:40:26 -0700 Subject: [PATCH 05/38] Allow Platform-Bounded GPU Contexts --- deploy/verify_gpu_compatibility.py | 12 ++++++- .../tests/test_coding_agent_qualification.py | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index 7d7f2e3d..2322c495 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -17,6 +17,7 @@ _SCHEMA = "heartwood.gpu-compatibility.v1" _CATALOG_SCHEMA = "heartwood.model-snapshot-catalog.v2" _QUALIFICATION_TEST = "heartwood.coding-agent-e2e.v1" +_MINIMUM_AGENT_CONTEXT_WINDOW = 18_432 _CONFIGURATION_FIELDS = { "configuration_id", "status", @@ -153,7 +154,6 @@ def _verify_configuration( "model_repository": "source_repository", "model_revision": "source_revision", "precision": "precision", - "context_window": "context_window", "tensor_parallel_size": "tensor_parallel_size", "tool_call_parser": "tool_call_parser", "startup_seconds_min": "startup_seconds_min", @@ -189,6 +189,16 @@ def _verify_configuration( raise CompatibilityError(f"GPU matrix field {field} must be positive") if configuration["startup_seconds_min"] > configuration["startup_seconds_max"]: raise CompatibilityError("GPU startup estimate is invalid") + context_window = configuration["context_window"] + maximum_context_window = snapshot.get("maximum_context_window") + if ( + not isinstance(maximum_context_window, int) + or isinstance(maximum_context_window, bool) + or not _MINIMUM_AGENT_CONTEXT_WINDOW <= context_window <= maximum_context_window + ): + raise CompatibilityError( + "GPU matrix context must be agent-capable and within model capacity" + ) if configuration["gpu_count"] < snapshot.get("minimum_gpu_count", 0): raise CompatibilityError("GPU matrix does not satisfy the model GPU count") if configuration["minimum_gpu_memory_bytes"] < snapshot.get("minimum_gpu_memory_bytes", 0): diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 16520a73..b9822c35 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -10,6 +10,7 @@ import importlib.util import json +import tomllib from collections.abc import Callable from pathlib import Path from types import ModuleType @@ -230,3 +231,33 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["configuration"]["model_revision"] == ( "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" ) + + +def test_gpu_qualification_context_can_be_bounded_by_platform_memory() -> None: + verifier = _module( + "gpu_compatibility_verifier", + _root() / "deploy/verify_gpu_compatibility.py", + ) + loader = _module( + "gpu_qualification_config_bounded_context", + _root() / "images/gpu/qualification_config.py", + ) + resolved = loader.load_configuration( + _root() / "images/gpu/compatibility.toml", + "terra-t4-qwen25-coder-7b-awq", + ) + with (_root() / "images/generic/local-runtime/snapshots.toml").open("rb") as file: + snapshot = tomllib.load(file)["snapshots"][resolved["configuration"]["model_snapshot"]] + + verifier._verify_configuration( + resolved["configuration"], + snapshot, + resolved["runtime"], + ) + + invalid = { + **resolved["configuration"], + "context_window": snapshot["maximum_context_window"] + 1, + } + with pytest.raises(verifier.CompatibilityError, match="within model capacity"): + verifier._verify_configuration(invalid, snapshot, resolved["runtime"]) From 7fc481bb1feee8258b0ef380b66e6a218cc34a24 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 10:44:43 -0700 Subject: [PATCH 06/38] Cache GPU Environment Discovery --- documentation/reference/glossary.md | 12 ++++---- .../gateway/src/heartwood/gateway/_gateway.py | 10 +++++-- .../gateway/tests/test_gateway_contract.py | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/documentation/reference/glossary.md b/documentation/reference/glossary.md index 3ef65312..6acd5d8d 100644 --- a/documentation/reference/glossary.md +++ b/documentation/reference/glossary.md @@ -27,12 +27,12 @@ SPDX-License-Identifier: MIT **Context window** : The token budget available to a model for instructions, conversation history, project content, tool results, and output. -**CUDA** -: NVIDIA's software platform and application binary interface for GPU computing. - **Credential binding** : A non-secret identifier that tells Heartwood where an authorized process or platform supplies a provider credential. +**CUDA** +: NVIDIA's software platform and application binary interface for GPU computing. + **GGUF** : A single-file model format commonly used with llama.cpp and quantized CPU inference. @@ -72,11 +72,11 @@ SPDX-License-Identifier: MIT **Slurm** : A scheduler that allocates compute resources for jobs on platforms such as Stanford Carina. -**Terra** -: A cloud platform for biomedical research workspaces, data, workflows, and interactive Jupyter compute. - **Tensor parallelism** : A runtime layout that divides one model across a fixed number of GPUs. +**Terra** +: A cloud platform for biomedical research workspaces, data, workflows, and interactive Jupyter compute. + **vLLM** : A GPU-oriented inference server used by supported Heartwood NVIDIA deployments. diff --git a/packages/gateway/src/heartwood/gateway/_gateway.py b/packages/gateway/src/heartwood/gateway/_gateway.py index 4784aa4c..676f6daf 100644 --- a/packages/gateway/src/heartwood/gateway/_gateway.py +++ b/packages/gateway/src/heartwood/gateway/_gateway.py @@ -264,6 +264,7 @@ def __init__( self.env = dict(os.environ if env is None else env) self.backend_id = backend_id self._state_lock: AbstractContextManager[object] = RLock() + self._gpu_environment: GpuEnvironment | None = None adapter = select_platform_adapter(self.env) self.config_store = ProjectConfigStore( self.project, @@ -840,9 +841,14 @@ def model_artifacts(self) -> dict[str, object]: }, } - def gpu_environment(self) -> GpuEnvironment: + def gpu_environment(self, *, refresh: bool = False) -> GpuEnvironment: """Return the shared GPU and scheduler inventory for this deployment.""" - return inspect_gpu_environment(self.config_store.load().platform_id, self.env) + if refresh or self._gpu_environment is None: + self._gpu_environment = inspect_gpu_environment( + self.config_store.load().platform_id, + self.env, + ) + return self._gpu_environment def recommend_managed_model( self, diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index 8f6f9524..df572006 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -728,6 +728,34 @@ def test_rest_reserves_the_heartwood_managed_profile(tmp_path: Path) -> None: assert "managed by Heartwood" in str(removed.body["error"]) +def test_gpu_environment_is_cached_and_can_be_refreshed( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = 0 + + def inspect(platform_id: str, _env: Mapping[str, str]) -> GpuEnvironment: + nonlocal calls + calls += 1 + return GpuEnvironment( + platform_id=platform_id, + visible_devices=(), + slurm_partitions=(), + capacities=(), + ) + + monkeypatch.setattr("heartwood.gateway._gateway.inspect_gpu_environment", inspect) + gateway = _gateway(tmp_path) + + first = gateway.gpu_environment() + second = gateway.gpu_environment() + refreshed = gateway.gpu_environment(refresh=True) + + assert first is second + assert refreshed is not first + assert calls == 2 + + def test_local_model_availability_reflects_installed_runtime_executables( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, From 1a8c5c51c04d23cf6d0273684100cdb7e8a037d8 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 10:49:44 -0700 Subject: [PATCH 07/38] Validate GPU Execution Mode Metadata --- deploy/verify_gpu_compatibility.py | 3 +++ packages/compliance/tests/test_coding_agent_qualification.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index 2322c495..04837649 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -189,6 +189,9 @@ def _verify_configuration( raise CompatibilityError(f"GPU matrix field {field} must be positive") if configuration["startup_seconds_min"] > configuration["startup_seconds_max"]: raise CompatibilityError("GPU startup estimate is invalid") + enforce_eager = configuration.get("enforce_eager", False) + if not isinstance(enforce_eager, bool): + raise CompatibilityError("GPU matrix field enforce_eager must be a boolean") context_window = configuration["context_window"] maximum_context_window = snapshot.get("maximum_context_window") if ( diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index b9822c35..d244d560 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -261,3 +261,7 @@ def test_gpu_qualification_context_can_be_bounded_by_platform_memory() -> None: } with pytest.raises(verifier.CompatibilityError, match="within model capacity"): verifier._verify_configuration(invalid, snapshot, resolved["runtime"]) + + invalid_eager = {**resolved["configuration"], "enforce_eager": "true"} + with pytest.raises(verifier.CompatibilityError, match="must be a boolean"): + verifier._verify_configuration(invalid_eager, snapshot, resolved["runtime"]) From ff11c8249a8f6b1047baf7d03a427b406791bc05 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 12:23:58 -0700 Subject: [PATCH 08/38] Harden Terra GPU Qualification --- documentation/platforms/terra.md | 14 +++++++++++-- images/generic/scripts/coding_agent_e2e.sh | 21 ++++++++++++++----- images/gpu/coding_agent_e2e.sh | 15 ++++++++----- .../compliance/tests/test_container_assets.py | 18 ++++++++++++++++ 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 07ae4d60..50f6ae83 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -27,8 +27,17 @@ Terra does not apply those changes to an existing environment; create a new envi Deleting the compute environment and deleting its persistent disk are separate choices, so retain the disk when it contains project files you still need. See Terra's [GPU Cloud Environment guide](https://support.terra.bio/hc/en-us/articles/4403006001947-Getting-started-with-GPUs-in-a-Cloud-Environment). -Open the workspace's Jupyter Cloud Environment settings, select **Customize**, and choose **Custom Environment** under application configuration. -Enter one image: +Open the workspace's Jupyter Cloud Environment settings and configure the environment in this order: + +1. Select **Customize**, then choose **Custom Environment** under application configuration. +2. Select the CPU and memory combination from the table below. +3. Enter the corresponding container image. +4. Enable the GPU, when required, and verify the GPU type and count. +5. Set auto-pause and review every value before selecting **Create**. + +Terra can reset the image or GPU selection when the CPU choice changes, so set compute resources first and verify the complete form before creation. + +Use one of these combinations: | Model Route | Image | Practical Starting Point | |---|---|---| @@ -162,6 +171,7 @@ Deleting the persistent disk removes `.heartwood/` and project files stored only ## Troubleshooting Terra - If Jupyter returns **404**, open Jupyter from the Terra workspace rather than using a guessed host path. +- If creation fails with `ZONE_RESOURCE_POOL_EXHAUSTED`, the requested Google Cloud resources are temporarily unavailable in Terra's default zone; the container has not started. Delete only the failed Cloud Environment, retain its persistent disk, and retry later. Terra also documents an [advanced Swagger API procedure](https://support.terra.bio/hc/en-us/articles/4403307463067-How-to-create-a-custom-Cloud-Environment-with-the-Swagger-API) for selecting another zone; use it only when you are comfortable creating and tracking a Cloud Environment outside the standard form. - If a Heartwood browser URL returns **401** or **404**, use the terminal or notebook interface; browser access is not supported on Terra. - If `import heartwood` fails in a notebook, switch the notebook kernel to **Python 3 (Heartwood)** and restart the kernel. - If a model download stops, rerun Heartwood from the same project; verified files in `.heartwood/models/` are reused. diff --git a/images/generic/scripts/coding_agent_e2e.sh b/images/generic/scripts/coding_agent_e2e.sh index 41f331ce..357168b1 100755 --- a/images/generic/scripts/coding_agent_e2e.sh +++ b/images/generic/scripts/coding_agent_e2e.sh @@ -33,19 +33,30 @@ esac script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" runtime_root="${HEARTWOOD_RUNTIME_ROOT:-$(cd "${script_dir}/../../.." && pwd)}" +heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}" +heartwood_cli="${HEARTWOOD_CLI:-${runtime_root}/.venv/bin/heartwood}" state_root="${project}/.heartwood" workspace="${state_root}/sessions" session_id="${HEARTWOOD_SESSION_ID:-session-capable-model}" transcript="${HEARTWOOD_TRANSCRIPT:-${project}/heartwood-transcript.txt}" replay="${HEARTWOOD_REPLAY_TRANSCRIPT:-${project}/heartwood-replay.txt}" report="${HEARTWOOD_QUALIFICATION_REPORT:-${project}/heartwood-qualification.json}" -inference="${state_root}/qualification-inference.json" +inference="${project}/qualification-inference.json" command_timeout="${HEARTWOOD_COMMAND_TIMEOUT:-900}" runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" cohort_path="${project}/cohort-summary.json" events_path="${workspace}/${session_id}/events.jsonl" audit_path="${state_root}/audit-export.jsonl" +if [[ ! -x "${heartwood_python}" ]]; then + echo "Heartwood Python is unavailable: ${heartwood_python}" >&2 + exit 69 +fi +if [[ ! -x "${heartwood_cli}" ]]; then + echo "Heartwood CLI is unavailable: ${heartwood_cli}" >&2 + exit 69 +fi + export HEARTWOOD_SESSION_ID="${session_id}" export HEARTWOOD_LOCAL_MODEL_PATH="${model_path}" export HEARTWOOD_MANAGED_MODEL_ALIAS="${HEARTWOOD_MANAGED_MODEL_ALIAS:-heartwood-managed-runtime}" @@ -61,7 +72,7 @@ cp "${runtime_root}/fixtures/synthetic/omop-like/"*.csv "${project}/input/" cd "${project}" echo "Checking direct model inference..." -python - "${inference}" <<'PY' +"${heartwood_python}" - "${inference}" <<'PY' import json import os import sys @@ -92,7 +103,7 @@ with open(sys.argv[1], "w", encoding="utf-8") as file: PY run_heartwood() { - timeout "${command_timeout}" heartwood "$@" + timeout "${command_timeout}" "${heartwood_cli}" "$@" } run_heartwood models refresh heartwood | tee -a "${transcript}" @@ -104,7 +115,7 @@ run_heartwood --session-id "${session_id}" \ | tee -a "${transcript}" for _ in 1 2 3 4; do - pending_id="$(python - "${events_path}" <<'PY' + pending_id="$("${heartwood_python}" - "${events_path}" <<'PY' import json import sys from pathlib import Path @@ -134,7 +145,7 @@ run_heartwood --session-id "${session_id}" replay | tee "${replay}" run_heartwood --session-id "${session_id}" audit export \ --output "${audit_path}" | tee -a "${transcript}" -python "${script_dir}/verify_coding_agent_e2e.py" \ +"${heartwood_python}" "${script_dir}/verify_coding_agent_e2e.py" \ --events "${events_path}" \ --audit "${audit_path}" \ --artifact "${cohort_path}" \ diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh index 236b7e3c..7c69b77d 100755 --- a/images/gpu/coding_agent_e2e.sh +++ b/images/gpu/coding_agent_e2e.sh @@ -15,6 +15,7 @@ runtime_root="${HEARTWOOD_RUNTIME_ROOT:-$(cd "${script_dir}/../.." && pwd)}" runtime_log="${HEARTWOOD_RUNTIME_LOG:-${project}/vllm.log}" runtime_metadata="${HEARTWOOD_QUALIFICATION_RUNTIME_METADATA:-${project}/gpu-runtime.json}" runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" +heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}" vllm_executable="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" vllm_python="${HEARTWOOD_VLLM_PYTHON:-/opt/heartwood-vllm/bin/python}" @@ -22,12 +23,16 @@ if [[ ! -d "${model_path}" ]]; then echo "vLLM model snapshot is unavailable: ${model_path}" >&2 exit 66 fi +if [[ ! -x "${heartwood_python}" ]]; then + echo "Heartwood Python is unavailable: ${heartwood_python}" >&2 + exit 69 +fi mkdir -p "${project}" project="$(cd "${project}" && pwd -P)" model_path="$(cd "${model_path}" && pwd -P)" rm -f "${runtime_log}" "${runtime_metadata}" -configuration="$(python "${script_dir}/qualification_config.py" "${configuration_id}")" +configuration="$("${heartwood_python}" "${script_dir}/qualification_config.py" "${configuration_id}")" snapshot_id="$(jq -er '.configuration.model_snapshot' <<<"${configuration}")" repository="$(jq -er '.configuration.model_repository' <<<"${configuration}")" revision="$(jq -er '.configuration.model_revision' <<<"${configuration}")" @@ -39,7 +44,7 @@ startup_min="$(jq -er '.configuration.startup_seconds_min' <<<"${configuration}" startup_max="$(jq -er '.configuration.startup_seconds_max' <<<"${configuration}")" echo "Verifying the pinned ${repository}@${revision} snapshot..." -python - "${model_path}" "${snapshot_id}" "${repository}" "${revision}" <<'PY' +"${heartwood_python}" - "${model_path}" "${snapshot_id}" "${repository}" "${revision}" <<'PY' import json import sys from pathlib import Path @@ -58,7 +63,7 @@ if any(source.get(key) != value for key, value in expected.items()): verify_model_snapshot(root) PY -python - "${configuration}" <<'PY' +"${heartwood_python}" - "${configuration}" <<'PY' import json import os import sys @@ -77,7 +82,7 @@ if not compatible or not environment.visible_devices: raise SystemExit("the requested qualification requires compatible GPUs visible in this process") PY -"${script_dir}/verify_runtime.sh" +"${script_dir}/verify_runtime.sh" /opt "${vllm_executable}" --version >/dev/null "${vllm_python}" - "${runtime_metadata}" <<'PY' import json @@ -137,7 +142,7 @@ cleanup() { } trap cleanup EXIT -python - "${runtime_port}" "${startup_max}" <<'PY' +"${heartwood_python}" - "${runtime_port}" "${startup_max}" <<'PY' import sys import time import urllib.error diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 95630659..fbbbe924 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -452,6 +452,24 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert denied.returncode == 64 +def test_gpu_qualification_uses_isolated_heartwood_python() -> None: + script = _read("images/gpu/coding_agent_e2e.sh") + coding_agent = _read("images/generic/scripts/coding_agent_e2e.sh") + + assert 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in script + assert 'configuration="$("${heartwood_python}"' in script + assert '"${script_dir}/verify_runtime.sh" /opt' in script + assert "\npython " not in script + assert ( + 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' + in coding_agent + ) + assert 'heartwood_cli="${HEARTWOOD_CLI:-${runtime_root}/.venv/bin/heartwood}"' in coding_agent + assert 'inference="${project}/qualification-inference.json"' in coding_agent + assert 'mkdir -p "${project}/input"' in coding_agent + assert "\npython " not in coding_agent + + def test_carina_native_launch_requires_verified_synthetic_allocation() -> None: bootstrap = _read("deploy/carina/bootstrap.sh") runtime_verifier = _read("images/gpu/verify_runtime.sh") From 7e02f0cd06e428abd1a0eb375a9385cc98aced75 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 12:25:36 -0700 Subject: [PATCH 09/38] Format Qualification Contract Tests --- packages/compliance/tests/test_container_assets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index fbbbe924..20504938 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -461,8 +461,7 @@ def test_gpu_qualification_uses_isolated_heartwood_python() -> None: assert '"${script_dir}/verify_runtime.sh" /opt' in script assert "\npython " not in script assert ( - 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' - in coding_agent + 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in coding_agent ) assert 'heartwood_cli="${HEARTWOOD_CLI:-${runtime_root}/.venv/bin/heartwood}"' in coding_agent assert 'inference="${project}/qualification-inference.json"' in coding_agent From b2c44020e69e4553d0b39428040e8b86a3f9959c Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 12:41:22 -0700 Subject: [PATCH 10/38] Use OpenHands Tool Conversion for Managed Models --- deploy/verify_gpu_compatibility.py | 3 +++ documentation/architecture/system.md | 1 + documentation/architecture/testing.md | 4 ++-- documentation/operate/index.md | 2 +- documentation/reference/gpu-compatibility.md | 20 +++++++++---------- images/gpu/compatibility.toml | 6 ++++++ .../tests/test_coding_agent_qualification.py | 1 + .../src/heartwood/gateway/_openhands_sdk.py | 1 + packages/gateway/tests/test_openhands_sdk.py | 11 ++++++++++ 9 files changed, 36 insertions(+), 13 deletions(-) diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index 04837649..63a69679 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -32,6 +32,7 @@ "context_window", "tensor_parallel_size", "tool_call_parser", + "agent_tool_mode", "vllm_version", "pytorch_version", "cuda_version", @@ -177,6 +178,8 @@ def _verify_configuration( ) if configuration.get("qualification_test") != _QUALIFICATION_TEST: raise CompatibilityError("GPU model uses an unsupported qualification test") + if configuration.get("agent_tool_mode") != "openhands-prompt": + raise CompatibilityError("GPU model uses an unsupported agent tool mode") for field in ( "gpu_count", "minimum_gpu_memory_bytes", diff --git a/documentation/architecture/system.md b/documentation/architecture/system.md index 85e3607b..c3a011c2 100644 --- a/documentation/architecture/system.md +++ b/documentation/architecture/system.md @@ -49,6 +49,7 @@ The terminal, browser, and notebook bridge may render these differently, but the The adapter creates an OpenHands conversation with the selected LiteLLM-compatible model profile, project workspace, Skills, persistence directory, and action-confirmation callback. Heartwood translates OpenHands messages, tool proposals, decisions, and results into its stable event contract rather than duplicating the agent loop. +Loopback model routes use OpenHands' prompt-based tool conversion so models do not need to implement provider-native automatic tool selection; hosted provider profiles retain their native tool-calling behavior. ### Platform Adapter diff --git a/documentation/architecture/testing.md b/documentation/architecture/testing.md index 7610a389..6319994c 100644 --- a/documentation/architecture/testing.md +++ b/documentation/architecture/testing.md @@ -18,7 +18,7 @@ No single layer establishes every property of a deployment. | Interface tests | Terminal, browser, and notebook projections over the same state | | Container smoke tests | Entrypoint, filesystem, architecture, no-secret image layers, and deterministic OpenHands integration | | No-network smoke tests | Gateway, OpenHands, grouped action, tool, replay, and audit operation without outbound network | -| Capable-model evaluation | Real Heartwood-managed inference, native tool proposal, bounded execution, and exact synthetic output | +| Capable-model evaluation | Real Heartwood-managed inference, OpenHands-compatible tool proposal, bounded execution, and exact synthetic output | | Platform-derived CI | Terra Jupyter inheritance, prefixed internal gateway routing, persistence, image media type, CI-only model rejection as an agent profile, and separate real inference | | Live synthetic validation | Exact published artifact in Terra or Carina without protected data | @@ -27,7 +27,7 @@ An optional protected self-hosted GPU job runs the same model qualification used Without GPU hardware, CI does not claim successful CUDA initialization or GPU model loading. The shared coding-agent acceptance test performs direct model inference and then drives the real Heartwood gateway and OpenHands adapter through a structured terminal proposal, grouped approval, synthetic file modification, independent file verification, fresh-process replay, and hash-chain-verified audit export. -It emits a machine-readable qualification record containing the exact runtime, model revision, GPU, driver, context, tensor parallelism, and parser. +It emits a machine-readable qualification record containing the exact runtime, model revision, GPU, driver, context, tensor parallelism, server parser, and agent tool mode. The CPU capable-model job and GPU qualification wrapper use this same acceptance contract instead of maintaining separate agent scenarios. Native packaging CI uses deterministic dependency-tool substitutes to verify failure paths and reproducibility, then installs the release archive in an empty Ubuntu 24.04 AMD64 container and runs the real CPU inference and browser paths. diff --git a/documentation/operate/index.md b/documentation/operate/index.md index 4823bb16..2fbd9d32 100644 --- a/documentation/operate/index.md +++ b/documentation/operate/index.md @@ -56,7 +56,7 @@ Before real data, use a synthetic project to verify: 1. exact artifact digest and platform capability response; 2. project persistence across process or compute restart; 3. model discovery and a real agent response; -4. a native OpenHands tool proposal; +4. an OpenHands-compatible structured tool proposal; 5. grouped allow and reject behavior; 6. tool execution confined to the project; 7. terminal, browser, and notebook parity where advertised; diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 4c936937..424abd34 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -31,14 +31,14 @@ The exact driver used in a live qualification is recorded with its machine-reada ## Model Configurations -| Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Tool Parser | Status | -|---|---|---|---|---|---:|---|---:|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | Candidate | -| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | Candidate | -| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | Candidate | -| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | Candidate | -| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | Candidate | -| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | Candidate | +| Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | +|---|---|---|---|---|---:|---|---:|---|---|---| +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands prompt conversion | Candidate | +| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | OpenHands prompt conversion | Candidate | +| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands prompt conversion | Candidate | +| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | OpenHands prompt conversion | Candidate | All listed model repositories declare the Apache-2.0 license at the pinned revision. Confirm that a model's license and intended use remain suitable for the project before downloading it. @@ -49,14 +49,14 @@ A configuration becomes **qualified** only after the exact model revision and lo The acceptance test must establish all of the following: 1. the model loads and returns a direct inference response; -2. OpenHands receives a native structured tool proposal; +2. OpenHands converts the model response into a structured tool proposal; 3. Heartwood presents the complete action set for approval; 4. approval executes the proposed operation and modifies only the synthetic project; 5. an independent check verifies the exact file result; 6. a fresh process replays the session; and 7. audit export validates event coverage, hash-chain integrity, and content scrubbing. -The result records the GPU model, count, memory, driver, runtime versions, model revision, context size, tensor parallelism, and parser. +The result records the GPU model, count, memory, driver, runtime versions, model revision, context size, tensor parallelism, server parser, and agent tool mode. A candidate remains visible for evaluation but cannot become an automatic recommendation until that result passes. ## Unsupported Hardware diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 6fd50ae5..9f3ad3dd 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -30,6 +30,7 @@ precision = "AWQ int4" context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" +agent_tool_mode = "openhands-prompt" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -53,6 +54,7 @@ precision = "AWQ int4" context_window = 32768 tensor_parallel_size = 1 tool_call_parser = "hermes" +agent_tool_mode = "openhands-prompt" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -75,6 +77,7 @@ precision = "FP8" context_window = 32768 tensor_parallel_size = 1 tool_call_parser = "qwen3_coder" +agent_tool_mode = "openhands-prompt" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -97,6 +100,7 @@ precision = "BF16" context_window = 65536 tensor_parallel_size = 2 tool_call_parser = "qwen3_coder" +agent_tool_mode = "openhands-prompt" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -119,6 +123,7 @@ precision = "FP8" context_window = 65536 tensor_parallel_size = 4 tool_call_parser = "qwen3_coder" +agent_tool_mode = "openhands-prompt" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -141,6 +146,7 @@ precision = "MXFP4" context_window = 65536 tensor_parallel_size = 2 tool_call_parser = "openai" +agent_tool_mode = "openhands-prompt" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index d244d560..34c31df9 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -226,6 +226,7 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "hermes" + assert resolved["configuration"]["agent_tool_mode"] == "openhands-prompt" assert resolved["configuration"]["context_window"] == 18_432 assert resolved["configuration"]["enforce_eager"] is True assert resolved["configuration"]["model_revision"] == ( diff --git a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py index ac8e4e07..1757b5ab 100644 --- a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py +++ b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py @@ -546,6 +546,7 @@ def _llm_options( "aws_profile_name": profile.aws_profile_name, "max_input_tokens": profile.max_input_tokens, "max_output_tokens": profile.max_output_tokens, + "native_tool_calling": not profile.is_local, "max_message_chars": _llm_max_message_chars(profile), "log_completions": False, "litellm_extra_body": dict(extra_body) or None, diff --git a/packages/gateway/tests/test_openhands_sdk.py b/packages/gateway/tests/test_openhands_sdk.py index 67a039bf..a437e45e 100644 --- a/packages/gateway/tests/test_openhands_sdk.py +++ b/packages/gateway/tests/test_openhands_sdk.py @@ -189,8 +189,19 @@ def test_openhands_forwards_managed_model_request_defaults() -> None: assert options["litellm_extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}} assert options["max_input_tokens"] == 16_384 assert options["max_output_tokens"] == 2_048 + assert options["native_tool_calling"] is False assert options["input_cost_per_token"] == 0.0 + hosted = ModelProfile( + profile_id="hosted", + model="openai/model", + policy_endpoint="https://api.openai.com/v1/chat/completions", + credential_kind="environment", + api_key_env="OPENAI_API_KEY", + ) + hosted_options = _llm_options(hosted, api_key="test-key", extra_body={}) + assert hosted_options["native_tool_calling"] is True + def test_openhands_context_condenser_uses_the_active_model_budget() -> None: copied: dict[str, object] = {} From b71657a085fd2b9a82d12bb3a5ce18abf29a8d58 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 13:07:00 -0700 Subject: [PATCH 11/38] Align Runtime Smoke Tests With OpenHands Tool Conversion --- images/generic/scripts/local_model_stub.py | 117 +++++++++++++----- .../scripts/terra_jupyter_demo_smoke.py | 6 +- .../compliance/tests/test_container_assets.py | 9 ++ .../scripts/smoke-reference-analysis.cjs | 6 +- 4 files changed, 102 insertions(+), 36 deletions(-) diff --git a/images/generic/scripts/local_model_stub.py b/images/generic/scripts/local_model_stub.py index 8110c0d0..8525b215 100644 --- a/images/generic/scripts/local_model_stub.py +++ b/images/generic/scripts/local_model_stub.py @@ -30,6 +30,19 @@ def _tool_result_failed(messages: list[object]) -> bool: return '"is_error": true' in serialized or any(code != 0 for code in exit_codes) +def _message_text(message: dict[str, object]) -> str: + content = message.get("content") + if isinstance(content, str): + return content + if isinstance(content, list): + return "\n".join( + str(part.get("text", "")) + for part in content + if isinstance(part, dict) and part.get("type") == "text" + ) + return "" + + def _terminal_call( call_id: str, command: str, @@ -57,6 +70,18 @@ def _terminal_call( } +def _prompt_terminal_call(command: str, summary: str, security_risk: str = "LOW") -> str: + return "\n".join( + ( + "", + f"{command}", + f"{security_risk}", + f"{summary}", + "", + ) + ) + + class LocalModelHandler(BaseHTTPRequestHandler): """Handle one content-free chat-completion request for the stub profile.""" @@ -105,39 +130,53 @@ def do_POST(self) -> None: ) + "\n" ) + serialized_messages = json.dumps(messages).lower() + native_tool_mode = bool(payload.get("tools")) + prompt_tool_mode = "" in serialized_messages researcher_messages = [ message for message in messages if isinstance(message, dict) and message.get("role") == "user" ] latest_researcher_message = researcher_messages[-1] if researcher_messages else {} - serialized_researcher_message = json.dumps(latest_researcher_message).lower() - latest_researcher_index = max( + task_message = next( ( - index - for index, candidate in enumerate(messages) - if isinstance(candidate, dict) and candidate.get("role") == "user" + message + for message in reversed(researcher_messages) + if not _message_text(message).lower().lstrip().startswith("execution result of [") ), + {}, + ) + serialized_task_message = json.dumps(task_message).lower() + task_index = max( + (index for index, message in enumerate(messages) if message is task_message), default=-1, ) - tool_results = [ + native_tool_results = [ message for index, message in enumerate(messages) - if index > latest_researcher_index - and isinstance(message, dict) - and message.get("role") == "tool" + if index > task_index and isinstance(message, dict) and message.get("role") == "tool" ] + prompt_tool_results = ( + [latest_researcher_message] + if _message_text(latest_researcher_message) + .lower() + .lstrip() + .startswith("execution result of [") + else [] + ) + tool_results = [*native_tool_results, *prompt_tool_results] has_tool_result = bool(tool_results) - medium_risk = "medium-risk network check" in serialized_researcher_message + medium_risk = "medium-risk network check" in serialized_task_message task_kind = ( "cohort" - if "target-condition cohort" in serialized_researcher_message + if "target-condition cohort" in serialized_task_message else "baseline" - if "age-only baseline" in serialized_researcher_message + if "age-only baseline" in serialized_task_message else "export" - if "aggregate export" in serialized_researcher_message + if "aggregate export" in serialized_task_message else "failure" - if "failing-action" in serialized_researcher_message + if "failing-action" in serialized_task_message else "generic" ) message: dict[str, object] @@ -159,7 +198,7 @@ def do_POST(self) -> None: ), } finish_reason = "stop" - else: + elif native_tool_mode or prompt_tool_mode: runtime_root = os.environ.get("HEARTWOOD_RUNTIME_ROOT") or None tool_python = os.environ.get("HEARTWOOD_TOOL_PYTHON") or sys.executable script_root = ( @@ -224,28 +263,42 @@ def do_POST(self) -> None: } command = "curl https://example.invalid" if medium_risk else commands[task_kind] summary = "run a medium-risk network command" if medium_risk else summaries[task_kind] - tool_calls = [ - _terminal_call( - call_ids[task_kind], - command, - summary, - security_risk="MEDIUM" if medium_risk else "LOW", - ) - ] - if task_kind == "cohort" and not medium_risk: - tool_calls.append( + risk = "MEDIUM" if medium_risk else "LOW" + if native_tool_mode: + tool_calls = [ _terminal_call( - "call-heartwood-reference-analysis-read", - "cat cohort-summary.json", - "read the generated aggregate cohort summary", + call_ids[task_kind], + command, + summary, + security_risk=risk, ) - ) + ] + if task_kind == "cohort" and not medium_risk: + tool_calls.append( + _terminal_call( + "call-heartwood-reference-analysis-read", + "cat cohort-summary.json", + "read the generated aggregate cohort summary", + ) + ) + message = { + "role": "assistant", + "content": None, + "tool_calls": tool_calls, + } + finish_reason = "tool_calls" + else: + message = { + "role": "assistant", + "content": _prompt_terminal_call(command, summary, risk), + } + finish_reason = "stop" + else: message = { "role": "assistant", - "content": None, - "tool_calls": tool_calls, + "content": "Synthetic Heartwood-managed model response.", } - finish_reason = "tool_calls" + finish_reason = "stop" response = { "id": "chatcmpl-heartwood-managed-runtime", "object": "chat.completion", diff --git a/images/generic/scripts/terra_jupyter_demo_smoke.py b/images/generic/scripts/terra_jupyter_demo_smoke.py index 894d3871..a5218687 100644 --- a/images/generic/scripts/terra_jupyter_demo_smoke.py +++ b/images/generic/scripts/terra_jupyter_demo_smoke.py @@ -310,6 +310,10 @@ def _verify_gateway_session(external_base: str) -> None: task_kinds = {event["kind"] for event in task["events"]} if "confirmation.requested" not in task_kinds: raise AssertionError("gateway chat did not return an OpenHands confirmation") + confirmation = next( + event for event in task["events"] if event["kind"] == "confirmation.requested" + ) + target_id = confirmation["payload"]["request"]["tool_call_id"] _trace("approving OpenHands tool call") allowed = _request_json( @@ -318,7 +322,7 @@ def _verify_gateway_session(external_base: str) -> None: "approve", "terra-demo-smoke-allow", { - "target_id": "call-heartwood-reference-analysis", + "target_id": target_id, "target_type": "tool-call", }, ), diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 20504938..9ecb97c9 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -803,6 +803,15 @@ def test_local_model_stub_preserves_explicit_action_risk() -> None: assert arguments["security_risk"] == "MEDIUM" + prompt_call = module._prompt_terminal_call( + "printf heartwood-openhands-action", + "run a bounded offline smoke command", + ) + assert prompt_call.startswith("\n") + assert "printf heartwood-openhands-action" in prompt_call + assert "LOW" in prompt_call + assert prompt_call.endswith("") + def test_launch_scripts_are_valid_and_require_explicit_local_artifact() -> None: scripts = ( diff --git a/packages/webui/scripts/smoke-reference-analysis.cjs b/packages/webui/scripts/smoke-reference-analysis.cjs index 07d2725d..c7b18f23 100644 --- a/packages/webui/scripts/smoke-reference-analysis.cjs +++ b/packages/webui/scripts/smoke-reference-analysis.cjs @@ -225,9 +225,9 @@ async function main() { .getByRole("button", { name: "Activity & audit", exact: true }) .click(); await expect(page.getByText("Tool execution", { exact: true })).toHaveCount( - 5, + 4, ); - await expect(page.getByText("exit=0", { exact: true })).toHaveCount(4); + await expect(page.getByText("exit=0", { exact: true })).toHaveCount(3); await expect(page.getByText("exit=1", { exact: true })).toHaveCount(1); await page.getByRole("button", { name: "Close", exact: true }).click(); @@ -259,7 +259,7 @@ async function main() { const replay = runCli("--session-id", sessionId, "replay"); if ( !replay.includes("Action set approved") || - replay.match(/Tool terminal exit=0/gu)?.length !== 4 || + replay.match(/Tool terminal exit=0/gu)?.length !== 3 || replay.match(/Tool terminal exit=1/gu)?.length !== 1 || !replay.includes( "Agent: The synthetic target-condition cohort summary is ready for review.", From 9cc0ea5fe5e92ba216480f1f2edff011940709a2 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 14:04:31 -0700 Subject: [PATCH 12/38] Qualify The Terra T4 Model Configuration --- documentation/models/choose-managed.md | 2 +- documentation/platforms/terra.md | 3 +- documentation/reference/gpu-compatibility.md | 2 +- images/generic/local-runtime/snapshots.toml | 6 ++-- images/gpu/compatibility.toml | 2 +- .../gateway/tests/test_gateway_contract.py | 32 +++++++++++++++++++ packages/gateway/tests/test_local_models.py | 14 +++++++- .../gateway/tests/test_model_snapshots.py | 28 ++++++++-------- 8 files changed, 68 insertions(+), 21 deletions(-) diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 1fe7408f..e3a9c9a4 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -47,7 +47,7 @@ The selection screen uses the release catalog as its authoritative source and re | Tier | Model Configuration | Download | GPU Memory | Recommended RAM | Recommended Free Disk | Default Context | Estimated First Start | |---|---|---:|---:|---:|---:|---:|---:| | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | -| Standard candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 32,768 | 2-8 minutes | +| Standard, qualified on Terra | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | | Powerful candidate | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Powerful candidate | Qwen3 Coder 30B BF16 | 56.88 GiB | 2 x 48 GB | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 50f6ae83..f96d7bc5 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -112,8 +112,7 @@ The first-use flow confirms the project and asks where the model runs. - Choose OpenAI, Anthropic, or **Other compatible service** only when that endpoint is authorized for the intended data. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. -For managed GPU inference, choose the **Standard** Qwen2.5 Coder 7B AWQ configuration when Heartwood labels it **Recommended** for the detected T4. -If it is labeled **Evaluation candidate**, use it only for a synthetic qualification task; Heartwood does not automatically recommend unqualified configurations. +For managed GPU inference, choose the **Standard** Qwen2.5 Coder 7B AWQ configuration that Heartwood labels **Recommended** for the detected T4. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 424abd34..2c2637c0 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -33,7 +33,7 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | | Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | OpenHands prompt conversion | Candidate | diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 0e480051..4e182d74 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -31,7 +31,9 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 7B" precision = "AWQ int4" tier = "standard" -qualification = "candidate" +qualification = "qualified" +validated_platforms = ["terra"] +qualification_test = "heartwood.coding-agent-e2e.v1" minimum_gpu_count = 1 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 34359738368 @@ -45,7 +47,7 @@ startup_seconds_max = 480 download_policy = "transformers-safetensors" minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 CPU cores, 32 GB RAM, and 12 GB free project storage." recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 or more CPU cores, 32 GB RAM, and 16 GB free project storage." -recommended = false +recommended = true [snapshots.qwen3-coder-30b-a3b-instruct-fp8-vllm] runtime_profile = "vllm-cuda" diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 9f3ad3dd..c7d6ebb7 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -18,7 +18,7 @@ cuda_13_qualified = false [[configurations]] configuration_id = "terra-t4-qwen25-coder-7b-awq" -status = "candidate" +status = "qualified" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 1 diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index df572006..fb9c4f07 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -839,6 +839,38 @@ def without_packaged_runtimes(path: Path) -> bool: else: assert fully_available[0]["availability_reason"] == "Available on this deployment" + monkeypatch.setattr( + gateway, + "gpu_environment", + lambda: GpuEnvironment( + platform_id="terra", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + label="1 visible NVIDIA T4 GPU", + gpu_model="NVIDIA T4", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + allocation_required=False, + ), + ), + ), + ) + terra_models = cast(list[dict[str, JsonValue]], gateway.model_artifacts()["models"]) + terra_standard = next( + model + for model in terra_models + if model["model_id"] == "qwen25-coder-7b-instruct-awq-vllm" + ) + assert terra_standard["qualification"] == "qualified" + assert str(terra_standard["availability_reason"]).startswith( + "Recommended for this deployment" + ) + assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( + terra_standard["availability_reason"] + ) + def test_inaccessible_packaged_runtime_is_reported_as_unavailable( tmp_path: Path, diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index 5cdae0fc..eb64cb7f 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -468,6 +468,7 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert {choice.model_id for choice in choices} == { "qwen25-7b-instruct-q4_k_m", + "qwen25-coder-7b-instruct-awq-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) assert all(choice.context_window == 32_768 for choice in choices) @@ -481,8 +482,12 @@ def test_central_catalog_exposes_only_recommended_models() -> None: "gpt-oss-120b-vllm", } <= {choice.model_id for choice in downloadable} assert all(choice.catalog_source == "catalog" for choice in downloadable) + gpu_choices = {choice.model_id: choice for choice in downloadable if choice.runtime == "vllm"} + assert gpu_choices["qwen25-coder-7b-instruct-awq-vllm"].qualification == "qualified" assert all( - choice.qualification == "candidate" for choice in downloadable if choice.runtime == "vllm" + choice.qualification == "candidate" + for model_id, choice in gpu_choices.items() + if model_id != "qwen25-coder-7b-instruct-awq-vllm" ) @@ -495,10 +500,17 @@ def test_catalog_qualification_is_scoped_to_the_validated_platform() -> None: root / "images" / "generic" / "local-runtime" / "snapshots.toml" ) cpu = catalog_model_choices(artifacts.artifacts, snapshots.snapshots)[0] + terra_gpu = next( + choice + for choice in catalog_model_choices(artifacts.artifacts, snapshots.snapshots) + if choice.model_id == "qwen25-coder-7b-instruct-awq-vllm" + ) assert cpu.qualification_for("generic") == "qualified" assert cpu.qualification_for("terra") == "qualified" assert cpu.qualification_for("carina") == "candidate" + assert terra_gpu.qualification_for("terra") == "qualified" + assert terra_gpu.qualification_for("carina") == "candidate" def _repository( diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index d4942b6c..76cce066 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -79,7 +79,7 @@ ), ], ) -def test_repository_snapshot_catalog_pins_gpu_candidates( +def test_repository_snapshot_catalog_pins_gpu_model_variants( snapshot_id: str, repository: str, revision: str, @@ -101,10 +101,18 @@ def test_repository_snapshot_catalog_pins_gpu_candidates( assert snapshot.minimum_free_bytes >= snapshot.expected_size_bytes assert snapshot.recommended_disk_bytes >= snapshot.minimum_free_bytes assert snapshot.context_window <= snapshot.maximum_context_window - assert snapshot.qualification == "candidate" - assert snapshot.validated_platforms == () - assert snapshot.qualification_test is None - assert snapshot.recommended is False + expected_qualification = ( + "qualified" if snapshot_id == "qwen25-coder-7b-instruct-awq-vllm" else "candidate" + ) + assert snapshot.qualification == expected_qualification + if expected_qualification == "qualified": + assert snapshot.validated_platforms == ("terra",) + assert snapshot.qualification_test == "heartwood.coding-agent-e2e.v1" + assert snapshot.recommended is True + else: + assert snapshot.validated_platforms == () + assert snapshot.qualification_test is None + assert snapshot.recommended is False @pytest.mark.parametrize( @@ -137,13 +145,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> source = load_model_snapshot_catalog( _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" ) - standard = replace( - source.snapshot("qwen25-coder-7b-instruct-awq-vllm"), - qualification="qualified", - validated_platforms=("terra",), - qualification_test="heartwood.coding-agent-e2e.v1", - recommended=True, - ) + standard = source.snapshot("qwen25-coder-7b-instruct-awq-vllm") powerful = replace( source.snapshot("qwen3-coder-30b-a3b-instruct-fp8-vllm"), qualification="qualified", @@ -187,7 +189,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> gpu_memory_bytes=16_000_000_000, maximum_tier="maximum", ) - is None + == standard ) assert ( From 1badbaff8fcd90747173329e734b5e7e2ee6a52a Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 14:07:54 -0700 Subject: [PATCH 13/38] Record The Terra Qualification Evidence --- images/gpu/compatibility.toml | 3 +++ packages/gateway/tests/test_gateway_contract.py | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index c7d6ebb7..e2f077da 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -19,6 +19,9 @@ cuda_13_qualified = false [[configurations]] configuration_id = "terra-t4-qwen25-coder-7b-awq" status = "qualified" +validated_at = "2026-07-21" +validated_driver_version = "535.154.05" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 1 diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index fb9c4f07..1c50c6be 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -859,14 +859,10 @@ def without_packaged_runtimes(path: Path) -> bool: ) terra_models = cast(list[dict[str, JsonValue]], gateway.model_artifacts()["models"]) terra_standard = next( - model - for model in terra_models - if model["model_id"] == "qwen25-coder-7b-instruct-awq-vllm" + model for model in terra_models if model["model_id"] == "qwen25-coder-7b-instruct-awq-vllm" ) assert terra_standard["qualification"] == "qualified" - assert str(terra_standard["availability_reason"]).startswith( - "Recommended for this deployment" - ) + assert str(terra_standard["availability_reason"]).startswith("Recommended for this deployment") assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( terra_standard["availability_reason"] ) From 6054501c9e5a8f06555d55dafe59914e04a8b569 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 14:34:52 -0700 Subject: [PATCH 14/38] Reuse GPU Images Across Qualification Profiles --- .github/workflows/gpu-container-image.yml | 11 ++++ documentation/architecture/testing.md | 2 + documentation/models/choose-managed.md | 2 + documentation/platforms/terra.md | 6 +- documentation/reference/gpu-compatibility.md | 2 + images/generic/local-runtime/snapshots.toml | 56 +++++++++++++++++++ images/gpu/compatibility.toml | 48 ++++++++++++++++ images/gpu/qualification_config.py | 25 ++++++++- packages/cli/src/heartwood/cli/_launch.py | 3 +- .../tests/test_coding_agent_qualification.py | 18 ++++++ .../tests/test_model_source_verification.py | 8 +++ .../gateway/src/heartwood/gateway/__init__.py | 2 + .../gateway/src/heartwood/gateway/_gateway.py | 3 +- .../src/heartwood/gateway/_model_snapshots.py | 9 +++ packages/gateway/tests/test_local_models.py | 2 + .../gateway/tests/test_model_snapshots.py | 50 +++++++++++++++++ 16 files changed, 241 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index 9e2cdcd1..e1f08c39 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -27,6 +27,8 @@ on: type: choice options: - terra-t4-qwen25-coder-7b-awq + - terra-t4-qwen25-coder-14b-awq + - terra-4xt4-qwen3-coder-30b-fp8 - carina-l40s-qwen25-coder-7b-awq - carina-l40s-qwen3-coder-30b-fp8 - carina-2xl40s-qwen3-coder-30b-bf16 @@ -284,6 +286,15 @@ jobs: docker pull --platform linux/amd64 "${CANDIDATE}" docker run --rm --platform linux/amd64 \ --entrypoint /opt/heartwood/images/gpu/verify_runtime.sh "${CANDIDATE}" + profiles="$( + docker run --rm --platform linux/amd64 \ + --entrypoint heartwood-python "${CANDIDATE}" \ + /opt/heartwood/images/gpu/qualification_config.py --list + )" + jq --exit-status ' + length > 0 + and all(.[].configuration_id; type == "string" and length > 0) + ' <<<"${profiles}" >/dev/null - name: Verify shared agent and platform interfaces env: CANDIDATE: ${{ steps.candidate.outputs.reference }} diff --git a/documentation/architecture/testing.md b/documentation/architecture/testing.md index 6319994c..ab63d921 100644 --- a/documentation/architecture/testing.md +++ b/documentation/architecture/testing.md @@ -23,6 +23,8 @@ No single layer establishes every property of a deployment. | Live synthetic validation | Exact published artifact in Terra or Carina without protected data | GPU image CI verifies the fully hashed CUDA 12.9 environment, exact vLLM and PyTorch versions, compatibility guards, available tool parsers, launcher, and absence of bundled model weights on standard runners. +Each immutable GPU candidate embeds the complete compatibility matrix for its commit. +Qualification profiles select external model weights and runtime arguments against that candidate; they do not produce profile-specific images. An optional protected self-hosted GPU job runs the same model qualification used on managed platforms when an eligible runner is configured. Without GPU hardware, CI does not claim successful CUDA initialization or GPU model loading. diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index e3a9c9a4..3af799a6 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -48,6 +48,8 @@ The selection screen uses the release catalog as its authoritative source and re |---|---|---:|---:|---:|---:|---:|---:| | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard, qualified on Terra | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | +| Powerful candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | +| Maximum candidate | Qwen3 Coder 30B FP8 W8A16 | 29.06 GiB | 4 x 16 GB | 120 GiB | 150 GiB | 32,768 | 5-15 minutes | | Powerful candidate | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Powerful candidate | Qwen3 Coder 30B BF16 | 56.88 GiB | 2 x 48 GB | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index f96d7bc5..15f45bfc 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -43,7 +43,8 @@ Use one of these combinations: |---|---|---| | Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | | Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | -| Heartwood-managed NVIDIA GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | +| Standard managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | +| Maximum managed GPU evaluation | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, four T4 GPUs with 16 GB each, 200 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. @@ -52,7 +53,8 @@ CPU inference is portable but can be too slow for an interactive coding workflow These are starting points rather than universal requirements. Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. -The GPU path is designed around a T4 and the release-pinned Qwen2.5 Coder 7B AWQ configuration. +The qualified GPU path uses one T4 and the release-pinned Qwen2.5 Coder 7B AWQ configuration. +The same image exposes larger configurations as evaluation candidates when the selected Terra environment has sufficient T4 GPUs, RAM, and persistent storage. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. For the first model download and startup, set auto-pause to at least 120 minutes; image creation, model verification, and inference startup can each take several minutes without terminal output from the model itself. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 2c2637c0..c626721c 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -34,6 +34,8 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | +| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Terra | Maximum capability | 4 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 W8A16 | 32,768 | Eager | 4 | `qwen3_coder` | OpenHands prompt conversion | Candidate | | Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | OpenHands prompt conversion | Candidate | diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 4e182d74..8f5cd5f7 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -49,6 +49,34 @@ minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 CPU core recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 or more CPU cores, 32 GB RAM, and 16 GB free project storage." recommended = true +[snapshots.qwen25-coder-14b-instruct-awq-vllm] +runtime_profile = "vllm-cuda" +purpose = "Powerful coding-agent model under evaluation for one 16 GB NVIDIA GPU." +source_repository = "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ" +source_revision = "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c" +expected_size_bytes = 9991784616 +minimum_free_bytes = 21474836480 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen2.5 Coder 14B" +precision = "AWQ int4" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 1 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 64424509440 +recommended_disk_bytes = 34359738368 +context_window = 18432 +maximum_context_window = 32768 +tool_call_parser = "hermes" +tensor_parallel_size = 1 +startup_seconds_min = 180 +startup_seconds_max = 600 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 12 CPU cores, 48 GB RAM, and 20 GB free project storage." +recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 16 CPU cores, 60 GB RAM, and 32 GB free project storage." +recommended = false + [snapshots.qwen3-coder-30b-a3b-instruct-fp8-vllm] runtime_profile = "vllm-cuda" purpose = "Powerful coding-agent model for one 48 GB NVIDIA GPU." @@ -77,6 +105,34 @@ minimum_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 12 CPU c recommended_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 16 CPU cores, 96 GB RAM, and 64 GB free project storage." recommended = false +[snapshots.qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm] +runtime_profile = "vllm-cuda" +purpose = "Maximum-capability coding-agent model under evaluation for four 16 GB NVIDIA GPUs." +source_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" +source_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" +expected_size_bytes = 31195099643 +minimum_free_bytes = 53687091200 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen3 Coder 30B (4 GPUs)" +precision = "FP8 W8A16" +tier = "maximum" +qualification = "candidate" +minimum_gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 128849018880 +recommended_disk_bytes = 161061273600 +context_window = 32768 +maximum_context_window = 262144 +tool_call_parser = "qwen3_coder" +tensor_parallel_size = 4 +startup_seconds_min = 300 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 50 GB free project storage." +recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 150 GB free project storage." +recommended = false + [snapshots.qwen3-coder-30b-a3b-instruct-bf16-vllm] runtime_profile = "vllm-cuda" purpose = "Powerful higher-precision coding-agent model for two 48 GB NVIDIA GPUs." diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index e2f077da..4a713aa9 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -43,6 +43,30 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 120 startup_seconds_max = 480 +[[configurations]] +configuration_id = "terra-t4-qwen25-coder-14b-awq" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 1 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen25-coder-14b-instruct-awq-vllm" +model_repository = "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ" +model_revision = "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c" +precision = "AWQ int4" +context_window = 18432 +tensor_parallel_size = 1 +tool_call_parser = "hermes" +agent_tool_mode = "openhands-prompt" +enforce_eager = true +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 180 +startup_seconds_max = 600 + [[configurations]] configuration_id = "carina-l40s-qwen25-coder-7b-awq" status = "candidate" @@ -89,6 +113,30 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 180 startup_seconds_max = 600 +[[configurations]] +configuration_id = "terra-4xt4-qwen3-coder-30b-fp8" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm" +model_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" +model_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" +precision = "FP8 W8A16" +context_window = 32768 +tensor_parallel_size = 4 +tool_call_parser = "qwen3_coder" +agent_tool_mode = "openhands-prompt" +enforce_eager = true +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 300 +startup_seconds_max = 900 + [[configurations]] configuration_id = "carina-2xl40s-qwen3-coder-30b-bf16" status = "candidate" diff --git a/images/gpu/qualification_config.py b/images/gpu/qualification_config.py index 0c80a198..2d7998f3 100644 --- a/images/gpu/qualification_config.py +++ b/images/gpu/qualification_config.py @@ -15,6 +15,20 @@ from typing import Any +def list_configurations(path: Path, *, platform: str | None = None) -> list[dict[str, Any]]: + """List reviewed configurations, optionally restricted to one platform.""" + with path.open("rb") as file: + configurations = tomllib.load(file).get("configurations") + if not isinstance(configurations, list): + raise ValueError("GPU compatibility matrix is malformed") + return [ + configuration + for configuration in configurations + if isinstance(configuration, dict) + and (platform is None or configuration.get("platform") == platform) + ] + + def load_configuration(path: Path, configuration_id: str) -> dict[str, Any]: """Load one compatibility entry together with its runtime contract.""" with path.open("rb") as file: @@ -33,15 +47,22 @@ def load_configuration(path: Path, configuration_id: str) -> dict[str, Any]: def main() -> int: - """Print one resolved configuration for shell and CI consumers.""" + """Print reviewed configuration metadata for shell and CI consumers.""" parser = argparse.ArgumentParser() - parser.add_argument("configuration_id") + parser.add_argument("configuration_id", nargs="?") + parser.add_argument("--list", action="store_true") + parser.add_argument("--platform") parser.add_argument( "--matrix", type=Path, default=Path(__file__).with_name("compatibility.toml"), ) args = parser.parse_args() + if args.list: + print(json.dumps(list_configurations(args.matrix, platform=args.platform), sort_keys=True)) + return 0 + if args.configuration_id is None: + parser.error("configuration_id is required unless --list is used") print(json.dumps(load_configuration(args.matrix, args.configuration_id), sort_keys=True)) return 0 diff --git a/packages/cli/src/heartwood/cli/_launch.py b/packages/cli/src/heartwood/cli/_launch.py index 767209e6..e675c923 100644 --- a/packages/cli/src/heartwood/cli/_launch.py +++ b/packages/cli/src/heartwood/cli/_launch.py @@ -33,6 +33,7 @@ ProjectConfigStore, ProjectContext, SessionGateway, + automatic_model_tier, discover_slurm_gpu_partitions, estimate_local_runtime_memory, inspect_gpu_environment, @@ -257,7 +258,7 @@ def _recommend_model( ) -> ModelSnapshot | None: task_profile = options.task_profile if task_profile == "auto": - task_profile = "powerful" if platform_id == "carina" else "standard" + task_profile = automatic_model_tier(platform_id) gateway = SessionGateway(project=options.project, env=env) try: gpu_environment = gateway.gpu_environment() diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 34c31df9..308a9cc8 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -234,6 +234,24 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: ) +def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: + module = _module( + "gpu_qualification_config_list", + _root() / "images/gpu/qualification_config.py", + ) + + configurations = module.list_configurations( + _root() / "images/gpu/compatibility.toml", + platform="terra", + ) + + assert {configuration["configuration_id"] for configuration in configurations} == { + "terra-t4-qwen25-coder-7b-awq", + "terra-t4-qwen25-coder-14b-awq", + "terra-4xt4-qwen3-coder-30b-fp8", + } + + def test_gpu_qualification_context_can_be_bounded_by_platform_memory() -> None: verifier = _module( "gpu_compatibility_verifier", diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index 6a4fafb5..09fe1dce 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -40,6 +40,10 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ", "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a", ), + "qwen25-coder-14b-instruct-awq-vllm": ( + "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ", + "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c", + ), "qwen3-coder-30b-a3b-instruct-bf16-vllm": ( "Qwen/Qwen3-Coder-30B-A3B-Instruct", "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", @@ -48,6 +52,10 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", ), + "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm": ( + "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", + "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", + ), "qwen3-coder-next-fp8-vllm": ( "Qwen/Qwen3-Coder-Next-FP8", "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c", diff --git a/packages/gateway/src/heartwood/gateway/__init__.py b/packages/gateway/src/heartwood/gateway/__init__.py index b94850a6..119c19d7 100644 --- a/packages/gateway/src/heartwood/gateway/__init__.py +++ b/packages/gateway/src/heartwood/gateway/__init__.py @@ -94,6 +94,7 @@ ModelSnapshotError, ModelTier, ToolCallParser, + automatic_model_tier, download_model_snapshot, load_model_snapshot_catalog, verify_model_snapshot, @@ -206,6 +207,7 @@ "StartupPlan", "ToolCallParser", "action_settings_from_mapping", + "automatic_model_tier", "catalog_model_choices", "custom_model_connection", "custom_model_connection_requires_token", diff --git a/packages/gateway/src/heartwood/gateway/_gateway.py b/packages/gateway/src/heartwood/gateway/_gateway.py index 676f6daf..37f35527 100644 --- a/packages/gateway/src/heartwood/gateway/_gateway.py +++ b/packages/gateway/src/heartwood/gateway/_gateway.py @@ -82,6 +82,7 @@ ModelSnapshotCatalog, ModelSnapshotError, ModelTier, + automatic_model_tier, download_model_snapshot, load_model_snapshot_catalog, ) @@ -783,7 +784,7 @@ def model_artifacts(self) -> dict[str, object]: ) ) recommendation = self.recommend_managed_model( - maximum_tier=("powerful" if gpu_environment.platform_id == "carina" else "standard"), + maximum_tier=automatic_model_tier(gpu_environment.platform_id), gpu_environment=gpu_environment, ) preferred_id = ( diff --git a/packages/gateway/src/heartwood/gateway/_model_snapshots.py b/packages/gateway/src/heartwood/gateway/_model_snapshots.py index 482d8db2..4d6659f0 100644 --- a/packages/gateway/src/heartwood/gateway/_model_snapshots.py +++ b/packages/gateway/src/heartwood/gateway/_model_snapshots.py @@ -51,6 +51,15 @@ _VALIDATED_PLATFORMS = {"carina", "generic", "terra"} +def automatic_model_tier(platform_id: str) -> ModelTier: + """Return the highest tier considered by automatic model selection.""" + if platform_id == "terra": + return "maximum" + if platform_id == "carina": + return "powerful" + return "standard" + + class SnapshotDownloader(Protocol): """Callable contract implemented by ``huggingface_hub.snapshot_download``.""" diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index eb64cb7f..c51b5eb1 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -476,7 +476,9 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert "qwen25-coder-7b-instruct-q4_k_m" in {choice.model_id for choice in downloadable} assert { "qwen25-coder-7b-instruct-awq-vllm", + "qwen25-coder-14b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm", "qwen3-coder-30b-a3b-instruct-bf16-vllm", "qwen3-coder-next-fp8-vllm", "gpt-oss-120b-vllm", diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 76cce066..7ea9b6d1 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -20,6 +20,7 @@ ModelSnapshot, ModelSnapshotCatalog, ModelSnapshotError, + automatic_model_tier, download_model_snapshot, load_model_snapshot_catalog, plan_local_context_window, @@ -45,6 +46,14 @@ 1, "hermes", ), + ( + "qwen25-coder-14b-instruct-awq-vllm", + "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ", + "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c", + "powerful", + 1, + "hermes", + ), ( "qwen3-coder-30b-a3b-instruct-fp8-vllm", "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", @@ -53,6 +62,14 @@ 1, "qwen3_coder", ), + ( + "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm", + "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", + "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", + "maximum", + 4, + "qwen3_coder", + ), ( "qwen3-coder-30b-a3b-instruct-bf16-vllm", "Qwen/Qwen3-Coder-30B-A3B-Instruct", @@ -115,6 +132,22 @@ def test_repository_snapshot_catalog_pins_gpu_model_variants( assert snapshot.recommended is False +@pytest.mark.parametrize( + ("platform_id", "tier"), + [ + ("generic", "standard"), + ("terra", "maximum"), + ("carina", "powerful"), + ("custom", "standard"), + ], +) +def test_automatic_model_tier_is_shared_across_interfaces( + platform_id: str, + tier: str, +) -> None: + assert automatic_model_tier(platform_id) == tier + + @pytest.mark.parametrize( ("snapshot_id", "minimum_vram_gib"), [ @@ -430,6 +463,23 @@ def test_snapshot_catalog_reports_unknown_ids_and_invalid_documents(tmp_path: Pa with pytest.raises(ModelSnapshotError, match="snapshots table"): load_model_snapshot_catalog(missing_snapshots) + missing_policies = tmp_path / "missing-policies.toml" + missing_policies.write_text( + 'schema_version = "heartwood.model-snapshot-catalog.v2"\n[snapshots]\n', + encoding="utf-8", + ) + with pytest.raises(ModelSnapshotError, match="download policies"): + load_model_snapshot_catalog(missing_policies) + + invalid_policy = tmp_path / "invalid-policy.toml" + invalid_policy.write_text( + 'schema_version = "heartwood.model-snapshot-catalog.v2"\n' + '[download_policies]\ninvalid = "value"\n[snapshots]\n', + encoding="utf-8", + ) + with pytest.raises(ModelSnapshotError, match="policy entries must be tables"): + load_model_snapshot_catalog(invalid_policy) + invalid_entry = tmp_path / "entry.toml" invalid_entry.write_text( 'schema_version = "heartwood.model-snapshot-catalog.v2"\n' From ddd3e69ff8e07632bf91edbec2bda0921bb16691 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 16:25:42 -0700 Subject: [PATCH 15/38] Record Terra Model Qualification Results --- .github/workflows/gpu-container-image.yml | 1 - documentation/models/choose-managed.md | 6 ++-- documentation/platforms/terra.md | 13 +++---- documentation/reference/gpu-compatibility.md | 6 ++-- images/generic/local-runtime/snapshots.toml | 36 +++---------------- images/gpu/compatibility.toml | 25 +++++-------- .../tests/test_coding_agent_qualification.py | 25 ++++++++++++- .../tests/test_model_source_verification.py | 4 --- .../gateway/tests/test_gateway_contract.py | 13 ++++++- packages/gateway/tests/test_local_models.py | 13 ++++--- .../gateway/tests/test_model_snapshots.py | 28 ++++++++------- 11 files changed, 88 insertions(+), 82 deletions(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index e1f08c39..2e426104 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -28,7 +28,6 @@ on: options: - terra-t4-qwen25-coder-7b-awq - terra-t4-qwen25-coder-14b-awq - - terra-4xt4-qwen3-coder-30b-fp8 - carina-l40s-qwen25-coder-7b-awq - carina-l40s-qwen3-coder-30b-fp8 - carina-2xl40s-qwen3-coder-30b-bf16 diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 3af799a6..b1236554 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -48,8 +48,7 @@ The selection screen uses the release catalog as its authoritative source and re |---|---|---:|---:|---:|---:|---:|---:| | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard, qualified on Terra | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | -| Powerful candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Maximum candidate | Qwen3 Coder 30B FP8 W8A16 | 29.06 GiB | 4 x 16 GB | 120 GiB | 150 GiB | 32,768 | 5-15 minutes | +| Powerful, qualified on Terra | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | | Powerful candidate | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Powerful candidate | Qwen3 Coder 30B BF16 | 56.88 GiB | 2 x 48 GB | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | @@ -59,6 +58,9 @@ Model weights are only part of the memory requirement. The runtime also needs space for temporary downloads, key/value cache, request handling, and the project itself. Heartwood therefore uses conservative headroom and may choose a smaller context than the model's advertised maximum. +Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot compatible with the current runtime. +Heartwood rejects that combination because vLLM's FP8 Mixture-of-Experts kernel cannot load the model on T4 hardware. + ## Other Hugging Face Models Choose **Other Hugging Face model** or enter an `owner/model` identifier: diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 15f45bfc..67c314ae 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -43,8 +43,7 @@ Use one of these combinations: |---|---|---| | Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | | Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | -| Standard managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | -| Maximum managed GPU evaluation | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, four T4 GPUs with 16 GB each, 200 GB persistent disk | +| Managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. @@ -53,8 +52,9 @@ CPU inference is portable but can be too slow for an interactive coding workflow These are starting points rather than universal requirements. Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. -The qualified GPU path uses one T4 and the release-pinned Qwen2.5 Coder 7B AWQ configuration. -The same image exposes larger configurations as evaluation candidates when the selected Terra environment has sufficient T4 GPUs, RAM, and persistent storage. +The qualified GPU path uses one T4 and offers release-pinned Qwen2.5 Coder 7B and 14B AWQ configurations. +The 14B model is the strongest qualified Terra recommendation; the 7B model is the lower-resource alternative. +Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 snapshot compatible, so Heartwood does not offer that failed combination. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. For the first model download and startup, set auto-pause to at least 120 minutes; image creation, model verification, and inference startup can each take several minutes without terminal output from the model itself. @@ -114,11 +114,12 @@ The first-use flow confirms the project and asks where the model runs. - Choose OpenAI, Anthropic, or **Other compatible service** only when that endpoint is authorized for the intended data. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. -For managed GPU inference, choose the **Standard** Qwen2.5 Coder 7B AWQ configuration that Heartwood labels **Recommended** for the detected T4. +For managed GPU inference, choose the **Powerful** Qwen2.5 Coder 14B AWQ configuration that Heartwood labels **Recommended** for the detected T4. +Choose the **Standard** 7B configuration when you prefer a smaller download and lower memory pressure. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. -The pinned AWQ snapshot downloads about 5.2 GiB; allow at least 16 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. +The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index c626721c..d08da518 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -34,8 +34,7 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | -| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Terra | Maximum capability | 4 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 W8A16 | 32,768 | Eager | 4 | `qwen3_coder` | OpenHands prompt conversion | Candidate | +| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | | Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | OpenHands prompt conversion | Candidate | @@ -45,6 +44,9 @@ The exact driver used in a live qualification is recorded with its machine-reada All listed model repositories declare the Apache-2.0 license at the pinned revision. Confirm that a model's license and intended use remain suitable for the project before downloading it. +The Qwen3 Coder 30B FP8 snapshot was also tested with four T4 GPUs and vLLM 0.25.1 on CUDA 12.9. +That combination is unsupported because the FP8 Mixture-of-Experts kernel cannot load the model's quantization dimensions on T4 hardware. + ## Qualification Requirement A configuration becomes **qualified** only after the exact model revision and locked runtime complete one bounded Heartwood task on the named platform. diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 8f5cd5f7..2d4d9fc8 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -51,7 +51,7 @@ recommended = true [snapshots.qwen25-coder-14b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful coding-agent model under evaluation for one 16 GB NVIDIA GPU." +purpose = "Powerful coding-agent model qualified for one 16 GB NVIDIA GPU." source_repository = "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ" source_revision = "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c" expected_size_bytes = 9991784616 @@ -61,7 +61,9 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 14B" precision = "AWQ int4" tier = "powerful" -qualification = "candidate" +qualification = "qualified" +validated_platforms = ["terra"] +qualification_test = "heartwood.coding-agent-e2e.v1" minimum_gpu_count = 1 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 64424509440 @@ -75,7 +77,7 @@ startup_seconds_max = 600 download_policy = "transformers-safetensors" minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 12 CPU cores, 48 GB RAM, and 20 GB free project storage." recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 16 CPU cores, 60 GB RAM, and 32 GB free project storage." -recommended = false +recommended = true [snapshots.qwen3-coder-30b-a3b-instruct-fp8-vllm] runtime_profile = "vllm-cuda" @@ -105,34 +107,6 @@ minimum_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 12 CPU c recommended_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 16 CPU cores, 96 GB RAM, and 64 GB free project storage." recommended = false -[snapshots.qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm] -runtime_profile = "vllm-cuda" -purpose = "Maximum-capability coding-agent model under evaluation for four 16 GB NVIDIA GPUs." -source_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" -source_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" -expected_size_bytes = 31195099643 -minimum_free_bytes = 53687091200 -license_id = "Apache-2.0" -license_posture = "Apache-2.0; retain the model card and source provenance." -model_alias = "Qwen3 Coder 30B (4 GPUs)" -precision = "FP8 W8A16" -tier = "maximum" -qualification = "candidate" -minimum_gpu_count = 4 -minimum_gpu_memory_bytes = 15000000000 -recommended_ram_bytes = 128849018880 -recommended_disk_bytes = 161061273600 -context_window = 32768 -maximum_context_window = 262144 -tool_call_parser = "qwen3_coder" -tensor_parallel_size = 4 -startup_seconds_min = 300 -startup_seconds_max = 900 -download_policy = "transformers-safetensors" -minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 50 GB free project storage." -recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 150 GB free project storage." -recommended = false - [snapshots.qwen3-coder-30b-a3b-instruct-bf16-vllm] runtime_profile = "vllm-cuda" purpose = "Powerful higher-precision coding-agent model for two 48 GB NVIDIA GPUs." diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 4a713aa9..6cd9d53a 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -45,7 +45,10 @@ startup_seconds_max = 480 [[configurations]] configuration_id = "terra-t4-qwen25-coder-14b-awq" -status = "candidate" +status = "qualified" +validated_at = "2026-07-21" +validated_driver_version = "535.154.05" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 1 @@ -113,29 +116,17 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 180 startup_seconds_max = 600 -[[configurations]] +[[unsupported_configurations]] configuration_id = "terra-4xt4-qwen3-coder-30b-fp8" -status = "candidate" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 4 -minimum_gpu_memory_bytes = 15000000000 -model_snapshot = "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm" model_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8" model_revision = "dcaee4d4dfc5ee71ad501f01f530e5652438fde0" -precision = "FP8 W8A16" -context_window = 32768 -tensor_parallel_size = 4 -tool_call_parser = "qwen3_coder" -agent_tool_mode = "openhands-prompt" -enforce_eager = true vllm_version = "0.25.1+cu129" -pytorch_version = "2.11.0+cu129" -cuda_version = "12.9" -minimum_driver_version = "525.60.13" -qualification_test = "heartwood.coding-agent-e2e.v1" -startup_seconds_min = 300 -startup_seconds_max = 900 +validated_at = "2026-07-21" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" +reason = "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions that this model does not provide on NVIDIA T4 GPUs." [[configurations]] configuration_id = "carina-2xl40s-qwen3-coder-30b-bf16" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 308a9cc8..a60ec516 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -248,10 +248,33 @@ def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: assert {configuration["configuration_id"] for configuration in configurations} == { "terra-t4-qwen25-coder-7b-awq", "terra-t4-qwen25-coder-14b-awq", - "terra-4xt4-qwen3-coder-30b-fp8", } +def test_gpu_compatibility_records_rejected_terra_configuration() -> None: + with (_root() / "images/gpu/compatibility.toml").open("rb") as file: + matrix = tomllib.load(file) + + unsupported = matrix["unsupported_configurations"] + assert unsupported == [ + { + "configuration_id": "terra-4xt4-qwen3-coder-30b-fp8", + "platform": "terra", + "gpu_model": "NVIDIA T4", + "gpu_count": 4, + "model_repository": "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", + "model_revision": "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", + "vllm_version": "0.25.1+cu129", + "validated_at": "2026-07-21", + "evidence": "https://github.com/SchmiedmayerLab/heartwood/pull/72", + "reason": ( + "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions " + "that this model does not provide on NVIDIA T4 GPUs." + ), + } + ] + + def test_gpu_qualification_context_can_be_bounded_by_platform_memory() -> None: verifier = _module( "gpu_compatibility_verifier", diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index 09fe1dce..5ad8962d 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -52,10 +52,6 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", ), - "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm": ( - "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", - "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", - ), "qwen3-coder-next-fp8-vllm": ( "Qwen/Qwen3-Coder-Next-FP8", "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c", diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index 1c50c6be..b9c4d759 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -862,7 +862,18 @@ def without_packaged_runtimes(path: Path) -> bool: model for model in terra_models if model["model_id"] == "qwen25-coder-7b-instruct-awq-vllm" ) assert terra_standard["qualification"] == "qualified" - assert str(terra_standard["availability_reason"]).startswith("Recommended for this deployment") + assert str(terra_standard["availability_reason"]).startswith( + "Compatible with 1 visible NVIDIA T4 GPU(s)" + ) + terra_powerful = next( + model + for model in terra_models + if model["model_id"] == "qwen25-coder-14b-instruct-awq-vllm" + ) + assert terra_powerful["qualification"] == "qualified" + assert str(terra_powerful["availability_reason"]).startswith( + "Recommended for this deployment" + ) assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( terra_standard["availability_reason"] ) diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index c51b5eb1..5f6e25b5 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -469,16 +469,16 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert {choice.model_id for choice in choices} == { "qwen25-7b-instruct-q4_k_m", "qwen25-coder-7b-instruct-awq-vllm", + "qwen25-coder-14b-instruct-awq-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) - assert all(choice.context_window == 32_768 for choice in choices) + assert {choice.context_window for choice in choices} == {18_432, 32_768} assert "llama-cpp-stories260k-ci" in {choice.model_id for choice in downloadable} assert "qwen25-coder-7b-instruct-q4_k_m" in {choice.model_id for choice in downloadable} assert { "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", - "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm", "qwen3-coder-30b-a3b-instruct-bf16-vllm", "qwen3-coder-next-fp8-vllm", "gpt-oss-120b-vllm", @@ -486,10 +486,15 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert all(choice.catalog_source == "catalog" for choice in downloadable) gpu_choices = {choice.model_id: choice for choice in downloadable if choice.runtime == "vllm"} assert gpu_choices["qwen25-coder-7b-instruct-awq-vllm"].qualification == "qualified" + assert gpu_choices["qwen25-coder-14b-instruct-awq-vllm"].qualification == "qualified" assert all( choice.qualification == "candidate" for model_id, choice in gpu_choices.items() - if model_id != "qwen25-coder-7b-instruct-awq-vllm" + if model_id + not in { + "qwen25-coder-7b-instruct-awq-vllm", + "qwen25-coder-14b-instruct-awq-vllm", + } ) @@ -505,7 +510,7 @@ def test_catalog_qualification_is_scoped_to_the_validated_platform() -> None: terra_gpu = next( choice for choice in catalog_model_choices(artifacts.artifacts, snapshots.snapshots) - if choice.model_id == "qwen25-coder-7b-instruct-awq-vllm" + if choice.model_id == "qwen25-coder-14b-instruct-awq-vllm" ) assert cpu.qualification_for("generic") == "qualified" diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 7ea9b6d1..110e5486 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -62,14 +62,6 @@ 1, "qwen3_coder", ), - ( - "qwen3-coder-30b-a3b-instruct-fp8-tp4-vllm", - "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", - "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", - "maximum", - 4, - "qwen3_coder", - ), ( "qwen3-coder-30b-a3b-instruct-bf16-vllm", "Qwen/Qwen3-Coder-30B-A3B-Instruct", @@ -119,7 +111,13 @@ def test_repository_snapshot_catalog_pins_gpu_model_variants( assert snapshot.recommended_disk_bytes >= snapshot.minimum_free_bytes assert snapshot.context_window <= snapshot.maximum_context_window expected_qualification = ( - "qualified" if snapshot_id == "qwen25-coder-7b-instruct-awq-vllm" else "candidate" + "qualified" + if snapshot_id + in { + "qwen25-coder-7b-instruct-awq-vllm", + "qwen25-coder-14b-instruct-awq-vllm", + } + else "candidate" ) assert snapshot.qualification == expected_qualification if expected_qualification == "qualified": @@ -179,6 +177,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" ) standard = source.snapshot("qwen25-coder-7b-instruct-awq-vllm") + terra_powerful = source.snapshot("qwen25-coder-14b-instruct-awq-vllm") powerful = replace( source.snapshot("qwen3-coder-30b-a3b-instruct-fp8-vllm"), qualification="qualified", @@ -186,7 +185,10 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> qualification_test="heartwood.coding-agent-e2e.v1", recommended=True, ) - catalog = ModelSnapshotCatalog(source.schema_version, (standard, powerful)) + catalog = ModelSnapshotCatalog( + source.schema_version, + (standard, terra_powerful, powerful), + ) assert ( catalog.recommend( @@ -195,7 +197,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> gpu_memory_bytes=16_000_000_000, maximum_tier="maximum", ) - == standard + == terra_powerful ) assert ( catalog.recommend( @@ -222,7 +224,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> gpu_memory_bytes=16_000_000_000, maximum_tier="maximum", ) - == standard + == terra_powerful ) assert ( @@ -231,7 +233,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> capacities=((1, 16_000_000_000), (1, 48_000_000_000)), maximum_tier="maximum", ) - == standard + == terra_powerful ) assert ( catalog.recommend_for_capacities( From 62fe0fff7a649263a8e54ce900f9b6d2becb5b9d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 16:27:37 -0700 Subject: [PATCH 16/38] Format Terra Qualification Tests --- packages/gateway/tests/test_gateway_contract.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index b9c4d759..575b9612 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -866,14 +866,10 @@ def without_packaged_runtimes(path: Path) -> bool: "Compatible with 1 visible NVIDIA T4 GPU(s)" ) terra_powerful = next( - model - for model in terra_models - if model["model_id"] == "qwen25-coder-14b-instruct-awq-vllm" + model for model in terra_models if model["model_id"] == "qwen25-coder-14b-instruct-awq-vllm" ) assert terra_powerful["qualification"] == "qualified" - assert str(terra_powerful["availability_reason"]).startswith( - "Recommended for this deployment" - ) + assert str(terra_powerful["availability_reason"]).startswith("Recommended for this deployment") assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( terra_standard["availability_reason"] ) From 14e6adf3f2ebe3945f85896f744bb0e87e445925 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 16:34:11 -0700 Subject: [PATCH 17/38] Resolve Sharp Security Advisories --- packages/webui/package-lock.json | 364 +++++++++++++++++++------------ packages/webui/package.json | 4 +- 2 files changed, 227 insertions(+), 141 deletions(-) diff --git a/packages/webui/package-lock.json b/packages/webui/package-lock.json index 84c66635..cfb85fb6 100644 --- a/packages/webui/package-lock.json +++ b/packages/webui/package-lock.json @@ -12,7 +12,6 @@ "@stanfordspezi/spezi-web-design-system": "0.20.0", "@tanstack/react-query": "5.101.2", "lucide-react": "1.24.0", - "next-intl": "4.13.2", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -1561,9 +1560,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -1574,19 +1573,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -1597,19 +1596,39 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -1624,9 +1643,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -1641,9 +1660,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -1661,9 +1680,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -1681,9 +1700,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -1701,9 +1720,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -1721,9 +1740,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -1741,9 +1760,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -1761,9 +1780,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -1781,9 +1800,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -1801,9 +1820,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -1817,19 +1836,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -1843,19 +1862,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -1869,19 +1888,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -1895,19 +1914,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -1921,19 +1940,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -1947,19 +1966,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -1973,19 +1992,19 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -1999,39 +2018,56 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "peer": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -2042,16 +2078,16 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -2062,16 +2098,16 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -2082,7 +2118,7 @@ ], "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -2384,6 +2420,7 @@ "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", @@ -2425,6 +2462,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2445,6 +2483,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2465,6 +2504,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2485,6 +2525,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2508,6 +2549,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2531,6 +2573,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2554,6 +2597,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2577,6 +2621,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2600,6 +2645,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2623,6 +2669,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2643,6 +2690,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2663,6 +2711,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2683,6 +2732,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -4659,7 +4709,8 @@ "version": "1.21.5", "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@standard-schema/spec": { "version": "1.1.0", @@ -4753,6 +4804,7 @@ "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.27" @@ -4799,6 +4851,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -4815,6 +4868,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -4831,6 +4885,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4850,6 +4905,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4869,6 +4925,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4888,6 +4945,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4907,6 +4965,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4926,6 +4985,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4945,6 +5005,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4961,6 +5022,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -4977,6 +5039,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -4993,6 +5056,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -5001,7 +5065,8 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@swc/helpers": { "version": "0.5.23", @@ -5017,6 +5082,7 @@ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -8568,6 +8634,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@formatjs/icu-messageformat-parser": "^3.4.0" } @@ -8577,6 +8644,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.14.tgz", "integrity": "sha512-jDvgtoLqe3U6yzoBlToMTkWBe38qSi7LN7kFlnXzd5ig8nn+4tSlED0xtEtdYakZVZGJJY2rW1D5xS3BFZh6kA==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" } @@ -8585,7 +8653,8 @@ "version": "2.1.11", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/idb": { "version": "7.1.1", @@ -9866,6 +9935,7 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -9935,6 +10005,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", @@ -9959,19 +10030,22 @@ "version": "4.13.2", "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.2.tgz", "integrity": "sha512-O30N/Y4ifzRe5Sz80jD1Qkg4VY6Zfef4SbHNNE166QkHswBf3/Kygpdd1X52sUUwTCk6uvdisO8ybfAN/VYJHQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next-intl/node_modules/@formatjs/fast-memoize": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next-intl/node_modules/@formatjs/intl-localematcher": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.12.tgz", "integrity": "sha512-5H3r5ZJ2jZqHEv9K343lvHmeMDKMxssawAVD2H4J9xtu0ZXb6MlNxwLqdwBxJSHFU0C24KSZnffgmAi+59mK4A==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "3.1.7" } @@ -9990,7 +10064,8 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-exports-info": { "version": "1.6.2", @@ -10340,7 +10415,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/possible-typed-array-names": { "version": "1.1.0", @@ -11152,49 +11228,54 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "optional": true, "peer": true, "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/sharp/node_modules/semver": { @@ -12060,6 +12141,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", @@ -12074,13 +12156,15 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/use-intl/node_modules/@formatjs/icu-messageformat-parser": { "version": "3.5.14", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.14.tgz", "integrity": "sha512-jDvgtoLqe3U6yzoBlToMTkWBe38qSi7LN7kFlnXzd5ig8nn+4tSlED0xtEtdYakZVZGJJY2rW1D5xS3BFZh6kA==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" } @@ -12089,13 +12173,15 @@ "version": "2.1.11", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/use-intl/node_modules/intl-messageformat": { "version": "11.2.11", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.11.tgz", "integrity": "sha512-aDG5bvFRbQvRoT2Bh9FV6yV8t7o0MjEGknZ6pnin5Wt52PJwaBOHDfvz+oPEe78Pl3InQYKugBgCdXijLj6viQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "3.1.7", "@formatjs/icu-messageformat-parser": "3.5.14" diff --git a/packages/webui/package.json b/packages/webui/package.json index d46f569b..29580f98 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -27,13 +27,13 @@ "@stanfordspezi/spezi-web-design-system": "0.20.0", "@tanstack/react-query": "5.101.2", "lucide-react": "1.24.0", - "next-intl": "4.13.2", "react": "19.2.7", "react-dom": "19.2.7" }, "overrides": { "esbuild": "0.28.1", - "postcss": "8.5.10" + "postcss": "8.5.10", + "sharp": "0.35.3" }, "devDependencies": { "@playwright/test": "1.61.1", From dcf93bd72267b122bda83b277c8e272a5c9564c9 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 17:32:35 -0700 Subject: [PATCH 18/38] Add Reusable GPU Qualification Profiles --- images/generic/local-runtime/snapshots.toml | 28 +++++++++++++++++++ images/gpu/coding_agent_e2e.sh | 14 ++++++++-- images/gpu/compatibility.toml | 24 ++++++++++++++++ .../tests/test_coding_agent_qualification.py | 11 ++++++++ .../compliance/tests/test_container_assets.py | 3 +- .../tests/test_model_source_verification.py | 4 +++ 6 files changed, 81 insertions(+), 3 deletions(-) diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 2d4d9fc8..117a928d 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -107,6 +107,34 @@ minimum_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 12 CPU c recommended_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 16 CPU cores, 96 GB RAM, and 64 GB free project storage." recommended = false +[snapshots.qwen25-coder-32b-instruct-awq-vllm] +runtime_profile = "vllm-cuda" +purpose = "Powerful 32B coding-agent candidate for four 16 GB NVIDIA GPUs." +source_repository = "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ" +source_revision = "1ed0a6145da0ce550c628e8e8b678f51e695995d" +expected_size_bytes = 19328993904 +minimum_free_bytes = 34359738368 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen2.5 Coder 32B" +precision = "AWQ int4" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 128849018880 +recommended_disk_bytes = 51539607552 +context_window = 32768 +maximum_context_window = 32768 +tool_call_parser = "hermes" +tensor_parallel_size = 4 +startup_seconds_min = 240 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 32 GB free project storage." +recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 48 GB free project storage." +recommended = false + [snapshots.qwen3-coder-30b-a3b-instruct-bf16-vllm] runtime_profile = "vllm-cuda" purpose = "Powerful higher-precision coding-agent model for two 48 GB NVIDIA GPUs." diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh index 7c69b77d..a9109c8d 100755 --- a/images/gpu/coding_agent_e2e.sh +++ b/images/gpu/coding_agent_e2e.sh @@ -12,12 +12,14 @@ model_path="${HEARTWOOD_LOCAL_MODEL_PATH:?HEARTWOOD_LOCAL_MODEL_PATH is required project="${HEARTWOOD_CAPABLE_PROJECT:-/tmp/heartwood-gpu-qualification}" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" runtime_root="${HEARTWOOD_RUNTIME_ROOT:-$(cd "${script_dir}/../.." && pwd)}" +compatibility_matrix="${HEARTWOOD_GPU_COMPATIBILITY_MATRIX:-${script_dir}/compatibility.toml}" runtime_log="${HEARTWOOD_RUNTIME_LOG:-${project}/vllm.log}" runtime_metadata="${HEARTWOOD_QUALIFICATION_RUNTIME_METADATA:-${project}/gpu-runtime.json}" runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}" vllm_executable="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" vllm_python="${HEARTWOOD_VLLM_PYTHON:-/opt/heartwood-vllm/bin/python}" +vllm_root="${HEARTWOOD_VLLM_ROOT:-$(cd "$(dirname "${vllm_python}")/.." && pwd)}" if [[ ! -d "${model_path}" ]]; then echo "vLLM model snapshot is unavailable: ${model_path}" >&2 @@ -27,12 +29,17 @@ if [[ ! -x "${heartwood_python}" ]]; then echo "Heartwood Python is unavailable: ${heartwood_python}" >&2 exit 69 fi +if [[ ! -f "${compatibility_matrix}" ]]; then + echo "GPU compatibility matrix is unavailable: ${compatibility_matrix}" >&2 + exit 66 +fi mkdir -p "${project}" project="$(cd "${project}" && pwd -P)" model_path="$(cd "${model_path}" && pwd -P)" rm -f "${runtime_log}" "${runtime_metadata}" -configuration="$("${heartwood_python}" "${script_dir}/qualification_config.py" "${configuration_id}")" +configuration="$("${heartwood_python}" "${script_dir}/qualification_config.py" \ + --matrix "${compatibility_matrix}" "${configuration_id}")" snapshot_id="$(jq -er '.configuration.model_snapshot' <<<"${configuration}")" repository="$(jq -er '.configuration.model_repository' <<<"${configuration}")" revision="$(jq -er '.configuration.model_revision' <<<"${configuration}")" @@ -82,7 +89,10 @@ if not compatible or not environment.visible_devices: raise SystemExit("the requested qualification requires compatible GPUs visible in this process") PY -"${script_dir}/verify_runtime.sh" /opt +HEARTWOOD_VLLM_ROOT="${vllm_root}" \ +HEARTWOOD_VLLM_PYTHON="${vllm_python}" \ +HEARTWOOD_VLLM_EXECUTABLE="${vllm_executable}" \ + "${script_dir}/verify_runtime.sh" "${vllm_root}" "${vllm_executable}" --version >/dev/null "${vllm_python}" - "${runtime_metadata}" <<'PY' import json diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 6cd9d53a..cb816274 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -128,6 +128,30 @@ validated_at = "2026-07-21" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" reason = "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions that this model does not provide on NVIDIA T4 GPUs." +[[configurations]] +configuration_id = "terra-4xt4-qwen25-coder-32b-awq" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen25-coder-32b-instruct-awq-vllm" +model_repository = "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ" +model_revision = "1ed0a6145da0ce550c628e8e8b678f51e695995d" +precision = "AWQ int4" +context_window = 32768 +tensor_parallel_size = 4 +tool_call_parser = "hermes" +agent_tool_mode = "openhands-prompt" +enforce_eager = true +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 240 +startup_seconds_max = 900 + [[configurations]] configuration_id = "carina-2xl40s-qwen3-coder-30b-bf16" status = "candidate" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index a60ec516..343e46f5 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -248,9 +248,20 @@ def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: assert {configuration["configuration_id"] for configuration in configurations} == { "terra-t4-qwen25-coder-7b-awq", "terra-t4-qwen25-coder-14b-awq", + "terra-4xt4-qwen25-coder-32b-awq", } +def test_gpu_qualification_script_supports_native_runtime_and_external_matrix() -> None: + script = (_root() / "images/gpu/coding_agent_e2e.sh").read_text(encoding="utf-8") + + assert "HEARTWOOD_GPU_COMPATIBILITY_MATRIX" in script + assert '--matrix "${compatibility_matrix}"' in script + assert 'HEARTWOOD_VLLM_ROOT="${vllm_root}"' in script + assert 'verify_runtime.sh" "${vllm_root}"' in script + assert 'verify_runtime.sh" /opt' not in script + + def test_gpu_compatibility_records_rejected_terra_configuration() -> None: with (_root() / "images/gpu/compatibility.toml").open("rb") as file: matrix = tomllib.load(file) diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 9ecb97c9..c3d11c14 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -458,7 +458,8 @@ def test_gpu_qualification_uses_isolated_heartwood_python() -> None: assert 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in script assert 'configuration="$("${heartwood_python}"' in script - assert '"${script_dir}/verify_runtime.sh" /opt' in script + assert 'HEARTWOOD_VLLM_ROOT="${vllm_root}"' in script + assert '"${script_dir}/verify_runtime.sh" "${vllm_root}"' in script assert "\npython " not in script assert ( 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in coding_agent diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index 5ad8962d..8d2527ec 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -44,6 +44,10 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ", "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c", ), + "qwen25-coder-32b-instruct-awq-vllm": ( + "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ", + "1ed0a6145da0ce550c628e8e8b678f51e695995d", + ), "qwen3-coder-30b-a3b-instruct-bf16-vllm": ( "Qwen/Qwen3-Coder-30B-A3B-Instruct", "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", From 4387f955844621cbcc9080e1a1a06072a7293177 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 17:37:13 -0700 Subject: [PATCH 19/38] Align External Qualification Evidence --- .../scripts/verify_coding_agent_e2e.py | 8 ++++++- .../tests/test_coding_agent_qualification.py | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/images/generic/scripts/verify_coding_agent_e2e.py b/images/generic/scripts/verify_coding_agent_e2e.py index 06a299e4..c33a9e2b 100644 --- a/images/generic/scripts/verify_coding_agent_e2e.py +++ b/images/generic/scripts/verify_coding_agent_e2e.py @@ -174,7 +174,13 @@ def _mapping(value: object) -> dict[str, Any]: def _configuration(root: Path, configuration_id: str) -> dict[str, Any]: - with (root / "images/gpu/compatibility.toml").open("rb") as file: + matrix_path = Path( + os.environ.get( + "HEARTWOOD_GPU_COMPATIBILITY_MATRIX", + root / "images/gpu/compatibility.toml", + ) + ) + with matrix_path.open("rb") as file: matrix = tomllib.load(file) for item in matrix.get("configurations", []): if isinstance(item, dict) and item.get("configuration_id") == configuration_id: diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 343e46f5..526ec9c6 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -262,6 +262,29 @@ def test_gpu_qualification_script_supports_native_runtime_and_external_matrix() assert 'verify_runtime.sh" /opt' not in script +def test_gpu_qualification_report_uses_external_matrix( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + module = _module( + "verify_coding_agent_e2e_external_matrix", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + matrix = tmp_path / "compatibility.toml" + matrix.write_text( + "[[configurations]]\nconfiguration_id = \"external-profile\"\nplatform = \"terra\"\n", + encoding="utf-8", + ) + monkeypatch.setenv("HEARTWOOD_GPU_COMPATIBILITY_MATRIX", str(matrix)) + + configuration = module._configuration(_root(), "external-profile") + + assert configuration == { + "configuration_id": "external-profile", + "platform": "terra", + } + + def test_gpu_compatibility_records_rejected_terra_configuration() -> None: with (_root() / "images/gpu/compatibility.toml").open("rb") as file: matrix = tomllib.load(file) From 3cafe76da5b3be8e503813fff7b72a54ff5597e2 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 17:40:04 -0700 Subject: [PATCH 20/38] Format Qualification Matrix Tests --- packages/compliance/tests/test_coding_agent_qualification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 526ec9c6..c66552ee 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -272,7 +272,7 @@ def test_gpu_qualification_report_uses_external_matrix( ) matrix = tmp_path / "compatibility.toml" matrix.write_text( - "[[configurations]]\nconfiguration_id = \"external-profile\"\nplatform = \"terra\"\n", + '[[configurations]]\nconfiguration_id = "external-profile"\nplatform = "terra"\n', encoding="utf-8", ) monkeypatch.setenv("HEARTWOOD_GPU_COMPATIBILITY_MATRIX", str(matrix)) From 56ace23053edd3d4ee41b195b82bfb696534260d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 18:01:37 -0700 Subject: [PATCH 21/38] Tighten GPU Qualification Contracts --- .github/workflows/gpu-container-image.yml | 1 + .../scripts/verify_coding_agent_e2e.py | 13 +++- images/gpu/qualification_config.py | 54 +++++++++++--- .../tests/test_coding_agent_qualification.py | 70 ++++++++++++++++++- .../compliance/tests/test_container_assets.py | 9 ++- .../gateway/src/heartwood/gateway/_gateway.py | 8 ++- .../gateway/tests/test_gateway_contract.py | 45 ++++++++++++ 7 files changed, 183 insertions(+), 17 deletions(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index 2e426104..f6686f73 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -28,6 +28,7 @@ on: options: - terra-t4-qwen25-coder-7b-awq - terra-t4-qwen25-coder-14b-awq + - terra-4xt4-qwen25-coder-32b-awq - carina-l40s-qwen25-coder-7b-awq - carina-l40s-qwen3-coder-30b-fp8 - carina-2xl40s-qwen3-coder-30b-bf16 diff --git a/images/generic/scripts/verify_coding_agent_e2e.py b/images/generic/scripts/verify_coding_agent_e2e.py index c33a9e2b..861149b2 100644 --- a/images/generic/scripts/verify_coding_agent_e2e.py +++ b/images/generic/scripts/verify_coding_agent_e2e.py @@ -21,7 +21,6 @@ _TEST_ID = "heartwood.coding-agent-e2e.v1" _REQUIRED_EVENT_KINDS = { - "agent_message.emitted", "confirmation.requested", "confirmation.resolved", "model_call.decision.recorded", @@ -94,6 +93,18 @@ def verify_run( raise ValueError("coding-agent tool execution has no valid exit code") if any(event.payload["exit_code"] != 0 for event in tool_executions): raise ValueError("coding-agent tool execution failed") + completed_with_message = any( + event.kind == "agent_message.emitted" + and isinstance(event.payload.get("content"), str) + and bool(str(event.payload["content"]).strip()) + for event in events + ) + completed_with_finish = any( + event.payload.get("tool_name") == "finish" and event.payload.get("exit_code") == 0 + for event in tool_executions + ) + if not completed_with_message and not completed_with_finish: + raise ValueError("coding-agent session has no successful completion action or message") routes = [ decision.get("decision") diff --git a/images/gpu/qualification_config.py b/images/gpu/qualification_config.py index 2d7998f3..aaed4fe2 100644 --- a/images/gpu/qualification_config.py +++ b/images/gpu/qualification_config.py @@ -14,6 +14,44 @@ from pathlib import Path from typing import Any +_REQUIRED_CONFIGURATION_FIELDS = { + "configuration_id": str, + "status": str, + "platform": str, + "gpu_model": str, + "gpu_count": int, + "minimum_gpu_memory_bytes": int, + "model_snapshot": str, + "model_repository": str, + "model_revision": str, + "precision": str, + "context_window": int, + "tensor_parallel_size": int, + "tool_call_parser": str, + "agent_tool_mode": str, + "vllm_version": str, + "pytorch_version": str, + "cuda_version": str, + "minimum_driver_version": str, + "qualification_test": str, + "startup_seconds_min": int, + "startup_seconds_max": int, +} + + +def _validate_configuration(configuration: object) -> dict[str, Any]: + if not isinstance(configuration, dict): + raise ValueError("GPU compatibility matrix contains a malformed configuration") + for field, expected_type in _REQUIRED_CONFIGURATION_FIELDS.items(): + value = configuration.get(field) + if not isinstance(value, expected_type) or isinstance(value, bool): + raise ValueError(f"GPU configuration {field} must be a {expected_type.__name__}") + if expected_type is str and not value: + raise ValueError(f"GPU configuration {field} must not be empty") + if expected_type is int and value <= 0: + raise ValueError(f"GPU configuration {field} must be positive") + return configuration + def list_configurations(path: Path, *, platform: str | None = None) -> list[dict[str, Any]]: """List reviewed configurations, optionally restricted to one platform.""" @@ -21,11 +59,11 @@ def list_configurations(path: Path, *, platform: str | None = None) -> list[dict configurations = tomllib.load(file).get("configurations") if not isinstance(configurations, list): raise ValueError("GPU compatibility matrix is malformed") + validated = [_validate_configuration(configuration) for configuration in configurations] return [ configuration - for configuration in configurations - if isinstance(configuration, dict) - and (platform is None or configuration.get("platform") == platform) + for configuration in validated + if platform is None or configuration["platform"] == platform ] @@ -37,11 +75,9 @@ def load_configuration(path: Path, configuration_id: str) -> dict[str, Any]: configurations = matrix.get("configurations") if not isinstance(runtime, dict) or not isinstance(configurations, list): raise ValueError("GPU compatibility matrix is malformed") - for configuration in configurations: - if ( - isinstance(configuration, dict) - and configuration.get("configuration_id") == configuration_id - ): + for item in configurations: + configuration = _validate_configuration(item) + if configuration["configuration_id"] == configuration_id: return {"runtime": runtime, "configuration": configuration} raise ValueError(f"unknown GPU qualification configuration: {configuration_id}") @@ -58,6 +94,8 @@ def main() -> int: default=Path(__file__).with_name("compatibility.toml"), ) args = parser.parse_args() + if args.list and args.configuration_id is not None: + parser.error("configuration_id cannot be combined with --list") if args.list: print(json.dumps(list_configurations(args.matrix, platform=args.platform), sort_keys=True)) return 0 diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index c66552ee..68ddf93a 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -10,6 +10,7 @@ import importlib.util import json +import sys import tomllib from collections.abc import Callable from pathlib import Path @@ -76,7 +77,11 @@ def _acceptance_files(tmp_path: Path) -> tuple[Path, Path, Path, Path, Path]: "tool.execution.recorded", {"tool_name": "terminal", "exit_code": 0}, ), - _event(5, "agent_message.emitted", {"content": "Complete"}), + _event( + 5, + "tool.execution.recorded", + {"tool_name": "finish", "exit_code": 0}, + ), _event(6, "audit.export.recorded", {"scrubbed": True}), ) events_path = tmp_path / "events.jsonl" @@ -137,10 +142,42 @@ def test_coding_agent_qualification_verifies_complete_acceptance_evidence( inference_path=inference, ) - assert summary["tool_execution_count"] == 1 + assert summary["tool_execution_count"] == 2 assert cast(dict[str, bool], summary["checks"])["audit_export_verified"] is True +def test_coding_agent_qualification_requires_successful_completion( + tmp_path: Path, +) -> None: + module = _module( + "verify_coding_agent_e2e_completion", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + payloads = [json.loads(line) for line in events.read_text(encoding="utf-8").splitlines()] + finish = next( + payload + for payload in payloads + if payload["kind"] == "tool.execution.recorded" + and payload["payload"].get("tool_name") == "finish" + ) + finish["payload"]["tool_name"] = "unknown" + events.write_text( + "".join(json.dumps(payload) + "\n" for payload in payloads), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="no successful completion"): + verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) + + def test_coding_agent_qualification_rejects_incomplete_replay(tmp_path: Path) -> None: module = _module( "verify_coding_agent_e2e_incomplete", @@ -209,7 +246,7 @@ def test_coding_agent_qualification_accepts_relative_artifact_path( inference_path=inference, ) - assert summary["tool_execution_count"] == 1 + assert summary["tool_execution_count"] == 2 def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: @@ -252,6 +289,33 @@ def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: } +def test_gpu_qualification_catalog_rejects_malformed_entries(tmp_path: Path) -> None: + module = _module( + "gpu_qualification_config_invalid_list", + _root() / "images/gpu/qualification_config.py", + ) + matrix = tmp_path / "compatibility.toml" + matrix.write_text('configurations = ["invalid"]\n', encoding="utf-8") + + with pytest.raises(ValueError, match="malformed configuration"): + module.list_configurations(matrix) + + +def test_gpu_qualification_cli_rejects_id_with_list( + monkeypatch: pytest.MonkeyPatch, +) -> None: + module = _module( + "gpu_qualification_config_conflicting_arguments", + _root() / "images/gpu/qualification_config.py", + ) + monkeypatch.setattr(sys, "argv", ["qualification_config.py", "profile", "--list"]) + + with pytest.raises(SystemExit) as error: + module.main() + + assert error.value.code == 2 + + def test_gpu_qualification_script_supports_native_runtime_and_external_matrix() -> None: script = (_root() / "images/gpu/coding_agent_e2e.sh").read_text(encoding="utf-8") diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index c3d11c14..6aa1e705 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -456,18 +456,23 @@ def test_gpu_qualification_uses_isolated_heartwood_python() -> None: script = _read("images/gpu/coding_agent_e2e.sh") coding_agent = _read("images/generic/scripts/coding_agent_e2e.sh") + system_python = re.compile( + r"(?:^|[;&|]\s*|\bexec\s+)(?:/[^\s;|&]+/)?python(?:3(?:\.\d+)?)?\s", + re.MULTILINE, + ) + assert 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in script assert 'configuration="$("${heartwood_python}"' in script assert 'HEARTWOOD_VLLM_ROOT="${vllm_root}"' in script assert '"${script_dir}/verify_runtime.sh" "${vllm_root}"' in script - assert "\npython " not in script + assert system_python.search(script) is None assert ( 'heartwood_python="${HEARTWOOD_PYTHON:-${runtime_root}/.venv/bin/python}"' in coding_agent ) assert 'heartwood_cli="${HEARTWOOD_CLI:-${runtime_root}/.venv/bin/heartwood}"' in coding_agent assert 'inference="${project}/qualification-inference.json"' in coding_agent assert 'mkdir -p "${project}/input"' in coding_agent - assert "\npython " not in coding_agent + assert system_python.search(coding_agent) is None def test_carina_native_launch_requires_verified_synthetic_allocation() -> None: diff --git a/packages/gateway/src/heartwood/gateway/_gateway.py b/packages/gateway/src/heartwood/gateway/_gateway.py index 37f35527..d8b19732 100644 --- a/packages/gateway/src/heartwood/gateway/_gateway.py +++ b/packages/gateway/src/heartwood/gateway/_gateway.py @@ -1478,9 +1478,11 @@ def _require_local_model_runtime(self, model_id: str) -> LocalModelChoice: choice = self._downloadable_local_model_choices.get(model_id) if choice is None: raise ModelRepositoryError(f"unknown Heartwood-managed model: {model_id}") - if not self._local_runtime_available(choice.runtime): - reason = self._local_model_choice_dict(choice)["availability_reason"] - raise ModelRepositoryError(f"{choice.label} is unavailable: {reason}") + details = self._local_model_choice_dict(choice) + if not details["available"]: + raise ModelRepositoryError( + f"{choice.label} is unavailable: {details['availability_reason']}" + ) return choice def _local_runtime_available(self, runtime: str) -> bool: diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index 575b9612..01f95457 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -873,6 +873,15 @@ def without_packaged_runtimes(path: Path) -> bool: assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( terra_standard["availability_reason"] ) + rejected_large_model = RestGateway(gateway).handle( + RestRequest( + method="POST", + path="/settings/models/downloads", + body=json.dumps({"model_id": "qwen25-coder-32b-instruct-awq-vllm"}), + ) + ) + assert rejected_large_model.status_code == 422 + assert "requires 4 gpu(s)" in str(rejected_large_model.body["error"]) def test_inaccessible_packaged_runtime_is_reported_as_unavailable( @@ -1254,6 +1263,24 @@ def test_gateway_downloads_recommended_artifacts_and_snapshots_through_one_inter ) -> None: gateway = _gateway(tmp_path) monkeypatch.setattr(gateway, "_local_runtime_available", lambda _runtime: True) + monkeypatch.setattr( + gateway, + "gpu_environment", + lambda: GpuEnvironment( + platform_id="terra", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + label="1 visible NVIDIA T4 GPU", + gpu_model="NVIDIA T4", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + allocation_required=False, + ), + ), + ), + ) observed: list[tuple[str, str, Path]] = [] def artifact_download( @@ -1323,6 +1350,24 @@ def test_gateway_download_uses_the_normalized_model_catalog( ) -> None: gateway = _gateway(tmp_path) monkeypatch.setattr(gateway, "_local_runtime_available", lambda _runtime: True) + monkeypatch.setattr( + gateway, + "gpu_environment", + lambda: GpuEnvironment( + platform_id="terra", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + label="1 visible NVIDIA T4 GPU", + gpu_model="NVIDIA T4", + gpu_count=1, + gpu_memory_bytes=16_000_000_000, + allocation_required=False, + ), + ), + ), + ) snapshot_id = "qwen25-coder-7b-instruct-awq-vllm" destination = tmp_path / ".heartwood" / "models" / snapshot_id From 2139e119a669f2360781bff4deb722c22c5d81a5 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 19:30:47 -0700 Subject: [PATCH 22/38] Qualify Capable GPU Model Profiles --- .github/workflows/gpu-container-image.yml | 2 - deploy/verify_gpu_compatibility.py | 2 +- documentation/architecture/system.md | 1 - documentation/models/choose-managed.md | 5 +- documentation/platforms/carina.md | 6 +- documentation/platforms/terra.md | 9 ++- documentation/reference/gpu-compatibility.md | 15 ++-- images/generic/local-runtime/snapshots.toml | 42 +++--------- images/generic/scripts/coding_agent_e2e.sh | 2 +- images/gpu/compatibility.toml | 68 ++++--------------- images/gpu/start_vllm.sh | 6 ++ .../tests/test_coding_agent_qualification.py | 2 +- .../compliance/tests/test_container_assets.py | 16 ++++- .../tests/test_model_source_verification.py | 4 -- .../src/heartwood/gateway/_openhands_sdk.py | 1 - packages/gateway/tests/test_local_models.py | 8 ++- .../gateway/tests/test_model_snapshots.py | 24 +++---- packages/gateway/tests/test_openhands_sdk.py | 11 --- 18 files changed, 81 insertions(+), 143 deletions(-) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index f6686f73..e0b5d95f 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -29,9 +29,7 @@ on: - terra-t4-qwen25-coder-7b-awq - terra-t4-qwen25-coder-14b-awq - terra-4xt4-qwen25-coder-32b-awq - - carina-l40s-qwen25-coder-7b-awq - carina-l40s-qwen3-coder-30b-fp8 - - carina-2xl40s-qwen3-coder-30b-bf16 - carina-4xl40s-qwen3-coder-next-fp8 - carina-2xl40s-gpt-oss-120b qualification_runner: diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index 63a69679..a57cda35 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -178,7 +178,7 @@ def _verify_configuration( ) if configuration.get("qualification_test") != _QUALIFICATION_TEST: raise CompatibilityError("GPU model uses an unsupported qualification test") - if configuration.get("agent_tool_mode") != "openhands-prompt": + if configuration.get("agent_tool_mode") != "openhands-native": raise CompatibilityError("GPU model uses an unsupported agent tool mode") for field in ( "gpu_count", diff --git a/documentation/architecture/system.md b/documentation/architecture/system.md index c3a011c2..85e3607b 100644 --- a/documentation/architecture/system.md +++ b/documentation/architecture/system.md @@ -49,7 +49,6 @@ The terminal, browser, and notebook bridge may render these differently, but the The adapter creates an OpenHands conversation with the selected LiteLLM-compatible model profile, project workspace, Skills, persistence directory, and action-confirmation callback. Heartwood translates OpenHands messages, tool proposals, decisions, and results into its stable event contract rather than duplicating the agent loop. -Loopback model routes use OpenHands' prompt-based tool conversion so models do not need to implement provider-native automatic tool selection; hosted provider profiles retain their native tool-calling behavior. ### Platform Adapter diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index b1236554..3ceaf8b3 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -49,8 +49,8 @@ The selection screen uses the release catalog as its authoritative source and re | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard, qualified on Terra | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | | Powerful, qualified on Terra | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Powerful candidate | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | -| Powerful candidate | Qwen3 Coder 30B BF16 | 56.88 GiB | 2 x 48 GB | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | +| Powerful, qualified on Terra | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | +| Powerful, qualified on Carina | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | | Maximum alternative candidate | GPT-OSS 120B MXFP4 | 60.79 GiB | 2 x 48 GB | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | @@ -60,6 +60,7 @@ Heartwood therefore uses conservative headroom and may choose a smaller context Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot compatible with the current runtime. Heartwood rejects that combination because vLLM's FP8 Mixture-of-Experts kernel cannot load the model on T4 hardware. +For four T4 GPUs, use the qualified dense Qwen2.5 Coder 32B AWQ configuration instead. ## Other Hugging Face Models diff --git a/documentation/platforms/carina.md b/documentation/platforms/carina.md index 3ab61723..82d928e6 100644 --- a/documentation/platforms/carina.md +++ b/documentation/platforms/carina.md @@ -92,13 +92,11 @@ Heartwood labels one **Recommended** only after its complete tool, approval, edi | Tier | Model Configuration | GPUs | Recommended RAM | Free Project Storage | Default Context | Estimated First Start | |---|---|---:|---:|---:|---:|---:| -| Standard fallback | Qwen2.5 Coder 7B AWQ | 1 x L40S | 32 GiB | 16 GiB | 32,768 | 2-8 minutes | -| Powerful | Qwen3 Coder 30B FP8 | 1 x L40S | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | -| Powerful, higher precision | Qwen3 Coder 30B BF16 | 2 x L40S | 128 GiB | 96 GiB | 65,536 | 4-12 minutes | +| Powerful, qualified | Qwen3 Coder 30B FP8 | 1 x L40S | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum capability | Qwen3 Coder Next FP8 | 4 x L40S | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | | Maximum alternative | GPT-OSS 120B MXFP4 | 2 x L40S | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | -Download sizes range from about 5.2 GiB for the Standard GPU model to 74.9 GiB for the largest candidate. +Download sizes range from about 29.1 GiB for the qualified one-GPU model to 74.9 GiB for the largest candidate. See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for complete sizes and [GPU Compatibility](../reference/gpu-compatibility.md) for exact revisions and runtime settings. For a short interactive session, normal `heartwood` startup selects Slurm's default compatible GPU partition. diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 67c314ae..d91b31b1 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -44,6 +44,7 @@ Use one of these combinations: | Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | | Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | | Managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | +| Managed 32B GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, four T4 GPUs with 16 GB each, 200 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. @@ -52,8 +53,8 @@ CPU inference is portable but can be too slow for an interactive coding workflow These are starting points rather than universal requirements. Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. -The qualified GPU path uses one T4 and offers release-pinned Qwen2.5 Coder 7B and 14B AWQ configurations. -The 14B model is the strongest qualified Terra recommendation; the 7B model is the lower-resource alternative. +The qualified GPU paths offer release-pinned Qwen2.5 Coder 7B and 14B AWQ configurations on one T4, plus Qwen2.5 Coder 32B AWQ on four T4s. +The 32B model is the strongest qualified Terra recommendation when four T4 GPUs are available; the 14B and 7B models are lower-cost alternatives on one T4. Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 snapshot compatible, so Heartwood does not offer that failed combination. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. @@ -114,12 +115,14 @@ The first-use flow confirms the project and asks where the model runs. - Choose OpenAI, Anthropic, or **Other compatible service** only when that endpoint is authorized for the intended data. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. -For managed GPU inference, choose the **Powerful** Qwen2.5 Coder 14B AWQ configuration that Heartwood labels **Recommended** for the detected T4. +For managed GPU inference on one T4, choose the **Powerful** Qwen2.5 Coder 14B AWQ configuration that Heartwood labels **Recommended**. +With four T4 GPUs, choose the **Powerful** Qwen2.5 Coder 32B AWQ configuration instead. Choose the **Standard** 7B configuration when you prefer a smaller download and lower memory pressure. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. +The pinned 32B AWQ snapshot downloads about 18.0 GiB; use at least 120 GB RAM and retain a 200 GB persistent disk for the four-T4 environment, model cache, notebooks, and results. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index d08da518..86bc4f20 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -33,13 +33,12 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | -| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | -| Carina | Standard fallback | 1 x L40S, 48 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 32,768 | CUDA graphs | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands prompt conversion | Candidate | -| Carina | Powerful | 2 x L40S, 48 GB each | [Qwen3-Coder-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct/tree/b2cff646eb4bb1d68355c01b18ae02e7cf42d120) | BF16 | 65,536 | CUDA graphs | 2 | `qwen3_coder` | OpenHands prompt conversion | Candidate | -| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands prompt conversion | Candidate | -| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | OpenHands prompt conversion | Candidate | +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands native tools | Qualified | +| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands native tools | Qualified | +| Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands native tools | Qualified | +| Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | +| Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | +| Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | OpenHands native tools | Candidate | All listed model repositories declare the Apache-2.0 license at the pinned revision. Confirm that a model's license and intended use remain suitable for the project before downloading it. @@ -53,7 +52,7 @@ A configuration becomes **qualified** only after the exact model revision and lo The acceptance test must establish all of the following: 1. the model loads and returns a direct inference response; -2. OpenHands converts the model response into a structured tool proposal; +2. OpenHands sends its native tool definitions and receives a structured proposal through the configured vLLM parser; 3. Heartwood presents the complete action set for approval; 4. approval executes the proposed operation and modifies only the synthetic project; 5. an independent check verifies the exact file result; diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 117a928d..542c5037 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -91,7 +91,7 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen3 Coder 30B FP8" precision = "FP8" tier = "powerful" -qualification = "candidate" +qualification = "qualified" minimum_gpu_count = 1 minimum_gpu_memory_bytes = 42000000000 recommended_ram_bytes = 103079215104 @@ -105,11 +105,13 @@ startup_seconds_max = 600 download_policy = "transformers-safetensors" minimum_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 12 CPU cores, 64 GB RAM, and 50 GB free project storage." recommended_resource_envelope = "One NVIDIA L40S-class GPU with 48 GB VRAM, 16 CPU cores, 96 GB RAM, and 64 GB free project storage." -recommended = false +validated_platforms = ["carina"] +qualification_test = "heartwood.coding-agent-e2e.v1" +recommended = true [snapshots.qwen25-coder-32b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful 32B coding-agent candidate for four 16 GB NVIDIA GPUs." +purpose = "Powerful 32B coding-agent model for four 16 GB NVIDIA GPUs." source_repository = "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ" source_revision = "1ed0a6145da0ce550c628e8e8b678f51e695995d" expected_size_bytes = 19328993904 @@ -119,7 +121,9 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 32B" precision = "AWQ int4" tier = "powerful" -qualification = "candidate" +qualification = "qualified" +validated_platforms = ["terra"] +qualification_test = "heartwood.coding-agent-e2e.v1" minimum_gpu_count = 4 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 128849018880 @@ -133,35 +137,7 @@ startup_seconds_max = 900 download_policy = "transformers-safetensors" minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 32 GB free project storage." recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 48 GB free project storage." -recommended = false - -[snapshots.qwen3-coder-30b-a3b-instruct-bf16-vllm] -runtime_profile = "vllm-cuda" -purpose = "Powerful higher-precision coding-agent model for two 48 GB NVIDIA GPUs." -source_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct" -source_revision = "b2cff646eb4bb1d68355c01b18ae02e7cf42d120" -expected_size_bytes = 61079793662 -minimum_free_bytes = 85899345920 -license_id = "Apache-2.0" -license_posture = "Apache-2.0; retain the model card and source provenance." -model_alias = "Qwen3 Coder 30B" -precision = "BF16" -tier = "powerful" -qualification = "candidate" -minimum_gpu_count = 2 -minimum_gpu_memory_bytes = 42000000000 -recommended_ram_bytes = 137438953472 -recommended_disk_bytes = 103079215104 -context_window = 65536 -maximum_context_window = 262144 -tool_call_parser = "qwen3_coder" -tensor_parallel_size = 2 -startup_seconds_min = 240 -startup_seconds_max = 720 -download_policy = "transformers-safetensors" -minimum_resource_envelope = "Two NVIDIA L40S-class GPUs with 48 GB VRAM each, 16 CPU cores, 96 GB RAM, and 80 GB free project storage." -recommended_resource_envelope = "Two NVIDIA L40S-class GPUs with 48 GB VRAM each, 24 CPU cores, 128 GB RAM, and 96 GB free project storage." -recommended = false +recommended = true [snapshots.qwen3-coder-next-fp8-vllm] runtime_profile = "vllm-cuda" diff --git a/images/generic/scripts/coding_agent_e2e.sh b/images/generic/scripts/coding_agent_e2e.sh index 357168b1..f219c5b8 100755 --- a/images/generic/scripts/coding_agent_e2e.sh +++ b/images/generic/scripts/coding_agent_e2e.sh @@ -111,7 +111,7 @@ run_heartwood models connect heartwood heartwood-managed-runtime | tee -a "${tra run_heartwood models validate heartwood | tee -a "${transcript}" run_heartwood actions set ask-every-time | tee -a "${transcript}" run_heartwood --session-id "${session_id}" \ - --prompt "Call the terminal tool to execute this exact command: python ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ + --prompt "Call the terminal tool to execute this exact command: ${heartwood_python} ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ | tee -a "${transcript}" for _ in 1 2 3 4; do diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index cb816274..336d55c4 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -33,7 +33,7 @@ precision = "AWQ int4" context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -60,7 +60,7 @@ precision = "AWQ int4" context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -70,32 +70,12 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 180 startup_seconds_max = 600 -[[configurations]] -configuration_id = "carina-l40s-qwen25-coder-7b-awq" -status = "candidate" -platform = "carina" -gpu_model = "NVIDIA L40S" -gpu_count = 1 -minimum_gpu_memory_bytes = 15000000000 -model_snapshot = "qwen25-coder-7b-instruct-awq-vllm" -model_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" -model_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" -precision = "AWQ int4" -context_window = 32768 -tensor_parallel_size = 1 -tool_call_parser = "hermes" -agent_tool_mode = "openhands-prompt" -vllm_version = "0.25.1+cu129" -pytorch_version = "2.11.0+cu129" -cuda_version = "12.9" -minimum_driver_version = "525.60.13" -qualification_test = "heartwood.coding-agent-e2e.v1" -startup_seconds_min = 120 -startup_seconds_max = 480 - [[configurations]] configuration_id = "carina-l40s-qwen3-coder-30b-fp8" -status = "candidate" +status = "qualified" +validated_at = "2026-07-21" +validated_driver_version = "590.48.01" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "carina" gpu_model = "NVIDIA L40S" gpu_count = 1 @@ -107,7 +87,7 @@ precision = "FP8" context_window = 32768 tensor_parallel_size = 1 tool_call_parser = "qwen3_coder" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -130,7 +110,10 @@ reason = "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions th [[configurations]] configuration_id = "terra-4xt4-qwen25-coder-32b-awq" -status = "candidate" +status = "qualified" +validated_at = "2026-07-21" +validated_driver_version = "535.154.05" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 4 @@ -142,7 +125,7 @@ precision = "AWQ int4" context_window = 32768 tensor_parallel_size = 4 tool_call_parser = "hermes" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -152,29 +135,6 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 240 startup_seconds_max = 900 -[[configurations]] -configuration_id = "carina-2xl40s-qwen3-coder-30b-bf16" -status = "candidate" -platform = "carina" -gpu_model = "NVIDIA L40S" -gpu_count = 2 -minimum_gpu_memory_bytes = 42000000000 -model_snapshot = "qwen3-coder-30b-a3b-instruct-bf16-vllm" -model_repository = "Qwen/Qwen3-Coder-30B-A3B-Instruct" -model_revision = "b2cff646eb4bb1d68355c01b18ae02e7cf42d120" -precision = "BF16" -context_window = 65536 -tensor_parallel_size = 2 -tool_call_parser = "qwen3_coder" -agent_tool_mode = "openhands-prompt" -vllm_version = "0.25.1+cu129" -pytorch_version = "2.11.0+cu129" -cuda_version = "12.9" -minimum_driver_version = "525.60.13" -qualification_test = "heartwood.coding-agent-e2e.v1" -startup_seconds_min = 240 -startup_seconds_max = 720 - [[configurations]] configuration_id = "carina-4xl40s-qwen3-coder-next-fp8" status = "candidate" @@ -189,7 +149,7 @@ precision = "FP8" context_window = 65536 tensor_parallel_size = 4 tool_call_parser = "qwen3_coder" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" @@ -212,7 +172,7 @@ precision = "MXFP4" context_window = 65536 tensor_parallel_size = 2 tool_call_parser = "openai" -agent_tool_mode = "openhands-prompt" +agent_tool_mode = "openhands-native" vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" diff --git a/images/gpu/start_vllm.sh b/images/gpu/start_vllm.sh index 15e59bfe..7d62109e 100755 --- a/images/gpu/start_vllm.sh +++ b/images/gpu/start_vllm.sh @@ -17,6 +17,7 @@ tensor_parallel_size="${HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE:-1}" gpu_memory_utilization="${HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION:-0.90}" enforce_eager="${HEARTWOOD_VLLM_ENFORCE_EAGER:-0}" vllm="${HEARTWOOD_VLLM_EXECUTABLE:-/opt/heartwood-vllm/bin/heartwood-vllm}" +flashinfer_sampler="${HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER:-0}" if [[ "${host}" != "127.0.0.1" && "${host}" != "localhost" && "${host}" != "::1" ]]; then echo "vLLM must bind to loopback, got ${host}" >&2 @@ -30,6 +31,10 @@ if [[ ! -x "${vllm}" ]]; then echo "vLLM executable is unavailable: ${vllm}" >&2 exit 69 fi +if [[ "${flashinfer_sampler}" != "0" && "${flashinfer_sampler}" != "1" ]]; then + echo "HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER must be 0 or 1" >&2 + exit 64 +fi arguments=( serve "${model_path}" @@ -49,4 +54,5 @@ elif [[ "${enforce_eager}" != "0" ]]; then exit 64 fi +export VLLM_USE_FLASHINFER_SAMPLER="${flashinfer_sampler}" exec "${vllm}" "${arguments[@]}" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 68ddf93a..dbc0482c 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -263,7 +263,7 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "hermes" - assert resolved["configuration"]["agent_tool_mode"] == "openhands-prompt" + assert resolved["configuration"]["agent_tool_mode"] == "openhands-native" assert resolved["configuration"]["context_window"] == 18_432 assert resolved["configuration"]["enforce_eager"] is True assert resolved["configuration"]["model_revision"] == ( diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 6aa1e705..ca06cbb8 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -325,7 +325,8 @@ def test_gpu_runtime_is_isolated_pinned_and_no_weight() -> None: assert 'host="${HEARTWOOD_LOCAL_RUNTIME_HOST:-127.0.0.1}"' in launcher assert "--enable-auto-tool-choice" in launcher assert 'tool_parser="${HEARTWOOD_VLLM_TOOL_PARSER:-hermes}"' in launcher - assert "VLLM_USE_FLASHINFER_SAMPLER" not in launcher + assert 'flashinfer_sampler="${HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER:-0}"' in launcher + assert 'export VLLM_USE_FLASHINFER_SAMPLER="${flashinfer_sampler}"' in launcher assert "huggingface.co" not in launcher assert "/opt/heartwood-vllm/bin/heartwood-vllm" in launcher assert "/opt/heartwood-vllm/bin/python" in verifier @@ -414,9 +415,14 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non model = tmp_path / "model" model.mkdir() arguments = tmp_path / "arguments.txt" + environment = tmp_path / "environment.txt" executable = tmp_path / "vllm" executable.write_text( - f"#!/usr/bin/env bash\nprintf '%s\\n' \"$@\" > {arguments}\n", + ( + "#!/usr/bin/env bash\n" + f"printf '%s\\n' \"$@\" > {arguments}\n" + f"printf '%s\\n' \"$VLLM_USE_FLASHINFER_SAMPLER\" > {environment}\n" + ), encoding="utf-8", ) executable.chmod(0o755) @@ -436,6 +442,7 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert values[values.index("--host") + 1] == "127.0.0.1" assert values[values.index("--served-model-name") + 1] == "test-model" assert values[values.index("--tool-call-parser") + 1] == "hermes" + assert environment.read_text(encoding="utf-8") == "0\n" env["HEARTWOOD_VLLM_ENFORCE_EAGER"] = "1" eager = subprocess.run(["bash", str(script)], env=env, check=False) @@ -447,6 +454,11 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert invalid.returncode == 64 env.pop("HEARTWOOD_VLLM_ENFORCE_EAGER") + env["HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER"] = "invalid" + invalid_sampler = subprocess.run(["bash", str(script)], env=env, check=False) + assert invalid_sampler.returncode == 64 + + env.pop("HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER") env["HEARTWOOD_LOCAL_RUNTIME_HOST"] = "0.0.0.0" denied = subprocess.run(["bash", str(script)], env=env, check=False) assert denied.returncode == 64 diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index 8d2527ec..d3b2352b 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -48,10 +48,6 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ", "1ed0a6145da0ce550c628e8e8b678f51e695995d", ), - "qwen3-coder-30b-a3b-instruct-bf16-vllm": ( - "Qwen/Qwen3-Coder-30B-A3B-Instruct", - "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", - ), "qwen3-coder-30b-a3b-instruct-fp8-vllm": ( "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", diff --git a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py index 1757b5ab..ac8e4e07 100644 --- a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py +++ b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py @@ -546,7 +546,6 @@ def _llm_options( "aws_profile_name": profile.aws_profile_name, "max_input_tokens": profile.max_input_tokens, "max_output_tokens": profile.max_output_tokens, - "native_tool_calling": not profile.is_local, "max_message_chars": _llm_max_message_chars(profile), "log_completions": False, "litellm_extra_body": dict(extra_body) or None, diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index 5f6e25b5..5daf0941 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -470,6 +470,8 @@ def test_central_catalog_exposes_only_recommended_models() -> None: "qwen25-7b-instruct-q4_k_m", "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", + "qwen25-coder-32b-instruct-awq-vllm", + "qwen3-coder-30b-a3b-instruct-fp8-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) assert {choice.context_window for choice in choices} == {18_432, 32_768} @@ -478,8 +480,8 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert { "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", + "qwen25-coder-32b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", - "qwen3-coder-30b-a3b-instruct-bf16-vllm", "qwen3-coder-next-fp8-vllm", "gpt-oss-120b-vllm", } <= {choice.model_id for choice in downloadable} @@ -487,6 +489,8 @@ def test_central_catalog_exposes_only_recommended_models() -> None: gpu_choices = {choice.model_id: choice for choice in downloadable if choice.runtime == "vllm"} assert gpu_choices["qwen25-coder-7b-instruct-awq-vllm"].qualification == "qualified" assert gpu_choices["qwen25-coder-14b-instruct-awq-vllm"].qualification == "qualified" + assert gpu_choices["qwen25-coder-32b-instruct-awq-vllm"].qualification == "qualified" + assert gpu_choices["qwen3-coder-30b-a3b-instruct-fp8-vllm"].qualification == "qualified" assert all( choice.qualification == "candidate" for model_id, choice in gpu_choices.items() @@ -494,6 +498,8 @@ def test_central_catalog_exposes_only_recommended_models() -> None: not in { "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", + "qwen25-coder-32b-instruct-awq-vllm", + "qwen3-coder-30b-a3b-instruct-fp8-vllm", } ) diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 110e5486..f9cca17b 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -62,14 +62,6 @@ 1, "qwen3_coder", ), - ( - "qwen3-coder-30b-a3b-instruct-bf16-vllm", - "Qwen/Qwen3-Coder-30B-A3B-Instruct", - "b2cff646eb4bb1d68355c01b18ae02e7cf42d120", - "powerful", - 2, - "qwen3_coder", - ), ( "qwen3-coder-next-fp8-vllm", "Qwen/Qwen3-Coder-Next-FP8", @@ -116,12 +108,17 @@ def test_repository_snapshot_catalog_pins_gpu_model_variants( in { "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", + "qwen25-coder-32b-instruct-awq-vllm", + "qwen3-coder-30b-a3b-instruct-fp8-vllm", } else "candidate" ) assert snapshot.qualification == expected_qualification if expected_qualification == "qualified": - assert snapshot.validated_platforms == ("terra",) + expected_platform = ( + "carina" if snapshot_id == "qwen3-coder-30b-a3b-instruct-fp8-vllm" else "terra" + ) + assert snapshot.validated_platforms == (expected_platform,) assert snapshot.qualification_test == "heartwood.coding-agent-e2e.v1" assert snapshot.recommended is True else: @@ -257,11 +254,10 @@ def test_catalog_capacity_recommendation_prefers_more_parallelism_within_tier() recommended=True, ) dual = replace( - source.snapshot("qwen3-coder-30b-a3b-instruct-bf16-vllm"), - qualification="qualified", - validated_platforms=("carina",), - qualification_test="heartwood.coding-agent-e2e.v1", - recommended=True, + single, + snapshot_id="synthetic-dual-gpu-model", + minimum_gpu_count=2, + tensor_parallel_size=2, ) catalog = ModelSnapshotCatalog(source.schema_version, (single, dual)) diff --git a/packages/gateway/tests/test_openhands_sdk.py b/packages/gateway/tests/test_openhands_sdk.py index a437e45e..67a039bf 100644 --- a/packages/gateway/tests/test_openhands_sdk.py +++ b/packages/gateway/tests/test_openhands_sdk.py @@ -189,19 +189,8 @@ def test_openhands_forwards_managed_model_request_defaults() -> None: assert options["litellm_extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}} assert options["max_input_tokens"] == 16_384 assert options["max_output_tokens"] == 2_048 - assert options["native_tool_calling"] is False assert options["input_cost_per_token"] == 0.0 - hosted = ModelProfile( - profile_id="hosted", - model="openai/model", - policy_endpoint="https://api.openai.com/v1/chat/completions", - credential_kind="environment", - api_key_env="OPENAI_API_KEY", - ) - hosted_options = _llm_options(hosted, api_key="test-key", extra_body={}) - assert hosted_options["native_tool_calling"] is True - def test_openhands_context_condenser_uses_the_active_model_budget() -> None: copied: dict[str, object] = {} From e76969a076139e0180f95ec45600029d05ff2c36 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 19:39:41 -0700 Subject: [PATCH 23/38] Cover FlashInfer Sampler Opt-In --- packages/compliance/tests/test_container_assets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index ca06cbb8..c5ab6811 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -454,6 +454,11 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert invalid.returncode == 64 env.pop("HEARTWOOD_VLLM_ENFORCE_EAGER") + env["HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER"] = "1" + enabled_sampler = subprocess.run(["bash", str(script)], env=env, check=False) + assert enabled_sampler.returncode == 0 + assert environment.read_text(encoding="utf-8") == "1\n" + env["HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER"] = "invalid" invalid_sampler = subprocess.run(["bash", str(script)], env=env, check=False) assert invalid_sampler.returncode == 64 From d4867fddfaabe36e4b45806c2994193adf883e7b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 19:55:02 -0700 Subject: [PATCH 24/38] Restore Native Tool Browser Expectations --- packages/webui/scripts/smoke-reference-analysis.cjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webui/scripts/smoke-reference-analysis.cjs b/packages/webui/scripts/smoke-reference-analysis.cjs index c7b18f23..07d2725d 100644 --- a/packages/webui/scripts/smoke-reference-analysis.cjs +++ b/packages/webui/scripts/smoke-reference-analysis.cjs @@ -225,9 +225,9 @@ async function main() { .getByRole("button", { name: "Activity & audit", exact: true }) .click(); await expect(page.getByText("Tool execution", { exact: true })).toHaveCount( - 4, + 5, ); - await expect(page.getByText("exit=0", { exact: true })).toHaveCount(3); + await expect(page.getByText("exit=0", { exact: true })).toHaveCount(4); await expect(page.getByText("exit=1", { exact: true })).toHaveCount(1); await page.getByRole("button", { name: "Close", exact: true }).click(); @@ -259,7 +259,7 @@ async function main() { const replay = runCli("--session-id", sessionId, "replay"); if ( !replay.includes("Action set approved") || - replay.match(/Tool terminal exit=0/gu)?.length !== 3 || + replay.match(/Tool terminal exit=0/gu)?.length !== 4 || replay.match(/Tool terminal exit=1/gu)?.length !== 1 || !replay.includes( "Agent: The synthetic target-condition cohort summary is ready for review.", From b79343d57a90c30ac573984e7f086c6da3a0119b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 20:26:09 -0700 Subject: [PATCH 25/38] Select OpenHands Tool Mode By Model --- deploy/verify_gpu_compatibility.py | 12 +++++++++++- documentation/reference/gpu-compatibility.md | 8 ++++---- images/gpu/compatibility.toml | 6 +++--- .../tests/test_coding_agent_qualification.py | 2 +- packages/gateway/src/heartwood/gateway/_gateway.py | 5 +++++ .../gateway/src/heartwood/gateway/_openhands_sdk.py | 5 +++++ packages/gateway/tests/test_openhands_sdk.py | 10 ++++++++++ 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index a57cda35..cb95e228 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -178,8 +178,18 @@ def _verify_configuration( ) if configuration.get("qualification_test") != _QUALIFICATION_TEST: raise CompatibilityError("GPU model uses an unsupported qualification test") - if configuration.get("agent_tool_mode") != "openhands-native": + if configuration.get("agent_tool_mode") not in { + "openhands-native", + "openhands-prompt-conversion", + }: raise CompatibilityError("GPU model uses an unsupported agent tool mode") + expected_tool_mode = ( + "openhands-native" + if configuration.get("tool_call_parser") in {"openai", "qwen3_coder"} + else "openhands-prompt-conversion" + ) + if configuration.get("agent_tool_mode") != expected_tool_mode: + raise CompatibilityError("GPU model agent tool mode disagrees with its parser") for field in ( "gpu_count", "minimum_gpu_memory_bytes", diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 86bc4f20..fa5a4401 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -33,9 +33,9 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands native tools | Qualified | -| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands native tools | Qualified | -| Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands native tools | Qualified | +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | +| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | +| Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Qualified | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | | Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | OpenHands native tools | Candidate | @@ -52,7 +52,7 @@ A configuration becomes **qualified** only after the exact model revision and lo The acceptance test must establish all of the following: 1. the model loads and returns a direct inference response; -2. OpenHands sends its native tool definitions and receives a structured proposal through the configured vLLM parser; +2. OpenHands uses the catalog-qualified tool mode: native structured tools for supported parsers or its prompt-conversion path for models that do not reliably emit native calls; 3. Heartwood presents the complete action set for approval; 4. approval executes the proposed operation and modifies only the synthetic project; 5. an independent check verifies the exact file result; diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 336d55c4..251a529a 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -33,7 +33,7 @@ precision = "AWQ int4" context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" -agent_tool_mode = "openhands-native" +agent_tool_mode = "openhands-prompt-conversion" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -60,7 +60,7 @@ precision = "AWQ int4" context_window = 18432 tensor_parallel_size = 1 tool_call_parser = "hermes" -agent_tool_mode = "openhands-native" +agent_tool_mode = "openhands-prompt-conversion" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" @@ -125,7 +125,7 @@ precision = "AWQ int4" context_window = 32768 tensor_parallel_size = 4 tool_call_parser = "hermes" -agent_tool_mode = "openhands-native" +agent_tool_mode = "openhands-prompt-conversion" enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index dbc0482c..3fa98155 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -263,7 +263,7 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "hermes" - assert resolved["configuration"]["agent_tool_mode"] == "openhands-native" + assert resolved["configuration"]["agent_tool_mode"] == "openhands-prompt-conversion" assert resolved["configuration"]["context_window"] == 18_432 assert resolved["configuration"]["enforce_eager"] is True assert resolved["configuration"]["model_revision"] == ( diff --git a/packages/gateway/src/heartwood/gateway/_gateway.py b/packages/gateway/src/heartwood/gateway/_gateway.py index d8b19732..2f5c46a5 100644 --- a/packages/gateway/src/heartwood/gateway/_gateway.py +++ b/packages/gateway/src/heartwood/gateway/_gateway.py @@ -1114,6 +1114,11 @@ def _backend( llm_extra_body=managed_model_request_body( selected_model.model_type if selected_model is not None else None ), + native_tool_calling=( + selected_model.tool_call_parser in {"openai", "qwen3_coder"} + if selected_model is not None + else None + ), ) def _policy_profile(self) -> PolicyProfile: diff --git a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py index ac8e4e07..bc87e23a 100644 --- a/packages/gateway/src/heartwood/gateway/_openhands_sdk.py +++ b/packages/gateway/src/heartwood/gateway/_openhands_sdk.py @@ -129,6 +129,7 @@ def __init__( action_confirmation_mode: ActionConfirmationMode = "always-confirm", env: Mapping[str, str] | None = None, llm_extra_body: Mapping[str, object] | None = None, + native_tool_calling: bool | None = None, conversation_factory: ConversationFactory | None = None, ) -> None: profile.validate() @@ -145,6 +146,7 @@ def __init__( self._credential_environment_names = tuple(sorted(set(credential_environment_names))) self.env = env self._llm_extra_body = dict(llm_extra_body or {}) + self._native_tool_calling = native_tool_calling self._captured: list[object] = [] self._pending: dict[str, ProposedToolCall] = {} self._security_analyzer: _SecurityAnalyzer | None = None @@ -320,6 +322,7 @@ def _default_conversation_factory( # pragma: no cover - container integration self.profile, api_key=api_key, extra_body=self._llm_extra_body, + native_tool_calling=self._native_tool_calling, ) ) context = _agent_context(sdk, skills) @@ -535,6 +538,7 @@ def _llm_options( *, api_key: str | None, extra_body: Mapping[str, object], + native_tool_calling: bool | None = None, ) -> dict[str, Any]: """Build the complete OpenHands LLM configuration for one model profile.""" options: dict[str, Any] = { @@ -549,6 +553,7 @@ def _llm_options( "max_message_chars": _llm_max_message_chars(profile), "log_completions": False, "litellm_extra_body": dict(extra_body) or None, + "native_tool_calling": native_tool_calling, **_llm_resilience_options(profile), } if profile.is_local: diff --git a/packages/gateway/tests/test_openhands_sdk.py b/packages/gateway/tests/test_openhands_sdk.py index 67a039bf..7c57b390 100644 --- a/packages/gateway/tests/test_openhands_sdk.py +++ b/packages/gateway/tests/test_openhands_sdk.py @@ -184,12 +184,22 @@ def test_openhands_forwards_managed_model_request_defaults() -> None: profile, api_key=None, extra_body={"chat_template_kwargs": {"enable_thinking": False}}, + native_tool_calling=False, ) assert options["litellm_extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}} assert options["max_input_tokens"] == 16_384 assert options["max_output_tokens"] == 2_048 assert options["input_cost_per_token"] == 0.0 + assert options["native_tool_calling"] is False + + native_options = _llm_options( + profile, + api_key=None, + extra_body={}, + native_tool_calling=True, + ) + assert native_options["native_tool_calling"] is True def test_openhands_context_condenser_uses_the_active_model_budget() -> None: From 5a380f9fbc3f83b4347721bba44e0dd013b078aa Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 21:00:43 -0700 Subject: [PATCH 26/38] Refine Terra Model Qualification Catalog --- images/generic/local-runtime/snapshots.toml | 80 +++++++++++++++---- images/gpu/compatibility.toml | 44 ++++++++-- .../tests/test_coding_agent_qualification.py | 28 +++++-- .../tests/test_model_source_verification.py | 8 ++ .../gateway/tests/test_gateway_contract.py | 10 ++- packages/gateway/tests/test_local_models.py | 21 +++-- .../gateway/tests/test_model_snapshots.py | 30 ++++--- 7 files changed, 169 insertions(+), 52 deletions(-) diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 542c5037..0a445c02 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -21,7 +21,7 @@ ignore_patterns = ["*.bin", "*.py", ".git/*", "metal/*", "original/*"] [snapshots.qwen25-coder-7b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Standard coding-agent model for a single 16 GB NVIDIA GPU." +purpose = "Standard single-GPU coding model candidate; Heartwood tool-use qualification is pending a native tool parser." source_repository = "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ" source_revision = "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" expected_size_bytes = 5582398811 @@ -31,9 +31,7 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 7B" precision = "AWQ int4" tier = "standard" -qualification = "qualified" -validated_platforms = ["terra"] -qualification_test = "heartwood.coding-agent-e2e.v1" +qualification = "candidate" minimum_gpu_count = 1 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 34359738368 @@ -47,11 +45,11 @@ startup_seconds_max = 480 download_policy = "transformers-safetensors" minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 CPU cores, 32 GB RAM, and 12 GB free project storage." recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 8 or more CPU cores, 32 GB RAM, and 16 GB free project storage." -recommended = true +recommended = false [snapshots.qwen25-coder-14b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful coding-agent model qualified for one 16 GB NVIDIA GPU." +purpose = "Powerful single-GPU coding model candidate; Heartwood tool-use qualification is pending a native tool parser." source_repository = "Qwen/Qwen2.5-Coder-14B-Instruct-AWQ" source_revision = "eb3172f06a6d6b3a15f08947b0668d782e4d2d2c" expected_size_bytes = 9991784616 @@ -61,9 +59,7 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 14B" precision = "AWQ int4" tier = "powerful" -qualification = "qualified" -validated_platforms = ["terra"] -qualification_test = "heartwood.coding-agent-e2e.v1" +qualification = "candidate" minimum_gpu_count = 1 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 64424509440 @@ -77,7 +73,7 @@ startup_seconds_max = 600 download_policy = "transformers-safetensors" minimum_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 12 CPU cores, 48 GB RAM, and 20 GB free project storage." recommended_resource_envelope = "One NVIDIA T4-class GPU with 16 GB VRAM, 16 CPU cores, 60 GB RAM, and 32 GB free project storage." -recommended = true +recommended = false [snapshots.qwen3-coder-30b-a3b-instruct-fp8-vllm] runtime_profile = "vllm-cuda" @@ -109,9 +105,37 @@ validated_platforms = ["carina"] qualification_test = "heartwood.coding-agent-e2e.v1" recommended = true +[snapshots.qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm] +runtime_profile = "vllm-cuda" +purpose = "Powerful native tool-calling coding-agent candidate for four 16 GB NVIDIA GPUs." +source_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" +source_revision = "e69e73813144d9b715648d8384b3f2c035397411" +expected_size_bytes = 18049253118 +minimum_free_bytes = 30064771072 +license_id = "Apache-2.0" +license_posture = "Apache-2.0; retain the model card and source provenance." +model_alias = "Qwen3 Coder 30B AWQ" +precision = "W4A16 AWQ" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 128849018880 +recommended_disk_bytes = 53687091200 +context_window = 32768 +maximum_context_window = 262144 +tool_call_parser = "qwen3_coder" +tensor_parallel_size = 4 +startup_seconds_min = 240 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 30 GB free project storage." +recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 50 GB free project storage." +recommended = false + [snapshots.qwen25-coder-32b-instruct-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful 32B coding-agent model for four 16 GB NVIDIA GPUs." +purpose = "Powerful 32B coding model candidate for four 16 GB NVIDIA GPUs; Heartwood tool-use qualification is pending a native tool parser." source_repository = "Qwen/Qwen2.5-Coder-32B-Instruct-AWQ" source_revision = "1ed0a6145da0ce550c628e8e8b678f51e695995d" expected_size_bytes = 19328993904 @@ -121,9 +145,7 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen2.5 Coder 32B" precision = "AWQ int4" tier = "powerful" -qualification = "qualified" -validated_platforms = ["terra"] -qualification_test = "heartwood.coding-agent-e2e.v1" +qualification = "candidate" minimum_gpu_count = 4 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 128849018880 @@ -137,7 +159,35 @@ startup_seconds_max = 900 download_policy = "transformers-safetensors" minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 32 GB free project storage." recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 48 GB free project storage." -recommended = true +recommended = false + +[snapshots.gpt-oss-20b-vllm] +runtime_profile = "vllm-cuda" +purpose = "Powerful native tool-calling coding-agent model for four 16 GB NVIDIA GPUs." +source_repository = "openai/gpt-oss-20b" +source_revision = "6cee5e81ee83917806bbde320786a8fb61efebee" +expected_size_bytes = 13789262904 +minimum_free_bytes = 21474836480 +license_id = "Apache-2.0" +license_posture = "Apache-2.0 with the source repository usage policy; retain both with provenance." +model_alias = "GPT-OSS 20B" +precision = "MXFP4" +tier = "powerful" +qualification = "candidate" +minimum_gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +recommended_ram_bytes = 128849018880 +recommended_disk_bytes = 42949672960 +context_window = 32768 +maximum_context_window = 131072 +tool_call_parser = "openai" +tensor_parallel_size = 4 +startup_seconds_min = 240 +startup_seconds_max = 900 +download_policy = "transformers-safetensors" +minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 20 GB free project storage." +recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 40 GB free project storage." +recommended = false [snapshots.qwen3-coder-next-fp8-vllm] runtime_profile = "vllm-cuda" diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 251a529a..26051543 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -18,8 +18,7 @@ cuda_13_qualified = false [[configurations]] configuration_id = "terra-t4-qwen25-coder-7b-awq" -status = "qualified" -validated_at = "2026-07-21" +status = "candidate" validated_driver_version = "535.154.05" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" @@ -45,8 +44,7 @@ startup_seconds_max = 480 [[configurations]] configuration_id = "terra-t4-qwen25-coder-14b-awq" -status = "qualified" -validated_at = "2026-07-21" +status = "candidate" validated_driver_version = "535.154.05" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" @@ -108,10 +106,32 @@ validated_at = "2026-07-21" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" reason = "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions that this model does not provide on NVIDIA T4 GPUs." +[[configurations]] +configuration_id = "terra-4xt4-qwen3-coder-30b-awq" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 4 +minimum_gpu_memory_bytes = 15000000000 +model_snapshot = "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm" +model_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" +model_revision = "e69e73813144d9b715648d8384b3f2c035397411" +precision = "W4A16 AWQ" +context_window = 32768 +tensor_parallel_size = 4 +tool_call_parser = "qwen3_coder" +agent_tool_mode = "openhands-native" +vllm_version = "0.25.1+cu129" +pytorch_version = "2.11.0+cu129" +cuda_version = "12.9" +minimum_driver_version = "525.60.13" +qualification_test = "heartwood.coding-agent-e2e.v1" +startup_seconds_min = 240 +startup_seconds_max = 900 + [[configurations]] configuration_id = "terra-4xt4-qwen25-coder-32b-awq" -status = "qualified" -validated_at = "2026-07-21" +status = "candidate" validated_driver_version = "535.154.05" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" @@ -135,6 +155,18 @@ qualification_test = "heartwood.coding-agent-e2e.v1" startup_seconds_min = 240 startup_seconds_max = 900 +[[unsupported_configurations]] +configuration_id = "terra-4xt4-gpt-oss-20b" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 4 +model_repository = "openai/gpt-oss-20b" +model_revision = "6cee5e81ee83917806bbde320786a8fb61efebee" +vllm_version = "0.25.1+cu129" +validated_at = "2026-07-21" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" +reason = "vLLM rejects GPT-OSS MXFP4 on NVIDIA T4 because it requires compute capability 8.0 and the T4 provides compute capability 7.5." + [[configurations]] configuration_id = "carina-4xl40s-qwen3-coder-next-fp8" status = "candidate" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 3fa98155..491b57b6 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -258,16 +258,16 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: resolved = load( _root() / "images/gpu/compatibility.toml", - "terra-t4-qwen25-coder-7b-awq", + "terra-4xt4-qwen3-coder-30b-awq", ) assert resolved["runtime"]["cuda_version"] == "12.9" - assert resolved["configuration"]["tool_call_parser"] == "hermes" - assert resolved["configuration"]["agent_tool_mode"] == "openhands-prompt-conversion" - assert resolved["configuration"]["context_window"] == 18_432 - assert resolved["configuration"]["enforce_eager"] is True + assert resolved["configuration"]["tool_call_parser"] == "qwen3_coder" + assert resolved["configuration"]["agent_tool_mode"] == "openhands-native" + assert resolved["configuration"]["context_window"] == 32_768 + assert "enforce_eager" not in resolved["configuration"] assert resolved["configuration"]["model_revision"] == ( - "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a" + "e69e73813144d9b715648d8384b3f2c035397411" ) @@ -285,6 +285,7 @@ def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: assert {configuration["configuration_id"] for configuration in configurations} == { "terra-t4-qwen25-coder-7b-awq", "terra-t4-qwen25-coder-14b-awq", + "terra-4xt4-qwen3-coder-30b-awq", "terra-4xt4-qwen25-coder-32b-awq", } @@ -369,6 +370,21 @@ def test_gpu_compatibility_records_rejected_terra_configuration() -> None: "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions " "that this model does not provide on NVIDIA T4 GPUs." ), + }, + { + "configuration_id": "terra-4xt4-gpt-oss-20b", + "platform": "terra", + "gpu_model": "NVIDIA T4", + "gpu_count": 4, + "model_repository": "openai/gpt-oss-20b", + "model_revision": "6cee5e81ee83917806bbde320786a8fb61efebee", + "vllm_version": "0.25.1+cu129", + "validated_at": "2026-07-21", + "evidence": "https://github.com/SchmiedmayerLab/heartwood/pull/72", + "reason": ( + "vLLM rejects GPT-OSS MXFP4 on NVIDIA T4 because it requires compute " + "capability 8.0 and the T4 provides compute capability 7.5." + ), } ] diff --git a/packages/compliance/tests/test_model_source_verification.py b/packages/compliance/tests/test_model_source_verification.py index d3b2352b..c9030425 100644 --- a/packages/compliance/tests/test_model_source_verification.py +++ b/packages/compliance/tests/test_model_source_verification.py @@ -36,6 +36,10 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "openai/gpt-oss-120b", "b5c939de8f754692c1647ca79fbf85e8c1e70f8a", ), + "gpt-oss-20b-vllm": ( + "openai/gpt-oss-20b", + "6cee5e81ee83917806bbde320786a8fb61efebee", + ), "qwen25-coder-7b-instruct-awq-vllm": ( "Qwen/Qwen2.5-Coder-7B-Instruct-AWQ", "8e8ed243bbe6f9a5aff549a0924562fc719b2b8a", @@ -52,6 +56,10 @@ def test_repository_model_sources_are_immutable_and_use_revision_routes() -> Non "Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8", "dcaee4d4dfc5ee71ad501f01f530e5652438fde0", ), + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm": ( + "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ", + "e69e73813144d9b715648d8384b3f2c035397411", + ), "qwen3-coder-next-fp8-vllm": ( "Qwen/Qwen3-Coder-Next-FP8", "da6e2ed27304dd39abadd9c82ef50e8de67bdd4c", diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index 01f95457..e08af33f 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -861,15 +861,17 @@ def without_packaged_runtimes(path: Path) -> bool: terra_standard = next( model for model in terra_models if model["model_id"] == "qwen25-coder-7b-instruct-awq-vllm" ) - assert terra_standard["qualification"] == "qualified" + assert terra_standard["qualification"] == "candidate" assert str(terra_standard["availability_reason"]).startswith( - "Compatible with 1 visible NVIDIA T4 GPU(s)" + "Evaluation candidate; not yet a recommended model" ) terra_powerful = next( model for model in terra_models if model["model_id"] == "qwen25-coder-14b-instruct-awq-vllm" ) - assert terra_powerful["qualification"] == "qualified" - assert str(terra_powerful["availability_reason"]).startswith("Recommended for this deployment") + assert terra_powerful["qualification"] == "candidate" + assert str(terra_powerful["availability_reason"]).startswith( + "Evaluation candidate; not yet a recommended model" + ) assert "Compatible with 1 visible NVIDIA T4 GPU(s)" in str( terra_standard["availability_reason"] ) diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index 5daf0941..fcb48d32 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -468,37 +468,30 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert {choice.model_id for choice in choices} == { "qwen25-7b-instruct-q4_k_m", - "qwen25-coder-7b-instruct-awq-vllm", - "qwen25-coder-14b-instruct-awq-vllm", - "qwen25-coder-32b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) - assert {choice.context_window for choice in choices} == {18_432, 32_768} + assert {choice.context_window for choice in choices} == {32_768} assert "llama-cpp-stories260k-ci" in {choice.model_id for choice in downloadable} assert "qwen25-coder-7b-instruct-q4_k_m" in {choice.model_id for choice in downloadable} assert { "qwen25-coder-7b-instruct-awq-vllm", "qwen25-coder-14b-instruct-awq-vllm", "qwen25-coder-32b-instruct-awq-vllm", + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", + "gpt-oss-20b-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", "qwen3-coder-next-fp8-vllm", "gpt-oss-120b-vllm", } <= {choice.model_id for choice in downloadable} assert all(choice.catalog_source == "catalog" for choice in downloadable) gpu_choices = {choice.model_id: choice for choice in downloadable if choice.runtime == "vllm"} - assert gpu_choices["qwen25-coder-7b-instruct-awq-vllm"].qualification == "qualified" - assert gpu_choices["qwen25-coder-14b-instruct-awq-vllm"].qualification == "qualified" - assert gpu_choices["qwen25-coder-32b-instruct-awq-vllm"].qualification == "qualified" assert gpu_choices["qwen3-coder-30b-a3b-instruct-fp8-vllm"].qualification == "qualified" assert all( choice.qualification == "candidate" for model_id, choice in gpu_choices.items() if model_id not in { - "qwen25-coder-7b-instruct-awq-vllm", - "qwen25-coder-14b-instruct-awq-vllm", - "qwen25-coder-32b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", } ) @@ -515,14 +508,18 @@ def test_catalog_qualification_is_scoped_to_the_validated_platform() -> None: cpu = catalog_model_choices(artifacts.artifacts, snapshots.snapshots)[0] terra_gpu = next( choice - for choice in catalog_model_choices(artifacts.artifacts, snapshots.snapshots) + for choice in catalog_model_choices( + artifacts.artifacts, + snapshots.snapshots, + recommended_only=False, + ) if choice.model_id == "qwen25-coder-14b-instruct-awq-vllm" ) assert cpu.qualification_for("generic") == "qualified" assert cpu.qualification_for("terra") == "qualified" assert cpu.qualification_for("carina") == "candidate" - assert terra_gpu.qualification_for("terra") == "qualified" + assert terra_gpu.qualification_for("terra") == "candidate" assert terra_gpu.qualification_for("carina") == "candidate" diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index f9cca17b..3d0f253f 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -70,6 +70,22 @@ 4, "qwen3_coder", ), + ( + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", + "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ", + "e69e73813144d9b715648d8384b3f2c035397411", + "powerful", + 4, + "qwen3_coder", + ), + ( + "gpt-oss-20b-vllm", + "openai/gpt-oss-20b", + "6cee5e81ee83917806bbde320786a8fb61efebee", + "powerful", + 4, + "openai", + ), ( "gpt-oss-120b-vllm", "openai/gpt-oss-120b", @@ -106,9 +122,6 @@ def test_repository_snapshot_catalog_pins_gpu_model_variants( "qualified" if snapshot_id in { - "qwen25-coder-7b-instruct-awq-vllm", - "qwen25-coder-14b-instruct-awq-vllm", - "qwen25-coder-32b-instruct-awq-vllm", "qwen3-coder-30b-a3b-instruct-fp8-vllm", } else "candidate" @@ -173,8 +186,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> source = load_model_snapshot_catalog( _repo_root() / "images" / "generic" / "local-runtime" / "snapshots.toml" ) - standard = source.snapshot("qwen25-coder-7b-instruct-awq-vllm") - terra_powerful = source.snapshot("qwen25-coder-14b-instruct-awq-vllm") + terra_candidate = source.snapshot("qwen25-coder-14b-instruct-awq-vllm") powerful = replace( source.snapshot("qwen3-coder-30b-a3b-instruct-fp8-vllm"), qualification="qualified", @@ -184,7 +196,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> ) catalog = ModelSnapshotCatalog( source.schema_version, - (standard, terra_powerful, powerful), + (terra_candidate, powerful), ) assert ( @@ -194,7 +206,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> gpu_memory_bytes=16_000_000_000, maximum_tier="maximum", ) - == terra_powerful + is None ) assert ( catalog.recommend( @@ -221,7 +233,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> gpu_memory_bytes=16_000_000_000, maximum_tier="maximum", ) - == terra_powerful + is None ) assert ( @@ -230,7 +242,7 @@ def test_catalog_recommends_only_qualified_models_with_compatible_resources() -> capacities=((1, 16_000_000_000), (1, 48_000_000_000)), maximum_tier="maximum", ) - == terra_powerful + is None ) assert ( catalog.recommend_for_capacities( From c4adc66fca11081a9b818ef34ad0640d1570ecc0 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 21:49:52 -0700 Subject: [PATCH 27/38] Add GPU Capability Preflight Checks --- deploy/verify_gpu_compatibility.py | 14 ++ documentation/models/choose-managed.md | 13 +- documentation/platforms/terra.md | 16 +- documentation/reference/gpu-compatibility.md | 9 +- images/gpu/coding_agent_e2e.sh | 5 + images/gpu/compatibility.toml | 1 + packages/cli/src/heartwood/cli/_launch.py | 14 ++ packages/cli/tests/test_launch.py | 43 ++++++ .../tests/test_coding_agent_qualification.py | 1 + .../gateway/src/heartwood/gateway/__init__.py | 2 + .../src/heartwood/gateway/_gpu_environment.py | 84 ++++++++++- .../gateway/tests/test_gpu_environment.py | 139 +++++++++++++++++- 12 files changed, 322 insertions(+), 19 deletions(-) diff --git a/deploy/verify_gpu_compatibility.py b/deploy/verify_gpu_compatibility.py index cb95e228..a16e9946 100644 --- a/deploy/verify_gpu_compatibility.py +++ b/deploy/verify_gpu_compatibility.py @@ -102,6 +102,20 @@ def verify_repository(root: Path) -> None: _string(configuration, "platform") ) + for unsupported in matrix.get("unsupported_configurations", ()): + if not isinstance(unsupported, dict): + raise CompatibilityError("GPU compatibility unsupported entries must be tables") + snapshot_id = unsupported.get("model_snapshot") + if snapshot_id is None: + continue + if not isinstance(snapshot_id, str) or not snapshot_id: + raise CompatibilityError("unsupported GPU model_snapshot must be a non-empty string") + if snapshot_id not in snapshots: + raise CompatibilityError( + f"unknown model snapshot in unsupported GPU matrix: {snapshot_id}" + ) + covered_snapshots.add(snapshot_id) + if covered_snapshots != set(snapshots): missing = sorted(set(snapshots) - covered_snapshots) raise CompatibilityError( diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 3ceaf8b3..81d12e8f 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -47,9 +47,10 @@ The selection screen uses the release catalog as its authoritative source and re | Tier | Model Configuration | Download | GPU Memory | Recommended RAM | Recommended Free Disk | Default Context | Estimated First Start | |---|---|---:|---:|---:|---:|---:|---:| | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | -| Standard, qualified on Terra | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | -| Powerful, qualified on Terra | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Powerful, qualified on Terra | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | +| Standard Terra candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | +| Powerful Terra candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | +| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 4 x 16 GB | 120 GiB | 50 GiB | 32,768 | 4-15 minutes | +| Powerful Terra candidate | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | | Powerful, qualified on Carina | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | | Maximum alternative candidate | GPT-OSS 120B MXFP4 | 60.79 GiB | 2 x 48 GB | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | @@ -58,9 +59,9 @@ Model weights are only part of the memory requirement. The runtime also needs space for temporary downloads, key/value cache, request handling, and the project itself. Heartwood therefore uses conservative headroom and may choose a smaller context than the model's advertised maximum. -Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot compatible with the current runtime. -Heartwood rejects that combination because vLLM's FP8 Mixture-of-Experts kernel cannot load the model on T4 hardware. -For four T4 GPUs, use the qualified dense Qwen2.5 Coder 32B AWQ configuration instead. +Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot or GPT-OSS MXFP4 snapshots compatible with the current runtime. +Heartwood rejects those combinations before startup when metadata or runtime evidence shows the selected quantization requires a newer GPU generation. +For four T4 GPUs, evaluate the Qwen3 Coder 30B W4A16 AWQ or Qwen2.5 Coder 32B AWQ candidate and treat the result as unqualified until the full Heartwood acceptance workflow passes. ## Other Hugging Face Models diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index d91b31b1..0c4b08d2 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -53,9 +53,10 @@ CPU inference is portable but can be too slow for an interactive coding workflow These are starting points rather than universal requirements. Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. -The qualified GPU paths offer release-pinned Qwen2.5 Coder 7B and 14B AWQ configurations on one T4, plus Qwen2.5 Coder 32B AWQ on four T4s. -The 32B model is the strongest qualified Terra recommendation when four T4 GPUs are available; the 14B and 7B models are lower-cost alternatives on one T4. -Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 snapshot compatible, so Heartwood does not offer that failed combination. +The GPU paths expose release-pinned Terra candidates while qualification is still in progress. +The one-T4 candidates are Qwen2.5 Coder 7B and 14B AWQ. +The four-T4 candidates are Qwen3 Coder 30B W4A16 AWQ and Qwen2.5 Coder 32B AWQ. +Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 or GPT-OSS MXFP4 snapshots compatible, so Heartwood rejects those combinations before startup. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. For the first model download and startup, set auto-pause to at least 120 minutes; image creation, model verification, and inference startup can each take several minutes without terminal output from the model itself. @@ -115,14 +116,15 @@ The first-use flow confirms the project and asks where the model runs. - Choose OpenAI, Anthropic, or **Other compatible service** only when that endpoint is authorized for the intended data. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. -For managed GPU inference on one T4, choose the **Powerful** Qwen2.5 Coder 14B AWQ configuration that Heartwood labels **Recommended**. -With four T4 GPUs, choose the **Powerful** Qwen2.5 Coder 32B AWQ configuration instead. -Choose the **Standard** 7B configuration when you prefer a smaller download and lower memory pressure. +For managed GPU inference on one T4, start with the **Powerful** Qwen2.5 Coder 14B AWQ candidate. +With four T4 GPUs, start with the **Powerful** Qwen3 Coder 30B W4A16 AWQ candidate while qualification is in progress. +Choose the **Standard** 7B Qwen2.5 candidate when you prefer a smaller download and lower memory pressure. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. -The pinned 32B AWQ snapshot downloads about 18.0 GiB; use at least 120 GB RAM and retain a 200 GB persistent disk for the four-T4 environment, model cache, notebooks, and results. +The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 120 GB RAM and retain a 200 GB persistent disk for the four-T4 environment, model cache, notebooks, and results. +The pinned Qwen2.5 Coder 32B AWQ snapshot downloads about 18.0 GiB and remains an evaluation candidate until its tool-calling path passes end-to-end. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index fa5a4401..498932f4 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -33,9 +33,10 @@ The exact driver used in a live qualification is recorded with its machine-reada | Platform | Capability Tier | GPU | Model and Immutable Revision | Precision | Context | Execution | Tensor Parallelism | Server Tool Parser | Agent Tool Mode | Status | |---|---|---|---|---|---:|---|---:|---|---|---| -| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | -| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Qualified | -| Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Qualified | +| Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | +| Terra | Powerful | 4 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 32,768 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | +| Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | | Carina | Maximum capability alternative | 2 x L40S, 48 GB each | [GPT-OSS 120B](https://huggingface.co/openai/gpt-oss-120b/tree/b5c939de8f754692c1647ca79fbf85e8c1e70f8a) | MXFP4 | 65,536 | CUDA graphs | 2 | `openai` | OpenHands native tools | Candidate | @@ -45,6 +46,8 @@ Confirm that a model's license and intended use remain suitable for the project The Qwen3 Coder 30B FP8 snapshot was also tested with four T4 GPUs and vLLM 0.25.1 on CUDA 12.9. That combination is unsupported because the FP8 Mixture-of-Experts kernel cannot load the model's quantization dimensions on T4 hardware. +The GPT-OSS 20B MXFP4 snapshot was also tested with four T4 GPUs. +That combination is unsupported because vLLM requires compute capability 8.0 or newer for GPT-OSS MXFP4 while T4 provides compute capability 7.5. ## Qualification Requirement diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh index a9109c8d..36192746 100755 --- a/images/gpu/coding_agent_e2e.sh +++ b/images/gpu/coding_agent_e2e.sh @@ -76,6 +76,7 @@ import os import sys from heartwood.gateway import inspect_gpu_environment +from heartwood.gateway import minimum_compute_capability_for_model payload = json.loads(sys.argv[1]) configuration = payload["configuration"] @@ -83,6 +84,10 @@ environment = inspect_gpu_environment(os.environ.get("HEARTWOOD_PLATFORM", "gene compatible, reason = environment.assess( gpu_count=configuration["gpu_count"], gpu_memory_bytes=configuration["minimum_gpu_memory_bytes"], + minimum_compute_capability=minimum_compute_capability_for_model( + model_id=configuration["model_snapshot"], + precision=configuration["precision"], + ), ) print(reason) if not compatible or not environment.visible_devices: diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 26051543..19e0d1a5 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -160,6 +160,7 @@ configuration_id = "terra-4xt4-gpt-oss-20b" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 4 +model_snapshot = "gpt-oss-20b-vllm" model_repository = "openai/gpt-oss-20b" model_revision = "6cee5e81ee83917806bbde320786a8fb61efebee" vllm_version = "0.25.1+cu129" diff --git a/packages/cli/src/heartwood/cli/_launch.py b/packages/cli/src/heartwood/cli/_launch.py index e675c923..149739f2 100644 --- a/packages/cli/src/heartwood/cli/_launch.py +++ b/packages/cli/src/heartwood/cli/_launch.py @@ -38,6 +38,7 @@ estimate_local_runtime_memory, inspect_gpu_environment, managed_model_token_budgets, + minimum_compute_capability_for_model, plan_local_context_window, verify_model_artifact, ) @@ -236,6 +237,7 @@ def _validate_gpu_environment( available, reason = environment.assess( gpu_count=selection.tensor_parallel_size, gpu_memory_bytes=selection.minimum_gpu_memory_bytes, + minimum_compute_capability=_minimum_compute_capability(selection), ) if not available: raise LaunchConfigurationError(reason) @@ -250,6 +252,18 @@ def _validate_gpu_environment( return tuple(notes) +def _minimum_compute_capability( + selection: LocalRuntimeSelection, +) -> tuple[int, int] | None: + """Return the minimum GPU generation required by reviewed quantization paths.""" + if selection.runtime != "vllm": + return None + return minimum_compute_capability_for_model( + model_id=selection.model_id, + precision=selection.precision, + ) + + def _recommend_model( options: LaunchOptions, env: Mapping[str, str], diff --git a/packages/cli/tests/test_launch.py b/packages/cli/tests/test_launch.py index b39c5f09..2867b1d1 100644 --- a/packages/cli/tests/test_launch.py +++ b/packages/cli/tests/test_launch.py @@ -30,6 +30,7 @@ _gguf_file, _interaction_command, _local_model_selection, + _minimum_compute_capability, _model_size, _persist_effective_context, _preflight_vllm, @@ -224,6 +225,48 @@ def test_launch_plan_labels_evaluation_candidate(tmp_path: Path) -> None: assert "Qualification: Evaluation candidate" in plan.format() +@pytest.mark.parametrize( + ("model_id", "precision", "expected"), + [ + ("gpt-oss-120b-vllm", "MXFP4", (8, 0)), + ("test-model", "FP8", (8, 9)), + ("qwen3-coder-awq", "W4A16 AWQ", None), + ], +) +def test_launch_preflight_infers_minimum_compute_capability( + tmp_path: Path, + model_id: str, + precision: str, + expected: tuple[int, int] | None, +) -> None: + selection = _local_model_selection(_options(tmp_path).project, {}) + assert selection is not None + selection = LocalRuntimeSelection( + artifact_id=selection.artifact_id, + model_root=selection.model_root, + runtime=selection.runtime, + model_id=model_id, + size_bytes=selection.size_bytes, + artifact_sha256=selection.artifact_sha256, + context_window=selection.context_window, + maximum_context_window=selection.maximum_context_window, + tier=selection.tier, + precision=precision, + qualification=selection.qualification, + minimum_gpu_count=selection.minimum_gpu_count, + minimum_gpu_memory_bytes=selection.minimum_gpu_memory_bytes, + recommended_ram_bytes=selection.recommended_ram_bytes, + recommended_disk_bytes=selection.recommended_disk_bytes, + tool_call_parser=selection.tool_call_parser, + tensor_parallel_size=selection.tensor_parallel_size, + startup_seconds_min=selection.startup_seconds_min, + startup_seconds_max=selection.startup_seconds_max, + catalog_source=selection.catalog_source, + ) + + assert _minimum_compute_capability(selection) == expected + + def test_launch_requires_consent_and_honors_dry_run_and_no_allocate( tmp_path: Path, capsys: pytest.CaptureFixture[str], diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 491b57b6..0064e234 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -376,6 +376,7 @@ def test_gpu_compatibility_records_rejected_terra_configuration() -> None: "platform": "terra", "gpu_model": "NVIDIA T4", "gpu_count": 4, + "model_snapshot": "gpt-oss-20b-vllm", "model_repository": "openai/gpt-oss-20b", "model_revision": "6cee5e81ee83917806bbde320786a8fb61efebee", "vllm_version": "0.25.1+cu129", diff --git a/packages/gateway/src/heartwood/gateway/__init__.py b/packages/gateway/src/heartwood/gateway/__init__.py index 119c19d7..14bfe69e 100644 --- a/packages/gateway/src/heartwood/gateway/__init__.py +++ b/packages/gateway/src/heartwood/gateway/__init__.py @@ -36,6 +36,7 @@ discover_slurm_gpu_partitions, discover_visible_gpus, inspect_gpu_environment, + minimum_compute_capability_for_model, ) from heartwood.gateway._local_import import LocalModelImport, import_local_model from heartwood.gateway._local_model_contract import ( @@ -226,6 +227,7 @@ "load_model_snapshot_catalog", "managed_model_request_body", "managed_model_token_budgets", + "minimum_compute_capability_for_model", "model_connections_from_mapping", "model_profile_from_mapping", "model_profile_from_preset", diff --git a/packages/gateway/src/heartwood/gateway/_gpu_environment.py b/packages/gateway/src/heartwood/gateway/_gpu_environment.py index 40247b74..06d0fc58 100644 --- a/packages/gateway/src/heartwood/gateway/_gpu_environment.py +++ b/packages/gateway/src/heartwood/gateway/_gpu_environment.py @@ -13,7 +13,7 @@ import re import shutil import subprocess -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from dataclasses import dataclass from packaging.version import InvalidVersion, Version @@ -97,6 +97,7 @@ class GpuCapacity: gpu_memory_bytes: int allocation_required: bool partition: str | None = None + compute_capability: tuple[int, int] | None = None @dataclass(frozen=True, slots=True) @@ -113,12 +114,15 @@ def assess( *, gpu_count: int, gpu_memory_bytes: int, + minimum_compute_capability: tuple[int, int] | None = None, ) -> tuple[bool, str]: """Explain whether the inventory can run one catalog configuration.""" eligible = tuple( capacity for capacity in self.capacities - if capacity.gpu_count >= gpu_count and capacity.gpu_memory_bytes >= gpu_memory_bytes + if capacity.gpu_count >= gpu_count + and capacity.gpu_memory_bytes >= gpu_memory_bytes + and _meets_compute_capability(capacity, minimum_compute_capability) ) if eligible: capacity = min( @@ -150,6 +154,40 @@ def assess( ) if issues: return False, "; ".join(issues) + if minimum_compute_capability is not None: + capability = ".".join(str(part) for part in minimum_compute_capability) + insufficient = tuple( + capacity + for capacity in self.capacities + if capacity.gpu_count >= gpu_count + and capacity.gpu_memory_bytes >= gpu_memory_bytes + and capacity.compute_capability is not None + and capacity.compute_capability < minimum_compute_capability + ) + if insufficient: + observed = ", ".join( + sorted( + { + ( + f"{capacity.gpu_model} " + f"{capacity.compute_capability[0]}." + f"{capacity.compute_capability[1]}" + ) + for capacity in insufficient + if capacity.compute_capability is not None + } + ) + ) + return ( + False, + f"Requires GPU compute capability {capability} or newer; detected {observed}.", + ) + if self.capacities: + return ( + False, + f"Requires GPU compute capability {capability} or newer, but the detected " + "GPU capability could not be verified before startup.", + ) if not self.capacities: if self.platform_id == "terra": return ( @@ -186,6 +224,21 @@ def inspect_gpu_environment(platform_id: str, env: Mapping[str, str]) -> GpuEnvi ) +def minimum_compute_capability_for_model( + *, + model_id: str, + precision: str, +) -> tuple[int, int] | None: + """Return the minimum GPU generation required by reviewed quantization paths.""" + normalized_model = model_id.casefold().replace("_", "-") + normalized_precision = precision.casefold().replace("_", "-") + if "gpt-oss" in normalized_model or "mxfp4" in normalized_precision: + return (8, 0) + if normalized_precision.startswith("fp8"): + return (8, 9) + return None + + def discover_visible_gpus(env: Mapping[str, str]) -> tuple[GpuDevice, ...]: """Inspect visible NVIDIA devices without initializing CUDA.""" executable = shutil.which("nvidia-smi", path=env.get("PATH")) @@ -277,6 +330,10 @@ def _visible_capacities(devices: tuple[GpuDevice, ...]) -> tuple[GpuCapacity, .. gpu_count=len(eligible), gpu_memory_bytes=threshold, allocation_required=False, + compute_capability=_minimum_capability( + device.compute_capability or _capability_from_name(device.name) + for device in eligible + ), ) ) return tuple(capacities) @@ -293,6 +350,7 @@ def _slurm_capacities( gpu_memory_bytes=partition.gpu_memory_bytes, allocation_required=True, partition=partition.name, + compute_capability=_capability_from_name(partition.gpu_model or ""), ) for partition in partitions if partition.gpu_memory_bytes is not None @@ -342,6 +400,28 @@ def _capability_from_name(name: str) -> tuple[int, int] | None: ) +def _minimum_capability( + capabilities: Iterable[tuple[int, int] | None], +) -> tuple[int, int] | None: + values = tuple( + capability + for capability in capabilities + if isinstance(capability, tuple) + and len(capability) == 2 + and all(isinstance(part, int) for part in capability) + ) + return min(values) if values else None + + +def _meets_compute_capability( + capacity: GpuCapacity, + minimum: tuple[int, int] | None, +) -> bool: + if minimum is None: + return True + return capacity.compute_capability is not None and capacity.compute_capability >= minimum + + def _parse_gres(value: str) -> tuple[str | None, int]: best_model: str | None = None best_count = 0 diff --git a/packages/gateway/tests/test_gpu_environment.py b/packages/gateway/tests/test_gpu_environment.py index 854dede6..d8eac8aa 100644 --- a/packages/gateway/tests/test_gpu_environment.py +++ b/packages/gateway/tests/test_gpu_environment.py @@ -18,6 +18,7 @@ discover_slurm_gpu_partitions, discover_visible_gpus, inspect_gpu_environment, + minimum_compute_capability_for_model, ) @@ -201,6 +202,106 @@ def test_gpu_environment_assesses_visible_scheduler_and_insufficient_capacity() assert "allocation approval is required" in reason +def test_gpu_environment_rejects_models_requiring_newer_compute_capability() -> None: + environment = GpuEnvironment( + platform_id="terra", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + "4 visible Tesla T4 GPU(s)", + "Tesla T4", + 4, + 16_000_000_000, + False, + compute_capability=(7, 5), + ), + ), + ) + + compatible, reason = environment.assess( + gpu_count=4, + gpu_memory_bytes=15_000_000_000, + minimum_compute_capability=(8, 0), + ) + + assert not compatible + assert "Requires GPU compute capability 8.0 or newer" in reason + assert "Tesla T4 7.5" in reason + + +def test_gpu_environment_accepts_scheduled_l40s_for_mxfp4() -> None: + environment = GpuEnvironment( + platform_id="carina", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + "Slurm partition dev", + "nvidia_l40s", + 8, + 48_000_000_000, + True, + "dev", + (8, 9), + ), + ), + ) + + compatible, reason = environment.assess( + gpu_count=2, + gpu_memory_bytes=42_000_000_000, + minimum_compute_capability=(8, 0), + ) + + assert compatible + assert "Slurm partition dev" in reason + + +def test_gpu_environment_rejects_unverified_compute_capability_for_model_requirement() -> None: + environment = GpuEnvironment( + platform_id="generic", + visible_devices=(), + slurm_partitions=(), + capacities=( + GpuCapacity( + "1 visible NVIDIA GPU", + "NVIDIA GPU", + 1, + 48_000_000_000, + False, + ), + ), + ) + + compatible, reason = environment.assess( + gpu_count=1, + gpu_memory_bytes=42_000_000_000, + minimum_compute_capability=(8, 0), + ) + + assert not compatible + assert "could not be verified before startup" in reason + + +@pytest.mark.parametrize( + ("model_id", "precision", "expected"), + [ + ("gpt-oss-20b-vllm", "MXFP4", (8, 0)), + ("qwen3-coder-30b-a3b-instruct-fp8-vllm", "FP8", (8, 9)), + ("qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", "W4A16 AWQ", None), + ], +) +def test_model_quantization_declares_minimum_compute_capability( + model_id: str, + precision: str, + expected: tuple[int, int] | None, +) -> None: + assert ( + minimum_compute_capability_for_model(model_id=model_id, precision=precision) == expected + ) + + @pytest.mark.parametrize( ("platform_id", "message"), [ @@ -291,11 +392,47 @@ def test_environment_inspection_builds_known_slurm_capacity( environment = inspect_gpu_environment("carina", {}) assert environment.capacities == ( - GpuCapacity("Slurm partition dev", "nvidia_l40s", 8, 48_000_000_000, True, "dev"), + GpuCapacity( + "Slurm partition dev", + "nvidia_l40s", + 8, + 48_000_000_000, + True, + "dev", + (8, 9), + ), ) assert environment.slurm_partitions[1].gpu_memory_bytes is None +def test_slurm_discovery_accepts_generic_gpu_gres_and_invalid_memory( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, + 0, + stdout="generic|gpu:2|up|unknown|8\n", + ), + ) + + partitions = discover_slurm_gpu_partitions({"PATH": "/usr/bin"}) + + assert partitions == (SlurmGpuPartition("generic", False, None, 2, None, 8, "up"),) + + +def test_slurm_discovery_returns_empty_when_sinfo_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.subprocess.run", + lambda command, **_kwargs: subprocess.CompletedProcess(command, 1, stdout=""), + ) + + assert discover_slurm_gpu_partitions({"PATH": "/usr/bin"}) == () + + def test_visible_gpu_discovery_handles_missing_malformed_and_failed_queries( monkeypatch: pytest.MonkeyPatch, ) -> None: From 98221ac82cf5701752db910cb1695bb2e293480e Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 22:25:49 -0700 Subject: [PATCH 28/38] Correct Terra Tensor Parallel Configuration --- documentation/models/choose-managed.md | 2 +- documentation/platforms/terra.md | 4 ++-- documentation/reference/gpu-compatibility.md | 4 +++- images/generic/local-runtime/snapshots.toml | 12 +++++----- images/gpu/compatibility.toml | 19 +++++++++++++--- .../tests/test_coding_agent_qualification.py | 22 +++++++++++++++++-- .../gateway/tests/test_model_snapshots.py | 2 +- 7 files changed, 49 insertions(+), 16 deletions(-) diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 81d12e8f..42b1f46c 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -49,7 +49,7 @@ The selection screen uses the release catalog as its authoritative source and re | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard Terra candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | | Powerful Terra candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 4 x 16 GB | 120 GiB | 50 GiB | 32,768 | 4-15 minutes | +| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 2 x 16 GB | 96 GiB | 50 GiB | 32,768 | 4-15 minutes | | Powerful Terra candidate | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | | Powerful, qualified on Carina | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 0c4b08d2..a5d55b07 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -117,13 +117,13 @@ The first-use flow confirms the project and asks where the model runs. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. For managed GPU inference on one T4, start with the **Powerful** Qwen2.5 Coder 14B AWQ candidate. -With four T4 GPUs, start with the **Powerful** Qwen3 Coder 30B W4A16 AWQ candidate while qualification is in progress. +With two or more T4 GPUs, start with the **Powerful** Qwen3 Coder 30B W4A16 AWQ candidate while qualification is in progress. Choose the **Standard** 7B Qwen2.5 candidate when you prefer a smaller download and lower memory pressure. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. -The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 120 GB RAM and retain a 200 GB persistent disk for the four-T4 environment, model cache, notebooks, and results. +The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 96 GB RAM and retain a 200 GB persistent disk for the two-T4 environment, model cache, notebooks, and results. The pinned Qwen2.5 Coder 32B AWQ snapshot downloads about 18.0 GiB and remains an evaluation candidate until its tool-calling path passes end-to-end. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 498932f4..8dd1539e 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -35,7 +35,7 @@ The exact driver used in a live qualification is recorded with its machine-reada |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Terra | Powerful | 4 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 32,768 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | +| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 32,768 | CUDA graphs | 2 | `qwen3_coder` | OpenHands native tools | Candidate | | Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | @@ -48,6 +48,8 @@ The Qwen3 Coder 30B FP8 snapshot was also tested with four T4 GPUs and vLLM 0.25 That combination is unsupported because the FP8 Mixture-of-Experts kernel cannot load the model's quantization dimensions on T4 hardware. The GPT-OSS 20B MXFP4 snapshot was also tested with four T4 GPUs. That combination is unsupported because vLLM requires compute capability 8.0 or newer for GPT-OSS MXFP4 while T4 provides compute capability 7.5. +Qwen3 Coder 30B W4A16 AWQ cannot use tensor parallelism 4 because its quantization group size crosses four-way tensor shards. +The two-way configuration is the supported qualification candidate. ## Qualification Requirement diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 0a445c02..12fb221d 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -107,7 +107,7 @@ recommended = true [snapshots.qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful native tool-calling coding-agent candidate for four 16 GB NVIDIA GPUs." +purpose = "Powerful native tool-calling coding-agent candidate for two 16 GB NVIDIA GPUs." source_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" source_revision = "e69e73813144d9b715648d8384b3f2c035397411" expected_size_bytes = 18049253118 @@ -118,19 +118,19 @@ model_alias = "Qwen3 Coder 30B AWQ" precision = "W4A16 AWQ" tier = "powerful" qualification = "candidate" -minimum_gpu_count = 4 +minimum_gpu_count = 2 minimum_gpu_memory_bytes = 15000000000 -recommended_ram_bytes = 128849018880 +recommended_ram_bytes = 103079215104 recommended_disk_bytes = 53687091200 context_window = 32768 maximum_context_window = 262144 tool_call_parser = "qwen3_coder" -tensor_parallel_size = 4 +tensor_parallel_size = 2 startup_seconds_min = 240 startup_seconds_max = 900 download_policy = "transformers-safetensors" -minimum_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 30 GB free project storage." -recommended_resource_envelope = "Four NVIDIA T4-class GPUs with 16 GB VRAM each, 32 CPU cores, 120 GB RAM, and 50 GB free project storage." +minimum_resource_envelope = "Two NVIDIA T4-class GPUs with 16 GB VRAM each, 16 CPU cores, 64 GB RAM, and 30 GB free project storage." +recommended_resource_envelope = "Two NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 50 GB free project storage." recommended = false [snapshots.qwen25-coder-32b-instruct-awq-vllm] diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 19e0d1a5..e5039939 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -106,19 +106,32 @@ validated_at = "2026-07-21" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" reason = "vLLM's FP8 Mixture-of-Experts path requires quantization dimensions that this model does not provide on NVIDIA T4 GPUs." -[[configurations]] +[[unsupported_configurations]] configuration_id = "terra-4xt4-qwen3-coder-30b-awq" -status = "candidate" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 4 +model_snapshot = "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm" +model_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" +model_revision = "e69e73813144d9b715648d8384b3f2c035397411" +vllm_version = "0.25.1+cu129" +validated_at = "2026-07-21" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" +reason = "The model's W4A16 quantization group size crosses four-way tensor-parallel shard boundaries; use the two-GPU configuration." + +[[configurations]] +configuration_id = "terra-2xt4-qwen3-coder-30b-awq" +status = "candidate" +platform = "terra" +gpu_model = "NVIDIA T4" +gpu_count = 2 minimum_gpu_memory_bytes = 15000000000 model_snapshot = "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm" model_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" model_revision = "e69e73813144d9b715648d8384b3f2c035397411" precision = "W4A16 AWQ" context_window = 32768 -tensor_parallel_size = 4 +tensor_parallel_size = 2 tool_call_parser = "qwen3_coder" agent_tool_mode = "openhands-native" vllm_version = "0.25.1+cu129" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 0064e234..dc8a7bd7 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -258,13 +258,15 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: resolved = load( _root() / "images/gpu/compatibility.toml", - "terra-4xt4-qwen3-coder-30b-awq", + "terra-2xt4-qwen3-coder-30b-awq", ) assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "qwen3_coder" assert resolved["configuration"]["agent_tool_mode"] == "openhands-native" assert resolved["configuration"]["context_window"] == 32_768 + assert resolved["configuration"]["gpu_count"] == 2 + assert resolved["configuration"]["tensor_parallel_size"] == 2 assert "enforce_eager" not in resolved["configuration"] assert resolved["configuration"]["model_revision"] == ( "e69e73813144d9b715648d8384b3f2c035397411" @@ -285,7 +287,7 @@ def test_gpu_qualification_catalog_lists_all_terra_profiles() -> None: assert {configuration["configuration_id"] for configuration in configurations} == { "terra-t4-qwen25-coder-7b-awq", "terra-t4-qwen25-coder-14b-awq", - "terra-4xt4-qwen3-coder-30b-awq", + "terra-2xt4-qwen3-coder-30b-awq", "terra-4xt4-qwen25-coder-32b-awq", } @@ -371,6 +373,22 @@ def test_gpu_compatibility_records_rejected_terra_configuration() -> None: "that this model does not provide on NVIDIA T4 GPUs." ), }, + { + "configuration_id": "terra-4xt4-qwen3-coder-30b-awq", + "platform": "terra", + "gpu_model": "NVIDIA T4", + "gpu_count": 4, + "model_snapshot": "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", + "model_repository": "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ", + "model_revision": "e69e73813144d9b715648d8384b3f2c035397411", + "vllm_version": "0.25.1+cu129", + "validated_at": "2026-07-21", + "evidence": "https://github.com/SchmiedmayerLab/heartwood/pull/72", + "reason": ( + "The model's W4A16 quantization group size crosses four-way " + "tensor-parallel shard boundaries; use the two-GPU configuration." + ), + }, { "configuration_id": "terra-4xt4-gpt-oss-20b", "platform": "terra", diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 3d0f253f..19b01446 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -75,7 +75,7 @@ "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ", "e69e73813144d9b715648d8384b3f2c035397411", "powerful", - 4, + 2, "qwen3_coder", ), ( From 804968413868a510497528edd33f50bc8fe9ee30 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 22:43:11 -0700 Subject: [PATCH 29/38] Format GPU Qualification Tests --- packages/compliance/tests/test_coding_agent_qualification.py | 2 +- packages/gateway/tests/test_gpu_environment.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index dc8a7bd7..a98d2fa9 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -404,7 +404,7 @@ def test_gpu_compatibility_records_rejected_terra_configuration() -> None: "vLLM rejects GPT-OSS MXFP4 on NVIDIA T4 because it requires compute " "capability 8.0 and the T4 provides compute capability 7.5." ), - } + }, ] diff --git a/packages/gateway/tests/test_gpu_environment.py b/packages/gateway/tests/test_gpu_environment.py index d8eac8aa..68d50369 100644 --- a/packages/gateway/tests/test_gpu_environment.py +++ b/packages/gateway/tests/test_gpu_environment.py @@ -297,9 +297,7 @@ def test_model_quantization_declares_minimum_compute_capability( precision: str, expected: tuple[int, int] | None, ) -> None: - assert ( - minimum_compute_capability_for_model(model_id=model_id, precision=precision) == expected - ) + assert minimum_compute_capability_for_model(model_id=model_id, precision=precision) == expected @pytest.mark.parametrize( From 3bccbe7a150d93b275b9948afcbf53c3354538ff Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 22:51:11 -0700 Subject: [PATCH 30/38] Fit Terra Context To T4 Memory --- documentation/models/choose-managed.md | 5 +++-- documentation/platforms/terra.md | 5 +++-- documentation/reference/gpu-compatibility.md | 3 ++- images/generic/local-runtime/snapshots.toml | 2 +- images/gpu/compatibility.toml | 2 +- packages/compliance/tests/test_coding_agent_qualification.py | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 42b1f46c..6dfa7abe 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -49,7 +49,7 @@ The selection screen uses the release catalog as its authoritative source and re | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard Terra candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | | Powerful Terra candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 2 x 16 GB | 96 GiB | 50 GiB | 32,768 | 4-15 minutes | +| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 2 x 16 GB | 96 GiB | 50 GiB | 18,432 | 4-15 minutes | | Powerful Terra candidate | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | | Powerful, qualified on Carina | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | @@ -61,7 +61,8 @@ Heartwood therefore uses conservative headroom and may choose a smaller context Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot or GPT-OSS MXFP4 snapshots compatible with the current runtime. Heartwood rejects those combinations before startup when metadata or runtime evidence shows the selected quantization requires a newer GPU generation. -For four T4 GPUs, evaluate the Qwen3 Coder 30B W4A16 AWQ or Qwen2.5 Coder 32B AWQ candidate and treat the result as unqualified until the full Heartwood acceptance workflow passes. +For two T4 GPUs, evaluate the Qwen3 Coder 30B W4A16 AWQ candidate with its conservative 18,432-token context and treat the result as unqualified until the full Heartwood acceptance workflow passes. +For four T4 GPUs, the Qwen2.5 Coder 32B AWQ profile remains an evaluation candidate; the Qwen3 W4A16 quantization cannot be sharded four ways. ## Other Hugging Face Models diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index a5d55b07..95d5b79e 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -55,7 +55,8 @@ Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. The GPU paths expose release-pinned Terra candidates while qualification is still in progress. The one-T4 candidates are Qwen2.5 Coder 7B and 14B AWQ. -The four-T4 candidates are Qwen3 Coder 30B W4A16 AWQ and Qwen2.5 Coder 32B AWQ. +The two-T4 candidate is Qwen3 Coder 30B W4A16 AWQ with a conservative 18,432-token context. +The four-T4 candidate is Qwen2.5 Coder 32B AWQ; the Qwen3 W4A16 quantization cannot be sharded four ways. Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 or GPT-OSS MXFP4 snapshots compatible, so Heartwood rejects those combinations before startup. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. It stops before launching modern vLLM on P4, P100, or V100 GPUs because their compute capability is below the supported floor. @@ -123,7 +124,7 @@ You can instead choose **Other Hugging Face model** and enter another public rep Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. -The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 96 GB RAM and retain a 200 GB persistent disk for the two-T4 environment, model cache, notebooks, and results. +The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 96 GB RAM, retain a 200 GB persistent disk, and keep the catalog's 18,432-token context so the two T4 GPUs retain key/value-cache headroom. The pinned Qwen2.5 Coder 32B AWQ snapshot downloads about 18.0 GiB and remains an evaluation candidate until its tool-calling path passes end-to-end. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 8dd1539e..98e50a57 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -35,7 +35,7 @@ The exact driver used in a live qualification is recorded with its machine-reada |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 32,768 | CUDA graphs | 2 | `qwen3_coder` | OpenHands native tools | Candidate | +| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 18,432 | CUDA graphs | 2 | `qwen3_coder` | OpenHands native tools | Candidate | | Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | @@ -50,6 +50,7 @@ The GPT-OSS 20B MXFP4 snapshot was also tested with four T4 GPUs. That combination is unsupported because vLLM requires compute capability 8.0 or newer for GPT-OSS MXFP4 while T4 provides compute capability 7.5. Qwen3 Coder 30B W4A16 AWQ cannot use tensor parallelism 4 because its quantization group size crosses four-way tensor shards. The two-way configuration is the supported qualification candidate. +Its context is capped at 18,432 because a 32,768-token key/value cache leaves no cache blocks on two 16 GB T4 GPUs at the validated memory ceiling. ## Qualification Requirement diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index 12fb221d..c89847af 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -122,7 +122,7 @@ minimum_gpu_count = 2 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 103079215104 recommended_disk_bytes = 53687091200 -context_window = 32768 +context_window = 18432 maximum_context_window = 262144 tool_call_parser = "qwen3_coder" tensor_parallel_size = 2 diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index e5039939..1a22ba6e 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -130,7 +130,7 @@ model_snapshot = "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm" model_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" model_revision = "e69e73813144d9b715648d8384b3f2c035397411" precision = "W4A16 AWQ" -context_window = 32768 +context_window = 18432 tensor_parallel_size = 2 tool_call_parser = "qwen3_coder" agent_tool_mode = "openhands-native" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index a98d2fa9..ac576c71 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -264,7 +264,7 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["runtime"]["cuda_version"] == "12.9" assert resolved["configuration"]["tool_call_parser"] == "qwen3_coder" assert resolved["configuration"]["agent_tool_mode"] == "openhands-native" - assert resolved["configuration"]["context_window"] == 32_768 + assert resolved["configuration"]["context_window"] == 18_432 assert resolved["configuration"]["gpu_count"] == 2 assert resolved["configuration"]["tensor_parallel_size"] == 2 assert "enforce_eager" not in resolved["configuration"] From df7c333ecdee98bae79a9473b98ca81088a7356c Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 22:56:25 -0700 Subject: [PATCH 31/38] Expose All GPU Qualification Profiles --- .github/workflows/gpu-container-image.yml | 1 + .../compliance/tests/test_container_assets.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/gpu-container-image.yml b/.github/workflows/gpu-container-image.yml index e0b5d95f..5f63af89 100644 --- a/.github/workflows/gpu-container-image.yml +++ b/.github/workflows/gpu-container-image.yml @@ -28,6 +28,7 @@ on: options: - terra-t4-qwen25-coder-7b-awq - terra-t4-qwen25-coder-14b-awq + - terra-2xt4-qwen3-coder-30b-awq - terra-4xt4-qwen25-coder-32b-awq - carina-l40s-qwen3-coder-30b-fp8 - carina-4xl40s-qwen3-coder-next-fp8 diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index c5ab6811..2445bd2d 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -722,6 +722,24 @@ def test_gpu_publication_builds_only_explicit_main_variants() -> None: assert "GHSA-8fr4-5q9j-m8gm" not in dependency_review +def test_gpu_qualification_workflow_offers_every_candidate_configuration() -> None: + workflow = _read(".github/workflows/gpu-container-image.yml") + matrix = _toml("images/gpu/compatibility.toml") + configuration_input = workflow.split(" qualification_configuration:\n", maxsplit=1)[ + 1 + ].split(" qualification_runner:\n", maxsplit=1)[0] + options = { + line.removeprefix("- ") + for line in (item.strip() for item in configuration_input.splitlines()) + if line.startswith("- ") + } + configuration_ids = { + configuration["configuration_id"] for configuration in matrix["configurations"] + } + + assert options == configuration_ids + + def test_vllm_advisory_exceptions_remain_isolated_to_gpu_dependencies() -> None: root = _repo_root() lock = root / "images/gpu/vllm-requirements.txt" From eeca731a64b9dd6dd0085fa53c52e035b852bd43 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 23:02:00 -0700 Subject: [PATCH 32/38] Simplify GPU Capability Aggregation --- .../gateway/src/heartwood/gateway/_gpu_environment.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/gateway/src/heartwood/gateway/_gpu_environment.py b/packages/gateway/src/heartwood/gateway/_gpu_environment.py index 06d0fc58..c7cc5530 100644 --- a/packages/gateway/src/heartwood/gateway/_gpu_environment.py +++ b/packages/gateway/src/heartwood/gateway/_gpu_environment.py @@ -403,13 +403,7 @@ def _capability_from_name(name: str) -> tuple[int, int] | None: def _minimum_capability( capabilities: Iterable[tuple[int, int] | None], ) -> tuple[int, int] | None: - values = tuple( - capability - for capability in capabilities - if isinstance(capability, tuple) - and len(capability) == 2 - and all(isinstance(part, int) for part in capability) - ) + values = tuple(capability for capability in capabilities if capability is not None) return min(values) if values else None From c50a3c717e8a0a22be3ebad1c2c970c135f96473 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 23:15:01 -0700 Subject: [PATCH 33/38] Use Eager Inference For Terra T4 --- documentation/reference/gpu-compatibility.md | 2 +- images/gpu/compatibility.toml | 1 + packages/compliance/tests/test_coding_agent_qualification.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index 98e50a57..cfc9eec2 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -35,7 +35,7 @@ The exact driver used in a live qualification is recorded with its machine-reada |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 18,432 | CUDA graphs | 2 | `qwen3_coder` | OpenHands native tools | Candidate | +| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 18,432 | Eager | 2 | `qwen3_coder` | OpenHands native tools | Candidate | | Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 1a22ba6e..51e3aca9 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -134,6 +134,7 @@ context_window = 18432 tensor_parallel_size = 2 tool_call_parser = "qwen3_coder" agent_tool_mode = "openhands-native" +enforce_eager = true vllm_version = "0.25.1+cu129" pytorch_version = "2.11.0+cu129" cuda_version = "12.9" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index ac576c71..eb711daf 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -267,7 +267,7 @@ def test_gpu_qualification_configuration_resolves_runtime_and_model() -> None: assert resolved["configuration"]["context_window"] == 18_432 assert resolved["configuration"]["gpu_count"] == 2 assert resolved["configuration"]["tensor_parallel_size"] == 2 - assert "enforce_eager" not in resolved["configuration"] + assert resolved["configuration"]["enforce_eager"] is True assert resolved["configuration"]["model_revision"] == ( "e69e73813144d9b715648d8384b3f2c035397411" ) From b664f2f9cc66f1258f9c5b6b4cf46096eb31c0a8 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 23:24:42 -0700 Subject: [PATCH 34/38] Qualify Qwen3 Coder On Terra --- documentation/models/choose-managed.md | 4 ++-- documentation/platforms/terra.md | 6 ++--- documentation/reference/gpu-compatibility.md | 4 ++-- images/generic/local-runtime/snapshots.toml | 8 ++++--- images/gpu/compatibility.toml | 5 ++++- packages/gateway/tests/test_local_models.py | 22 +++++++++++++++++-- .../gateway/tests/test_model_snapshots.py | 1 + 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/documentation/models/choose-managed.md b/documentation/models/choose-managed.md index 6dfa7abe..c1766938 100644 --- a/documentation/models/choose-managed.md +++ b/documentation/models/choose-managed.md @@ -49,7 +49,7 @@ The selection screen uses the release catalog as its authoritative source and re | Standard fallback | Qwen2.5 7B Instruct Q4_K_M, CPU | 4.36 GiB | None | 32 GiB | 50 GiB | 32,768 | Hardware dependent | | Standard Terra candidate | Qwen2.5 Coder 7B AWQ | 5.20 GiB | 1 x 16 GB | 32 GiB | 16 GiB | 18,432 on Terra | 2-8 minutes | | Powerful Terra candidate | Qwen2.5 Coder 14B AWQ | 9.31 GiB | 1 x 16 GB | 60 GiB | 32 GiB | 18,432 | 3-10 minutes | -| Powerful Terra candidate | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 2 x 16 GB | 96 GiB | 50 GiB | 18,432 | 4-15 minutes | +| Powerful, qualified on Terra | Qwen3 Coder 30B W4A16 AWQ | 16.81 GiB | 2 x 16 GB | 96 GiB | 50 GiB | 18,432 | 4-15 minutes | | Powerful Terra candidate | Qwen2.5 Coder 32B AWQ | 18.00 GiB | 4 x 16 GB | 120 GiB | 48 GiB | 32,768 | 4-15 minutes | | Powerful, qualified on Carina | Qwen3 Coder 30B FP8 | 29.06 GiB | 1 x 48 GB | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | | Maximum candidate | Qwen3 Coder Next FP8 | 74.88 GiB | 4 x 48 GB | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | @@ -61,7 +61,7 @@ Heartwood therefore uses conservative headroom and may choose a smaller context Four T4 GPUs do not make the Qwen3 Coder 30B FP8 snapshot or GPT-OSS MXFP4 snapshots compatible with the current runtime. Heartwood rejects those combinations before startup when metadata or runtime evidence shows the selected quantization requires a newer GPU generation. -For two T4 GPUs, evaluate the Qwen3 Coder 30B W4A16 AWQ candidate with its conservative 18,432-token context and treat the result as unqualified until the full Heartwood acceptance workflow passes. +For two T4 GPUs, use the qualified Qwen3 Coder 30B W4A16 AWQ recommendation with its conservative 18,432-token context. For four T4 GPUs, the Qwen2.5 Coder 32B AWQ profile remains an evaluation candidate; the Qwen3 W4A16 quantization cannot be sharded four ways. ## Other Hugging Face Models diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index 95d5b79e..c25b4deb 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -53,9 +53,9 @@ CPU inference is portable but can be too slow for an interactive coding workflow These are starting points rather than universal requirements. Terra's current standard machine choices pair 8 CPUs with 30 GB RAM and 16 CPUs with 60 GB RAM. The 16 CPU option preserves the catalog's recommended system-memory headroom; 8 CPUs and 30 GB RAM is a lower-cost evaluation configuration that may leave less room for model loading and concurrent notebook work. -The GPU paths expose release-pinned Terra candidates while qualification is still in progress. +The GPU paths expose one release-pinned Terra recommendation and additional evaluation candidates. The one-T4 candidates are Qwen2.5 Coder 7B and 14B AWQ. -The two-T4 candidate is Qwen3 Coder 30B W4A16 AWQ with a conservative 18,432-token context. +The qualified two-T4 recommendation is Qwen3 Coder 30B W4A16 AWQ with a conservative 18,432-token context. The four-T4 candidate is Qwen2.5 Coder 32B AWQ; the Qwen3 W4A16 quantization cannot be sharded four ways. Four T4 GPUs do not make the catalog's Qwen3 Coder 30B FP8 or GPT-OSS MXFP4 snapshots compatible, so Heartwood rejects those combinations before startup. Heartwood reports the detected GPU, memory, driver, model cache, and compatible catalog entries before startup. @@ -118,7 +118,7 @@ The first-use flow confirms the project and asks where the model runs. - Choose **Run with Heartwood** to download and serve model weights inside the Terra environment. For managed GPU inference on one T4, start with the **Powerful** Qwen2.5 Coder 14B AWQ candidate. -With two or more T4 GPUs, start with the **Powerful** Qwen3 Coder 30B W4A16 AWQ candidate while qualification is in progress. +With two or more T4 GPUs, start with the qualified **Powerful** Qwen3 Coder 30B W4A16 AWQ recommendation. Choose the **Standard** 7B Qwen2.5 candidate when you prefer a smaller download and lower memory pressure. You can instead choose **Other Hugging Face model** and enter another public repository. Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index cfc9eec2..f68247bb 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -35,7 +35,7 @@ The exact driver used in a live qualification is recorded with its machine-reada |---|---|---|---|---|---:|---|---:|---|---|---| | Terra | Standard | 1 x T4, 16 GB | [Qwen2.5-Coder-7B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-AWQ/tree/8e8ed243bbe6f9a5aff549a0924562fc719b2b8a) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | | Terra | Powerful | 1 x T4, 16 GB | [Qwen2.5-Coder-14B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ/tree/eb3172f06a6d6b3a15f08947b0668d782e4d2d2c) | AWQ int4 | 18,432 | Eager | 1 | `hermes` | OpenHands prompt conversion | Candidate | -| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 18,432 | Eager | 2 | `qwen3_coder` | OpenHands native tools | Candidate | +| Terra | Powerful | 2 x T4, 16 GB each | [Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ](https://huggingface.co/YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ/tree/e69e73813144d9b715648d8384b3f2c035397411) | W4A16 AWQ | 18,432 | Eager | 2 | `qwen3_coder` | OpenHands native tools | Qualified | | Terra | Powerful | 4 x T4, 16 GB each | [Qwen2.5-Coder-32B-Instruct-AWQ](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct-AWQ/tree/1ed0a6145da0ce550c628e8e8b678f51e695995d) | AWQ int4 | 32,768 | Eager | 4 | `hermes` | OpenHands prompt conversion | Candidate | | Carina | Powerful | 1 x L40S, 48 GB | [Qwen3-Coder-30B-A3B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8/tree/dcaee4d4dfc5ee71ad501f01f530e5652438fde0) | FP8 | 32,768 | CUDA graphs | 1 | `qwen3_coder` | OpenHands native tools | Qualified | | Carina | Maximum capability | 4 x L40S, 48 GB each | [Qwen3-Coder-Next-FP8](https://huggingface.co/Qwen/Qwen3-Coder-Next-FP8/tree/da6e2ed27304dd39abadd9c82ef50e8de67bdd4c) | FP8 | 65,536 | CUDA graphs | 4 | `qwen3_coder` | OpenHands native tools | Candidate | @@ -49,7 +49,7 @@ That combination is unsupported because the FP8 Mixture-of-Experts kernel cannot The GPT-OSS 20B MXFP4 snapshot was also tested with four T4 GPUs. That combination is unsupported because vLLM requires compute capability 8.0 or newer for GPT-OSS MXFP4 while T4 provides compute capability 7.5. Qwen3 Coder 30B W4A16 AWQ cannot use tensor parallelism 4 because its quantization group size crosses four-way tensor shards. -The two-way configuration is the supported qualification candidate. +The two-way eager configuration is qualified. Its context is capped at 18,432 because a 32,768-token key/value cache leaves no cache blocks on two 16 GB T4 GPUs at the validated memory ceiling. ## Qualification Requirement diff --git a/images/generic/local-runtime/snapshots.toml b/images/generic/local-runtime/snapshots.toml index c89847af..825b70ff 100644 --- a/images/generic/local-runtime/snapshots.toml +++ b/images/generic/local-runtime/snapshots.toml @@ -107,7 +107,7 @@ recommended = true [snapshots.qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm] runtime_profile = "vllm-cuda" -purpose = "Powerful native tool-calling coding-agent candidate for two 16 GB NVIDIA GPUs." +purpose = "Powerful native tool-calling coding-agent model for two 16 GB NVIDIA GPUs." source_repository = "YCWTG/Qwen3-Coder-30B-A3B-Instruct-W4A16-mixed-AWQ" source_revision = "e69e73813144d9b715648d8384b3f2c035397411" expected_size_bytes = 18049253118 @@ -117,7 +117,7 @@ license_posture = "Apache-2.0; retain the model card and source provenance." model_alias = "Qwen3 Coder 30B AWQ" precision = "W4A16 AWQ" tier = "powerful" -qualification = "candidate" +qualification = "qualified" minimum_gpu_count = 2 minimum_gpu_memory_bytes = 15000000000 recommended_ram_bytes = 103079215104 @@ -131,7 +131,9 @@ startup_seconds_max = 900 download_policy = "transformers-safetensors" minimum_resource_envelope = "Two NVIDIA T4-class GPUs with 16 GB VRAM each, 16 CPU cores, 64 GB RAM, and 30 GB free project storage." recommended_resource_envelope = "Two NVIDIA T4-class GPUs with 16 GB VRAM each, 24 CPU cores, 96 GB RAM, and 50 GB free project storage." -recommended = false +validated_platforms = ["terra"] +qualification_test = "heartwood.coding-agent-e2e.v1" +recommended = true [snapshots.qwen25-coder-32b-instruct-awq-vllm] runtime_profile = "vllm-cuda" diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index 51e3aca9..bba8090d 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -121,7 +121,10 @@ reason = "The model's W4A16 quantization group size crosses four-way tensor-para [[configurations]] configuration_id = "terra-2xt4-qwen3-coder-30b-awq" -status = "candidate" +status = "qualified" +validated_at = "2026-07-21" +validated_driver_version = "535.154.05" +evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" gpu_model = "NVIDIA T4" gpu_count = 2 diff --git a/packages/gateway/tests/test_local_models.py b/packages/gateway/tests/test_local_models.py index fcb48d32..8e6e41f3 100644 --- a/packages/gateway/tests/test_local_models.py +++ b/packages/gateway/tests/test_local_models.py @@ -469,9 +469,10 @@ def test_central_catalog_exposes_only_recommended_models() -> None: assert {choice.model_id for choice in choices} == { "qwen25-7b-instruct-q4_k_m", "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", } assert all(choice.recommended_resource_envelope for choice in choices) - assert {choice.context_window for choice in choices} == {32_768} + assert {choice.context_window for choice in choices} == {18_432, 32_768} assert "llama-cpp-stories260k-ci" in {choice.model_id for choice in downloadable} assert "qwen25-coder-7b-instruct-q4_k_m" in {choice.model_id for choice in downloadable} assert { @@ -486,13 +487,19 @@ def test_central_catalog_exposes_only_recommended_models() -> None: } <= {choice.model_id for choice in downloadable} assert all(choice.catalog_source == "catalog" for choice in downloadable) gpu_choices = {choice.model_id: choice for choice in downloadable if choice.runtime == "vllm"} - assert gpu_choices["qwen3-coder-30b-a3b-instruct-fp8-vllm"].qualification == "qualified" + assert { + model_id for model_id, choice in gpu_choices.items() if choice.qualification == "qualified" + } == { + "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", + } assert all( choice.qualification == "candidate" for model_id, choice in gpu_choices.items() if model_id not in { "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", } ) @@ -515,12 +522,23 @@ def test_catalog_qualification_is_scoped_to_the_validated_platform() -> None: ) if choice.model_id == "qwen25-coder-14b-instruct-awq-vllm" ) + qualified_terra_gpu = next( + choice + for choice in catalog_model_choices( + artifacts.artifacts, + snapshots.snapshots, + recommended_only=False, + ) + if choice.model_id == "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm" + ) assert cpu.qualification_for("generic") == "qualified" assert cpu.qualification_for("terra") == "qualified" assert cpu.qualification_for("carina") == "candidate" assert terra_gpu.qualification_for("terra") == "candidate" assert terra_gpu.qualification_for("carina") == "candidate" + assert qualified_terra_gpu.qualification_for("terra") == "qualified" + assert qualified_terra_gpu.qualification_for("carina") == "candidate" def _repository( diff --git a/packages/gateway/tests/test_model_snapshots.py b/packages/gateway/tests/test_model_snapshots.py index 19b01446..76730407 100644 --- a/packages/gateway/tests/test_model_snapshots.py +++ b/packages/gateway/tests/test_model_snapshots.py @@ -123,6 +123,7 @@ def test_repository_snapshot_catalog_pins_gpu_model_variants( if snapshot_id in { "qwen3-coder-30b-a3b-instruct-fp8-vllm", + "qwen3-coder-30b-a3b-instruct-w4a16-awq-vllm", } else "candidate" ) From 41b6679dec8bb15e1b92c2558ed01e138e9e811d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Jul 2026 23:30:24 -0700 Subject: [PATCH 35/38] Tighten GPU And Model Metadata Contracts --- images/generic/local-runtime/model-catalog.toml | 2 +- images/gpu/start_vllm.sh | 9 +++++++++ .../compliance/tests/test_container_assets.py | 16 ++++++++++++++++ .../src/heartwood/gateway/_local_import.py | 5 ++++- .../src/heartwood/gateway/_model_artifacts.py | 2 +- packages/gateway/tests/test_gateway_contract.py | 2 +- packages/gateway/tests/test_local_import.py | 2 ++ packages/gateway/tests/test_model_artifacts.py | 16 ++++++++-------- packages/webui/src/App.test.tsx | 2 +- packages/webui/src/client.test.ts | 2 +- packages/webui/src/e2e/app.spec.ts | 2 +- packages/webui/src/types.ts | 2 +- 12 files changed, 46 insertions(+), 16 deletions(-) diff --git a/images/generic/local-runtime/model-catalog.toml b/images/generic/local-runtime/model-catalog.toml index 90e6d005..c948083d 100644 --- a/images/generic/local-runtime/model-catalog.toml +++ b/images/generic/local-runtime/model-catalog.toml @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: MIT -schema_version = "heartwood.local-model-catalog.v1" +schema_version = "heartwood.local-model-catalog.v2" default_smoke_model = "llama-cpp-stories260k-ci" default_demo_agent_model = "qwen25-7b-instruct-q4_k_m" default_demo_coding_model = "qwen25-coder-7b-instruct-q4_k_m" diff --git a/images/gpu/start_vllm.sh b/images/gpu/start_vllm.sh index 7d62109e..9610e4c5 100755 --- a/images/gpu/start_vllm.sh +++ b/images/gpu/start_vllm.sh @@ -31,6 +31,15 @@ if [[ ! -x "${vllm}" ]]; then echo "vLLM executable is unavailable: ${vllm}" >&2 exit 69 fi +if [[ ! "${tensor_parallel_size}" =~ ^[1-9][0-9]*$ ]]; then + echo "HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE must be a positive integer" >&2 + exit 64 +fi +if [[ ! "${gpu_memory_utilization}" =~ ^(0\.[0-9]+|1(\.0+)?)$ ]] || \ + [[ "${gpu_memory_utilization}" =~ ^0\.0+$ ]]; then + echo "HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION must be greater than 0 and at most 1" >&2 + exit 64 +fi if [[ "${flashinfer_sampler}" != "0" && "${flashinfer_sampler}" != "1" ]]; then echo "HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER must be 0 or 1" >&2 exit 64 diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 2445bd2d..a708d084 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -464,6 +464,22 @@ def test_vllm_launcher_enforces_loopback_and_tool_calling(tmp_path: Path) -> Non assert invalid_sampler.returncode == 64 env.pop("HEARTWOOD_VLLM_USE_FLASHINFER_SAMPLER") + env["HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE"] = "0" + invalid_tensor_parallel = subprocess.run(["bash", str(script)], env=env, check=False) + assert invalid_tensor_parallel.returncode == 64 + + env["HEARTWOOD_VLLM_TENSOR_PARALLEL_SIZE"] = "2" + env["HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION"] = "0" + invalid_memory_utilization = subprocess.run(["bash", str(script)], env=env, check=False) + assert invalid_memory_utilization.returncode == 64 + + env["HEARTWOOD_VLLM_GPU_MEMORY_UTILIZATION"] = "1.0" + valid_resources = subprocess.run(["bash", str(script)], env=env, check=False) + assert valid_resources.returncode == 0 + values = arguments.read_text(encoding="utf-8").splitlines() + assert values[values.index("--tensor-parallel-size") + 1] == "2" + assert values[values.index("--gpu-memory-utilization") + 1] == "1.0" + env["HEARTWOOD_LOCAL_RUNTIME_HOST"] = "0.0.0.0" denied = subprocess.run(["bash", str(script)], env=env, check=False) assert denied.returncode == 64 diff --git a/packages/gateway/src/heartwood/gateway/_local_import.py b/packages/gateway/src/heartwood/gateway/_local_import.py index 32b0d9ce..891c175c 100644 --- a/packages/gateway/src/heartwood/gateway/_local_import.py +++ b/packages/gateway/src/heartwood/gateway/_local_import.py @@ -23,6 +23,7 @@ LocalModelChoice, LocalModelRuntime, ModelRepositoryError, + _license_id_from_posture, infer_model_type, infer_tool_call_parser, safe_snapshot_download_policy, @@ -76,6 +77,7 @@ def import_local_model( raise ModelRepositoryError("source revision must be an immutable commit hash") if not license_posture.strip(): raise ModelRepositoryError("the upstream model license must be recorded") + license_id = _license_id_from_posture(license_posture) if context_window < MINIMUM_AGENT_RUNTIME_CONTEXT_WINDOW: raise ModelRepositoryError( "Heartwood agent sessions require an imported model context window of at least " @@ -144,7 +146,7 @@ def import_local_model( artifact_sha256=checksum, minimum_resource_envelope=_minimum_resource_envelope(runtime, size_bytes), recommended_resource_envelope=_recommended_resource_envelope(runtime, size_bytes), - license_id="Unspecified", + license_id=license_id, precision="Repository-defined safetensors" if runtime == "vllm" else "GGUF quantized", minimum_gpu_count=1 if runtime == "vllm" else 0, minimum_gpu_memory_bytes=( @@ -176,6 +178,7 @@ def import_local_model( "source_revision": source_revision, "source_path": source.name if runtime == "llama-cpp" else None, "license_posture": license_posture.strip(), + "license_id": license_id, "size_bytes": size_bytes, "runtime": runtime, "model_type": model_type, diff --git a/packages/gateway/src/heartwood/gateway/_model_artifacts.py b/packages/gateway/src/heartwood/gateway/_model_artifacts.py index 815fc619..f397326d 100644 --- a/packages/gateway/src/heartwood/gateway/_model_artifacts.py +++ b/packages/gateway/src/heartwood/gateway/_model_artifacts.py @@ -338,7 +338,7 @@ def load_model_artifact_catalog(path: Path) -> ModelArtifactCatalog: msg = f"unable to load model artifact catalog {path}: {error}" raise ModelArtifactError(msg) from error schema_version = _string(data, "schema_version") - if schema_version != "heartwood.local-model-catalog.v1": + if schema_version != "heartwood.local-model-catalog.v2": msg = f"unsupported model artifact catalog schema: {schema_version}" raise ModelArtifactError(msg) models = data.get("models") diff --git a/packages/gateway/tests/test_gateway_contract.py b/packages/gateway/tests/test_gateway_contract.py index e08af33f..f32a6358 100644 --- a/packages/gateway/tests/test_gateway_contract.py +++ b/packages/gateway/tests/test_gateway_contract.py @@ -681,7 +681,7 @@ def test_rest_manages_model_profiles_and_artifact_metadata(tmp_path: Path) -> No assert selected.body["active_profile"] == "custom-loopback" assert validated.status_code == 200 assert artifacts.status_code == 200 - assert artifacts.body["schema_version"] == "heartwood.local-model-catalog.v1" + assert artifacts.body["schema_version"] == "heartwood.local-model-catalog.v2" assert artifacts.body["snapshot_schema_version"] == "heartwood.model-snapshot-catalog.v2" assert artifacts.body["snapshots"] assert removed.body["active_profile"] is None diff --git a/packages/gateway/tests/test_local_import.py b/packages/gateway/tests/test_local_import.py index 09906622..4385676b 100644 --- a/packages/gateway/tests/test_local_import.py +++ b/packages/gateway/tests/test_local_import.py @@ -43,8 +43,10 @@ def test_imports_gguf_atomically_with_provenance_and_integrity(tmp_path: Path) - assert imported.path != source assert imported.model.runtime == "llama-cpp" assert imported.model.artifact_sha256 is not None + assert imported.model.license_id == "Apache-2.0" manifest = json.loads((imported.path.parent / "heartwood-model.json").read_text()) assert manifest["source_repository"] == "example/research-model-gguf" + assert manifest["license_id"] == "Apache-2.0" assert str(source) not in json.dumps(manifest) diff --git a/packages/gateway/tests/test_model_artifacts.py b/packages/gateway/tests/test_model_artifacts.py index fe9dc6b8..14083e08 100644 --- a/packages/gateway/tests/test_model_artifacts.py +++ b/packages/gateway/tests/test_model_artifacts.py @@ -168,7 +168,7 @@ def test_background_manager_reports_ready_download( ) -> None: artifact = _artifact(b"content") catalog = ModelArtifactCatalog( - schema_version="heartwood.local-model-catalog.v1", + schema_version="heartwood.local-model-catalog.v2", artifacts=(artifact,), ) installed = tmp_path / "models" / artifact.artifact_id / "model.gguf" @@ -213,7 +213,7 @@ def test_background_manager_exposes_in_progress_byte_count( ) -> None: artifact = _artifact(b"content") catalog = ModelArtifactCatalog( - schema_version="heartwood.local-model-catalog.v1", + schema_version="heartwood.local-model-catalog.v2", artifacts=(artifact,), ) started = Event() @@ -261,7 +261,7 @@ def test_background_manager_reports_actionable_safe_errors( ) -> None: artifact = _artifact(b"content") catalog = ModelArtifactCatalog( - schema_version="heartwood.local-model-catalog.v1", + schema_version="heartwood.local-model-catalog.v2", artifacts=(artifact,), ) @@ -345,7 +345,7 @@ def download( selected: list[tuple[str, Path, str]] = [] manager = LocalModelDownloadManager( artifact_catalog=ModelArtifactCatalog( - schema_version="heartwood.local-model-catalog.v1", + schema_version="heartwood.local-model-catalog.v2", artifacts=(), ), snapshot_catalog=ModelSnapshotCatalog( @@ -393,7 +393,7 @@ def test_artifact_metadata_rejects_unsafe_values( def test_catalog_lookup_and_safe_serialization() -> None: artifact = _artifact(b"content") catalog = ModelArtifactCatalog( - schema_version="heartwood.local-model-catalog.v1", + schema_version="heartwood.local-model-catalog.v2", artifacts=(artifact,), ) @@ -415,7 +415,7 @@ def test_catalog_loader_rejects_malformed_catalogs_and_manifests(tmp_path: Path) load_model_artifact_catalog(catalog_path) catalog_path.write_text( - 'schema_version = "heartwood.local-model-catalog.v1"\n', + 'schema_version = "heartwood.local-model-catalog.v2"\n', encoding="utf-8", ) with pytest.raises(ModelArtifactError, match="models table"): @@ -424,7 +424,7 @@ def test_catalog_loader_rejects_malformed_catalogs_and_manifests(tmp_path: Path) catalog_path.write_text( "\n".join( ( - 'schema_version = "heartwood.local-model-catalog.v1"', + 'schema_version = "heartwood.local-model-catalog.v2"', "[models.invalid]", 'artifact_manifest = "manifest.toml"', ) @@ -444,7 +444,7 @@ def test_catalog_loader_rejects_duplicate_artifact_ids(tmp_path: Path) -> None: catalog_path.write_text( "\n".join( ( - 'schema_version = "heartwood.local-model-catalog.v1"', + 'schema_version = "heartwood.local-model-catalog.v2"', "[models.one]", 'artifact_manifest = "one.toml"', "[models.ignored]", diff --git a/packages/webui/src/App.test.tsx b/packages/webui/src/App.test.tsx index 7762da47..e257a2a3 100644 --- a/packages/webui/src/App.test.tsx +++ b/packages/webui/src/App.test.tsx @@ -497,7 +497,7 @@ class FakeClient implements HeartwoodClient { }; } return Promise.resolve({ - schema_version: "heartwood.local-model-catalog.v1", + schema_version: "heartwood.local-model-catalog.v2", snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [ { diff --git a/packages/webui/src/client.test.ts b/packages/webui/src/client.test.ts index 6ac77848..4126cecd 100644 --- a/packages/webui/src/client.test.ts +++ b/packages/webui/src/client.test.ts @@ -396,7 +396,7 @@ describe("GatewayClient", () => { it("lists and starts recommended model downloads", async () => { const artifacts = { - schema_version: "heartwood.local-model-catalog.v1", + schema_version: "heartwood.local-model-catalog.v2", snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [], snapshots: [], diff --git a/packages/webui/src/e2e/app.spec.ts b/packages/webui/src/e2e/app.spec.ts index 145ce5b8..9c8c8136 100644 --- a/packages/webui/src/e2e/app.spec.ts +++ b/packages/webui/src/e2e/app.spec.ts @@ -481,7 +481,7 @@ const installGatewayRoutes = async (page: Page): Promise => { await page.route("**/settings/models/artifacts", (route) => json(route, { - schema_version: "heartwood.local-model-catalog.v1", + schema_version: "heartwood.local-model-catalog.v2", snapshot_schema_version: "heartwood.model-snapshot-catalog.v2", artifacts: [], snapshots: [], diff --git a/packages/webui/src/types.ts b/packages/webui/src/types.ts index 76de395d..af65bb0d 100644 --- a/packages/webui/src/types.ts +++ b/packages/webui/src/types.ts @@ -493,7 +493,7 @@ export interface ModelSnapshot { } export interface ModelArtifacts { - schema_version: "heartwood.local-model-catalog.v1"; + schema_version: "heartwood.local-model-catalog.v2"; snapshot_schema_version: "heartwood.model-snapshot-catalog.v2"; artifacts: ModelArtifact[]; snapshots: ModelSnapshot[]; From 617c8c94d7250b3ce98c11f988a48156c7ea911e Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 22 Jul 2026 01:07:45 -0700 Subject: [PATCH 36/38] Bound GPU Qualification Cleanup --- images/gpu/coding_agent_e2e.sh | 15 +++++++++++++-- .../compliance/tests/test_container_assets.py | 4 ++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/images/gpu/coding_agent_e2e.sh b/images/gpu/coding_agent_e2e.sh index 36192746..b8a4620b 100755 --- a/images/gpu/coding_agent_e2e.sh +++ b/images/gpu/coding_agent_e2e.sh @@ -141,13 +141,24 @@ export HEARTWOOD_QUALIFICATION_MODEL_REVISION="${revision}" export HEARTWOOD_QUALIFICATION_RUNTIME_METADATA="${runtime_metadata}" echo "Loading ${repository}; expected startup is approximately ${startup_min}-${startup_max} seconds." -bash "${script_dir}/start_vllm.sh" >"${runtime_log}" 2>&1 & +if ! command -v setsid >/dev/null 2>&1; then + echo "setsid is required to isolate the vLLM process group" >&2 + exit 69 +fi +setsid bash "${script_dir}/start_vllm.sh" >"${runtime_log}" 2>&1 & runtime_pid="$!" cleanup() { status="$?" trap - EXIT - kill "${runtime_pid}" >/dev/null 2>&1 || true + kill -TERM -- "-${runtime_pid}" >/dev/null 2>&1 || true + for _ in {1..10}; do + if ! kill -0 "${runtime_pid}" >/dev/null 2>&1; then + break + fi + sleep 1 + done + kill -KILL -- "-${runtime_pid}" >/dev/null 2>&1 || true wait "${runtime_pid}" >/dev/null 2>&1 || true if ((status != 0)) && [[ -f "${runtime_log}" ]]; then echo "vLLM runtime log (last 240 lines):" >&2 diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index a708d084..e94d0199 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -489,6 +489,10 @@ def test_gpu_qualification_uses_isolated_heartwood_python() -> None: script = _read("images/gpu/coding_agent_e2e.sh") coding_agent = _read("images/generic/scripts/coding_agent_e2e.sh") + assert "command -v setsid" in script + assert 'setsid bash "${script_dir}/start_vllm.sh"' in script + assert 'kill -TERM -- "-${runtime_pid}"' in script + assert 'kill -KILL -- "-${runtime_pid}"' in script system_python = re.compile( r"(?:^|[;&|]\s*|\bexec\s+)(?:/[^\s;|&]+/)?python(?:3(?:\.\d+)?)?\s", re.MULTILINE, From 6dd9521fe23f9b0abdd5d458917e9e4634004bb2 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 22 Jul 2026 01:10:02 -0700 Subject: [PATCH 37/38] Clarify Carina Allocation Limits --- documentation/platforms/carina.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/platforms/carina.md b/documentation/platforms/carina.md index 82d928e6..cd1964ac 100644 --- a/documentation/platforms/carina.md +++ b/documentation/platforms/carina.md @@ -93,8 +93,8 @@ Heartwood labels one **Recommended** only after its complete tool, approval, edi | Tier | Model Configuration | GPUs | Recommended RAM | Free Project Storage | Default Context | Estimated First Start | |---|---|---:|---:|---:|---:|---:| | Powerful, qualified | Qwen3 Coder 30B FP8 | 1 x L40S | 96 GiB | 64 GiB | 32,768 | 3-10 minutes | -| Maximum capability | Qwen3 Coder Next FP8 | 4 x L40S | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | -| Maximum alternative | GPT-OSS 120B MXFP4 | 2 x L40S | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | +| Maximum candidate | Qwen3 Coder Next FP8 | 4 x L40S | 192 GiB | 128 GiB | 65,536 | 5-15 minutes | +| Maximum alternative candidate | GPT-OSS 120B MXFP4 | 2 x L40S | 160 GiB | 112 GiB | 65,536 | 5-15 minutes | Download sizes range from about 29.1 GiB for the qualified one-GPU model to 74.9 GiB for the largest candidate. See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for complete sizes and [GPU Compatibility](../reference/gpu-compatibility.md) for exact revisions and runtime settings. @@ -137,6 +137,7 @@ The interactive Slurm allocation and supervised vLLM process end with the Heartw - If a command disappears or is killed on a login node, stop and use Slurm for the compute work; Carina documents strict login-node limits. - If a partition is unavailable, run `sinfo --noheader --format='%P|%G|%a'` and choose one of the GPU-capable partitions Heartwood reports. +- If Slurm reports `QOSMaxGRESPerUser` or `QOSMaxMemoryPerUser`, the account cannot request the planned GPU or RAM total; choose the strongest qualified lower tier or ask the Carina project owner to review the account limits. - If the requested model does not fit the available GPU count or memory, choose the strongest compatible lower tier instead of changing tensor parallelism or precision manually. - If startup reports a driver or CUDA incompatibility, retain the released CUDA 12.9 environment and report the detected driver; do not install CUDA 13 into the Heartwood runtime. - If model startup fails, inspect `.heartwood/logs/` and the `HW-COMPUTE-*` checks from `heartwood doctor` without sharing project content or secrets. From e2c3318971e4e920bee8bda328efe717f8f492a8 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 22 Jul 2026 02:19:06 -0700 Subject: [PATCH 38/38] Prepare 0.2.0 Beta 6 Release --- README.md | 2 +- VERSION.toml | 2 +- docker-bake.hcl | 2 +- documentation/contribute/releases.md | 2 +- documentation/models/offline.md | 2 +- documentation/platforms/carina.md | 2 +- documentation/platforms/containers.md | 14 +++++----- documentation/platforms/native-linux.md | 2 +- documentation/platforms/terra.md | 13 +++++----- .../skills/omop-cohort-summary/SKILL.md | 2 +- .../skills/omop-cohort-summary/metadata.json | 2 +- images/gpu/compatibility.toml | 2 +- packages/adapters/pyproject.toml | 2 +- .../src/heartwood/adapters/__init__.py | 2 +- .../src/heartwood/adapters/conformance.py | 2 +- packages/adapters/tests/test_conformance.py | 2 +- packages/audit/pyproject.toml | 2 +- .../audit/src/heartwood/audit/__init__.py | 2 +- packages/cli/pyproject.toml | 2 +- packages/cli/src/heartwood/cli/__init__.py | 2 +- packages/compliance/pyproject.toml | 2 +- .../src/heartwood/compliance/__init__.py | 2 +- .../compliance/tests/test_container_assets.py | 2 +- .../tests/test_release_governance.py | 4 +-- packages/core-adapter/pyproject.toml | 2 +- .../src/heartwood/core_adapter/__init__.py | 2 +- packages/detector/pyproject.toml | 2 +- .../src/heartwood/detector/__init__.py | 2 +- packages/fixtures/pyproject.toml | 2 +- .../src/heartwood/fixtures/__init__.py | 2 +- packages/gateway/pyproject.toml | 2 +- packages/model-policy/pyproject.toml | 2 +- .../src/heartwood/model_policy/__init__.py | 2 +- packages/notebook/pyproject.toml | 2 +- .../src/heartwood/notebook/__init__.py | 2 +- packages/schemas/pyproject.toml | 2 +- .../schemas/src/heartwood/schemas/__init__.py | 2 +- packages/schemas/tests/test_schema_records.py | 4 +-- packages/session/pyproject.toml | 2 +- .../session/src/heartwood/session/__init__.py | 2 +- packages/skills/pyproject.toml | 2 +- packages/webui/package-lock.json | 4 +-- packages/webui/package.json | 2 +- skills/verified/aggregate-export/SKILL.md | 2 +- .../verified/aggregate-export/metadata.json | 2 +- skills/verified/baseline-model/SKILL.md | 2 +- skills/verified/baseline-model/metadata.json | 2 +- skills/verified/omop-cohort-summary/SKILL.md | 2 +- .../omop-cohort-summary/metadata.json | 2 +- uv.lock | 26 +++++++++---------- 50 files changed, 77 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 0f052634..69f5e58f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ docker run --rm -it \ --env HOME=/tmp \ -p 127.0.0.1:8767:8767 \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6 \ heartwood --interface web --host 0.0.0.0 ``` diff --git a/VERSION.toml b/VERSION.toml index deb9b60e..680948f7 100644 --- a/VERSION.toml +++ b/VERSION.toml @@ -2,4 +2,4 @@ # SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) # SPDX-License-Identifier: MIT -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" diff --git a/docker-bake.hcl b/docker-bake.hcl index 5f0b5df7..9a786aac 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -17,7 +17,7 @@ variable "GIT_SHA" { } variable "HEARTWOOD_VERSION" { - default = "0.2.0-beta.5" + default = "0.2.0-beta.6" } variable "TERRA_BASE_IMAGE" { diff --git a/documentation/contribute/releases.md b/documentation/contribute/releases.md index 02dbc46e..a57b0647 100644 --- a/documentation/contribute/releases.md +++ b/documentation/contribute/releases.md @@ -23,7 +23,7 @@ The workflow verifies immutable container candidates, builds and tests native as ## Stable and Preview Documentation A stable version updates the `stable` alias and the documentation root. -A prerelease such as `0.2.0-beta.5` updates the `preview` alias without replacing the stable root. +A prerelease such as `0.2.0-beta.6` updates the `preview` alias without replacing the stable root. The version store is deployed to GitHub Pages and retains immutable version paths. Publishing the same version with different content is rejected. diff --git a/documentation/models/offline.md b/documentation/models/offline.md index d359060e..82020674 100644 --- a/documentation/models/offline.md +++ b/documentation/models/offline.md @@ -32,7 +32,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6 \ heartwood ``` diff --git a/documentation/platforms/carina.md b/documentation/platforms/carina.md index cd1964ac..7f66a2e0 100644 --- a/documentation/platforms/carina.md +++ b/documentation/platforms/carina.md @@ -41,7 +41,7 @@ Do not use a shared project root itself as the Heartwood project. ```bash cd heartwood-installation curl --fail --location --remote-name \ - https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.5/heartwood-installer + https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.6/heartwood-installer chmod 700 heartwood-installer ./heartwood-installer --platform carina export PATH="$PWD/bin:$PATH" diff --git a/documentation/platforms/containers.md b/documentation/platforms/containers.md index eb04ee5c..f56f5919 100644 --- a/documentation/platforms/containers.md +++ b/documentation/platforms/containers.md @@ -27,7 +27,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6 \ heartwood ``` @@ -44,7 +44,7 @@ docker run --rm -it \ --env HOME=/tmp \ -p 127.0.0.1:8767:8767 \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6 \ heartwood --interface web --host 0.0.0.0 ``` @@ -63,7 +63,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-gpu-nvidia \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-gpu-nvidia \ heartwood ``` @@ -75,10 +75,10 @@ Review the [GPU compatibility matrix](../reference/gpu-compatibility.md) before Use immutable release tags for research work: -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5` — standard AMD64/ARM64 image; -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-gpu-nvidia` — NVIDIA GPU image; -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` — Terra CPU image; and -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` — Terra NVIDIA image. +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6` — standard AMD64/ARM64 image; +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-gpu-nvidia` — NVIDIA GPU image; +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra` — Terra CPU image; and +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra-gpu-nvidia` — Terra NVIDIA image. The moving `edge` tags represent current `main` and are intended for development, not reproducible analyses. Release publication verifies candidate digests and manifest shape before creating version tags. diff --git a/documentation/platforms/native-linux.md b/documentation/platforms/native-linux.md index b2b7dcc5..45522269 100644 --- a/documentation/platforms/native-linux.md +++ b/documentation/platforms/native-linux.md @@ -48,7 +48,7 @@ mkdir -m 700 heartwood-installation cd heartwood-installation curl --fail --location --remote-name \ - https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.5/heartwood-installer + https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.6/heartwood-installer chmod 700 heartwood-installer ./heartwood-installer --platform generic export PATH="$PWD/bin:$PATH" diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index c25b4deb..350eb419 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -41,10 +41,11 @@ Use one of these combinations: | Model Route | Image | Practical Starting Point | |---|---|---| -| Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | -| Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | -| Managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | -| Managed 32B GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.5-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, four T4 GPUs with 16 GB each, 200 GB persistent disk | +| Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | +| Heartwood-managed CPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra` | 16 CPUs, 60 GB RAM, 75 GB persistent disk | +| Lower-cost GPU evaluation | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra-gpu-nvidia` | 16 CPUs, 60 GB RAM, one T4 with 16 GB GPU memory, 100 GB persistent disk | +| Qualified managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, two T4 GPUs with 16 GB each, 200 GB persistent disk | +| Four-GPU model evaluation | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.6-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, four T4 GPUs with 16 GB each, 200 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. @@ -124,11 +125,11 @@ You can instead choose **Other Hugging Face model** and enter another public rep Heartwood inspects its metadata and reports a clear unsupported-model error when the available runtime cannot serve it safely. The pinned 14B AWQ snapshot downloads about 9.3 GiB; allow at least 32 GiB of free project storage and retain a 100 GB Terra persistent disk for the image, model cache, notebooks, and results. -The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 18.1 GiB; use at least 96 GB RAM, retain a 200 GB persistent disk, and keep the catalog's 18,432-token context so the two T4 GPUs retain key/value-cache headroom. +The pinned Qwen3 Coder 30B W4A16 AWQ snapshot downloads about 16.8 GiB; use at least 96 GB RAM, retain a 200 GB persistent disk, and keep the catalog's 18,432-token context so the two T4 GPUs retain key/value-cache headroom. The pinned Qwen2.5 Coder 32B AWQ snapshot downloads about 18.0 GiB and remains an evaluation candidate until its tool-calling path passes end-to-end. Model download progress appears in the terminal and files persist under `.heartwood/models/`. Running `heartwood models download MODEL` is itself an explicit request to download that model; the guided `heartwood` flow presents the selected model and asks before downloading it. -The first inference startup is planned for approximately 2-8 minutes while vLLM loads the model and prepares GPU memory. +Depending on the model and persistent-disk throughput, the first verification and inference startup is planned for approximately 2-15 minutes while Heartwood verifies the snapshot and vLLM prepares GPU memory. Heartwood reports the active stage, elapsed time, selected context capacity, and memory assessment while you wait. Use `heartwood --plain` when the full-screen terminal is not rendered correctly. diff --git a/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md b/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md index b23806d0..8aabaf84 100644 --- a/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md +++ b/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md @@ -10,7 +10,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.5" + heartwood.version: "0.2.0-beta.6" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/fixtures/synthetic/skills/omop-cohort-summary/metadata.json b/fixtures/synthetic/skills/omop-cohort-summary/metadata.json index e6348c3f..511a9669 100644 --- a/fixtures/synthetic/skills/omop-cohort-summary/metadata.json +++ b/fixtures/synthetic/skills/omop-cohort-summary/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.5", + "heartwood.version": "0.2.0-beta.6", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/images/gpu/compatibility.toml b/images/gpu/compatibility.toml index bba8090d..2a4d87a2 100644 --- a/images/gpu/compatibility.toml +++ b/images/gpu/compatibility.toml @@ -122,7 +122,7 @@ reason = "The model's W4A16 quantization group size crosses four-way tensor-para [[configurations]] configuration_id = "terra-2xt4-qwen3-coder-30b-awq" status = "qualified" -validated_at = "2026-07-21" +validated_at = "2026-07-22" validated_driver_version = "535.154.05" evidence = "https://github.com/SchmiedmayerLab/heartwood/pull/72" platform = "terra" diff --git a/packages/adapters/pyproject.toml b/packages/adapters/pyproject.toml index a271a5d2..8d752361 100644 --- a/packages/adapters/pyproject.toml +++ b/packages/adapters/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-adapters" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Adapter service provider interfaces and conformance checks for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/adapters/src/heartwood/adapters/__init__.py b/packages/adapters/src/heartwood/adapters/__init__.py index ff26ccb2..7b835a73 100644 --- a/packages/adapters/src/heartwood/adapters/__init__.py +++ b/packages/adapters/src/heartwood/adapters/__init__.py @@ -39,4 +39,4 @@ "assert_registry_adapter_conforms", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/adapters/src/heartwood/adapters/conformance.py b/packages/adapters/src/heartwood/adapters/conformance.py index 256d3d5d..6637915e 100644 --- a/packages/adapters/src/heartwood/adapters/conformance.py +++ b/packages/adapters/src/heartwood/adapters/conformance.py @@ -64,7 +64,7 @@ def assert_data_source_adapter_conforms( def assert_registry_adapter_conforms( adapter: RegistryAdapter, skill_id: str = "heartwood.synthetic.omop-cohort-summary", - version: str = "0.2.0-beta.5", + version: str = "0.2.0-beta.6", ) -> None: """Assert the shared minimum contract for registry adapters.""" assert adapter.registry_id diff --git a/packages/adapters/tests/test_conformance.py b/packages/adapters/tests/test_conformance.py index 7e1d20b5..d74e17e2 100644 --- a/packages/adapters/tests/test_conformance.py +++ b/packages/adapters/tests/test_conformance.py @@ -119,7 +119,7 @@ def resolve_skill(self, skill_id: str, version: str) -> SkillReference: def verify_skill(self, reference: SkillReference) -> RegistryVerification: """Verify the synthetic skill reference.""" return RegistryVerification( - verified=reference.version == "0.2.0-beta.5", + verified=reference.version == "0.2.0-beta.6", reason="synthetic fixture registry result", ) diff --git a/packages/audit/pyproject.toml b/packages/audit/pyproject.toml index 31fc861e..eb3042b0 100644 --- a/packages/audit/pyproject.toml +++ b/packages/audit/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-audit" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Hash-chained audit logging for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/audit/src/heartwood/audit/__init__.py b/packages/audit/src/heartwood/audit/__init__.py index 13f9637d..48c35ef8 100644 --- a/packages/audit/src/heartwood/audit/__init__.py +++ b/packages/audit/src/heartwood/audit/__init__.py @@ -18,4 +18,4 @@ "scrub_json_value", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/cli/pyproject.toml b/packages/cli/pyproject.toml index 6750caaf..a0807787 100644 --- a/packages/cli/pyproject.toml +++ b/packages/cli/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-cli" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "The heartwood command-line interface — the primary interaction surface." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/cli/src/heartwood/cli/__init__.py b/packages/cli/src/heartwood/cli/__init__.py index 0d05efde..8c20a553 100644 --- a/packages/cli/src/heartwood/cli/__init__.py +++ b/packages/cli/src/heartwood/cli/__init__.py @@ -72,7 +72,7 @@ __all__ = ["__version__", "main"] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" _PROG = "heartwood" diff --git a/packages/compliance/pyproject.toml b/packages/compliance/pyproject.toml index db9f3b6e..0f156ed9 100644 --- a/packages/compliance/pyproject.toml +++ b/packages/compliance/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-compliance" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Synthetic-only reviewer packet and audit bundle generation for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/compliance/src/heartwood/compliance/__init__.py b/packages/compliance/src/heartwood/compliance/__init__.py index 03f4ed03..78879099 100644 --- a/packages/compliance/src/heartwood/compliance/__init__.py +++ b/packages/compliance/src/heartwood/compliance/__init__.py @@ -12,4 +12,4 @@ __all__ = ["ReviewerPacket", "ReviewerPacketGenerator", "__version__"] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index e94d0199..ba9d7806 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -217,7 +217,7 @@ def test_runtime_image_sets_the_release_version_label() -> None: assert "ARG HEARTWOOD_VERSION=development" in dockerfile assert 'org.opencontainers.image.version="${HEARTWOOD_VERSION}"' in dockerfile assert 'variable "HEARTWOOD_VERSION"' in bake - assert 'default = "0.2.0-beta.5"' in bake + assert 'default = "0.2.0-beta.6"' in bake assert bake.count('HEARTWOOD_VERSION = "${HEARTWOOD_VERSION}"') == 2 diff --git a/packages/compliance/tests/test_release_governance.py b/packages/compliance/tests/test_release_governance.py index 9346f3cf..01720c4f 100644 --- a/packages/compliance/tests/test_release_governance.py +++ b/packages/compliance/tests/test_release_governance.py @@ -189,9 +189,9 @@ def test_prerelease_sources_use_semver_and_python_lock_uses_pep440( assert _release_verifier().source_version_errors(tmp_path, version) == [] - skill_metadata.write_text('{"heartwood.version": "0.2.0-beta.5"}\n', encoding="utf-8") + skill_metadata.write_text('{"heartwood.version": "0.2.0-beta.6"}\n', encoding="utf-8") assert ( - "skills/verified/example/metadata.json: 0.2.0-beta.5" + "skills/verified/example/metadata.json: 0.2.0-beta.6" in _release_verifier().source_version_errors(tmp_path, version) ) diff --git a/packages/core-adapter/pyproject.toml b/packages/core-adapter/pyproject.toml index 9c23cbcc..b4f63450 100644 --- a/packages/core-adapter/pyproject.toml +++ b/packages/core-adapter/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-core-adapter" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Core harness orchestration for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/core-adapter/src/heartwood/core_adapter/__init__.py b/packages/core-adapter/src/heartwood/core_adapter/__init__.py index d7b8f9d9..bf1ac111 100644 --- a/packages/core-adapter/src/heartwood/core_adapter/__init__.py +++ b/packages/core-adapter/src/heartwood/core_adapter/__init__.py @@ -35,4 +35,4 @@ "__version__", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/detector/pyproject.toml b/packages/detector/pyproject.toml index daf0dd9d..19423013 100644 --- a/packages/detector/pyproject.toml +++ b/packages/detector/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-detector" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Deterministic, propose-not-commit environment and dataset detection for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/detector/src/heartwood/detector/__init__.py b/packages/detector/src/heartwood/detector/__init__.py index 33f68f6a..cbbc64df 100644 --- a/packages/detector/src/heartwood/detector/__init__.py +++ b/packages/detector/src/heartwood/detector/__init__.py @@ -27,4 +27,4 @@ "platform_detection_evidence", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/fixtures/pyproject.toml b/packages/fixtures/pyproject.toml index 97051129..8242c5c3 100644 --- a/packages/fixtures/pyproject.toml +++ b/packages/fixtures/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-fixtures" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Synthetic fixture linting for Heartwood tests and replay artifacts." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/fixtures/src/heartwood/fixtures/__init__.py b/packages/fixtures/src/heartwood/fixtures/__init__.py index a17828e9..591f36a0 100644 --- a/packages/fixtures/src/heartwood/fixtures/__init__.py +++ b/packages/fixtures/src/heartwood/fixtures/__init__.py @@ -12,4 +12,4 @@ __all__ = ["FixtureFinding", "__version__", "lint_fixture_tree", "main"] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/gateway/pyproject.toml b/packages/gateway/pyproject.toml index 4bf550f3..dde31b11 100644 --- a/packages/gateway/pyproject.toml +++ b/packages/gateway/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-gateway" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Session gateway for Heartwood command and event streams." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/model-policy/pyproject.toml b/packages/model-policy/pyproject.toml index 8850df72..e2a857e7 100644 --- a/packages/model-policy/pyproject.toml +++ b/packages/model-policy/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-model-policy" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Deny-by-default model-call policy evaluation for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/model-policy/src/heartwood/model_policy/__init__.py b/packages/model-policy/src/heartwood/model_policy/__init__.py index fc649235..0a33cd1f 100644 --- a/packages/model-policy/src/heartwood/model_policy/__init__.py +++ b/packages/model-policy/src/heartwood/model_policy/__init__.py @@ -23,4 +23,4 @@ "normalize_endpoint", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/notebook/pyproject.toml b/packages/notebook/pyproject.toml index 630584d6..2dc8286e 100644 --- a/packages/notebook/pyproject.toml +++ b/packages/notebook/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-notebook" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Notebook-facing Python API and widget bridge for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/notebook/src/heartwood/notebook/__init__.py b/packages/notebook/src/heartwood/notebook/__init__.py index b5336da1..23b4e934 100644 --- a/packages/notebook/src/heartwood/notebook/__init__.py +++ b/packages/notebook/src/heartwood/notebook/__init__.py @@ -39,4 +39,4 @@ "render_widgets", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/schemas/pyproject.toml b/packages/schemas/pyproject.toml index a5e44a04..aa8b99bd 100644 --- a/packages/schemas/pyproject.toml +++ b/packages/schemas/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-schemas" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Versioned typed schemas for Heartwood policy, audit, detection, and skill metadata records." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/schemas/src/heartwood/schemas/__init__.py b/packages/schemas/src/heartwood/schemas/__init__.py index c15a0337..75fad642 100644 --- a/packages/schemas/src/heartwood/schemas/__init__.py +++ b/packages/schemas/src/heartwood/schemas/__init__.py @@ -39,4 +39,4 @@ "schema_names", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/schemas/tests/test_schema_records.py b/packages/schemas/tests/test_schema_records.py index bb8abb6e..0a3f28d7 100644 --- a/packages/schemas/tests/test_schema_records.py +++ b/packages/schemas/tests/test_schema_records.py @@ -159,14 +159,14 @@ def test_skill_metadata_accepts_skill_md_aliases() -> None: "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.5", + "heartwood.version": "0.2.0-beta.6", "heartwood.sig": "sigstore:synthetic-bundle", } ) assert metadata.dataset_types == ("omop-cdm", "fhir") assert metadata.platforms == ("generic", "terra") assert metadata.requires_network is False - assert metadata.version == "0.2.0-beta.5" + assert metadata.version == "0.2.0-beta.6" @pytest.mark.parametrize( diff --git a/packages/session/pyproject.toml b/packages/session/pyproject.toml index 765892bc..88576cb4 100644 --- a/packages/session/pyproject.toml +++ b/packages/session/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-session" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Shared session command/event contract for Heartwood interfaces." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/session/src/heartwood/session/__init__.py b/packages/session/src/heartwood/session/__init__.py index 7e621488..eedf845e 100644 --- a/packages/session/src/heartwood/session/__init__.py +++ b/packages/session/src/heartwood/session/__init__.py @@ -35,4 +35,4 @@ "validate_session_id", ] -__version__ = "0.2.0-beta.5" +__version__ = "0.2.0-beta.6" diff --git a/packages/skills/pyproject.toml b/packages/skills/pyproject.toml index e086104b..bc13ff15 100644 --- a/packages/skills/pyproject.toml +++ b/packages/skills/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-skills" -version = "0.2.0-beta.5" +version = "0.2.0-beta.6" description = "Local SKILL.md verification and deterministic skill test harnesses for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/webui/package-lock.json b/packages/webui/package-lock.json index cfb85fb6..c21df555 100644 --- a/packages/webui/package-lock.json +++ b/packages/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@heartwood/webui", - "version": "0.2.0-beta.5", + "version": "0.2.0-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@heartwood/webui", - "version": "0.2.0-beta.5", + "version": "0.2.0-beta.6", "license": "MIT", "dependencies": { "@stanfordspezi/spezi-web-design-system": "0.20.0", diff --git a/packages/webui/package.json b/packages/webui/package.json index 29580f98..68a75edb 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,6 +1,6 @@ { "name": "@heartwood/webui", - "version": "0.2.0-beta.5", + "version": "0.2.0-beta.6", "private": true, "type": "module", "description": "Researcher web UI for Heartwood sessions.", diff --git a/skills/verified/aggregate-export/SKILL.md b/skills/verified/aggregate-export/SKILL.md index 1edb0934..a0a5b428 100644 --- a/skills/verified/aggregate-export/SKILL.md +++ b/skills/verified/aggregate-export/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.5" + heartwood.version: "0.2.0-beta.6" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/aggregate-export/metadata.json b/skills/verified/aggregate-export/metadata.json index e6348c3f..511a9669 100644 --- a/skills/verified/aggregate-export/metadata.json +++ b/skills/verified/aggregate-export/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.5", + "heartwood.version": "0.2.0-beta.6", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/skills/verified/baseline-model/SKILL.md b/skills/verified/baseline-model/SKILL.md index 65d9fc68..4e15b01c 100644 --- a/skills/verified/baseline-model/SKILL.md +++ b/skills/verified/baseline-model/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.5" + heartwood.version: "0.2.0-beta.6" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/baseline-model/metadata.json b/skills/verified/baseline-model/metadata.json index e6348c3f..511a9669 100644 --- a/skills/verified/baseline-model/metadata.json +++ b/skills/verified/baseline-model/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.5", + "heartwood.version": "0.2.0-beta.6", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/skills/verified/omop-cohort-summary/SKILL.md b/skills/verified/omop-cohort-summary/SKILL.md index e8d99e41..1d16827f 100644 --- a/skills/verified/omop-cohort-summary/SKILL.md +++ b/skills/verified/omop-cohort-summary/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.5" + heartwood.version: "0.2.0-beta.6" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/omop-cohort-summary/metadata.json b/skills/verified/omop-cohort-summary/metadata.json index e6348c3f..511a9669 100644 --- a/skills/verified/omop-cohort-summary/metadata.json +++ b/skills/verified/omop-cohort-summary/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.5", + "heartwood.version": "0.2.0-beta.6", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/uv.lock b/uv.lock index 735a41db..e3c96a92 100644 --- a/uv.lock +++ b/uv.lock @@ -1472,7 +1472,7 @@ wheels = [ [[package]] name = "heartwood-adapters" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/adapters" } dependencies = [ { name = "heartwood-detector" }, @@ -1489,7 +1489,7 @@ requires-dist = [ [[package]] name = "heartwood-audit" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/audit" } dependencies = [ { name = "heartwood-schemas" }, @@ -1500,7 +1500,7 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-cli" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/cli" } dependencies = [ { name = "heartwood-gateway" }, @@ -1521,7 +1521,7 @@ requires-dist = [ [[package]] name = "heartwood-compliance" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/compliance" } dependencies = [ { name = "heartwood-audit" }, @@ -1536,7 +1536,7 @@ requires-dist = [ [[package]] name = "heartwood-core-adapter" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/core-adapter" } dependencies = [ { name = "heartwood-adapters" }, @@ -1557,7 +1557,7 @@ requires-dist = [ [[package]] name = "heartwood-detector" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/detector" } dependencies = [ { name = "heartwood-schemas" }, @@ -1568,12 +1568,12 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-fixtures" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/fixtures" } [[package]] name = "heartwood-gateway" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/gateway" } dependencies = [ { name = "anthropic" }, @@ -1612,7 +1612,7 @@ requires-dist = [ [[package]] name = "heartwood-model-policy" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/model-policy" } dependencies = [ { name = "heartwood-schemas" }, @@ -1623,7 +1623,7 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-notebook" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/notebook" } dependencies = [ { name = "heartwood-gateway" }, @@ -1647,7 +1647,7 @@ provides-extras = ["widgets"] [[package]] name = "heartwood-schemas" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/schemas" } dependencies = [ { name = "pydantic" }, @@ -1658,7 +1658,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12" }] [[package]] name = "heartwood-session" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/session" } dependencies = [ { name = "pydantic" }, @@ -1669,7 +1669,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12" }] [[package]] name = "heartwood-skills" -version = "0.2.0b5" +version = "0.2.0b6" source = { editable = "packages/skills" } dependencies = [ { name = "heartwood-schemas" },