11 KiB
11 KiB
Multi-Request Streaming (MRS) on a Single Machine
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:
OmniLLMsupports 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.
- Inter-stage connection via IPC: copy-based
multiprocessing.Queuepassing 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.
- Device visibility
- Each stage sets
CUDA_VISIBLE_DEVICESor callstorch.cuda.set_deviceto bind to GPU sets. - A stage may use multiple GPUs internally (TP/PP/DP) but presents as a single stage unit.
- Each stage sets
- 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.
- 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.
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.
- Backpressure: queues are unbounded; pressure manifests as compute-rate differences. Optional SHM reduces large-object transfer cost; RX/decoding overhead is recorded for observability.
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.
- Cancellation/timeout: explicit cancellation/timeouts are not provided; graceful shutdown uses a
Nonesentinel sent to each stage input queue.
Short sequence example (req0/req1, stage A→B)
- t0: stage A handles req0
- t1: req0 completes on A → enters B; A immediately starts req1
- t2: B handles req0 while A handles req1 (parallel across stages)
9. Integration Points (by file)
vllm_omni/entrypoints/omni_llm.py(Orchestrator)- Class
OmniLLMorchestrates multi-process stages; constructsOmniStageinstances 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.
- Stage readiness: each stage emits
{"type": "stage_ready"}after initialization; the orchestrator waits for all stages or times out and logs diagnostic suggestions.
- Class
9.1 Process Device Visibility
- Device binding: set
CUDA_VISIBLE_DEVICESbefore process start, or calltorch.cuda.set_deviceearly in init; configs can be single/multi-GPU. - Cross-device transfer (simplified): via CPU copies; zero-copy paths are out of scope for now.
10. Worker Template (pseudo-code)
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)
12. Observability
- 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
- per-request (emitted by stages):
- Logs/tracing:
- optional per-stage log files:
{log_file}.stage{stage_id}.log - the orchestrator can log readiness, forward size/time, and summary information
- optional per-stage log files:
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):
- Before: Orchestrator created processes directly, passing
_stage_worker; device setup, LLM init, batching, and SHM were implemented there. - After: enhanced
OmniStageowns its subprocess (init_stage_worker), with_stage_workeras the entry; device setup, LLM init, batching, and SHM live inOmniStage.
Classes & responsibilities
- OmniLLM (orchestrator)
- Build
OmniStagelist in parallel (preserveprocess_engine_inputswiring) - Connect adjacent
Stageinput/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
- Build
- Stage (stage unit)
- Members:
stage_config(withengine_argsandruntime),in_q/out_q, subprocess handle, stats - API:
init_stage_worker(...): spawn subprocessstop_stage_worker(): graceful exitsubmit(ipc_payload): submit to input queue (may use shared memory)try_collect() -> Optional[payload]: non-blocking get from output queueprocess_engine_inputs(stages, prompts): reuse/delegate existing logic
- Subprocess entry
worker_main():set_stage_gpu_devices(CUDA_VISIBLE_DEVICES& logical index mapping)- build
OmniStageLLM(model, **engine_args) - while-loop:
- take first task + up to
runtime.max_batch_size-1non-blocking; supportmax_batch_size>=1 maybe_load_from_ipc_with_metricsperengine_inputsin batchstage_engine.generate(batched_inputs, sampling_params)(window-1)- write per-request aggregated outputs to output queue after
maybe_dump_to_shm - log start/finish; report
errorper request on exceptions
- take first task + up to
- Members:
Data flow (sketch)
OmniLLM
├─ 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)
runtime.devices:- 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
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)- 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
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)
- Enhanced
OmniStagewithinit_stage_worker/stop_stage_worker/submit/try_collect/_stage_worker. OmniLLMholdsOmniStageinstances, replacing the older pipelined class; uses Stage APIs.- Reuse
entrypoints/stage_utils: device/SHM/encode-decode utilities live here. - Regression: single/multi-request, various
max_batch_size, SHM threshold, device mapping, multi-stage parallelism, error paths, and shutdown flows.