Files
vllm-omni/vllm_omni/entrypoints/openai/rollout_session.py
T
2026-09-14 09:26:44 +08:00

108 lines
3.7 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""In-memory session store for RL rollout serving (RFC #3747, P0).
Each RolloutSession holds a per-session asyncio.Lock so that concurrent step
requests for the same session are serialised. committed_step_id advances only
on successful step completion; failed or timed-out steps leave it unchanged,
preserving the atomic-context-commit guarantee from section 6.5 of the RFC.
"""
from __future__ import annotations
import asyncio
import time
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Literal
class RolloutSessionNotFoundError(KeyError):
pass
class RolloutSessionClosedError(RuntimeError):
pass
class RolloutSessionStepError(ValueError):
pass
class RolloutSessionCapacityError(RuntimeError):
pass
@dataclass
class RolloutSession:
session_id: str
model: str
mode: Literal["world_model_env"]
created_at: float = field(default_factory=time.time)
committed_step_id: int = -1
context_length: int = 0
closed: bool = False
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
class RolloutSessionStore:
"""Thread-safe (asyncio) store for active rollout sessions."""
def __init__(self, max_sessions: int = 64) -> None:
self._sessions: OrderedDict[str, RolloutSession] = OrderedDict()
self._max_sessions = max_sessions
self._store_lock = asyncio.Lock()
async def create(self, session_id: str, model: str, mode: Literal["world_model_env"]) -> RolloutSession:
async with self._store_lock:
self._evict_closed_sessions_for_create()
if len(self._sessions) >= self._max_sessions:
raise RolloutSessionCapacityError(f"Maximum active rollout sessions reached: {self._max_sessions}.")
session = RolloutSession(
session_id=session_id,
model=model,
mode=mode,
)
self._sessions[session_id] = session
self._sessions.move_to_end(session_id)
return session
async def get(self, session_id: str, *, include_closed: bool = False) -> RolloutSession:
session = self._sessions.get(session_id)
if session is None:
raise RolloutSessionNotFoundError(session_id)
self._sessions.move_to_end(session_id)
if session.closed and not include_closed:
raise RolloutSessionClosedError(session_id)
return session
async def reset(self, session_id: str) -> RolloutSession:
session = await self.get(session_id)
async with session.lock:
session.committed_step_id = -1
session.context_length = 0
return session
async def close(self, session_id: str) -> None:
session = await self.get(session_id, include_closed=True)
async with session.lock:
session.closed = True
async def advance(self, session_id: str, step_id: int) -> None:
"""Commit a successfully completed step. Called only on success."""
session = await self.get(session_id)
expected_step_id = session.committed_step_id + 1
if step_id != expected_step_id:
raise RolloutSessionStepError(f"Expected step_id {expected_step_id}, got {step_id}.")
session.committed_step_id = step_id
session.context_length += 1
def _evict_closed_sessions_for_create(self) -> None:
while len(self._sessions) >= self._max_sessions:
for session_id, session in self._sessions.items():
if session.closed:
del self._sessions[session_id]
break
else:
break