|
| 1 | +# Proposal: `temporal` Endpoint Type in Launch |
| 2 | + |
| 3 | +**Author:** lily.zhu@scale.com |
| 4 | +**Status:** RFC |
| 5 | +**Ticket:** MLI-6425 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Problem |
| 10 | + |
| 11 | +Teams running multi-step GPU pipelines (e.g. robotics ego hand keypoints: SVO processing → Dyn-HaMR → hand annotations) need durable, retryable orchestration across heterogeneous GPU types. Today they have two options: |
| 12 | + |
| 13 | +1. **Launch async endpoints (Celery)** — Launch manages the pods, but Celery gives no cross-step durability. If the H100 pod running Dyn-HaMR crashes mid-run, the whole pipeline restarts from step 1. |
| 14 | +2. **Raw K8s Deployments via std-ml-srv** — bypasses Launch entirely; teams lose unified deployment API, GPU scheduling integration, and Launch dashboards. |
| 15 | + |
| 16 | +Temporal solves the durability problem: if a pod crashes mid-activity, Temporal retries *only that activity* from the last heartbeat. Phases already completed are not re-run. |
| 17 | + |
| 18 | +The gap is that Launch has no native way to create and manage Temporal activity worker pods. Teams either re-implement the same ~80-line Temporal worker boilerplate per service, or deploy outside Launch. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Proposed Solution |
| 23 | + |
| 24 | +Add `temporal` as a fourth endpoint type in `ModelEndpointType`. A `temporal` endpoint is a K8s Deployment whose pods connect to Temporal server and pull activity tasks from a named task queue — instead of polling a Celery queue. |
| 25 | + |
| 26 | +**What Launch manages:** |
| 27 | +- Pod lifecycle (create / update / delete) |
| 28 | +- GPU scheduling and node selection |
| 29 | +- Scaling (fixed replicas in MVP; Temporal-aware autoscaling as follow-up) |
| 30 | +- Unified visibility in Launch dashboards |
| 31 | + |
| 32 | +**What Launch does not touch:** |
| 33 | +- Task submission (the Temporal workflow dispatches activities directly) |
| 34 | +- Result routing (Temporal handles activity results) |
| 35 | +- `/v1/async-tasks` API (not applicable for `temporal` endpoints) |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## API Changes |
| 40 | + |
| 41 | +### `POST /v1/model-endpoints` |
| 42 | + |
| 43 | +New field on `CreateModelEndpointV1Request`: |
| 44 | + |
| 45 | +```python |
| 46 | +temporal_task_queue: Optional[str] |
| 47 | +# Required when endpoint_type="temporal". |
| 48 | +# The Temporal task queue that workers will poll. |
| 49 | +# Example: "robotics-hand-keypoints-temporal" |
| 50 | +``` |
| 51 | + |
| 52 | +Example request: |
| 53 | + |
| 54 | +```json |
| 55 | +{ |
| 56 | + "name": "hand-keypoints-temporal", |
| 57 | + "endpoint_type": "temporal", |
| 58 | + "temporal_task_queue": "robotics-hand-keypoints-temporal", |
| 59 | + "gpus": 1, |
| 60 | + "gpu_type": "nvidia-hopper-h100", |
| 61 | + "cpus": 20, |
| 62 | + "memory": "200Gi", |
| 63 | + "storage": "250Gi", |
| 64 | + "min_workers": 0, |
| 65 | + "max_workers": 10, |
| 66 | + "per_worker": 1, |
| 67 | + "model_bundle_id": "...", |
| 68 | + "labels": {"team": "robotics", "product": "ego"} |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +The `model_bundle_id` points to a bundle whose command runs the Temporal activity worker (e.g. `python -m ml_serve.exe.run_service --task-queue robotics-hand-keypoints-temporal`). |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## Implementation Plan |
| 77 | + |
| 78 | +### Phase 1 — MVP (fixed replicas, ~2 weeks) |
| 79 | + |
| 80 | +**`domain/entities/model_endpoint_entity.py`** |
| 81 | +```python |
| 82 | +class ModelEndpointType(str, Enum): |
| 83 | + ASYNC = "async" |
| 84 | + SYNC = "sync" |
| 85 | + STREAMING = "streaming" |
| 86 | + TEMPORAL = "temporal" # new |
| 87 | +``` |
| 88 | + |
| 89 | +**`common/dtos/model_endpoints.py`** |
| 90 | +- Add `temporal_task_queue: Optional[str]` to `CreateModelEndpointV1Request` and `UpdateModelEndpointV1Request` |
| 91 | +- Validation: required when `endpoint_type == "temporal"` |
| 92 | + |
| 93 | +**`domain/use_cases/model_endpoint_use_cases.py`** |
| 94 | +- `validate_deployment_resources`: allow `min_workers=0` for `TEMPORAL` (same as `ASYNC`) |
| 95 | +- No `concurrent_requests_per_worker` limit (workers process one activity at a time by default) |
| 96 | + |
| 97 | +**`infra/gateways/resources/k8s_resource_types.py`** |
| 98 | +- Add `_TemporalDeploymentArguments` TypedDict: |
| 99 | + ```python |
| 100 | + class _TemporalDeploymentArguments(TypedDict): |
| 101 | + TEMPORAL_TASK_QUEUE: str |
| 102 | + TEMPORAL_SERVER_HOSTNAME: str |
| 103 | + TEMPORAL_SERVER_PORT: str |
| 104 | + REPLICAS: int # fixed in MVP; driven by max_workers |
| 105 | + ``` |
| 106 | +- Add `DeploymentRunnableImageTemporalGpuArguments` and `...CpuArguments` composite TypedDicts |
| 107 | + |
| 108 | +**`infra/gateways/resources/templates/service_template_config_map*.yaml`** |
| 109 | +- Add `deployment-runnable-image-temporal-gpu.yaml` and `...-cpu.yaml` templates |
| 110 | +- Key differences from async template: |
| 111 | + - No `celery-forwarder` sidecar — the user container IS the Temporal worker |
| 112 | + - No `celery.scaleml.autoscaler/*` annotations |
| 113 | + - `replicas: ${REPLICAS}` (fixed) |
| 114 | + - Env vars injected: `TEMPORAL_TASK_QUEUE`, `TEMPORAL_SERVER_HOSTNAME`, `TEMPORAL_SERVER_PORT`, `CONCURRENCY` |
| 115 | + - Readiness probe: TCP check on Temporal worker port (or omit — workers have no HTTP endpoint) |
| 116 | + |
| 117 | +**`infra/gateways/resources/k8s_endpoint_resource_delegate.py`** |
| 118 | +- `delete_resources`: add `TEMPORAL` branch (reuses sync cleanup — no Celery queue to delete) |
| 119 | +- `create_or_update_resources`: route `TEMPORAL` to new template |
| 120 | + |
| 121 | +**`infra/gateways/resources/live_endpoint_resource_gateway.py`** |
| 122 | +- `create_or_update_resources`: skip Celery queue creation for `TEMPORAL` (add to `else` branch or make explicit) |
| 123 | +- `get_resources`: skip SQS queue depth polling for `TEMPORAL` |
| 124 | + |
| 125 | +### Phase 2 — Temporal-aware autoscaling (follow-up, ~3 weeks) |
| 126 | + |
| 127 | +Scale worker replicas based on Temporal task queue backlog. Options: |
| 128 | + |
| 129 | +1. **KEDA `temporal` trigger** — KEDA has a [Temporal scaler](https://keda.sh/docs/scalers/temporal/) that polls `GetTaskQueueStats`. Lowest implementation cost; reuses existing KEDA infrastructure. |
| 130 | +2. **Custom autoscaler** — mirrors the existing Celery autoscaler pattern but polls Temporal's gRPC API. |
| 131 | + |
| 132 | +Recommendation: KEDA Temporal trigger. Annotation format: |
| 133 | +```yaml |
| 134 | +temporal.keda.sh/task-queue: "${TEMPORAL_TASK_QUEUE}" |
| 135 | +temporal.keda.sh/namespace: "default" |
| 136 | +temporal.keda.sh/targetQueueSize: "${PER_WORKER}" |
| 137 | +``` |
| 138 | +
|
| 139 | +--- |
| 140 | +
|
| 141 | +## What Changes in Caller Code (ego example) |
| 142 | +
|
| 143 | +Before — each service writes ~80 lines of custom Temporal boilerplate: |
| 144 | +```python |
| 145 | +# launch_hand_keypoints/temporal_worker.py (custom, per-service) |
| 146 | +_predict = load_predict_fn(...) |
| 147 | + |
| 148 | +@activity.defn(name="handKeypointsActivity") |
| 149 | +async def hand_keypoints_activity(inp): |
| 150 | + heartbeat_task = loop.create_task(_heartbeat_loop()) # manual |
| 151 | + ... |
| 152 | + |
| 153 | +async def main(): |
| 154 | + client = await Client.connect(...) # manual |
| 155 | + worker = Worker(client, task_queue=..., ...) # manual |
| 156 | + await worker.run() |
| 157 | +``` |
| 158 | +
|
| 159 | +After — service implements one method; Launch manages the rest: |
| 160 | +```python |
| 161 | +# launch_hand_keypoints/service.py |
| 162 | +class HandKeypointsService(ModelServiceApi): |
| 163 | + def handle(self, req: dict) -> dict: |
| 164 | + result = self._predict(HandKeypointsRequest(**req)) |
| 165 | + return {"hands_npz_url": ..., "track_info_url": ...} |
| 166 | +``` |
| 167 | +
|
| 168 | +```bash |
| 169 | +# Deploy via Launch API (same as any other endpoint) |
| 170 | +launch create-endpoint \ |
| 171 | + --name hand-keypoints-temporal \ |
| 172 | + --endpoint-type temporal \ |
| 173 | + --temporal-task-queue robotics-hand-keypoints-temporal \ |
| 174 | + --gpu-type h100 --gpus 1 \ |
| 175 | + --min-workers 0 --max-workers 10 |
| 176 | +``` |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## What This Is Not |
| 181 | + |
| 182 | +- **Not a task submission API.** Launch does not expose `/v1/async-tasks` for `temporal` endpoints. The Temporal workflow is the caller; Launch only manages the worker pods. |
| 183 | +- **Not a workflow worker.** Launch manages activity workers only. The workflow definition lives in the caller's codebase and runs on a separate workflow worker (or Temporal Cloud). |
| 184 | +- **Not a replacement for Celery async endpoints.** `async` endpoints remain the right choice for request/response workloads where the caller submits tasks via Launch's API. `temporal` is for multi-step pipelines where an external orchestrator coordinates the work. |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +## Alternatives Considered |
| 189 | + |
| 190 | +| Option | Verdict | |
| 191 | +|--------|---------| |
| 192 | +| Raw K8s Deployment (std-ml-srv `deployment_template_TEMPORAL_gpu.yaml`) | Works today; loses Launch management. Good stopgap, not long-term. | |
| 193 | +| Temporal orchestrates existing Launch async endpoints | Extra HTTP round-trip per phase; doesn't use Temporal activities properly. | |
| 194 | +| Launch batch jobs per phase | `backoffLimit: 0`, cold start per request, no worker pool. Wrong tool. | |
| 195 | +| Temporal Cloud | Doesn't change the worker management problem; workers still need to run somewhere. | |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## Open Questions |
| 200 | + |
| 201 | +1. **Readiness probe**: Temporal workers have no HTTP endpoint. Should Launch skip the readiness probe for `temporal` endpoints, or should worker images expose a `/healthz` on a sidecar port? |
| 202 | +2. **Task submission API**: Should Launch eventually expose a way to *start a Temporal workflow* (not just manage workers)? This would be a larger API addition and is out of scope for Phase 1. |
| 203 | +3. **Namespace**: Should `temporal_namespace` be a configurable field, or default to `"default"`? |
| 204 | +4. **Multi-queue workers**: Some use cases may want one pod to pull from multiple task queues. Out of scope for now; each endpoint maps to one task queue. |
0 commit comments