sgl-project/sglang · warning · ValueError

Cannot transition {request_id} from terminal state {old_stat

Error message

Cannot transition {request_id} from terminal state {old_state.value} to {new_state.value}

What it means

Raised by RequestStateStore.transition when moving a request that is already in a terminal state (FAILED/TIMED_OUT, or any non-active state) into another terminal state. Terminal states are final; the state machine only allows FAILED/TIMED_OUT transitions from active states, so re-failing or transitioning a finished request is rejected.

Source

Thrown at python/sglang/multimodal_gen/runtime/disaggregation/request_state.py:111

        request_id: str,
        new_state: RequestState,
        *,
        error: str | None = None,
        encoder_instance: int | None = None,
        denoiser_instance: int | None = None,
        decoder_instance: int | None = None,
    ) -> RequestRecord:
        with self._lock:
            record = self._requests.get(request_id)
            if record is None:
                raise ValueError(f"Unknown request_id: {request_id}")

            old_state = record.state

            if new_state in _TERMINAL_STATES and new_state != RequestState.DONE:
                # FAILED / TIMED_OUT: allowed from any active state
                if old_state not in _ACTIVE_STATES:
                    raise ValueError(
                        f"Cannot transition {request_id} from terminal state "
                        f"{old_state.value} to {new_state.value}"
                    )
            elif new_state not in _VALID_TRANSITIONS.get(old_state, set()):
                raise ValueError(
                    f"Invalid transition for {request_id}: "
                    f"{old_state.value} -> {new_state.value}"
                )

            record.state = new_state
            record.last_transition_time = time.monotonic()
            if error is not None:
                record.error = error
            if encoder_instance is not None:
                record.encoder_instance = encoder_instance
            if denoiser_instance is not None:
                record.denoiser_instance = denoiser_instance
            if decoder_instance is not None:

View on GitHub (pinned to 0132848349)

Solutions

  1. Make error completion idempotent: check record.state is active (or catch this ValueError) before applying FAILED/TIMED_OUT
  2. Serialize completions per request (e.g. via the store lock or a per-request completion flag) so only the first terminal transition wins
  3. Cancel the watchdog/timeout task once a request reaches a terminal state
  4. Log and drop duplicate terminal transitions at debug level instead of letting them propagate

Example fix

# before
store.transition(request_id, RequestState.FAILED)  # may race -> ValueError

# after
record = store.get(request_id)
if record is None or record.state in _TERMINAL_STATES:
    return  # already finished; idempotent no-op
try:
    store.transition(request_id, RequestState.FAILED)
except ValueError:
    logger.debug("request %s already terminal", request_id)
Defensive patterns

Strategy: try-catch

Validate before calling

from sglang.multimodal_gen.runtime.disaggregation.request_state import _TERMINAL_STATES, _ACTIVE_STATES
record = store.get(request_id)
if record is not None and record.state not in _ACTIVE_STATES:
    logger.debug("%s already terminal (%s); skipping error completion", request_id, record.state)
    return

Try / catch

try:
    store.transition(request_id, RequestState.FAILED)
except ValueError as e:
    if "terminal state" in str(e):
        logger.debug("duplicate terminal transition for %s ignored", request_id)
        return
    raise

Prevention

When it happens

Trigger: Calling transition(id, FAILED or TIMED_OUT) when the record's current state is already terminal — e.g. two components race to report failure (timeout watchdog fires after _complete_with_error already marked the request FAILED), or a duplicate error frame is processed.

Common situations: Concurrent error paths (watchdog timeout + decoder error), duplicate error frames from retries, or handlers not checking the record state before completing with an error.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of sgl-project/sglang@0132848349 (2026-08-28). Data as JSON: /api/errors/194e256cf52c38e9. Report an issue: GitHub.