Skip to content

Commit 0a9af12

Browse files
committed
feat(lingbot): compile causal fast DiT
Configure LingBot DiT through ModelRuntimeConfig, enable compile in the v2 H100 example, and retain worker-local KV cache state across causal chunks. Add fixed 2/4/5/6-GPU VAE and DiT placement strategies, document compile benchmark results, and cover the runtime configuration and placement mappings with focused tests. Verification: .venv/bin/python -m pytest tests/unit/pipelines/lingbot_world_fast/test_module_loading.py tests/unit/pipelines/lingbot_world_fast/test_runtime_baseline.py tests/unit/pipelines/lingbot_world_fast/test_parallelism.py tests/unit/pipelines/lingbot_world_fast/test_streaming.py tests/unit/pipelines/lingbot_world_fast/test_stream_example.py tests/unit/pipelines/lingbot_world_v2/test_service.py -q; git diff --check.
1 parent 8730fc0 commit 0a9af12

9 files changed

Lines changed: 179 additions & 79 deletions

File tree

examples/lingbot/README.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ python examples/lingbot/lingbot_world_fast_image_to_video_h100.py \
129129

130130
| Option | Default | Description |
131131
| --- | --- | --- |
132-
| `--gpu_num` | `1` | Number of GPUs used for Ulysses sequence parallelism |
132+
| `--gpu_num` | `1` | Total visible GPUs; selects the LingBot VAE and DiT placement strategy |
133133
| `--model_root` | `${TF_MODEL_ZOO_PATH}/Wan2.2-I2V-A14B` | Wan2.2 I2V base model directory |
134134
| `--fast_model_root` | `${TF_MODEL_ZOO_PATH}/lingbot/lingbot-world-fast` | LingBot-World-Fast model directory |
135135
| `--image_path` | Bundled `image.jpg` | Input image path |
@@ -166,13 +166,33 @@ through `PPL_CONFIG`; `stream-serve --gpu-num` is passed to `get_service(gpu_num
166166

167167
### Scheduler and Stage Placement
168168

169-
LingBot offline and stream-server execution share the actor-based streaming scheduler.
170-
The bundled examples place VAE encode and decode on GPU 0, while direct
171-
`LingBotWorldFastPipelineConfig` users may set `vae_encode_config` and
172-
`vae_decode_config` independently. The scheduler does not infer a resource
173-
group from overlapping device IDs, so VAE encode, DiT, and VAE decode may
174-
overlap on one GPU. If a topology runs out of memory, move stages to different
175-
devices.
169+
LingBot offline and stream-server execution share the actor-based streaming scheduler. The bundled examples use
170+
fixed placement for the following total GPU counts:
171+
172+
| Total GPUs | DiT GPUs | VAE encode GPU | VAE decode GPU |
173+
| --- | --- | --- | --- |
174+
| 2 | `0-1` | `0` | `1` |
175+
| 4 | `0-3` | `0` | `1` |
176+
| 5 | `0-3` | `4` | `4` |
177+
| 6 | `0-4` | `5` | `5` |
178+
179+
For other counts, the examples retain the PPL-configured VAE devices and assign all visible GPUs to DiT. Direct
180+
`LingBotWorldFastPipelineConfig` users may set `vae_encode_config`, `vae_decode_config`, and `dit_config` independently.
181+
182+
### H100 Compile Benchmark
183+
184+
The v2 example was measured at 480p (832x464 internal size), 77 output frames, five latent chunks of four frames,
185+
BF16 DiT, FP32 VAE, SageAttention SM90, `torch.compile` enabled, and FSDP disabled. Each value is the mean from a
186+
second session after a complete warmup session. Pure DiT measures synchronous `denoise_and_update_cache`; chunk
187+
period is the mean interval between decoded chunk outputs while encode, DiT, and decode overlap.
188+
189+
| Total H100 GPUs | Pure DiT seconds/chunk | Overlapped chunk period seconds/chunk |
190+
| --- | --- | --- |
191+
| 2 | 1.587 | 2.096 |
192+
| 4 | 0.911 | 1.615 |
193+
194+
The scheduler does not infer a resource group from overlapping device IDs, so VAE encode, DiT, and VAE decode may
195+
overlap on a shared GPU.
176196

177197
See the [streaming scheduler guide](../../docs/en/stream_scheduler.md) for
178198
architecture, metric definitions, and lifecycle guarantees.

examples/lingbot/lingbot_world_fast_image_to_video_h100.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
target_fps=16,
6464
max_duration_seconds=5.0,
6565
attn_impl=AttnImplType.SAGE_ATTN_2_8_8_SM90,
66-
enable_fsdp=True,
66+
enable_fsdp=False,
6767
local_attn_size=-1,
6868
sink_size=0,
6969
timestep_indices=(0, 179, 358, 679),
@@ -76,14 +76,30 @@
7676
)
7777

7878

79+
def _resolve_stage_devices(total_gpu_count: int) -> tuple[list[int], int, int]:
80+
"""Return DiT, VAE encode, and VAE decode devices for available GPUs."""
81+
if total_gpu_count < 1:
82+
raise ValueError(f"parallelism must be positive, got {total_gpu_count}")
83+
if total_gpu_count in {2, 4}:
84+
return list(range(total_gpu_count)), 0, 1
85+
if total_gpu_count == 5:
86+
return [0, 1, 2, 3], 4, 4
87+
if total_gpu_count == 6:
88+
return [0, 1, 2, 3, 4], 5, 5
89+
return (
90+
list(range(total_gpu_count)),
91+
int(PPL_CONFIG["vae_encode_device_id"]),
92+
int(PPL_CONFIG["vae_decode_device_id"]),
93+
)
94+
95+
7996
def get_pipeline(
8097
parallelism: int = PPL_CONFIG["parallelism"],
8198
model_root: str | None = None,
8299
fast_model_root: str | None = None,
83100
) -> LingBotWorldFastPipeline:
84101
"""Load LingBot-World-Fast for offline chunked generation."""
85-
if parallelism < 1:
86-
raise ValueError(f"parallelism must be positive, got {parallelism}")
102+
dit_device_ids, vae_encode_device, vae_decode_device = _resolve_stage_devices(parallelism)
87103
model_root_path = Path(model_root).expanduser() if model_root else None
88104
fast_model_root_path = Path(fast_model_root).expanduser() if fast_model_root else None
89105
vae_path = str(model_root_path / "Wan2.1_VAE.pth") if model_root_path else PPL_CONFIG["vae_path"]
@@ -125,29 +141,33 @@ def get_pipeline(
125141
LingBotWorldFastPipelineConfig(
126142
vae_encode_config=ModelRuntimeConfig(
127143
device_type="cuda",
128-
device_id=int(PPL_CONFIG["vae_encode_device_id"]),
144+
device_id=vae_encode_device,
129145
torch_dtype=PPL_CONFIG["vae_torch_dtype"],
130-
parallel_config=ParallelConfig(device_ids=[int(PPL_CONFIG["vae_encode_device_id"])]),
146+
parallel_config=ParallelConfig(device_ids=[vae_encode_device]),
131147
),
132148
vae_decode_config=ModelRuntimeConfig(
133149
device_type="cuda",
134-
device_id=int(PPL_CONFIG["vae_decode_device_id"]),
150+
device_id=vae_decode_device,
135151
torch_dtype=PPL_CONFIG["vae_torch_dtype"],
136-
parallel_config=ParallelConfig(device_ids=[int(PPL_CONFIG["vae_decode_device_id"])]),
152+
parallel_config=ParallelConfig(device_ids=[vae_decode_device]),
153+
),
154+
text_encoding_config=ModelRuntimeConfig(device_type="cuda", device_id=dit_device_ids[0], torch_dtype=dtype),
155+
dit_config=ModelRuntimeConfig(
156+
device_type="cuda",
157+
device_id=dit_device_ids[0],
158+
torch_dtype=dtype,
159+
attention_config=AttentionConfig.dense_attention(PPL_CONFIG["attn_impl"]),
160+
parallel_config=ParallelConfig(
161+
device_ids=dit_device_ids if len(dit_device_ids) > 1 else None,
162+
sp_ulysses_degree=len(dit_device_ids),
163+
enable_fsdp=PPL_CONFIG["enable_fsdp"] and len(dit_device_ids) > 1,
164+
),
137165
),
138-
text_encoding_config=ModelRuntimeConfig(device_type="cuda", device_id=0, torch_dtype=dtype),
139-
dit_torch_dtype=dtype,
140166
control_type=PPL_CONFIG["control_mode"],
141167
max_area=RESOLUTION_AREAS[PPL_CONFIG["resolution"]],
142168
local_attn_size=PPL_CONFIG["local_attn_size"],
143169
sink_size=PPL_CONFIG["sink_size"],
144170
timestep_indices=PPL_CONFIG["timestep_indices"],
145-
attention_config=AttentionConfig.dense_attention(PPL_CONFIG["attn_impl"]),
146-
parallel_config=ParallelConfig(
147-
device_ids=list(range(parallelism)) if parallelism > 1 else None,
148-
sp_ulysses_degree=parallelism,
149-
enable_fsdp=PPL_CONFIG["enable_fsdp"] and parallelism > 1,
150-
),
151171
),
152172
)
153173
return pipeline
@@ -216,7 +236,7 @@ def run(
216236
"--gpu_num",
217237
default=PPL_CONFIG["parallelism"],
218238
type=int,
219-
help="Number of GPUs used for Ulysses sequence parallelism",
239+
help="Total GPUs; selects the LingBot VAE and DiT placement strategy",
220240
)
221241
@click.option("--image_path", default=DEFAULT_IMAGE_PATH, type=click.Path(exists=True))
222242
@click.option("--action_path", default=DEFAULT_ACTION_PATH, type=click.Path(exists=True, file_okay=False))

examples/lingbot/lingbot_world_v2_image_to_video_h100.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import torch
2323
from PIL import Image
2424

25-
from telefuser.core.config import AttentionConfig, AttnImplType, ModelRuntimeConfig, ParallelConfig
25+
from telefuser.core.config import AttentionConfig, AttnImplType, CompileConfig, ModelRuntimeConfig, ParallelConfig
2626
from telefuser.core.module_manager import ModuleManager
2727
from telefuser.models.lingbot_world_fast_dit import LingBotWorldFastDiT
2828
from telefuser.models.wan_video_text_encoder import WanTextEncoder
@@ -74,8 +74,9 @@
7474
seed=42,
7575
target_fps=16,
7676
max_duration_seconds=120.0,
77-
attn_impl=AttnImplType.TORCH_CUDNN,
78-
enable_fsdp=True,
77+
attn_impl=AttnImplType.SAGE_ATTN_2_8_8_SM90,
78+
compile_config=CompileConfig(enabled=True),
79+
enable_fsdp=False,
7980
local_attn_size=18,
8081
sink_size=6,
8182
timestep_indices=(0, 250, 500, 750),
@@ -90,14 +91,30 @@
9091
)
9192

9293

94+
def _resolve_stage_devices(total_gpu_count: int) -> tuple[list[int], int, int]:
95+
"""Return DiT, VAE encode, and VAE decode devices for available GPUs."""
96+
if total_gpu_count < 1:
97+
raise ValueError(f"parallelism must be positive, got {total_gpu_count}")
98+
if total_gpu_count in {2, 4}:
99+
return list(range(total_gpu_count)), 0, 1
100+
if total_gpu_count == 5:
101+
return [0, 1, 2, 3], 4, 4
102+
if total_gpu_count == 6:
103+
return [0, 1, 2, 3, 4], 5, 5
104+
return (
105+
list(range(total_gpu_count)),
106+
int(PPL_CONFIG["vae_encode_device_id"]),
107+
int(PPL_CONFIG["vae_decode_device_id"]),
108+
)
109+
110+
93111
def get_pipeline(
94112
parallelism: int = PPL_CONFIG["parallelism"],
95113
model_root: str | None = None,
96114
v2_model_root: str | None = None,
97115
) -> LingBotWorldV2Pipeline:
98116
"""Load LingBot-World v2 for offline chunked generation."""
99-
if parallelism < 1:
100-
raise ValueError(f"parallelism must be positive, got {parallelism}")
117+
dit_device_ids, vae_encode_device, vae_decode_device = _resolve_stage_devices(parallelism)
101118

102119
model_root_path = Path(model_root).expanduser() if model_root else None
103120
v2_model_root_path = Path(v2_model_root).expanduser() if v2_model_root else None
@@ -111,9 +128,6 @@ def get_pipeline(
111128
else PPL_CONFIG["dit_path_list"]
112129
)
113130

114-
vae_encode_device = int(PPL_CONFIG["vae_encode_device_id"])
115-
vae_decode_device = int(PPL_CONFIG["vae_decode_device_id"])
116-
dit_device_ids = list(range(parallelism))
117131
dtype = PPL_CONFIG["torch_dtype"]
118132
module_manager = ModuleManager(device="cpu")
119133
module_manager.load_model(
@@ -155,17 +169,17 @@ def get_pipeline(
155169
parallel_config=ParallelConfig(device_ids=[vae_decode_device]),
156170
),
157171
text_encoding_config=ModelRuntimeConfig(device_type="cuda", device_id=dit_device_ids[0], torch_dtype=dtype),
158-
dit_torch_dtype=dtype,
159-
control_type=PPL_CONFIG["control_mode"],
160-
max_area=RESOLUTION_AREAS[PPL_CONFIG["resolution"]],
161-
local_attn_size=PPL_CONFIG["local_attn_size"],
162-
sink_size=PPL_CONFIG["sink_size"],
163-
timestep_indices=PPL_CONFIG["timestep_indices"],
164-
attention_config=AttentionConfig.dense_attention(PPL_CONFIG["attn_impl"]),
165-
parallel_config=ParallelConfig(
166-
device_ids=dit_device_ids if len(dit_device_ids) > 1 else None,
167-
sp_ulysses_degree=len(dit_device_ids),
168-
enable_fsdp=PPL_CONFIG["enable_fsdp"] and len(dit_device_ids) > 1,
172+
dit_config=ModelRuntimeConfig(
173+
device_type="cuda",
174+
device_id=dit_device_ids[0],
175+
torch_dtype=dtype,
176+
attention_config=AttentionConfig.dense_attention(PPL_CONFIG["attn_impl"]),
177+
parallel_config=ParallelConfig(
178+
device_ids=dit_device_ids if len(dit_device_ids) > 1 else None,
179+
sp_ulysses_degree=len(dit_device_ids),
180+
enable_fsdp=PPL_CONFIG["enable_fsdp"] and len(dit_device_ids) > 1,
181+
),
182+
compile_config=PPL_CONFIG["compile_config"],
169183
),
170184
),
171185
)
@@ -241,7 +255,7 @@ def run(
241255
"--gpu_num",
242256
default=PPL_CONFIG["parallelism"],
243257
type=int,
244-
help="Number of GPUs used for Ulysses sequence parallelism",
258+
help="Total GPUs; selects the LingBot VAE and DiT placement strategy",
245259
)
246260
@click.option("--image_path", default=DEFAULT_IMAGE_PATH, type=click.Path(exists=True))
247261
@click.option("--action_path", default=DEFAULT_ACTION_PATH, type=click.Path(exists=True, file_okay=False))

telefuser/pipelines/lingbot_world_fast/denoising.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ def __init__(
6262
self.dit.set_attention_config(model_runtime_config.attention_config)
6363
self.model_names = ["dit"]
6464
self._cache_registry: dict[int, _DenoisingCacheState] = {}
65+
if model_runtime_config.parallel_config.world_size == 1 and model_runtime_config.compile_config.enabled:
66+
logger.info(f"Enabling torch.compile for {self.name}")
67+
self.dit = torch.compile(self.dit, **model_runtime_config.compile_config.get_compile_kwargs())
6568

6669
def parallel_models(self) -> None:
6770
"""Configure Ulysses SP and optional FSDP inside a ParallelWorker."""
@@ -81,6 +84,9 @@ def parallel_models(self) -> None:
8184
buffer_dtype=self.torch_dtype,
8285
)
8386
self.onload_models_flag = True
87+
if self.model_runtime_config.compile_config.enabled:
88+
logger.info(f"Enabling torch.compile for {self.name}")
89+
self.dit = torch.compile(self.dit, **self.model_runtime_config.compile_config.get_compile_kwargs())
8490

8591
def _init_self_kv_cache(
8692
self,

telefuser/pipelines/lingbot_world_fast/pipeline.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from PIL import Image
1515

1616
from telefuser.core.base_pipeline import BasePipeline
17-
from telefuser.core.config import AttentionConfig, ModelRuntimeConfig, ParallelConfig
17+
from telefuser.core.config import ModelRuntimeConfig, ParallelConfig
1818
from telefuser.core.module_manager import ModuleManager
1919
from telefuser.models.lingbot_world_fast_dit import LingBotWorldFastDiT
2020
from telefuser.models.t5_tokenizer import HuggingfaceTokenizer
@@ -53,16 +53,14 @@ class LingBotWorldFastPipelineConfig:
5353
vae_encode_config: ModelRuntimeConfig = field(default_factory=_default_vae_stage_runtime_config)
5454
vae_decode_config: ModelRuntimeConfig = field(default_factory=_default_vae_stage_runtime_config)
5555
text_encoding_config: ModelRuntimeConfig = field(default_factory=ModelRuntimeConfig)
56-
dit_torch_dtype: torch.dtype = torch.bfloat16
56+
dit_config: ModelRuntimeConfig = field(default_factory=ModelRuntimeConfig)
5757
control_type: str = "cam"
5858
orig_height: int = 480
5959
orig_width: int = 832
6060
max_area: int = 480 * 832
6161
local_attn_size: int = -1
6262
sink_size: int = 0
6363
timestep_indices: tuple[int, ...] = (0, 179, 358, 679)
64-
parallel_config: ParallelConfig = field(default_factory=ParallelConfig)
65-
attention_config: AttentionConfig = field(default_factory=AttentionConfig)
6664

6765

6866
class LingBotWorldFastPipeline(BasePipeline):
@@ -137,27 +135,22 @@ def init(self, module_manager: ModuleManager, config: LingBotWorldFastPipelineCo
137135
self.vae_encode_worker = ParallelWorker(vae_encode_stage)
138136
self.vae_decode_worker = ParallelWorker(vae_decode_stage)
139137

140-
dit_device = "cpu" if config.parallel_config.world_size > 1 else self.device
138+
dit_runtime_config = config.dit_config
139+
dit_device = "cpu" if dit_runtime_config.parallel_config.world_size > 1 else self.device
141140
self.dit = module_manager.fetch_module("lingbot_world_fast_dit")
142141
if self.dit is None:
143142
raise ValueError("LingBot requires a loaded lingbot_world_fast_dit module")
144-
self.dit = self.dit.to(dit_device, dtype=config.dit_torch_dtype).eval().requires_grad_(False)
143+
self.dit = self.dit.to(dit_device, dtype=dit_runtime_config.torch_dtype).eval().requires_grad_(False)
145144
if self.dit.control_type != config.control_type:
146145
raise ValueError(
147146
f"DiT checkpoint control type is {self.dit.control_type!r}, not requested {config.control_type!r}"
148147
)
149148
self.dit.set_causal_attention_window(config.local_attn_size, config.sink_size)
150149

151-
pipeline_device = torch.device(self.device)
152-
dit_runtime_config = ModelRuntimeConfig(
153-
device_type=pipeline_device.type,
154-
device_id=pipeline_device.index or 0,
155-
torch_dtype=config.dit_torch_dtype,
156-
attention_config=config.attention_config,
157-
parallel_config=config.parallel_config,
158-
)
159150
denoise_stage = LingBotWorldFastDenoisingStage("lingbot_world_fast_denoise", module_manager, dit_runtime_config)
160-
self.denoise_stage = ParallelWorker(denoise_stage) if config.parallel_config.world_size > 1 else denoise_stage
151+
self.denoise_stage = (
152+
ParallelWorker(denoise_stage) if dit_runtime_config.parallel_config.world_size > 1 else denoise_stage
153+
)
161154

162155
@staticmethod
163156
def _validate_vae_stage_runtime_config(runtime_config: ModelRuntimeConfig) -> None:

tests/unit/pipelines/lingbot_world_fast/test_module_loading.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import torch
44

5+
from telefuser.core.config import ModelRuntimeConfig
56
from telefuser.pipelines.lingbot_world_fast.pipeline import LingBotWorldFastPipeline, LingBotWorldFastPipelineConfig
67

78

@@ -38,9 +39,7 @@ def fetch_module(name: str, require_model_path: bool = False):
3839
pipeline = LingBotWorldFastPipeline(device="cpu", torch_dtype=torch.float32)
3940
pipeline.init(
4041
module_manager,
41-
LingBotWorldFastPipelineConfig(
42-
dit_torch_dtype=torch.float32,
43-
),
42+
LingBotWorldFastPipelineConfig(dit_config=ModelRuntimeConfig(torch_dtype=torch.float32)),
4443
)
4544

4645
assert module_manager.load_model.call_count == 0

tests/unit/pipelines/lingbot_world_fast/test_parallelism.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
import torch
66

7-
from telefuser.core.config import AttentionConfig, ModelRuntimeConfig, ParallelConfig
7+
from telefuser.core.config import AttentionConfig, CompileConfig, ModelRuntimeConfig, ParallelConfig
88
from telefuser.pipelines.lingbot_world_fast.denoising import LingBotWorldFastDenoisingStage
99

1010

@@ -96,3 +96,36 @@ def test_denoising_stage_rejects_uneven_ulysses_head_partition() -> None:
9696
pytest.raises(ValueError, match="divisible"),
9797
):
9898
stage._init_self_kv_cache(batch_size=1, kv_size=1)
99+
100+
101+
def test_denoising_stage_parallel_models_compiles_after_fsdp() -> None:
102+
dit = MagicMock()
103+
dit.get_fsdp_module_names.return_value = ["blocks"]
104+
parallel_config = ParallelConfig(
105+
device_ids=[0, 1, 2, 3],
106+
sp_ulysses_degree=4,
107+
enable_fsdp=True,
108+
)
109+
compile_config = CompileConfig(enabled=True, backend="eager")
110+
runtime_config = ModelRuntimeConfig(
111+
device_type="cuda",
112+
device_id=0,
113+
attention_config=AttentionConfig(),
114+
parallel_config=parallel_config,
115+
compile_config=compile_config,
116+
)
117+
module_manager = MagicMock()
118+
module_manager.fetch_module.return_value = dit
119+
stage = LingBotWorldFastDenoisingStage("denoise", module_manager, runtime_config)
120+
fsdp_model = MagicMock()
121+
compiled_model = MagicMock()
122+
123+
with (
124+
patch("telefuser.pipelines.lingbot_world_fast.denoising.create_device_mesh_from_config"),
125+
patch("telefuser.pipelines.lingbot_world_fast.denoising.shard_model", return_value=fsdp_model),
126+
patch("telefuser.pipelines.lingbot_world_fast.denoising.torch.compile", return_value=compiled_model) as compile,
127+
):
128+
stage.parallel_models()
129+
130+
compile.assert_called_once_with(fsdp_model, **compile_config.get_compile_kwargs())
131+
assert stage.dit is compiled_model

0 commit comments

Comments
 (0)