2025-11-11 14:49:45 +08:00
## Multi-Request Streaming (MRS) on a Single Machine
2025-11-07 15:48:47 +08:00
### 1. Background & Scope
- All processing runs on a single physical machine with multi-process, per-stage workers. No proxy or network transport involved.
- Current alignment with vllm-omni: `OmniLLM` supports multiple stages (`OmniStage` ). GPU runners already expose streamable steps (prefill/decoding/diffusion), but the entry layer still collects lists and lacks intra-stage streaming and window scheduling.
- Goal: implement multi-stage, multi-request streaming (MRS) locally. Each stage outputs segments; downstream stages stitch and trigger compute based on configured windows. Shared memory and zero-copy strategies reduce data movement overhead.
### 2. Key Constraints
- Multi-process per stage: each stage is an independent process with a while loop; device visibility can be configured (`CUDA_VISIBLE_DEVICES` /`torch.cuda.set_device` ).
- Simple IPC (copy-based): use `multiprocessing.Queue` /Pipe for inter-process communication with CPU copies/serialization; do not rely on CUDA IPC/SHM zero-copy in this version.
- Cross-stage pipeline: different stages can process different requests concurrently (e.g., stage A handles request 1 while stage B handles request 0).
### 3. Architecture Overview
- Processes & IPC queues
- Each "sub-stage" is an OS process (worker). The loop: take from input_queue → compute → put to output_queue.
2025-11-12 15:51:56 +08:00
- Inter-stage connection via IPC: copy-based `multiprocessing.Queue` passing dict payloads; use shared memory for large objects.
- Each link is SPSC (single-producer/single-consumer): the upstream is the orchestrator and the downstream is a single stage process; queues are unbounded (maxsize=0) on the orchestrator side.
2025-11-07 15:48:47 +08:00
- Device visibility
- Each stage sets `CUDA_VISIBLE_DEVICES` or calls `torch.cuda.set_device` to bind to GPU sets.
- A stage may use multiple GPUs internally (TP/PP/DP) but presents as a single stage unit.
- Simplified IPC: copy-based queues/pipes for data transfer; zero-copy is future work.
- Pipeline progression: when a stage finishes a request, it enqueues outputs to the downstream stage; if downstream is idle, it starts immediately.
2025-11-12 15:51:56 +08:00
- Scheduling
- A downstream stage triggers only after the upstream completes the request.
- Windowed segmentation/stitched triggering is not implemented; intra-stage streaming is not provided.
2025-11-07 15:48:47 +08:00
### 7. IPC Implementation (simplified: copy-based)
- Use `multiprocessing.Queue` /Pipe for inter-process communication (control + data).
- Data is serialized/copied via CPU; no CUDA IPC/SHM zero-copy in this version.
2025-11-12 15:51:56 +08:00
- Backpressure: queues are unbounded; pressure manifests as compute-rate differences. Optional SHM reduces large-object transfer cost; RX/decoding overhead is recorded for observability.
2025-11-07 15:48:47 +08:00
### 8. Scheduling & Cancellation (simplified)
- Pipeline: when a stage finishes a request, it enqueues to the next stage; that stage immediately pulls the next request from its input queue, enabling cross-stage concurrency.
2025-11-12 15:51:56 +08:00
- Cancellation/timeout: explicit cancellation/timeouts are not provided; graceful shutdown uses a `None` sentinel sent to each stage input queue.
2025-11-07 15:48:47 +08:00
#### Short sequence example (req0/req1, stage A→B)
1) t0: stage A handles req0
2) t1: req0 completes on A → enters B; A immediately starts req1
3) t2: B handles req0 while A handles req1 (parallel across stages)
### 9. Integration Points (by file)
2025-11-11 14:49:45 +08:00
- `vllm_omni/entrypoints/omni_llm.py` (Orchestrator)
- Class `OmniLLM` orchestrates multi-process stages; constructs `OmniStage` instances in parallel and spawns per-stage workers.
- Spawns stage processes per config (set `CUDA_VISIBLE_DEVICES` /`torch.cuda.set_device` ), creates control/data channels, builds simple full-trigger flow.
- Stats/logging are disabled by default; per-stage and orchestrator stats are only written when explicitly enabled.
- Manages process lifecycle: start/wait for readiness, graceful shutdown; forwards results between stages using copy-based IPC and optional SHM.
2025-11-12 15:51:56 +08:00
- Stage readiness: each stage emits `{"type": "stage_ready"}` after initialization; the orchestrator waits for all stages or times out and logs diagnostic suggestions.
2025-11-07 15:48:47 +08:00
### 9.1 Process Device Visibility
- Device binding: set `CUDA_VISIBLE_DEVICES` before process start, or call `torch.cuda.set_device` early in init; configs can be single/multi-GPU.
- Cross-device transfer (simplified): via CPU copies; zero-copy paths are out of scope for now.
2025-11-12 15:51:56 +08:00
### 10. Worker Template (pseudo-code)
2025-11-07 15:48:47 +08:00
```python
2025-11-12 15:51:56 +08:00
def stage_worker ( input_q , output_q , runtime , shm_threshold ):
# Device binding
set_stage_gpu_devices ( runtime . devices )
engine = OmniStageLLM ( model , ** engine_args )
batch_seq = 0
while True :
first = input_q . get ()
if first is None :
break
# Batch intake (up to runtime.max_batch_size)
batch = [ first ]
while len ( batch ) < int ( runtime . max_batch_size or 1 ) and not input_q . empty ():
nxt = input_q . get ()
if nxt is None :
input_q . put ( None )
break
batch . append ( nxt )
# Decode IPC payload and sampling params
request_ids = []
engine_inputs = []
rx_bytes = {}
rx_decode_ms = {}
for t in batch :
ein , rx = maybe_load_from_ipc_with_metrics ( t , "engine_inputs" , "engine_inputs_shm" )
request_ids . append ( t [ "request_id" ])
engine_inputs . extend ( ein if isinstance ( ein , list ) else [ ein ])
rx_bytes [ t [ "request_id" ]] = rx [ "rx_transfer_bytes" ]
rx_decode_ms [ t [ "request_id" ]] = rx [ "rx_decode_time_ms" ]
# Generate and dispatch (grouped by request_id)
batch_seq += 1
outputs = list ( engine . generate ( engine_inputs , batch [ 0 ][ "sampling_params" ], use_tqdm = False ))
grouped = group_by_request_id ( outputs , request_ids )
for rid in request_ids :
r_out = grouped . get ( rid , [])
use_shm , payload = maybe_dump_to_shm ( r_out , shm_threshold )
msg = {
"request_id" : rid ,
"metrics" : {
"batch_id" : batch_seq ,
"rx_transfer_bytes" : int ( rx_bytes . get ( rid , 0 )),
"rx_decode_time_ms" : float ( rx_decode_ms . get ( rid , 0.0 )),
# omitted: generation latency and token stats
},
}
if use_shm :
msg [ "engine_outputs_shm" ] = payload
else :
msg [ "engine_outputs" ] = payload
output_q . put ( msg )
2025-11-07 15:48:47 +08:00
```
### 12. Observability
2025-11-12 15:51:56 +08:00
- Metrics (as implemented):
- per-request (emitted by stages): `num_tokens_out` , `stage_gen_time_ms` , `rx_transfer_bytes` , `rx_decode_time_ms` , `rx_in_flight_time_ms` , `batch_id`
- orchestrator aggregates: E2E latency, tokens/s (written only when stats are enabled)
- optional per-stage JSONL: `{log_file}.stage{stage_id}.stats.jsonl`
- Logs/tracing:
- optional per-stage log files: `{log_file}.stage{stage_id}.log`
- the orchestrator can log readiness, forward size/time, and summary information
2025-11-07 15:48:47 +08:00
### 15. Risks & Mitigations
— End-to-end latency: full-trigger sacrifices interactivity; start simple/correct, add windowed streaming later.
— CPU copy overhead: copy-based IPC for maintainability; add SHM/zero-copy later as an optimization path.
### 16. Refactor: Sink LLM init and process into Stage (diagram)
Objective: without changing external call patterns, encapsulate per-stage LLM init, process creation, and worker logic inside `Stage` , so `PipelinedOmniLLM` focuses on orchestration (seeding, polling, forwarding, collecting) while keeping multi-process, shared memory, and device mapping capabilities.
Key changes (vs. current):
2025-11-11 14:49:45 +08:00
- Before: Orchestrator created processes directly, passing `_stage_worker` ; device setup, LLM init, batching, and SHM were implemented there.
- After: enhanced `OmniStage` owns its subprocess (`init_stage_worker` ), with `_stage_worker` as the entry; device setup, LLM init, batching, and SHM live in `OmniStage` .
2025-11-07 15:48:47 +08:00
Classes & responsibilities
2025-11-11 14:49:45 +08:00
- OmniLLM (orchestrator)
- Build `OmniStage` list in parallel (preserve `process_engine_inputs` wiring)
2025-11-07 15:48:47 +08:00
- Connect adjacent `Stage` input/output queues; seed requests to stage 0
- Poll stage outputs, decode results, call `process_engine_inputs` , then encode and forward to next stage
- Termination & cleanup: distribute shutdown signals; join/terminate subprocesses
- Stage (stage unit)
2025-11-11 14:49:45 +08:00
- Members: `stage_config` (with `engine_args` and `runtime` ), `in_q/out_q` , subprocess handle, stats
2025-11-07 15:48:47 +08:00
- API:
2025-11-11 14:49:45 +08:00
- `init_stage_worker(...)` : spawn subprocess
- `stop_stage_worker()` : graceful exit
- `submit(ipc_payload)` : submit to input queue (may use shared memory)
- `try_collect() -> Optional[payload]` : non-blocking get from output queue
2025-11-07 15:48:47 +08:00
- `process_engine_inputs(stages, prompts)` : reuse/delegate existing logic
- Subprocess entry `worker_main()` :
2025-11-11 14:49:45 +08:00
- `set_stage_gpu_devices` (`CUDA_VISIBLE_DEVICES` & logical index mapping)
2025-11-07 15:48:47 +08:00
- build `OmniStageLLM(model, **engine_args)`
- while-loop:
2025-11-11 14:49:45 +08:00
- take first task + up to `runtime.max_batch_size-1` non-blocking; support `max_batch_size>=1`
- `maybe_load_from_ipc_with_metrics` per `engine_inputs` in batch
2025-11-07 15:48:47 +08:00
- `stage_engine.generate(batched_inputs, sampling_params)` (window `-1` )
2025-11-11 14:49:45 +08:00
- write per-request aggregated outputs to output queue after `maybe_dump_to_shm`
2025-11-07 15:48:47 +08:00
- log start/finish; report `error` per request on exceptions
Data flow (sketch)
```
2025-11-11 14:49:45 +08:00
OmniLLM
2025-11-07 15:48:47 +08:00
├─ Stage[0].start_process(in0, out0)
├─ Stage[1].start_process(in1, out1)
├─ ...
└─ Orchestrator loop:
- stage0.submit(encode(engine_inputs))
- for each stage:
res = stage.try_collect()
outputs = decode(res)
stage.set_engine_outputs(outputs)
if !final: next_inputs = next_stage.process_engine_inputs(stages, original_prompt)
next_stage.submit(encode(next_inputs))
```
Config (preserve & extend)
2025-11-11 14:49:45 +08:00
- `runtime.devices` :
2025-11-07 15:48:47 +08:00
- Comma-separated visibility list (e.g., "2,5,7"): set visible set; default logical index 0 (first GPU)
- Number/string: treat as logical index (0/1-based per implementation) mapping to physical GPUs
2025-11-11 14:49:45 +08:00
- `runtime.max_batch_size` : max requests taken per intake (default 1)
- `shm_threshold_bytes` : objects larger than this go via shared memory (default 64KB; injected into stages)
2025-11-12 15:51:56 +08:00
- Stage link and inputs:
- `engine_input_source` : list of upstream stage_id(s) from which the downstream stage takes inputs (typically a single source)
- `custom_process_input_func` : function to construct downstream engine_inputs from upstream outputs
- Output flags:
- `final_output` , `final_output_type` : marks that a stage produces the final external output and its type
2025-11-07 15:48:47 +08:00
Kept & strengthened
- Multi-process: each stage in its own process; parallel across stages; serial/batching inside a stage controlled by `max_batch_size`
- Shared memory: large objects via SHM with strict `memoryview -> close -> unlink`
- Device mapping: unified `set_stage_gpu_devices` , support comma lists and logical indices
Migration (execution order)
2025-11-11 14:49:45 +08:00
1) Enhanced `OmniStage` with `init_stage_worker/stop_stage_worker/submit/try_collect/_stage_worker` .
2) `OmniLLM` holds `OmniStage` instances, replacing the older pipelined class; uses Stage APIs.
3) Reuse `entrypoints/stage_utils` : device/SHM/encode-decode utilities live here.
2025-11-07 15:48:47 +08:00
4) Regression: single/multi-request, various `max_batch_size` , SHM threshold, device mapping, multi-stage parallelism, error paths, and shutdown flows.