From 31fdc1c00833c75d589b9485175b9e25ac2e6b30 Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Wed, 15 Apr 2026 18:16:23 -0700 Subject: [PATCH 1/3] Add GPU-selectable cloud connections Introduce GPU-specific Livepeer Fal deployments. This can be selected via the SCOPE_CLOUD_GPU environment variable with the values of `rtx4090` or `rtx5090`. Unset will continue to default to H100. Note that this breaks the old cloud relay mode --- .github/workflows/fal-deploy.yml | 63 ++++++++++++++++++++--------- docs/livepeer.md | 37 ++++++++++++++++- src/scope/cloud/livepeer_fal_app.py | 23 ++++++++++- src/scope/server/app.py | 7 ++-- src/scope/server/livepeer_client.py | 36 +++++++++++++---- 5 files changed, 134 insertions(+), 32 deletions(-) diff --git a/.github/workflows/fal-deploy.yml b/.github/workflows/fal-deploy.yml index 00f4ce523..dc4766550 100644 --- a/.github/workflows/fal-deploy.yml +++ b/.github/workflows/fal-deploy.yml @@ -11,15 +11,6 @@ on: - main - prod default: 'main' - app: - description: 'Which app to deploy' - required: true - type: choice - options: - - both - - scope-app - - scope-livepeer - default: 'both' workflow_run: workflows: ["Build and Push Docker Image"] types: @@ -51,10 +42,6 @@ jobs: run: pip install fal - name: Deploy Scope app to fal - if: > - github.event_name == 'workflow_run' || - github.event.inputs.app == 'both' || - github.event.inputs.app == 'scope-app' env: FAL_KEY: ${{ secrets.FAL_KEY }} run: | @@ -68,11 +55,7 @@ jobs: echo "Deploying Scope app to $ENV" fal deploy --env $ENV --auth public src/scope/cloud/fal_app.py - - name: Deploy Livepeer runner to fal - if: > - github.event_name == 'workflow_run' || - github.event.inputs.app == 'both' || - github.event.inputs.app == 'scope-livepeer' + - name: Deploy default Livepeer runner to fal env: FAL_KEY: ${{ secrets.FAL_KEY }} run: | @@ -84,8 +67,52 @@ jobs: fi echo "Deploying Livepeer runner to $ENV as scope-livepeer" + echo "Using default deploy GPU" + fal deploy \ --app-name scope-livepeer \ --env $ENV \ --auth private \ src/scope/cloud/livepeer_fal_app.py + + - name: Deploy RTX 4090 Livepeer runner to fal + env: + FAL_KEY: ${{ secrets.FAL_KEY }} + SCOPE_DEPLOY_GPU: GPU-RTX4090 + run: | + # Use input environment for manual dispatch, default to main for workflow_run + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + ENV="${{ github.event.inputs.environment }}" + else + ENV="main" + fi + + echo "Deploying Livepeer runner to $ENV as scope-livepeer-rtx4090" + echo "Using deploy GPU override: ${SCOPE_DEPLOY_GPU}" + + fal deploy \ + --app-name scope-livepeer-rtx4090 \ + --env $ENV \ + --auth private \ + src/scope/cloud/livepeer_fal_app.py + + - name: Deploy RTX 5090 Livepeer runner to fal + env: + FAL_KEY: ${{ secrets.FAL_KEY }} + SCOPE_DEPLOY_GPU: GPU-RTX5090 + run: | + # Use input environment for manual dispatch, default to main for workflow_run + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + ENV="${{ github.event.inputs.environment }}" + else + ENV="main" + fi + + echo "Deploying Livepeer runner to $ENV as scope-livepeer-rtx5090" + echo "Using deploy GPU override: ${SCOPE_DEPLOY_GPU}" + + fal deploy \ + --app-name scope-livepeer-rtx5090 \ + --env $ENV \ + --auth private \ + src/scope/cloud/livepeer_fal_app.py diff --git a/docs/livepeer.md b/docs/livepeer.md index fee730e8f..745f03008 100644 --- a/docs/livepeer.md +++ b/docs/livepeer.md @@ -42,6 +42,23 @@ fal deploy --env main --auth public src/scope/cloud/livepeer_fal_app.py This starts the Livepeer runner as a subprocess inside the Fal container and proxies `/ws` traffic to it. +Use `SCOPE_DEPLOY_GPU` to select the Fal machine type at deploy time: + +```bash +# Default H100 deployment: +fal deploy --app-name scope-livepeer --env prod --auth private src/scope/cloud/livepeer_fal_app.py + +# RTX 4090 deployment: +SCOPE_DEPLOY_GPU=GPU-RTX4090 \ +fal deploy --app-name scope-livepeer-rtx4090 --env prod --auth private src/scope/cloud/livepeer_fal_app.py + +# RTX 5090 deployment: +SCOPE_DEPLOY_GPU=GPU-RTX5090 \ +fal deploy --app-name scope-livepeer-rtx5090 --env prod --auth private src/scope/cloud/livepeer_fal_app.py +``` + +Supported deploy values are `GPU-RTX4090` and `GPU-RTX5090`. When `SCOPE_DEPLOY_GPU` is unset, the wrapper keeps the current default `GPU-H100`. + ## Start the Scope Server Set environment variables and launch the server: @@ -71,7 +88,22 @@ LIVEPEER_WS_URL=wss://fal.run//ws \ uv run daydream-scope ``` -To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and `SCOPE_CLOUD_APP_ID`. In that case the runner URL uses the default Livepeer flow. +To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and `SCOPE_CLOUD_APP_ID`. In that case Scope uses the built-in default Livepeer app id. + +```bash +# Default H100-backed Livepeer deployment: +SCOPE_CLOUD_MODE=livepeer \ +LIVEPEER_TOKEN= \ +uv run daydream-scope + +# Select the RTX 4090 Livepeer deployment: +SCOPE_CLOUD_MODE=livepeer \ +LIVEPEER_TOKEN= \ +SCOPE_CLOUD_GPU=rtx4090 \ +uv run daydream-scope +``` + +If `SCOPE_CLOUD_APP_ID` is set, Scope will use that app id as-is and will not rewrite it based on `SCOPE_CLOUD_GPU`. ### Environment Variables @@ -81,7 +113,8 @@ To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and | `LIVEPEER_ORCH_URL` | No | Explicit orchestrator URL. Formats: `host[:port]` or `http(s)://host[:port]`. If unset, token discovery is used. | | `LIVEPEER_SIGNER` | No | Override signer URL used for Livepeer payments. To disable payments, set to a falsy value such as `"off"`. | | `LIVEPEER_WS_URL` | No | Explicit runner WebSocket URL (e.g. `ws://127.0.0.1:8001/ws`). | -| `SCOPE_CLOUD_APP_ID` | No | Fal app id used to construct `ws_url` as `wss://fal.run/`. Must include `/ws` suffix. Used when `LIVEPEER_WS_URL` is not set. | +| `SCOPE_CLOUD_APP_ID` | No | Fal app id used to construct `ws_url` as `wss://fal.run/`. Values must include the `/ws` suffix. When unset, Scope uses the built-in default `daydream/scope-livepeer--prod/ws`. | +| `SCOPE_CLOUD_GPU` | No | Livepeer GPU selector. Supported values: `h100`, `rtx4090`, `rtx5090`. Default `h100`. Ignored when `LIVEPEER_WS_URL` or `SCOPE_CLOUD_APP_ID` is set. | | `LIVEPEER_TOKEN` | No | Base64-encoded JSON token used to start the LV2V job. Can be used to override Livepeer orch / payments routing. | | `LIVEPEER_DEBUG` | No | Enables debug logging for the Livepeer Gateway SDK and local Livepeer modules. | | `LIVEPEER_DEV_MODE` | No | Used for developing against a local Livepeer orchestrator with self-signed certificates. | diff --git a/src/scope/cloud/livepeer_fal_app.py b/src/scope/cloud/livepeer_fal_app.py index 3a48ed49d..adc5dd16d 100644 --- a/src/scope/cloud/livepeer_fal_app.py +++ b/src/scope/cloud/livepeer_fal_app.py @@ -27,6 +27,7 @@ RUNNER_MAX_FAILURES_PER_WINDOW = 20 RUNNER_FAILURE_WINDOW_SECONDS = 60.0 ASSETS_DIR_PATH = "/tmp/.daydream-scope/assets" +SCOPE_DEPLOY_GPU_ENV = "SCOPE_DEPLOY_GPU" # Gates startup cleanup so only one cleanup run executes at a time. _cleanup_event: asyncio.Event | None = None @@ -129,6 +130,21 @@ def _get_git_sha() -> str: ) +def _get_livepeer_machine_type() -> str: + """Return the Fal machine type selected by SCOPE_DEPLOY_GPU.""" + deploy_gpu = os.getenv(SCOPE_DEPLOY_GPU_ENV, "").strip() + if not deploy_gpu: + return "GPU-H100" + if deploy_gpu in {"GPU-RTX4090", "GPU-RTX5090"}: + return deploy_gpu + raise ValueError( + "Invalid SCOPE_DEPLOY_GPU. Expected `GPU-RTX4090`, `GPU-RTX5090`, or unset." + ) + + +LIVEPEER_MACHINE_TYPE = _get_livepeer_machine_type() + + def _runner_is_ready() -> bool: """Return True when the local runner HTTP server responds.""" import urllib.error @@ -224,7 +240,7 @@ class LivepeerScopeApp(fal.App, keep_alive=300): """fal entrypoint that runs and proxies the existing Livepeer Scope runner.""" image = custom_image - machine_type = "GPU-H100" + machine_type = LIVEPEER_MACHINE_TYPE requirements = [ "websockets", "httpx", @@ -235,6 +251,11 @@ def setup(self): import subprocess print(f"Starting Livepeer runner wrapper setup... (version: {GIT_SHA})") + print( + "Resolved Livepeer deploy GPU: " + f"{os.getenv(SCOPE_DEPLOY_GPU_ENV, '').strip() or 'default'} " + f"-> {self.machine_type}" + ) try: result = subprocess.run( diff --git a/src/scope/server/app.py b/src/scope/server/app.py index 470771551..ffdd48b93 100644 --- a/src/scope/server/app.py +++ b/src/scope/server/app.py @@ -19,6 +19,7 @@ from typing import TYPE_CHECKING import click +import click.core import uvicorn from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, Request from fastapi.middleware.cors import CORSMiddleware @@ -3115,7 +3116,7 @@ async def connect_to_cloud( # Use request body credentials if provided, otherwise fall back to CLI/env app_id = request.app_id or os.environ.get("SCOPE_CLOUD_APP_ID") api_key = request.api_key or os.environ.get("SCOPE_CLOUD_API_KEY") - if not app_id: + if not app_id and not is_livepeer_enabled(): raise HTTPException( status_code=400, detail="cloud credentials not configured. Use --cloud-app-id and --cloud-api-key CLI args, " @@ -3425,7 +3426,7 @@ def run_server(reload: bool, host: str, port: int, no_browser: bool): ) @click.option( "--cloud-app-id", - default="Daydream/scope-app--prod/ws", + default=None, envvar="SCOPE_CLOUD_APP_ID", help="Cloud app ID for cloud mode (e.g., 'username/scope-app')", ) @@ -3460,8 +3461,6 @@ def main( # MCP mode: run the MCP stdio server instead of the HTTP server if mcp: - import click.core - from .mcp_server import run_mcp_server # Only pre-connect if --port was explicitly provided on the command line diff --git a/src/scope/server/livepeer_client.py b/src/scope/server/livepeer_client.py index 1b036da56..e3067a202 100644 --- a/src/scope/server/livepeer_client.py +++ b/src/scope/server/livepeer_client.py @@ -57,6 +57,31 @@ TASK_DRAIN_TIMEOUT_S = 0.25 RUNNER_RESTART_TIMEOUT_S = 30.0 PAYMENT_SEND_INTERVAL_S = 10.0 +SCOPE_CLOUD_GPU_ENV = "SCOPE_CLOUD_GPU" +DEFAULT_LIVEPEER_APP_ID = "daydream/scope-livepeer--prod/ws" + + +def _normalize_optional_string(value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None + + +def _resolve_livepeer_app_id( + app_id: str | None, +) -> str | None: + normalized_app_id = _normalize_optional_string(app_id) + if normalized_app_id is not None: + return normalized_app_id.strip("/") + + gpu = _normalize_optional_string(os.getenv(SCOPE_CLOUD_GPU_ENV)) + if gpu not in {None, "h100", "rtx4090", "rtx5090"}: + raise ValueError( + "Invalid SCOPE_CLOUD_GPU. Expected `h100`, `rtx4090`, `rtx5090`, or unset." + ) + gpu_suffix = "" if gpu in {None, "h100"} else f"-{gpu}" + return f"daydream/scope-livepeer{gpu_suffix}--prod/ws" @dataclass(slots=True) @@ -312,14 +337,11 @@ def _normalize_ws_url(value: str | None) -> str | None: @staticmethod def _ws_url_from_app_id(value: str | None) -> str | None: - # HACK: Ignore the default app_id to use the orchestrator's own config - if not value or value == "Daydream/scope-app--prod/ws": + resolved_app_id = _resolve_livepeer_app_id(value) + if not resolved_app_id: return None try: - trimmed = value.strip() - if not trimmed: - raise ValueError - app_id = trimmed.strip("/") + app_id = resolved_app_id.strip("/") if not app_id.endswith("/ws"): raise ValueError ws_url = f"wss://fal.run/{app_id}" @@ -329,7 +351,7 @@ def _ws_url_from_app_id(value: str | None) -> str | None: except Exception: raise ValueError( "Invalid cloud app id. Expected a non-empty app id ending in " - "`/ws` (for example `daydream/scope-app/ws`)." + "`/ws` (for example `daydream/custom-runner--prod/ws`)." ) from None if parsed.scheme not in {"ws", "wss"}: raise ValueError("Invalid ws_url. Expected a valid ws:// or wss:// URL.") From 17c2713619f931e0fef2d78c4f2e435861762caf Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Wed, 15 Apr 2026 15:42:24 -0700 Subject: [PATCH 2/3] Livepeer mode default --- src/scope/server/livepeer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scope/server/livepeer.py b/src/scope/server/livepeer.py index 696d216db..2b0ba119d 100644 --- a/src/scope/server/livepeer.py +++ b/src/scope/server/livepeer.py @@ -34,7 +34,7 @@ def is_livepeer_enabled() -> bool: """Check if Livepeer mode is enabled via environment variables.""" - return os.getenv("SCOPE_CLOUD_MODE", "").lower() == "livepeer" + return os.getenv("SCOPE_CLOUD_MODE", "livepeer").lower() == "livepeer" class LivepeerConnection: From 510baaa2a3a0a44cb6134e957737852cd043ffc3 Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Wed, 15 Apr 2026 20:16:14 -0700 Subject: [PATCH 3/3] README fixup --- docs/livepeer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/livepeer.md b/docs/livepeer.md index 745f03008..7939016fa 100644 --- a/docs/livepeer.md +++ b/docs/livepeer.md @@ -113,7 +113,7 @@ If `SCOPE_CLOUD_APP_ID` is set, Scope will use that app id as-is and will not re | `LIVEPEER_ORCH_URL` | No | Explicit orchestrator URL. Formats: `host[:port]` or `http(s)://host[:port]`. If unset, token discovery is used. | | `LIVEPEER_SIGNER` | No | Override signer URL used for Livepeer payments. To disable payments, set to a falsy value such as `"off"`. | | `LIVEPEER_WS_URL` | No | Explicit runner WebSocket URL (e.g. `ws://127.0.0.1:8001/ws`). | -| `SCOPE_CLOUD_APP_ID` | No | Fal app id used to construct `ws_url` as `wss://fal.run/`. Values must include the `/ws` suffix. When unset, Scope uses the built-in default `daydream/scope-livepeer--prod/ws`. | +| `SCOPE_CLOUD_APP_ID` | No | Fal app id used to construct `ws_url` as `wss://fal.run/`. Must include `/ws` suffix. Used when `LIVEPEER_WS_URL` is not set. | | `SCOPE_CLOUD_GPU` | No | Livepeer GPU selector. Supported values: `h100`, `rtx4090`, `rtx5090`. Default `h100`. Ignored when `LIVEPEER_WS_URL` or `SCOPE_CLOUD_APP_ID` is set. | | `LIVEPEER_TOKEN` | No | Base64-encoded JSON token used to start the LV2V job. Can be used to override Livepeer orch / payments routing. | | `LIVEPEER_DEBUG` | No | Enables debug logging for the Livepeer Gateway SDK and local Livepeer modules. |