Skip to content

fix(dflash): abandoned stream drain - #231

Merged
davide221 merged 1 commit into
Luce-Org:mainfrom
jkyamog:fix-abandoned-stream-drain
May 20, 2026
Merged

fix(dflash): abandoned stream drain#231
davide221 merged 1 commit into
Luce-Org:mainfrom
jkyamog:fix-abandoned-stream-drain

Conversation

@jkyamog

@jkyamog jkyamog commented May 20, 2026

Copy link
Copy Markdown
Contributor

Fixes a Python server.py stream-synchronization bug where an SSE client can disconnect before the dflash daemon has emitted its -1 sentinel. In that case the daemon continues writing tokens to the shared r_pipe, but the Python streaming generator exits and releases daemon_lock. The next request can then consume stale tokens from the abandoned request, producing impossible timings and wrong responses.

This PR keeps the daemon protocol synchronized by draining abandoned streams to the sentinel while still holding daemon_lock.

Observed Failure

From a real Hermes run:

05:58:06 INFO dflash.server: chat chatcmpl-eb00d86f14f14b9bb4837826  stream=True ...
[pc] lookup hit slot=0 prefix_len=52622 (of 53215 total)
05:58:59 WARNING dflash.server: stream ended before daemon sentinel; retaining prompt .bin for in-flight daemon read
  [daemon] ok N=53215 gen=247 prefix_len=53024 (RESTORE slot=0) stream_fd=5

After that, later requests showed impossible near-zero wall time and consumed output that belonged to earlier daemon work:

05:59:17 INFO dflash.server: chat chatcmpl-affa6ec8564f4ef4b6b1de92  stream=True ...
05:59:17 WARNING dflash.server: inline snapshot ack missing - dropped slot reservation
05:59:17 INFO dflash.server: chat DONE chatcmpl-affa6ec8564f4ef4b6b1de92  in=53217 out=235  0.1s  2170.2 tok/s ...

That is not realistic model performance. It is stale token-pipe data from a previously abandoned stream.

Root Cause

Streaming requests share one daemon token pipe (r_pipe) protected by daemon_lock.

Normal path:

  1. Python sends a daemon command.
  2. Python reads token IDs from r_pipe.
  3. Daemon emits -1.
  4. Python marks timing["daemon_done"] = True.
  5. Python releases daemon_lock.

Failure path:

  1. Client disconnects or cancels the SSE response.
  2. Python streaming generator exits before seeing daemon -1.
  3. Daemon keeps generating and writing token IDs to r_pipe.
  4. Python releases daemon_lock.
  5. Next request starts and reads stale token IDs from the previous request.

Fix

When a streaming response exits before timing["daemon_done"]:

  • log the existing warning
  • drain r_pipe to the daemon sentinel using _drain_until_sentinel
  • keep this drain shielded from cancellation so the server reaches a clean protocol boundary
  • only then allow the request to release daemon_lock

This does not cancel in-flight GPU work. It is a synchronization fix: the next request starts from a clean daemon pipe instead of inheriting stale output.

Logging And Timing

The abandoned-stream log should stay at warning level, not debug. A client disconnect is normal, but the server reaching the end of an SSE generator before the daemon sentinel is a protocol synchronization hazard: if it is not handled, the next request can read stale token IDs from the previous request.

This PR does not change timing math and does not count prefix-cache time differently. The low/impossible timings disappear because the next request no longer consumes already-buffered tokens from an abandoned prior stream. Normal cached requests should still report fast prefill when a prefix snapshot is restored, but they should not show impossible decode rates caused by stale pipe data.

The drain log includes how many stale daemon tokens were consumed during cleanup, which is enough evidence for this specific synchronization fix without adding broader empty-response diagnostics to this PR.

Relationship To Existing Issues

Related issue:

Related but distinct issue:

#216 fixed RESTORE prompt-delta correctness. This PR addresses a different stale-output path: abandoned streaming responses leaving unread daemon tokens in r_pipe.

@jkyamog
jkyamog force-pushed the fix-abandoned-stream-drain branch from 9e12596 to d11b0aa Compare May 20, 2026 08:32
@jkyamog

jkyamog commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

CAVEAT: Also I only tested this behavior in deployment together with PR #230.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@davide221
davide221 merged commit 11a4c7b into Luce-Org:main May 20, 2026
2 checks passed
@jkyamog
jkyamog deleted the fix-abandoned-stream-drain branch June 20, 2026 01:42
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