Skip to content

fix(server): close mkstemp fd in _tokenize_prompt to prevent fd leak#15

Merged
davide221 merged 1 commit into
Luce-Org:mainfrom
sanastasiou:fix/tokenize-fd-leak
Apr 23, 2026
Merged

fix(server): close mkstemp fd in _tokenize_prompt to prevent fd leak#15
davide221 merged 1 commit into
Luce-Org:mainfrom
sanastasiou:fix/tokenize-fd-leak

Conversation

@sanastasiou

Copy link
Copy Markdown
Contributor

The bug

In dflash/scripts/server.py, _tokenize_prompt() calls:

tmp = Path(tempfile.mkstemp(suffix=".bin")[1])

tempfile.mkstemp() returns (fd, path). This code keeps only the path (index [1]) and discards fd. Python's GC does not close integer file descriptors, so the fd stays open in the OS until the process dies. Every chat request leaks exactly one fd.

Under sustained load the process hits RLIMIT_NOFILE (default 1024 on a systemd service without explicit LimitNOFILE=). Once the wall is reached, socket.accept() starts throwing OSError: [Errno 24] Too many open files and the API stops accepting new HTTP connections — even though the model process itself is still healthy.

Reproduction

In our deployment we have Lucebox sitting behind ~6 concurrent client services (4 Hermes agents + a Hindsight memory server + a Honcho peer-model service + a Firecrawl extractor, all OpenAI-compatible). Within roughly an hour of normal aux traffic, FD count reached 1023/1024 and new requests were rejected.

/proc/<lucebox-pid>/fd showed the composition: only ~5 actual sockets and pipes, and ~1000 orphan /tmp/tmp*.bin file descriptors — exactly the pattern of a leaked mkstemp fd.

Journal log excerpt:

OSError: [Errno 24] Too many open files
socket.accept() out of system resource
socket: <asyncio.TransportSocket fd=8, family=2, type=1, proto=6, laddr=('0.0.0.0', 8001)>

The fix

Use os.fdopen(fd, "wb") which takes ownership of fd and closes it when the with block exits:

fd, path = tempfile.mkstemp(suffix=".bin")
tmp = Path(path)
with os.fdopen(fd, "wb") as f:
    for t in ids:
        f.write(struct.pack("<i", int(t)))

The on-disk temp file is still cleaned up by the existing prompt_bin.unlink() in chat_completions (both streaming and non-streaming paths), so there is no change to disk behaviour — only the fd lifecycle changes.

Verification

  • Before fix: FD count delta across 3 probe chat requests = +3 (one fd leaked per request)
  • After fix: FD count delta across 3 probe chat requests = 0

Also raised LimitNOFILE from the default 1024 to 65536 on our systemd unit as a defence-in-depth measure (unrelated to this PR). The code change alone is the correct fix; the limit bump is independent and up to the operator.

`tempfile.mkstemp()` returns `(fd, path)`. The previous code kept only
the path (index [1]) and discarded `fd`. Python's GC does not close
integer file descriptors, so the fd stays open in the OS until the
process dies. Every chat request leaks exactly one fd.

Under sustained load, the process hits `RLIMIT_NOFILE` (default 1024
on a systemd service without explicit `LimitNOFILE=`). Once that wall
is hit, `socket.accept()` starts throwing `OSError: [Errno 24] Too
many open files` and the API stops accepting new HTTP connections.

Reproduction in our deployment: Lucebox behind ~6 concurrent client
services (4 Hermes agents + a Hindsight memory server + a Honcho
peer-model service + a Firecrawl extractor). Within roughly an hour
of normal aux traffic, FD count reached 1023/1024 and new requests
were rejected even though the model itself was healthy.

Fix: use `os.fdopen(fd, "wb")` which takes ownership of `fd` and
closes it when the `with` block exits. The on-disk temp file is
still cleaned up by the existing `prompt_bin.unlink()` in
`chat_completions` (both streaming and non-streaming paths), so
there is no change to disk behavior.

Verified empirically: FD count delta across three probe chat
requests went from +3 (one leak per request) to 0 after the patch.
@davide221 davide221 merged commit e5ef959 into Luce-Org:main Apr 23, 2026
oliveagle pushed a commit to oliveagle/lucebox-hub that referenced this pull request May 22, 2026
fix(server): close mkstemp fd in _tokenize_prompt to prevent fd leak
easel added a commit to easel/lucebox-hub that referenced this pull request May 29, 2026
- lucebox/README.md: fix the relative link to `cli.py`; resolves to
  `src/lucebox/cli.py` (the actual location), not the nonexistent
  `lucebox/cli.py` (Luce-Org#15).
- luce-bench/NOTICE: the bundled forge_eval LICENSE says
  "Copyright (c) 2025-2026 Antoine Zambelli", not 2024 — sync NOTICE
  with the actual upstream LICENSE (Luce-Org#16).
- luce-bench/src/lucebench/areas/__init__.py: `__all__` was missing
  agent / agent_recorded / forge / longctx / smoke. Add the imports +
  list entries so `from lucebench.areas import *` matches the actual
  area surface (Luce-Org#17).

Addresses cubic Luce-Org#15Luce-Org#17 (P3) on PR Luce-Org#285.
davide221 added a commit that referenced this pull request Jun 7, 2026
The origin/main merge resolved the server/deps/llama.cpp gitlink to the PR's
older b896cf69 (llama #12 hip-shim), rewinding it from main's 570d9785 and
dropping llama #13 (crash), #14 (pflash-sparse stream-sync / BSA fix) and
#15 (fattn-sparse hip cudaStreamDefault). Point the gitlink back to 570d9785
(a strict superset — b896cf69 is its ancestor) so the PR no longer downgrades
llama.cpp / drops the BSA fix.

Rebuilt + smoke-tested against 570d9785: sg_b100 96/96 bit-exact vs all-GPU,
offload 60% 99.3 tok/s, all-GPU 119.5.

Co-Authored-By: WOZCODE <contact@withwoz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants