Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions components/src/dynamo/common/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ inference, metrics + Prometheus bridging, KV event publishing,
KV-aware (DP-rank) routing, health-check canaries, OpenTelemetry
tracing, and request-side guided decoding / structural tag.

> **Work in progress.** Multimodal, diffusion (image/video/DLLM), LoRA,
> **Work in progress.** Multimodal, diffusion (image/video/DLLM),
> LoRA (SGLang / TRT-LLM — vLLM is supported),
> engine routes (pause/resume, profiling, weight updates),
> text-in-text-out, and snapshot/CRIU are still on the non-unified
> path. See [Feature Gaps](#feature-gaps) for the per-engine matrix.
Expand Down Expand Up @@ -440,6 +441,31 @@ Lifecycle and runtime:
- `DynamoException` error chain wrapping
- Finish reason normalization handled by the Rust layer
- Engine control plumbing, with per-backend profiling, pause/resume, and supported weight-update controls
- **Dynamic LoRA (vLLM)** — load / unload / list adapters at runtime,
with ModelDeploymentCard publishing for frontend discovery,
per-adapter serialization locks, and per-request routing. Gated on
`--enable-lora` **and** `DYN_LORA_ENABLED=true`; SGLang / TRT-LLM
advertise no LoRA updates yet. Because LoRA ops mutate engine-managed
adapters rather than the serving lifecycle, they ride the generic
**engine-update** mechanism (a sibling of engine controls, kept separate
so the control surface isn't inflated):
- **Canonical API** (unified backend): `POST /engine/update/load_lora`
`{lora_name, source:{uri}}`, `POST /engine/update/unload_lora`
`{lora_name}`, `POST /engine/update/list_loras` `{}` (uniform `POST` +
JSON body). Engine updates return **HTTP 200** with a
`{"status": "error", ...}` body on semantic failure (5xx only when the
handler raises).
- **`/v1/loras` compatibility alias** — for the unified backend, the
legacy surface forwards to the engine updates (`POST /v1/loras` →
`load_lora`, `GET /v1/loras` → `list_loras`,
`DELETE /v1/loras/{name}` → `unload_lora`), preserving legacy HTTP
semantics (a `status:"error"` response maps to **HTTP 500** on
load/unload). When LoRA is unsupported, these return an explicit
"LoRA management not available" error rather than failing opaquely.
- **Legacy (non-unified) vLLM** continues to serve `/v1/loras`
unchanged.
- Loaded adapters appear in `GET /v1/models`; inference selects an
adapter by sending `"model": "<lora_name>"`.
- **Disaggregated serving** (`agg`/`prefill`/`decode`) — KV transfer
uses NIXL across all three engines; SGLang exchanges a Dynamo-level
bootstrap address, vLLM and TRT-LLM use an engine-internal handshake.
Expand Down Expand Up @@ -498,7 +524,7 @@ Request handling:
| Text-in-text-out mode | OpenAI-compatible chat/completion with engine-side tokenization. Unified hardcodes `ModelInput.Tokens`. |
| Multimodal | Images / video / embeddings, NIXL embedding transfer, encode workers. `worker.py:_to_rust_disaggregation_mode` rejects the `ENCODE` role. |
| Diffusion | Image (FLUX), video (Wan2.1), LLM diffusion (DLLM) workers; no diffusion engine, MediaOutput, or media scheduling on the unified path. |
| LoRA adapters | Dynamic load / unload / list, ModelDeploymentCard publishing, per-adapter serialization locks, per-request adapter threading on prefill. |
| LoRA adapters (SGLang / TRT-LLM) | Dynamic load / unload / list, ModelDeploymentCard publishing, per-adapter serialization locks, per-request adapter threading. **vLLM is supported on the unified path** — see [What works today](#what-works-today); SGLang and TRT-LLM advertise no LoRA updates yet. |
| Snapshot / checkpoint | CRIU-based engine state save/restore + identity reload. |

### vLLM-specific gaps
Expand All @@ -517,7 +543,6 @@ Request handling:
| `--benchmark-mode` family | The `--benchmark-*` flag family (mode, prefill/decode granularities, warmup, output path, timeout) injects into `vllm_config.additional_config` |
| "Omni" alternative entry point | `dynamo.vllm.omni.*` parallel mode for alternative tensor workflows |
| Multimodal (vLLM) | NIXL embedding transfer (`EmbeddingTransferMode`, `--embedding-transfer-mode`), embedding LRU cache (`--multimodal-embedding-cache-capacity-gb`), Qwen VL mRoPE, `EncodeWorkerHandler`, `--route-to-encoder` |
| LoRA (vLLM) | Three endpoints (`load_lora`, `unload_lora`, `list_loras`); also: unified prefill doesn't thread per-request LoRA adapters into the engine call |

### SGLang-specific gaps

Expand Down Expand Up @@ -563,9 +588,10 @@ For users picking what to land next on the unified path:

1. **Text-in-text-out** (`ModelInput.Text`) — common ask; needs
engine-side tokenization + chat templating path.
2. **LoRA dynamic load/unload + MDC publishing** — production-visible
feature with concrete API surface (three endpoints on vLLM
`handlers.py`).
2. **LoRA dynamic load/unload + MDC publishing** — **done for vLLM**
(engine updates `/engine/update/load_lora|unload_lora|list_loras` + a
`/v1/loras` compatibility alias; see [What works today](#what-works-today)).
Remaining: SGLang and TRT-LLM, which advertise no LoRA updates yet.
3. **Engine routes / lifecycle endpoints** — sleep/wake, profile
start/stop, weight updates, KV block clearing, prefix cache
reset. Visible in operator workflows.
Expand Down
36 changes: 32 additions & 4 deletions components/src/dynamo/common/backend/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ class GenerateRequest(TypedDict, total=False):
Disaggregated-serving keys (``prefill_result``, ``bootstrap_info``)
are set by the frontend's PrefillRouter on decode requests; engines
read them via ``dynamo.common.backend.disagg`` helpers.

``model`` carries the requested model name (set by the Rust
preprocessor). Engines that support dynamic LoRA read it to route a
request to a loaded adapter.
"""

token_ids: Required[list[int]]
model: str
sampling_options: dict[str, Any]
stop_conditions: dict[str, Any]
output_options: dict[str, Any]
Expand Down Expand Up @@ -273,11 +278,10 @@ async def health_check_payload(self) -> Optional[dict[str, Any]]:
return None

def supported_controls(self) -> set[str]:
"""Engine-control capability keys this engine supports.
"""Return the set of engine-control capability keys this engine supports.

The unified backend maps these keys to runtime endpoints. Engines only
advertise and implement semantic controls; they do not own transport or
route registration details.
Controls are semantic operations on the engine's serving lifecycle.
Engines advertise the keys they implement.
"""
return set()

Expand All @@ -290,6 +294,30 @@ async def engine_control(
"message": f"unsupported engine control: {control}",
}

def supported_updates(self) -> set[str]:
"""Return the set of engine-update capability keys this engine supports.

Updates are a sibling surface to :meth:`supported_controls` for
operations that mutate engine-managed assets rather than the engine's
serving lifecycle. Engines advertise the keys they implement.
"""
return set()

async def engine_update(self, update: str, body: dict[str, Any]) -> dict[str, Any]:
"""Handle one advertised engine-update request."""
return {
"status": "error",
"message": f"unsupported engine update: {update}",
}

async def on_endpoint_ready(self, endpoint) -> None:
"""Receive the runtime serving ``Endpoint`` once, before serving begins.

Default no-op. Engines that publish their own discovery records stash
it for use from :meth:`engine_update`. ``Worker`` calls this exactly
once; a raised exception is fatal to startup."""
return None


class LLMEngine(BaseEngine):
"""Abstract base for token-based inference engines (vLLM, SGLang, TRT-LLM).
Expand Down
19 changes: 10 additions & 9 deletions components/src/dynamo/sglang/request_handlers/handler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,28 +975,29 @@ def register_engine_routes(self, runtime: DistributedRuntime) -> None:
Args:
runtime: The DistributedRuntime instance to register routes on.
"""
runtime.register_engine_route("start_profile", self.start_profile)
runtime.register_engine_route("stop_profile", self.stop_profile)
runtime.register_engine_route("control/start_profile", self.start_profile)
runtime.register_engine_route("control/stop_profile", self.stop_profile)
runtime.register_engine_route(
"release_memory_occupation", self.release_memory_occupation
"control/release_memory_occupation", self.release_memory_occupation
)
runtime.register_engine_route(
"resume_memory_occupation", self.resume_memory_occupation
"control/resume_memory_occupation", self.resume_memory_occupation
)
runtime.register_engine_route(
"update_weights_from_disk", self.update_weights_from_disk
"control/update_weights_from_disk", self.update_weights_from_disk
)
runtime.register_engine_route(
"update_weights_from_tensor", self.update_weights_from_tensor
"control/update_weights_from_tensor", self.update_weights_from_tensor
)
runtime.register_engine_route(
"update_weights_from_distributed", self.update_weights_from_distributed
"control/update_weights_from_distributed",
self.update_weights_from_distributed,
)
runtime.register_engine_route(
"update_weights_from_ipc", self.update_weights_from_ipc
"control/update_weights_from_ipc", self.update_weights_from_ipc
)
runtime.register_engine_route(
"update_weight_version", self.update_weight_version
"control/update_weight_version", self.update_weight_version
)
if getattr(self.config, "dynamo_args", None) and getattr(
self.config.dynamo_args, "enable_rl", False
Expand Down
6 changes: 3 additions & 3 deletions components/src/dynamo/trtllm/workers/llm_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,16 @@ def _sync_config_from_engine_args(config: Config, engine_args: dict) -> None:

def _register_memory_routes(runtime, handler) -> None:
runtime.register_engine_route(
"release_memory_occupation",
"control/release_memory_occupation",
handler.release_memory_occupation,
)
runtime.register_engine_route(
"resume_memory_occupation",
"control/resume_memory_occupation",
handler.resume_memory_occupation,
)
logging.info(
"Registered engine routes: "
"/engine/release_memory_occupation, /engine/resume_memory_occupation"
"/engine/control/release_memory_occupation, /engine/control/resume_memory_occupation"
)


Expand Down
Loading
Loading