Skip to content

[Tinker] Keep SQLite off the forwarded-future hot path - #2127

Open
j316chuck wants to merge 7 commits into
NovaSky-AI:mainfrom
j316chuck:chuck-codex/tinker-sqlite-hot-path
Open

[Tinker] Keep SQLite off the forwarded-future hot path#2127
j316chuck wants to merge 7 commits into
NovaSky-AI:mainfrom
j316chuck:chuck-codex/tinker-sqlite-hot-path

Conversation

@j316chuck

@j316chuck j316chuck commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep active EXTERNAL sample futures in memory and batch their terminal SQLite persistence instead of contending on SQLite during admission and completion.
  • Serialize SQLite mutation paths for training futures and session heartbeats; cache immutable sampler-checkpoint validation.
  • Preserve failure propagation: a terminal-persistence failure reaches waiters and stops the engine safely.

Testing

  • uv run --extra dev --extra tinker pytest tests/tinker/test_external_future_store.py tests/tinker/test_future_waiting.py tests/tinker/skyrl_train/test_async_sample_routing.py -q — 18 passed, 4 skipped
  • uv run --extra dev pre-commit run --files tests/tinker/test_external_future_store.py — passed

Follow-up

The deterministic test-only contention repro is stacked at j316chuck#17.


Note

Medium Risk
Changes async sample routing, future persistence timing, and SQLite write serialization for training/heartbeat paths; incorrect locking or shutdown ordering could cause lost results, 404s, or stuck shutdown, but behavior is covered by new unit and integration tests.

Overview
For non-colocated SkyRL-Train inference forwarding, forwarded asample requests no longer insert or update FutureDB on the admission/completion path. A new ExternalFutureStore holds active EXTERNAL futures in memory (negative **request_id**s), retrieve_future waits on them directly, and a background worker batch-persists terminal rows under the shared db_write_lock.

SQLite contention is reduced elsewhere: db_write_lock serializes mutating paths on SQLite only (heartbeats, forward_backward commits); sampling-session → model lookups are cached; sampler checkpoint validation runs once per checkpoint with delete/validation locking and cache invalidation on delete.

Shutdown is ordered: tracked forwarding tasks finish before closing the inference client and flushing/closing the store; persistence failures propagate to waiters via RuntimeError. SkyRLTrainInferenceForwardingClient completes futures through the store when present instead of writing FutureDB immediately.

Reviewed by Cursor Bugbot for commit 6cae365. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an ExternalFutureStore to keep forwarded sample futures off the database hot path, along with corresponding integrations in the API and forwarding client, and database write locks to serialize SQLite writes. The review feedback suggests defensively using .get(request_id) in ExternalFutureStore.complete to prevent potential KeyError crashes if an entry is missing.

Comment on lines +74 to +75
async def complete(self, request_id: int, result_data: BaseModel, status: RequestStatus) -> None:
entry = self._entries[request_id]

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.

medium

To prevent potential KeyError crashes in the background forwarding task (for example, if a task is cancelled or completed after the store has been cleared or restarted), it is safer to use .get(request_id) defensively and handle the case where the entry is missing.

Suggested change
async def complete(self, request_id: int, result_data: BaseModel, status: RequestStatus) -> None:
entry = self._entries[request_id]
async def complete(self, request_id: int, result_data: BaseModel, status: RequestStatus) -> None:
entry = self._entries.get(request_id)
if entry is None:
logger.warning("Attempted to complete non-existent or already removed external future %s", request_id)
return

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 6cae365. Configure here.

for entry in entries
]
)
await session.commit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lock order can fail persistence

Medium Severity

_persist acquires db_write_lock before checking out a connection, while forward_backward and session_heartbeat check out a get_session connection first and then take that same lock. Under concurrent training writes the pool can fill with lock waiters, so persist blocks on a connection it cannot obtain. The worker then treats the checkout timeout as a terminal persistence failure, which surfaces to waiters and fails shutdown.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6cae365. Configure here.

for entry in entries
]
)
await session.commit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Negative IDs can yield zero

Medium Severity

Persisting explicit negative request_id values into FutureDB before any positive autoincrement row exists makes SQLite's next implicit INTEGER PRIMARY KEY max(rowid)+1, which is 0. create_future then hits assert future_db.request_id and fails the first training admission.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6cae365. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants