[object Object]

← back to Exo

Fix setrlimit crash when hard file descriptor limit < 65535 (#1430)

eead50b4cd355f06ddc292f40f2a30afbe531a62 · 2026-02-11 01:46:22 +0300 · Mustafa Alp Yılmaz

## Summary

`exo` crashes on startup when the system's hard file descriptor limit is
below 65535, which occurs in macOS LaunchDaemon environments, Docker
containers, and other restricted setups.

**Root cause:** `resource.setrlimit(resource.RLIMIT_NOFILE, (max(soft,
65535), hard))` raises `ValueError` when `hard < 65535` because the soft
limit cannot exceed the hard limit.

**Fix:**
- **`main.py`**: Clamp the target soft limit to `min(max(soft, 65535),
hard)` so it never exceeds the hard limit
- **`utils_mlx.py`**: Query current limits instead of hardcoding `(2048,
4096)`, which both crashed on restricted systems and incorrectly lowered
the hard limit when it was set higher

## Test plan

- [x] `basedpyright` passes with 0 errors
- [x] `ruff check` passes
- [x] Verify startup works on system with hard limit < 65535 (tested in
macOS LaunchDaemon with hard limit 10240)
- [x] Verify startup still works on default macOS (hard limit typically
unlimited)

Files touched

Diff

commit eead50b4cd355f06ddc292f40f2a30afbe531a62
Author: Mustafa Alp Yılmaz <96022931+mustafalpyilmaz@users.noreply.github.com>
Date:   Wed Feb 11 01:46:22 2026 +0300

    Fix setrlimit crash when hard file descriptor limit < 65535 (#1430)
    
    ## Summary
    
    `exo` crashes on startup when the system's hard file descriptor limit is
    below 65535, which occurs in macOS LaunchDaemon environments, Docker
    containers, and other restricted setups.
    
    **Root cause:** `resource.setrlimit(resource.RLIMIT_NOFILE, (max(soft,
    65535), hard))` raises `ValueError` when `hard < 65535` because the soft
    limit cannot exceed the hard limit.
    
    **Fix:**
    - **`main.py`**: Clamp the target soft limit to `min(max(soft, 65535),
    hard)` so it never exceeds the hard limit
    - **`utils_mlx.py`**: Query current limits instead of hardcoding `(2048,
    4096)`, which both crashed on restricted systems and incorrectly lowered
    the hard limit when it was set higher
    
    ## Test plan
    
    - [x] `basedpyright` passes with 0 errors
    - [x] `ruff check` passes
    - [x] Verify startup works on system with hard limit < 65535 (tested in
    macOS LaunchDaemon with hard limit 10240)
    - [x] Verify startup still works on default macOS (hard limit typically
    unlimited)
---
 src/exo/main.py                         | 3 ++-
 src/exo/worker/engines/mlx/utils_mlx.py | 3 ---
 src/exo/worker/runner/runner.py         | 4 ++++
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/exo/main.py b/src/exo/main.py
index c3f165b4..e1ef3a70 100644
--- a/src/exo/main.py
+++ b/src/exo/main.py
@@ -251,7 +251,8 @@ class Node:
 def main():
     args = Args.parse()
     soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
-    resource.setrlimit(resource.RLIMIT_NOFILE, (max(soft, 65535), hard))
+    target = min(max(soft, 65535), hard)
+    resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard))
 
     mp.set_start_method("spawn")
     # TODO: Refactor the current verbosity system
diff --git a/src/exo/worker/engines/mlx/utils_mlx.py b/src/exo/worker/engines/mlx/utils_mlx.py
index a1fa097d..8117c6cc 100644
--- a/src/exo/worker/engines/mlx/utils_mlx.py
+++ b/src/exo/worker/engines/mlx/utils_mlx.py
@@ -1,6 +1,5 @@
 import json
 import os
-import resource
 import sys
 import time
 from pathlib import Path
@@ -63,8 +62,6 @@ from exo.worker.engines.mlx.auto_parallel import (
 from exo.worker.runner.bootstrap import logger
 
 Group = mx.distributed.Group
-# Needed for 8 bit model
-resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
 
 
 # TODO: Test this
diff --git a/src/exo/worker/runner/runner.py b/src/exo/worker/runner/runner.py
index 2764f024..0b3cdb16 100644
--- a/src/exo/worker/runner/runner.py
+++ b/src/exo/worker/runner/runner.py
@@ -1,5 +1,6 @@
 import base64
 import json
+import resource
 import time
 from collections.abc import Generator
 from functools import cache
@@ -112,6 +113,9 @@ def main(
     event_sender: MpSender[Event],
     task_receiver: MpReceiver[Task],
 ):
+    soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+    resource.setrlimit(resource.RLIMIT_NOFILE, (min(max(soft, 2048), hard), hard))
+
     instance, runner_id, shard_metadata = (
         bound_instance.instance,
         bound_instance.bound_runner_id,

← 199df64c util: remove VecExt trait, inline at call site (#1446)  ·  back to Exo  ·  feat: add intermediate model-fit state for cluster-capacity- 48caea4a →