Skip to content

fix: ToT KV-pressure crashes — free pages on context drop + break collect_* on starvation#425

Merged
shsym merged 2 commits into
pie.app/v1-base-shmemfrom
task/679-tot-kv-drop-free
Jun 17, 2026
Merged

fix: ToT KV-pressure crashes — free pages on context drop + break collect_* on starvation#425
shsym merged 2 commits into
pie.app/v1-base-shmemfrom
task/679-tot-kv-drop-free

Conversation

@shsym

@shsym shsym commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Two complementary fixes for the tree-of-thought daemon crash under KV pressure.

1. fix(context): free KV pages on guest context drop (RAII)528e8bd

The host WIT context drop handler only deleted the wasm resource-table entry and deferred all KV-page cleanup to whole-instance DestroyAll. A long-lived daemon inferlet (one instance serves every request), and ToT forks many transient contexts per request (breadth children + a value-scoring fork per node + spent parents). Those dropped forks' pages were never reclaimed → a heavy tree's fork allocations defer in when_allocated waiting on pages only the leaked contexts hold → deadlock.

Free the context eagerly on drop (each WIT Context maps 1:1 to a ContextId). Snapshots unaffected (separate owner:None id). Verified before/after on real portable-Metal: unfixed deadlocks on a heavy tree, fixed completes 3/3.

2. fix(generation): break collect_* on forward-pass starvation2714bd2

The direct fix. pie's scheduler folds a device failure / per-batch timeout / mid-stream KV eviction into an empty Output (Ok, not Err). The SDK Generator::collect_tokens/collect_text sugars folded that as "0 tokens accepted" — never advancing tokens_generated, never hitting max_tokens/stop — so they spun, and the next empty-input forward pass hung the driver ("Server disconnected without sending a response", port dead, driver healthy).

The chat-completions loop already guards this in its own hand-rolled loop, but ToT generates every node + value score via collect_text, which had no guard. Add the same starvation predicate (a step whose raw host slots carry no Token slot) to the collect_* sugars — terminate with a clear error instead of spinning. A natural stop is not starvation (the host still samples Token(stop), later truncated out of Output::tokens). Unit-tested (4 cases).

Tests

  • cargo test --lib (inferlet SDK): 25 passed incl. 4 starvation-predicate tests.
  • Downstream: dummy-driver HTTP e2e 202/1-skip; chat-apc native 106; deep-tree deadlock regression 3/3 (real Metal).

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6cc680a-6ee4-49b0-bca6-64f6b0216997

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/679-tot-kv-drop-free

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@shsym shsym changed the title fix(context): free KV pages on guest context drop (RAII) fix: ToT KV-pressure crashes — free pages on context drop + break collect_* on starvation Jun 16, 2026
@shsym
shsym force-pushed the task/679-tot-kv-drop-free branch from 6a30ff1 to 6f8c42c Compare June 16, 2026 22:52
The host WIT context `drop` handler only deleted the wasm resource-table
entry and deferred all KV-page cleanup to the whole-instance DestroyAll on
teardown. For a long-lived daemon inferlet (chat-apc serves every request on
one instance) that leaks every intermediate context for the instance's
lifetime.

The tree-of-thought search forks many transient contexts per request —
`breadth` children per frontier node, a second fork per node for value
scoring, and spent parents. The Active scheduler can mask a slow leak by
evicting droppable pages, but a heavy tree forks faster than eviction frees:
a new fork's allocation is deferred in `when_allocated` waiting on pages that
only the not-yet-freed leaked contexts hold, and the request deadlocks (the
RemoteProtocol/ConnectError hang with a healthy driver reported downstream).

Free the context eagerly when the guest drops its handle: each WIT Context
resource maps 1:1 to a ContextId, so a dropped handle means the guest is
done with it. This bounds the live footprint to the active beam and unblocks
deferred forks immediately. Named snapshots are unaffected — save/snapshot
persist a separate owner:None context id, so destroying the live owned
context never touches them. The instance-teardown DestroyAll still cleans up
anything left live.
@shsym
shsym force-pushed the task/679-tot-kv-drop-free branch from 6f8c42c to d6eac10 Compare June 16, 2026 22:56
The SDK Generator's collect_tokens/collect_text sugars folded an empty
forward-pass output as "0 tokens accepted": pie's scheduler swallows a
device failure / per-batch timeout / mid-stream KV eviction into an empty
Output (Ok, not Err) rather than erroring. With zero accepted tokens the
loop never advances tokens_generated and never hits max_tokens or a stop
token, so it spins — and the next step issues an empty-input forward pass
that hangs the driver. The HTTP layer then closes with no response.

This is the tree-of-thought daemon crash under KV pressure (thinking-ON
nodes are large, so they pressure KV and provoke mid-stream eviction). The
chat-completions loop already guards this case in its own hand-rolled loop,
but tree-of-thought generates via collect_text, which had no guard.

Add the same starvation predicate to the collect_* sugars: a step whose raw
host slots carry no Token slot terminates the loop with a clear error
instead of spinning. A natural stop is not starvation — the host still
samples Token(stop) (later truncated out of Output::tokens), so a Token slot
is present. Unit-tested.
@shsym
shsym force-pushed the task/679-tot-kv-drop-free branch from d6eac10 to 2714bd2 Compare June 17, 2026 02:32
@ingim

ingim commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Fix 1 is good. But I don't understand the "starvation" mechanism for 2.

How come this can happen? If this is true, then we need to find and fix the root cause.

True when a generation step came back with no sampled token: the
/// host returned zero Token slots for a decode that had the auto-sampler
/// attached.

@shsym
shsym merged commit f8d5bf0 into pie.app/v1-base-shmem Jun 17, 2026
7 checks passed
@shsym
shsym deleted the task/679-tot-kv-drop-free branch June 17, 2026 02:40
@ingim

ingim commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@shsym Could you open a PR for fix 1? I think this should land in main

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