Skip to main content

Container Reuse & Scaling

This project provides two primary mechanisms for managing container lifecycles and scaling: ReusePolicy for optimizing task execution through warm container pools, and Scaling for managing the replica lifecycle of long-running applications.

Optimizing Task Performance with ReusePolicy

When environment creation is expensive (e.g., loading large ML models or initializing GPU drivers), the cold-start overhead can dominate task runtime. The ReusePolicy class in flyte._reusable_environment allows you to maintain a pool of warm containers that persist across multiple task invocations.

The Warm Pool Concept

By default, Flyte tasks run in isolated, ephemeral containers. When a TaskEnvironment is configured with a ReusePolicy, the system keeps a set of replicas running. Subsequent calls to tasks within that environment reuse the same Python process, significantly reducing latency.

import flyte

# Configure a warm pool for GPU tasks
gpu_env = flyte.TaskEnvironment(
name="gpu_worker",
resources=flyte.Resources(gpu="L4:1", memory="16Gi"),
reusable=flyte.ReusePolicy(
replicas=(1, 5), # Scale between 1 and 5 replicas
concurrency=10, # Support 10 concurrent tasks per replica
idle_ttl=300 # Shut down the pool after 5 minutes of total idleness
),
)

Key Configuration Parameters

  • replicas: Can be a fixed int or a tuple(min, max). A minimum of 2 replicas is recommended to prevent starvation (see below).
  • concurrency: The maximum number of concurrent tasks per replica. Values greater than 1 are only supported for async tasks.
  • idle_ttl: The duration (seconds or timedelta) after which the entire environment shuts down if no tasks are running.
  • scaledown_ttl: The minimum time an individual replica must be idle before it is removed during auto-scaling.

Important Constraints and Gotchas

  1. Starvation: If a parent task (running on a replica) invokes a child task that requires the same environment, and no other replicas are available, the child task will wait indefinitely for the parent to finish. The ReusePolicy implementation in src/flyte/_reusable_environment.py issues a warning if max_replicas and concurrency are both set to 1.
  2. Resource Overrides: When a task is part of a reusable environment, you cannot override its resources, env_vars, or secrets using task.override() unless you explicitly disable reusability for that call. This is enforced in src/flyte/_task.py:
    # This will raise a ValueError because reusable is active
    my_task.override(resources=flyte.Resources(cpu="4"))

    # Correct way to override resources for a specific call
    my_task.override(reusable="off", resources=flyte.Resources(cpu="4"))
  3. State Management: Because the Python process is reused, global state (like caches or singleton objects) persists between invocations. You must manage memory and global variables carefully to avoid side effects.

Scaling Long-Running Apps

For long-running services like APIs or model servers, the Scaling class in flyte.app._types controls how many replicas are active and how they respond to traffic. This is used primarily with AppEnvironment and its subclasses (e.g., FastAPIAppEnvironment).

Scaling Patterns

The Scaling class supports several common deployment patterns:

  • Scale-to-Zero (Default): Scaling(replicas=(0, 1)) ensures no resources are consumed when the app is idle. The first request will trigger a cold start.
  • Always-On: Scaling(replicas=(1, 1)) maintains exactly one replica at all times, eliminating cold starts for the first user.
  • Burstable: Scaling(replicas=(1, 10)) maintains a baseline of 1 replica and scales up to 10 based on traffic metrics.
  • High-Availability: Scaling(replicas=(2, 10)) ensures at least two replicas are always running across different nodes.

Autoscaling Metrics

You can define the trigger for scaling using the metric parameter:

  • Scaling.Concurrency(val): Scales up when the number of concurrent requests per replica exceeds val.
  • Scaling.RequestRate(val): Scales up when the number of requests per second per replica exceeds val.

Example: Scaling a Model Server

In examples/ml/batch_inference_saturate_app.py, a FastAPI application is configured to handle high-concurrency batch inference:

from flyte.app import Scaling, FastAPIAppEnvironment

app_env = FastAPIAppEnvironment(
name="batch-inference-app",
app=my_fastapi_app,
scaling=Scaling(
replicas=(0, 2),
metric=Scaling.Concurrency(val=10),
scaledown_after=300, # Wait 5 minutes before scaling down
),
)

Comparison: ReusePolicy vs. Scaling

While both manage replicas, they serve different purposes:

  • ReusePolicy is for Tasks. It manages a pool of workers that execute discrete units of work (functions) triggered by the Flyte orchestrator.
  • Scaling is for Apps. It manages replicas of a persistent server (like FastAPI or Streamlit) that responds to external network requests.