(serve-perf-tuning)= # Performance Tuning This section should help you: - understand Ray Serve's performance characteristics - find ways to debug and tune your Serve application's performance :::{note} This section offers some tips and tricks to improve your Ray Serve application's performance. Check out the [architecture page](serve-architecture) for helpful context, including an overview of the HTTP proxy actor and deployment replica actors. ::: ```{contents} ``` ## Performance and benchmarks Ray Serve is built on top of Ray, so its scalability is bounded by Ray’s scalability. See Ray’s [scalability envelope](https://github.com/ray-project/ray/blob/master/release/benchmarks/README.md) to learn more about the maximum number of nodes and other limitations. ## Debugging performance issues in request path The performance issue you're most likely to encounter is high latency or low throughput for requests. Once you set up [monitoring](serve-monitoring) with Ray and Ray Serve, these issues may appear as: * `serve_num_router_requests_total` staying constant while your load increases * `serve_deployment_processing_latency_ms` spiking up as queries queue up in the background The following are ways to address these issues: 1. Make sure you are using the right hardware and resources: * Are you reserving GPUs for your deployment replicas using `ray_actor_options` (e.g., `ray_actor_options={“num_gpus”: 1}`)? * Are you reserving one or more cores for your deployment replicas using `ray_actor_options` (e.g., `ray_actor_options={“num_cpus”: 2}`)? * Are you setting [OMP_NUM_THREADS](serve-omp-num-threads) to increase the performance of your deep learning framework? 2. Try batching your requests. See [Dynamic Request Batching](serve-performance-batching-requests). 3. Consider using `async` methods in your callable. See [the section below](serve-performance-async-methods). 4. Set an end-to-end timeout for your HTTP requests. See [the section below](serve-performance-e2e-timeout). (serve-performance-async-methods)= ### Using `async` methods :::{note} According to the [FastAPI documentation](https://fastapi.tiangolo.com/async/#very-technical-details), `def` endpoint functions are called in a separate threadpool, so you might observe many requests running at the same time inside one replica, and this scenario might cause OOM or resource starvation. In this case, you can try to use `async def` to control the workload performance. ::: Are you using `async def` in your callable? If you are using `asyncio` and hitting the same queuing issue mentioned above, you might want to increase `max_ongoing_requests`. By default, Serve sets this to a low value (5) to ensure clients receive proper backpressure. You can increase the value in the deployment decorator; for example, `@serve.deployment(max_ongoing_requests=1000)`. (serve-performance-e2e-timeout)= ### Set an end-to-end request timeout By default, Serve lets client HTTP requests run to completion no matter how long they take. However, slow requests could bottleneck the replica processing, blocking other requests that are waiting. Set an end-to-end timeout, so slow requests can be terminated and retried. You can set an end-to-end timeout for HTTP requests by setting the `request_timeout_s` parameter in the `http_options` field of the Serve config. HTTP Proxies wait for that many seconds before terminating an HTTP request. This config is global to your Ray cluster, and you can't update it during runtime. Use [client-side retries](serve-best-practices-http-requests) to retry requests that time out due to transient failures. :::{note} Serve returns a response with status code `408` when a request times out. Clients can retry when they receive this `408` response. ::: (serve-performance-per-request-headers)= ### Override timeout and disconnect behavior per request Two request headers let callers override the global `request_timeout_s` and client-disconnect policy on a per-request basis. `x-request-timeout-seconds` : Overrides `request_timeout_s` for this single request. - A **positive float** sets the timeout in seconds (for example, `x-request-timeout-seconds: 30`). - A **non-positive value** (for example, `0` or `-1`) disables the timeout for this request regardless of the global setting. - An **absent or non-numeric value** falls back to the global `request_timeout_s`. `x-request-disconnect-disabled` : Controls whether Serve monitors for client disconnects on this request. - `?1` — disables disconnect detection; Serve continues processing even if the client closes the connection. - `?0` or absent — disconnect detection is enabled (default behavior). **Performance impact of disabling both headers together** Setting `x-request-timeout-seconds` to a non-positive value *and* `x-request-disconnect-disabled: ?1` on the same request enables a fast path in the direct-ingress handler that yields a meaningful throughput improvement: - Serve skips wrapping the user handler in an `asyncio.create_task()` call. - Serve skips the `asyncio.wait()` call that would otherwise coordinate the handler task, the disconnect-monitoring task, and the timeout watcher. - Instead, Serve directly `await`s the user handler, eliminating all per-request event-loop scheduling overhead for monitoring. In the proxy path the gain is more modest: `asyncio.wait()` is called per response chunk with one fewer task and no timeout handle, which reduces event-loop overhead at high request rates. ### Set backoff time when choosing replica Ray Serve allows you to fine-tune the backoff behavior of the request router, which can help reduce latency when waiting for replicas to become ready. It uses exponential backoff strategy when retrying to route requests to replicas that are temporarily unavailable. You can optimize this behavior for your workload by configuring the following environment variables: - `RAY_SERVE_ROUTER_RETRY_INITIAL_BACKOFF_S`: The initial backoff time (in seconds) before retrying a request. Default is `0.025`. - `RAY_SERVE_ROUTER_RETRY_BACKOFF_MULTIPLIER`: The multiplier applied to the backoff time after each retry. Default is `2`. - `RAY_SERVE_ROUTER_RETRY_MAX_BACKOFF_S`: The maximum backoff time (in seconds) between retries. Default is `0.5`. ### Set timeouts while probing replicas for queue length Ray Serve's request router probes replicas for their queue lengths to make intelligent load balancing decisions. You can tune the following environment variables to optimize this behavior for your workload: - `RAY_SERVE_QUEUE_LENGTH_RESPONSE_DEADLINE_S`: The initial timeout (in seconds) for waiting for replicas to respond with their queue length information. Default is `0.1`. - `RAY_SERVE_MAX_QUEUE_LENGTH_RESPONSE_DEADLINE_S`: The maximum timeout (in seconds) for queue length responses. When retrying with exponential backoff, the deadline increases but is capped at this value. Default is `1.0`. - `RAY_SERVE_QUEUE_LENGTH_CACHE_TIMEOUT_S`: How long (in seconds) cached queue length information from replicas is considered valid. After this timeout, the cache entry expires and the router must probe the replica again. Default is `10.0`. ### Configure locality-based routing Ray Serve routes requests to replicas based on locality to reduce network latency. The system applies locality routing in two scenarios: proxy-to-replica communication (HTTP/gRPC requests) and inter-deployment communication (replica-to-replica calls through `DeploymentHandle`). #### Routing priority When locality routing is enabled, the system selects replicas in the following priority order: 1. **Same node**: Replicas running on the same node as the caller (lowest latency) 2. **Same availability zone**: Replicas in the same availability zone as the caller 3. **Any replica**: All available replicas (fallback when local replicas are busy) If replicas at a higher priority level are busy or unavailable, the system automatically falls back to the next level. #### Proxy-to-replica routing You can configure proxy routing behavior through environment variables: - `RAY_SERVE_PROXY_PREFER_LOCAL_NODE_ROUTING`: When enabled, the proxy prefers routing requests to replicas on the same node. Default is `1` (enabled). - `RAY_SERVE_PROXY_PREFER_LOCAL_AZ_ROUTING`: When enabled, the proxy prefers routing requests to replicas in the same availability zone. Default is `1` (enabled). #### Inter-deployment routing When one deployment calls another through a `DeploymentHandle`, you can enable locality routing to reduce latency between deployments. **Same-node routing**: By default, inter-deployment calls don't prefer same-node replicas. To enable same-node routing, initialize the handle with the `_prefer_local_routing` option: ```python from ray import serve from ray.serve.handle import DeploymentHandle @serve.deployment class Caller: def __init__(self, target_handle: DeploymentHandle): # Enable same-node routing for this handle self._handle = target_handle.options(_prefer_local_routing=True) async def call_target(self): # Requests prefer replicas on the same node as this Caller replica return await self._handle.remote() ``` **Same-AZ routing**: The `RAY_SERVE_PROXY_PREFER_LOCAL_AZ_ROUTING` environment variable controls availability zone routing for both proxy and inter-deployment communication. You can't configure AZ routing per-handle. (serve-high-throughput)= ### Enable throughput-optimized serving :::{note} In Ray v2.54.0, the defaults for `RAY_SERVE_RUN_USER_CODE_IN_SEPARATE_THREAD` and `RAY_SERVE_RUN_ROUTER_IN_SEPARATE_LOOP` will change to `0` for improved performance. ::: This section details how to enable Ray Serve options focused on improving throughput and reducing latency. These configurations focus on the following: - Reducing overhead associated with frequent logging. - Disabling behavior that allowed Serve applications to include blocking operations. If your Ray Serve code includes thread blocking operations, you must refactor your code to achieve enhanced throughput. The following table shows examples of blocking and non-blocking code:
| Blocking operation (❌) | Non-blocking operation (✅) |
|---|---|
| ```python from ray import serve from fastapi import FastAPI import time app = FastAPI() @serve.deployment @serve.ingress(app) class BlockingDeployment: @app.get("/process") async def process(self): # ❌ Blocking operation time.sleep(2) return {"message": "Processed (blocking)"} serve.run(BlockingDeployment.bind()) ``` | ```python from ray import serve from fastapi import FastAPI import asyncio app = FastAPI() @serve.deployment @serve.ingress(app) class NonBlockingDeployment: @app.get("/process") async def process(self): # ✅ Non-blocking operation await asyncio.sleep(2) return {"message": "Processed (non-blocking)"} serve.run(NonBlockingDeployment.bind()) ``` |