Skip to content

[Feat]Disagg diffusion hunyuan - #2

Open
zhou9402 wants to merge 22 commits into
mainfrom
feat/disagg-diffusion-hunyuan
Open

zhou9402 wants to merge 22 commits into
mainfrom
feat/disagg-diffusion-hunyuan

Conversation

@zhou9402

Copy link
Copy Markdown
Owner

Overview:

Details:

Where should the reviewer start?

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • closes GitHub issue: #xxx

Hongli Mi and others added 4 commits March 16, 2026 20:10
Switch disagg diffusion demo from Wan2.2-TI2V-5B to HunyuanVideo v1
(13B DiT + Llama3-8B + CLIP dual encoders). Key changes:

- Auto-detect dual text encoders from model_index.json
- Handle multi-element tensor lists (incompatible shapes) in
  IntermediateOutputStage and inject_tensors_to_req
- Patch HunyuanConfig missing task_type default
- Patch SGLang triton norm_infer for non-contiguous tensors
- Add .contiguous() in DeviceMoveStage for ZMQ-transferred tensors
- Save output as mp4 via imageio instead of raw npy
- Make prompt configurable via PROMPT env var

Tested: 2 concurrent requests, 4 GPUs (enc=GPU0, den=GPU1,2 TP=2,
vae=GPU3), HunyuanVideo 544x960 resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite design doc with high-level architecture diagrams and pipeline
  parallelism illustration
- Add benchmark results (native vs disagg, HunyuanVideo 544x960)
- Update milestone tracker: mark SGLang integration as completed
- Add scaling roadmap (NIXL transfer, independent stage scaling,
  encoder caching, sequence parallelism, heterogeneous hardware)
- Update README with quick-start commands and env var reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace ZMQ pickle tensor transfer with NIXL RDMA for inter-stage
communication. Only small metadata (~1.5 KB) now travels over ZMQ;
actual tensor data (embeddings, latents) transfers GPU-to-GPU without
CPU round-trips.

- New nixl_transfer.py: NixlTensorSender/Receiver with persistent
  Connector and async-to-sync bridge for PipelineStage.forward()
- NixlSendStage: registers GPU tensors as NIXL-readable, returns
  metadata in OutputBatch; handles dual-encoder indexed fields
- NixlReceiveStage: RDMA-pulls tensors from sender GPU, reconstructs
  indexed fields back into lists; falls back to CPU device-move
  when NIXL metadata is absent
- Orchestrator routes NIXL metadata between stages instead of tensors
- New serve.py: FastAPI HTTP server for curl-based requests
- Update design doc: remove benchmarks, reflect NIXL architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite README with full design, architecture diagram, data flow,
  implementation details, quick start, API reference, and roadmap
- Remove separate design doc (all content now in README)
- Improve serve.py: clear process architecture in docstring,
  detailed health endpoint showing GPU layout and worker count
- Verified: 3 sequential HTTP requests via curl all succeed with
  NIXL RDMA transfer on 4 GPUs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zhou9402 zhou9402 changed the title []disagg diffusion hunyuan [Feat]Disagg diffusion hunyuan Mar 16, 2026
Hongli Mi and others added 18 commits March 16, 2026 22:13
Replace ZMQ orchestrator (serve.py) with Dynamo RPC workers. Each worker
spawns SGLang Scheduler subprocess(es) via launch_partial_server() and
bridges Dynamo RPC <-> ZMQ, enabling TP support and NIXL RDMA GPU-direct
tensor transfer between stages.

Key changes:
- Workers use launch_partial_server() + StageClient instead of serve.py
- NixlSendStage/NixlReceiveStage for GPU-direct inter-stage transfer
- Dual encoder auto-detection (Llama 8B + CLIP for HunyuanVideo)
- Triton norm contiguous patch for HunyuanVideo transformer
- HunyuanVideo defaults (544x960, 50 steps, guidance_scale=1.0)
- Removed serve.py (ZMQ orchestrator, replaced by Dynamo RPC workers)
- Added Dynamo RPC endpoint wrappers in encoder/denoiser/vae_worker.py
- Simplified protocol.py (removed NixlTensorSender/Receiver)
- Updated run_disagg.py orchestrator for HunyuanVideo defaults

Tested: HunyuanVideo 13B, 4 GPUs (Enc=1, Den TP=2, VAE=1), NIXL RDMA,
30 steps 544x960 -> high quality output verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- run_all.sh: one-command launch of etcd + 3 workers + orchestrator
- README: remove phase terminology, add architecture/flow diagrams, roadmap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move StageClient, patch_hunyuan_config, detect_encoder_modules,
  save_video to sglang_utils.py (was duplicated in run_e2e_sglang.py
  and vae_worker.py)
- Workers now import from sglang_utils instead of run_e2e_sglang.py,
  avoiding module-level side effects on import
- Remove unused build_config() function
- Remove duplicate _save_video_frames from vae_worker.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… worker boilerplate

Performance:
- Replace asyncio.run() with loop.run_until_complete() in NIXL transfer
  to reuse existing event loop and keep _keep_alive tasks alive
- Remove unnecessary __count metadata tensors and CPU/GPU tensor
  filtering in NixlSendStage
- Use batch.logging_info directly instead of creating RequestTimings
  per call

Code quality:
- Remove dead build_server_args(), _ensure_distributed_init(), and
  extract_tensors_from_req() from sglang_utils
- Remove backward-compat aliases in run_e2e_sglang.py
- Add launch_stage_server() helper consolidating worker boilerplate
- Refactor encoder/denoiser/vae workers to use launch_stage_server()
- Fix sglang module paths (pipelines_core -> pipelines, etc.) for
  compatibility with sglang 0.5.5
- Clean up unused imports across all files

Verified: E2E test passes with 3 high-quality requests (61 frames,
50 steps, TP=2 denoiser, NIXL RDMA transfers).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each pipeline stage (Encoder, Denoiser, VAE) can now run N workers.
The orchestrator round-robins requests across workers per stage
independently, enabling full GPU utilization (e.g. 8 GPU with 2
workers per stage).

GPU spec uses `;` to separate workers: GPU_DEN="1,2;5,6" launches
2 TP=2 denoiser workers. Backward compatible — no `;` means 1 worker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add concurrent benchmark example to the multi-worker section.
Verified with 50-request run on 8×H20 (2 workers/stage):
  median=10.79s/req, std=0.55s, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NIXL/UCX crashes when multiple workers in the same stage do concurrent
RDMA transfers (NIXL_ERR_REMOTE_DISCONNECT).  Add a per-pool
asyncio.Lock so only one request per stage is in-flight at a time.

Pipeline parallelism across stages is preserved — Encoder, Denoiser,
and VAE each run simultaneously on different requests.  Use
CONCURRENCY>=3 to keep the 3-stage pipeline fully saturated.

Warmup now sends N requests (one per worker) to initialise all NIXL
connectors before concurrent traffic begins.

Verified: 50 requests, 8×H20, concurrency=4, 0 failures.
  Wall time: 272s (vs 545s sequential) — 2x throughput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove development-era directory prefixes (phase0_validate → validate,
phase1_workers → workers, phase2_orchestrator → orchestrator) and delete
the stale launch/ directory superseded by root run_all.sh.

Also update sglang imports for 0.5.8 API changes:
- pipelines.{schedule_batch,stages} → pipelines_core.*
- configs.pipelines → configs.pipeline_configs
- configs.sample.base → configs.sample.sampling_params
- scheduler_endpoint() method → property

Verified: 8-GPU multi-worker (2 enc, 2 den TP=2, 2 vae) with 3
concurrent requests, all completed successfully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sglang 0.5.8 removed Req.logging_info and OutputBatch's logging_info
parameter, and gpu_worker.execute_forward() now expects
OutputBatch.timings to be set (accesses timings.total_duration_ms).

Changes:
- NixlSendStage: drop logging_info usage, provide RequestTimings
  in every OutputBatch so gpu_worker can record total_duration_ms
- orchestrator: use round_robin() dispatch, dynamic per-stage
  semaphores based on discovered worker count

Verified: 4/4 requests (1 single + 3 concurrent) produce valid
mp4 videos via NIXL RDMA end-to-end.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log tensor shape/dtype/mean/std/min/max at NIXL send and receive
points for diagnosing data transfer issues. Uses logger.debug()
so no overhead at default INFO level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive design document covering the architecture, component
design, and SGLang/HunyuanVideo implementation details. Sections 1-3
are model-agnostic and backend-agnostic; Section 4 isolates SGLang
specifics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mpat

Replace round-robin semaphore-based orchestrator with WorkerManager that
tracks per-worker busy/idle state and dispatches via client.direct().
Add dispatch_with_retry for automatic failover to a different worker.

- Add ZMQ tensor fallback path (base64-encoded torch.save) for when NIXL
  is unavailable; controlled via DISABLE_NIXL env var
- Fix sglang 0.5.8 API: runtime.endpoint() replaces ns.component(),
  OutputBatch(metrics=) replaces timings=, fix add_stage arg order
- Add NIXL pull retry with exponential backoff for REMOTE_DISCONNECT
- Fix NIXL sender buffer leak: replace broken async _keep_alive with
  synchronous _sweep() that polls readable.status
- Add StageClient.forward() timeout (STAGE_FORWARD_TIMEOUT_S)
- Add stress_test.sh for concurrent load testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root causes and fixes:

1. PersistentConnector: replace _PersistentConnector singleton (which
   created a new Connection per operation) with a proper subclass that
   reuses one Connection/agent. Each Sender/Receiver owns its own
   instance, eagerly initialized at stage construction.

2. UCX transport: default UCX_TLS=all picks IB RDMA which fails across
   NUMA nodes. Must set UCX_TLS=cuda_ipc,tcp,self,cuda_copy,cma to
   force NVLink path for intra-node GPU-direct transfers.

3. Sender returns readable_op: NixlTensorSender.send() now returns
   (meta, readable_op). NixlSendStage holds readable_ops in
   _active_readables until receiver completes the RDMA pull, preventing
   premature descriptor deregistration.

4. TP broadcast: with TP>1, only rank 0 gets NIXL data. Added
   _tp_broadcast_fields to distribute tensors to all TP ranks via
   torch.distributed.broadcast after the NIXL pull.

5. ZMQ socket recovery: StageClient.forward() now resets the ZMQ REQ
   socket after timeout, preventing cascading EFSM errors.

6. Worker discovery: orchestrator now waits WORKER_SETTLE_S (default
   30s) after initial discovery to pick up slow-registering workers.

Verified: 20 concurrent requests, 3 TP=2 denoisers, 8 GPUs, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…N.md

- Add request_validation.py with pydantic-based HTTP body validation
- Orchestrator: observe all stages (encoder/denoiser/vae) during settle
  period instead of only denoiser
- Orchestrator: structured error handling (ValidationError → 400)
- protocol.py: add Field annotations
- DESIGN.md: streamline and align with current implementation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore ASCII art architecture/flow diagrams (more portable than mermaid).
Update to reflect current implementation:
- PersistentConnector pattern and agent lifecycle
- One-sided RDMA pull with buffer lifecycle diagram
- TP broadcast for multi-GPU denoisers
- UCX_TLS configuration for intra-node transfers
- StageClient ZMQ socket recovery
- Worker settle period for discovery
- Measured performance numbers (8-GPU HunyuanVideo)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove GPU tensor debug stats logging (.float() copy was 600ms+ per
  request even at DEBUG level due to GPU synchronization)
- Skip torch.cat when only one tensor (avoid unnecessary GPU copy)
- Reuse event loop in _run_coro instead of creating new one each call
- Add NIXL send timing breakdown log (flatten/descriptor/create/meta)

NixlSendStage: 640ms → 3ms (213x faster)
N=10 stress: 53s → 41s (23% faster)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant