[Model] Add LingBot World Ulysses sequence parallelism (#6841)
Signed-off-by: wtz2333 <2955110911@qq.com> Co-authored-by: Zhou Taichang <tzhouam@connect.ust.hk>
This commit is contained in:
1 parent
de9a133d48
commit
02aaa34059
8 files changed
+659
-45
No files matched your search
@@ -178,6 +178,24 @@ For LingBot World v2:
|
||||
- `lingbot_world/pipeline.py` constructs conditioning, owns small non-KV session state, and
|
||||
produces one block plus the standard metadata envelope.
|
||||
|
||||
## Ulysses sequence parallelism
|
||||
|
||||
LingBot supports pure Ulysses sequence parallelism for both direct and
|
||||
AR-Diffusion execution. With Ulysses degree greater than one, hidden tokens,
|
||||
camera features, token-expanded timestep modulation, and RoPE tables are sharded
|
||||
together. Without SP, timestep modulation retains the frame-broadcast path.
|
||||
Self-attention performs the sequence-to-head all-to-all before reading or writing
|
||||
paged KV, while static text K/V uses the same local head shard. Text K/V shards
|
||||
own compact storage; cross-attention exchanges query/output layouts to use these
|
||||
shards. This retains the shared cache geometry and reduces text K/V storage at
|
||||
the cost of two all-to-all calls per layer.
|
||||
|
||||
The output head projects local tokens before gathering the flow values. For
|
||||
the 14B model this reduces the gathered width from 5120 to 64; frame modulation
|
||||
uses each shard's global token offset, including shards that split a frame. Only
|
||||
`ulysses_mode="strict"` is supported; `advanced_uaa`, Ring, and AllGather-KV modes
|
||||
remain unsupported for this model.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This contract does not currently provide:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Generate a LingBot-World v2 video from an image and camera trajectory.
|
||||
|
||||
The official checkpoint is licensed separately under CC BY-NC-SA and is
|
||||
@@ -99,6 +99,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
default=1,
|
||||
help="Number of GPUs used for tensor parallelism inside the DiT.",
|
||||
)
|
||||
parser.add_argument("--ulysses-degree", type=int, default=1, help="Pure Ulysses sequence parallel degree.")
|
||||
parser.add_argument("--flow-shift", type=float, default=5.0, help="Positive FlowUniPC scheduler shift.")
|
||||
parser.add_argument("--fps", type=int, default=16, help="Frames per second in the exported MP4.")
|
||||
parser.add_argument("--output", default="lingbot_world_v2.mp4", help="Output MP4 path.")
|
||||
@@ -158,6 +159,8 @@ def build_omni_kwargs(
|
||||
|
||||
if args.tensor_parallel_size <= 0:
|
||||
raise ValueError("--tensor-parallel-size must be a positive integer.")
|
||||
if args.ulysses_degree <= 0:
|
||||
raise ValueError("--ulysses-degree must be a positive integer.")
|
||||
flow_shift = _positive_finite(args.flow_shift, "--flow-shift")
|
||||
model_path = Path(args.model).expanduser()
|
||||
model = str(model_path.resolve()) if model_path.exists() else args.model
|
||||
@@ -165,6 +168,7 @@ def build_omni_kwargs(
|
||||
"model": model,
|
||||
"flow_shift": flow_shift,
|
||||
"tensor_parallel_size": args.tensor_parallel_size,
|
||||
"ulysses_degree": args.ulysses_degree,
|
||||
"enforce_eager": args.enforce_eager,
|
||||
"model_config": {"lingbot_action_root": str(paths.action_root)},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,7 +7,7 @@ import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -33,6 +33,10 @@ _STUBBED_MODULE_NAMES = (
|
||||
"vllm_omni.diffusion",
|
||||
"vllm_omni.diffusion.attention",
|
||||
"vllm_omni.diffusion.attention.layer",
|
||||
"vllm_omni.diffusion.distributed",
|
||||
"vllm_omni.diffusion.distributed.comm",
|
||||
"vllm_omni.diffusion.distributed.parallel_state",
|
||||
"vllm_omni.diffusion.distributed.sp_plan",
|
||||
"vllm_omni.diffusion.layers",
|
||||
"vllm_omni.diffusion.layers.norm",
|
||||
"vllm_omni.diffusion.layers.rope",
|
||||
@@ -58,6 +62,47 @@ def _install_vllm_stubs() -> None:
|
||||
distributed.get_tensor_model_parallel_world_size = lambda: 1
|
||||
distributed.tensor_model_parallel_all_reduce = lambda value: value
|
||||
|
||||
class _SeqAllToAll4D:
|
||||
@staticmethod
|
||||
def apply(group, value, scatter_idx, gather_idx, use_sync=False):
|
||||
del group, scatter_idx, gather_idx, use_sync
|
||||
return value
|
||||
|
||||
setattr(
|
||||
sys.modules["vllm_omni.diffusion.distributed.comm"],
|
||||
"SeqAllToAll4D",
|
||||
_SeqAllToAll4D,
|
||||
)
|
||||
|
||||
def get_sp_group():
|
||||
return SimpleNamespace(
|
||||
ulysses_world_size=1,
|
||||
ulysses_rank=0,
|
||||
ulysses_group=None,
|
||||
)
|
||||
|
||||
setattr(
|
||||
sys.modules["vllm_omni.diffusion.distributed.parallel_state"],
|
||||
"get_sp_group",
|
||||
get_sp_group,
|
||||
)
|
||||
|
||||
class _SequenceParallelInput:
|
||||
def __init__(self, split_dim, expected_dims=None, split_output=False, auto_pad=False):
|
||||
self.split_dim = split_dim
|
||||
self.expected_dims = expected_dims
|
||||
self.split_output = split_output
|
||||
self.auto_pad = auto_pad
|
||||
|
||||
class _SequenceParallelOutput:
|
||||
def __init__(self, gather_dim, expected_dims=None):
|
||||
self.gather_dim = gather_dim
|
||||
self.expected_dims = expected_dims
|
||||
|
||||
sp_plan = sys.modules["vllm_omni.diffusion.distributed.sp_plan"]
|
||||
setattr(sp_plan, "SequenceParallelInput", _SequenceParallelInput)
|
||||
setattr(sp_plan, "SequenceParallelOutput", _SequenceParallelOutput)
|
||||
|
||||
def set_weight_attrs(weight: torch.Tensor, attrs: dict) -> None:
|
||||
for name, value in attrs.items():
|
||||
setattr(weight, name, value)
|
||||
@@ -541,3 +586,38 @@ def test_tp_rmsnorm_weight_loader_selects_rank_shard(monkeypatch: pytest.MonkeyP
|
||||
norm.weight.weight_loader(norm.weight, torch.tensor([10.0, 20.0, 30.0, 40.0]))
|
||||
|
||||
torch.testing.assert_close(norm.weight, torch.tensor([30.0, 40.0]))
|
||||
|
||||
|
||||
def test_single_token_text_kv_shard_owns_compact_storage(monkeypatch):
|
||||
module = _load_module()
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_sp_group",
|
||||
lambda: SimpleNamespace(
|
||||
ulysses_world_size=2,
|
||||
ulysses_rank=1,
|
||||
ulysses_group=None,
|
||||
),
|
||||
)
|
||||
attention = module.LingBotCrossAttention(dim=8, num_heads=4)
|
||||
full = torch.arange(8, dtype=torch.float32).reshape(1, 1, 4, 2)
|
||||
shard = attention.shard_kv_heads(full)
|
||||
torch.testing.assert_close(shard, full[:, :, 2:], rtol=0, atol=0)
|
||||
assert shard.is_contiguous()
|
||||
assert shard.untyped_storage().nbytes() == shard.numel() * shard.element_size()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attention_class", ["LingBotSelfAttention", "LingBotCrossAttention"])
|
||||
def test_ulysses_rejects_non_divisible_head_count(monkeypatch, attention_class):
|
||||
module = _load_module()
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"get_sp_group",
|
||||
lambda: SimpleNamespace(
|
||||
ulysses_world_size=3,
|
||||
ulysses_rank=0,
|
||||
ulysses_group=None,
|
||||
),
|
||||
)
|
||||
with pytest.raises(ValueError, match="heads must be divisible"):
|
||||
getattr(module, attention_class)(dim=8, num_heads=4)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,14 +7,17 @@ import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.diffusion.models.lingbot_world import test_lingbot_world_attention as attention_tests
|
||||
from tests.helpers.mark import hardware_test
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
|
||||
|
||||
_FIXTURE_PATH = Path(__file__).with_name("fixtures") / "lingbot_world_weight_index_names.json.fixture"
|
||||
_SHAPE_FIXTURE_PATH = Path(__file__).with_name("fixtures") / "lingbot_world_official_shapes.json.fixture"
|
||||
@@ -117,6 +120,7 @@ def _assert_self_cache_unchanged(cache, snapshot) -> None:
|
||||
attention_tests._assert_cache_unchanged(layer_cache, layer_snapshot)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_tiny_transformer_runs_four_chunks_with_explicit_cache_commit_and_camera_path() -> None:
|
||||
torch.manual_seed(7)
|
||||
module = attention_tests._load_module()
|
||||
@@ -191,6 +195,7 @@ def test_tiny_transformer_runs_four_chunks_with_explicit_cache_commit_and_camera
|
||||
assert not torch.equal(camera_output, alternate_output)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_partial_cross_attention_cache_projects_text_only_for_missing_layers() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=2).eval()
|
||||
@@ -229,6 +234,7 @@ def test_partial_cross_attention_cache_projects_text_only_for_missing_layers() -
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frames", [1, 6], ids=("partial", "multiple_blocks"))
|
||||
@pytest.mark.cpu
|
||||
def test_forward_rejects_chunks_that_do_not_equal_configured_block_size(frames: int) -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(
|
||||
@@ -257,6 +263,7 @@ def test_forward_rejects_chunks_that_do_not_equal_configured_block_size(frames:
|
||||
assert cache.cross_attention == [None]
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_forward_accepts_exactly_one_configured_frame_block() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(
|
||||
@@ -279,6 +286,7 @@ def test_forward_accepts_exactly_one_configured_frame_block() -> None:
|
||||
assert output.shape == (1, 2, 3, 4, 4)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_transformer_allocates_request_cache_from_its_configured_geometry() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(
|
||||
@@ -303,6 +311,7 @@ def test_transformer_allocates_request_cache_from_its_configured_geometry() -> N
|
||||
assert all(layer.value.shape == (2, 6 * 4 * 6, 2, 2) for layer in cache.self_attention)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_video_patch_embedding_uses_temporal_height_width_token_order() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
|
||||
@@ -323,6 +332,7 @@ def test_video_patch_embedding_uses_temporal_height_width_token_order() -> None:
|
||||
torch.testing.assert_close(tokens[0, :, 0], torch.tensor([1.0, 3.0, 9.0, 11.0]))
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_unpatchify_restores_two_frame_channel_and_spatial_order() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
|
||||
@@ -363,6 +373,7 @@ def test_unpatchify_restores_two_frame_channel_and_spatial_order() -> None:
|
||||
torch.testing.assert_close(output, expected)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_head_modulation_broadcasts_distinct_condition_per_frame() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
|
||||
@@ -381,6 +392,7 @@ def test_head_modulation_broadcasts_distinct_condition_per_frame() -> None:
|
||||
torch.testing.assert_close(output, expected)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_constructor_defaults_match_official_checkpoint_config() -> None:
|
||||
module = attention_tests._load_module()
|
||||
parameters = inspect.signature(module.CausalLingBotWorldTransformer3DModel.__init__).parameters
|
||||
@@ -406,6 +418,7 @@ def test_constructor_defaults_match_official_checkpoint_config() -> None:
|
||||
assert {name: parameters[name].default for name in expected} == expected
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_transformer_exposes_parameter_dtype_for_pipeline_runtime() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module)
|
||||
@@ -413,12 +426,14 @@ def test_transformer_exposes_parameter_dtype_for_pipeline_runtime() -> None:
|
||||
assert model.dtype == next(model.parameters()).dtype
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_transformer_declares_regional_compile_block() -> None:
|
||||
module = attention_tests._load_module()
|
||||
|
||||
assert module.CausalLingBotWorldTransformer3DModel._repeated_blocks == ["LingBotAttentionBlock"]
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_lingbot_rms_norm_uses_global_tp_square_mean(monkeypatch) -> None:
|
||||
module = attention_tests._load_module()
|
||||
reduced_values: list[torch.Tensor] = []
|
||||
@@ -440,6 +455,7 @@ def test_lingbot_rms_norm_uses_global_tp_square_mean(monkeypatch) -> None:
|
||||
torch.testing.assert_close(reduced_values[0], torch.tensor([[25.0]]))
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_attention_rejects_heads_not_divisible_by_tp_size(monkeypatch) -> None:
|
||||
module = attention_tests._load_module()
|
||||
monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 3)
|
||||
@@ -448,6 +464,7 @@ def test_attention_rejects_heads_not_divisible_by_tp_size(monkeypatch) -> None:
|
||||
module.LingBotSelfAttention(dim=4, num_heads=2)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_constructor_rejects_unsupported_qk_norm_with_config_error() -> None:
|
||||
module = attention_tests._load_module()
|
||||
|
||||
@@ -456,6 +473,7 @@ def test_constructor_rejects_unsupported_qk_norm_with_config_error() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["image_dim", "added_kv_proj_dim", "pos_embed_seq_len"])
|
||||
@pytest.mark.cpu
|
||||
def test_constructor_rejects_non_null_image_embedding_fields(field: str) -> None:
|
||||
module = attention_tests._load_module()
|
||||
|
||||
@@ -463,6 +481,7 @@ def test_constructor_rejects_non_null_image_embedding_fields(field: str) -> None
|
||||
module.CausalLingBotWorldTransformer3DModel(**{field: 4})
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_constructor_rejects_unsupported_quantization_with_runtime_error() -> None:
|
||||
module = attention_tests._load_module()
|
||||
|
||||
@@ -470,6 +489,7 @@ def test_constructor_rejects_unsupported_quantization_with_runtime_error() -> No
|
||||
module.CausalLingBotWorldTransformer3DModel(quant_config=object())
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_from_config_accepts_diffusers_metadata_and_normalizes_patch_size() -> None:
|
||||
module = attention_tests._load_module()
|
||||
|
||||
@@ -522,6 +542,7 @@ def test_from_config_accepts_diffusers_metadata_and_normalizes_patch_size() -> N
|
||||
("sink_size", 0),
|
||||
],
|
||||
)
|
||||
@pytest.mark.cpu
|
||||
def test_from_config_rejects_checkpoint_topology_drift(field: str, value: object) -> None:
|
||||
module = attention_tests._load_module()
|
||||
config = {
|
||||
@@ -554,6 +575,7 @@ def test_from_config_rejects_checkpoint_topology_drift(field: str, value: object
|
||||
module.CausalLingBotWorldTransformer3DModel.from_config(config)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_from_config_ignores_non_semantic_checkpoint_metadata() -> None:
|
||||
module = attention_tests._load_module()
|
||||
config = {
|
||||
@@ -569,6 +591,7 @@ def test_from_config_ignores_non_semantic_checkpoint_metadata() -> None:
|
||||
assert model.config.num_layers == 40
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_load_weights_uses_parameter_loaders_and_rejects_unknown_model_keys() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=1)
|
||||
@@ -606,6 +629,7 @@ def test_load_weights_uses_parameter_loaders_and_rejects_unknown_model_keys() ->
|
||||
model.load_weights([("unexpected_model.weight", torch.ones(1))])
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_load_weights_consumes_checkpoint_iterator_incrementally() -> None:
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_layers=1)
|
||||
@@ -619,6 +643,7 @@ def test_load_weights_consumes_checkpoint_iterator_incrementally() -> None:
|
||||
assert model.load_weights(weights()) == {first_name}
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_real_auto_weights_loader_covers_fused_model_parameter_namespace() -> None:
|
||||
utils = pytest.importorskip("vllm.model_executor.models.utils")
|
||||
module = attention_tests._load_module()
|
||||
@@ -638,6 +663,7 @@ def test_real_auto_weights_loader_covers_fused_model_parameter_namespace() -> No
|
||||
assert set(dict(pipeline.named_parameters())) <= loaded
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_checkpoint_weight_index_fixture_matches_model_namespaces() -> None:
|
||||
module = attention_tests._load_module()
|
||||
fixture = json.loads(_FIXTURE_PATH.read_text())
|
||||
@@ -667,6 +693,7 @@ def test_checkpoint_weight_index_fixture_matches_model_namespaces() -> None:
|
||||
assert all(len(shard["parameter_names_sha256"]) == 64 for shard in fixture["shards"].values())
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_official_default_shapes_match_public_safetensors_header_fixture() -> None:
|
||||
module = attention_tests._load_module()
|
||||
fixture = json.loads(_SHAPE_FIXTURE_PATH.read_text())
|
||||
@@ -683,3 +710,284 @@ def test_official_default_shapes_match_public_safetensors_header_fixture() -> No
|
||||
shape, dtype = parameter_specs[name]
|
||||
assert tuple(shape) == tuple(expected_shape), name
|
||||
assert dtype == expected_dtype
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_timestep_expansion_only_runs_with_ulysses(monkeypatch):
|
||||
dtype = torch.bfloat16
|
||||
module = attention_tests._load_module()
|
||||
hidden = torch.randn(2, 12, 4).to(dtype)
|
||||
camera = torch.randn_like(hidden)
|
||||
table = torch.randn(2, 3, 6, 4).to(dtype)
|
||||
rotary = (torch.randn(12, 2), torch.randn(12, 2))
|
||||
prepare = module._LingBotSPPrepare()
|
||||
assert prepare(hidden, camera, table, rotary)[2] is table
|
||||
monkeypatch.setattr(module, "get_sp_group", lambda: SimpleNamespace(ulysses_world_size=2))
|
||||
expanded = prepare(hidden, camera, table, rotary)[2]
|
||||
assert expanded.shape == (2, 12, 6, 4)
|
||||
torch.testing.assert_close(expanded, table.repeat_interleave(4, dim=1), rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsharded", [None, 3], ids=["missing-hook", "unsharded-rope"])
|
||||
@pytest.mark.cpu
|
||||
def test_missing_or_partial_sp_split_fails_before_attention(monkeypatch, unsharded):
|
||||
module = attention_tests._load_module()
|
||||
model = _tiny_model(module, num_frames_per_block=3, sliding_window_num_frames=6)
|
||||
cache = _cache(module, model)
|
||||
for block in model.blocks:
|
||||
block.self_attn.ulysses_world_size = 2
|
||||
monkeypatch.setattr(module, "get_sp_group", lambda: SimpleNamespace(ulysses_world_size=2))
|
||||
original = model.sp_prepare.forward
|
||||
|
||||
def partial_split(*args):
|
||||
values = original(*args)
|
||||
if unsharded is None:
|
||||
return values
|
||||
return tuple(
|
||||
value if i == unsharded else value.chunk(2, dim=1 if i < 3 else 0)[0] for i, value in enumerate(values)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(model.sp_prepare, "forward", partial_split)
|
||||
|
||||
def forbidden(*args, **kwargs):
|
||||
raise AssertionError("attention/collectives must not run before shard validation")
|
||||
|
||||
monkeypatch.setattr(model.blocks[0], "forward", forbidden)
|
||||
with pytest.raises(RuntimeError, match="SP input hooks"):
|
||||
model(
|
||||
torch.randn(1, 36, 3, 4, 4),
|
||||
torch.tensor([1.0]),
|
||||
torch.randn(1, 3, 6),
|
||||
torch.randn(1, 384, 3, 4, 4),
|
||||
cache=cache,
|
||||
start_frame=0,
|
||||
update_cache=False,
|
||||
)
|
||||
|
||||
|
||||
# Real multi-rank regression: synthetic weights, native SP/TP and attention.
|
||||
_HEADS, _HEAD_DIM, _LAYERS = 8, 32, 2
|
||||
_FRAMES, _SIDE, _TOKENS_PER_FRAME = 3, 32, 256
|
||||
|
||||
|
||||
def _model(dtype):
|
||||
from vllm_omni.diffusion.models.lingbot_world.transformer import CausalLingBotWorldTransformer3DModel
|
||||
|
||||
model = CausalLingBotWorldTransformer3DModel(
|
||||
num_attention_heads=_HEADS,
|
||||
attention_head_dim=_HEAD_DIM,
|
||||
num_layers=_LAYERS,
|
||||
in_channels=4,
|
||||
out_channels=4,
|
||||
text_dim=8,
|
||||
freq_dim=16,
|
||||
ffn_dim=512,
|
||||
patch_size=(1, 2, 2),
|
||||
sink_size=1,
|
||||
num_frames_per_block=_FRAMES,
|
||||
sliding_window_num_frames=6,
|
||||
rope_max_seq_len=32,
|
||||
).eval()
|
||||
return model.to(device=torch.device("cuda", torch.accelerator.current_device_index()), dtype=dtype)
|
||||
|
||||
|
||||
def _checkpoint(model):
|
||||
weights = {}
|
||||
generator = torch.Generator().manual_seed(17)
|
||||
for name, param in sorted(model.named_parameters()):
|
||||
data = torch.randn(param.shape, generator=generator) * 0.02
|
||||
if "norm" in name and name.endswith("weight"):
|
||||
data.fill_(1)
|
||||
if name.endswith("modulation") and data.shape[-2] == 6:
|
||||
data[..., 2, :] = 1
|
||||
data[..., 5, :] = 1
|
||||
data = data.to(param.dtype)
|
||||
param.copy_(data)
|
||||
if ".self_attn.qkv." in name:
|
||||
for component, chunk in zip(("q", "k", "v"), data.chunk(3), strict=True):
|
||||
weights[name.replace(".self_attn.qkv.", f".self_attn.{component}.")] = chunk
|
||||
else:
|
||||
weights[name] = data
|
||||
return weights
|
||||
|
||||
|
||||
def _rollout(model, mode, dtype, batch):
|
||||
from vllm_omni.diffusion.models.lingbot_world.transformer import LingBotTransformerCache
|
||||
from vllm_omni.experimental.ar_diffusion.capability import ARDiffusionKVBranchSpec
|
||||
from vllm_omni.experimental.ar_diffusion.kv_cache import ARDiffusionKVCache, ARDiffusionKVConfig
|
||||
from vllm_omni.experimental.ar_diffusion.kv_cache.state import ARDiffusionKVState
|
||||
|
||||
device = next(model.parameters()).device
|
||||
state = None
|
||||
if mode == "paged":
|
||||
kv = ARDiffusionKVCache(
|
||||
ARDiffusionKVConfig(enable=True, chunk_size=_TOKENS_PER_FRAME, window_chunks=5, sink_chunks=1),
|
||||
num_layers=_LAYERS,
|
||||
num_kv_heads=model.blocks[0].self_attn.num_sp_heads,
|
||||
head_size=_HEAD_DIM,
|
||||
dtype=dtype,
|
||||
block_size=_TOKENS_PER_FRAME,
|
||||
max_model_len=4096,
|
||||
available_bytes=1 << 27,
|
||||
kv_branches=(ARDiffusionKVBranchSpec("main", 0),),
|
||||
session_capacity=1,
|
||||
frames_per_block=_FRAMES,
|
||||
max_scratch_tokens_per_branch=_FRAMES * _TOKENS_PER_FRAME,
|
||||
cross_attention_lengths={"text": 5},
|
||||
device=device,
|
||||
)
|
||||
state = ARDiffusionKVState(kv, "numeric", {"main": kv.begin_request("numeric")}, num_layers=_LAYERS)
|
||||
cache = LingBotTransformerCache(self_attention=[], cross_attention=[None] * _LAYERS)
|
||||
else:
|
||||
cache = model.allocate_cache(
|
||||
batch_size=batch, latent_height=_SIDE, latent_width=_SIDE, device=device, dtype=dtype
|
||||
)
|
||||
generator = torch.Generator().manual_seed(123)
|
||||
text = torch.randn(batch, 5, 8, generator=generator).to(device, dtype)
|
||||
outputs = []
|
||||
try:
|
||||
for start in range(0, 12, _FRAMES):
|
||||
latent = torch.randn(batch, 4, _FRAMES, _SIDE, _SIDE, generator=generator).to(device, dtype)
|
||||
camera = torch.randn(batch, 384, _FRAMES, _SIDE, _SIDE, generator=generator).to(device, dtype)
|
||||
for commit in (False, False, True):
|
||||
if state is not None:
|
||||
cache.self_attention = state.get_kv_caches(
|
||||
"main", seq_len=_FRAMES * _TOKENS_PER_FRAME, commit_current=commit
|
||||
)
|
||||
output = model(
|
||||
latent,
|
||||
torch.tensor([100.0, 400.0, 900.0], device=device).expand(batch, -1),
|
||||
text,
|
||||
camera,
|
||||
cache=cache,
|
||||
start_frame=start,
|
||||
update_cache=commit,
|
||||
)
|
||||
outputs.append(output.float().cpu())
|
||||
if state is not None:
|
||||
if not state.is_cross_attention_populated("main", "text"):
|
||||
state.populate_cross_attention(
|
||||
"main", "text", [(layer.key, layer.value) for layer in cache.cross_attention]
|
||||
)
|
||||
for layer, pool in zip(
|
||||
cache.cross_attention, state.get_cross_attention_kv("main", "text"), strict=True
|
||||
):
|
||||
layer.key, layer.value = pool["k"], pool["v"]
|
||||
state.commit_paged_context("main")
|
||||
cross = [
|
||||
(layer.key.detach().float().cpu(), layer.value.detach().float().cpu()) for layer in cache.cross_attention
|
||||
]
|
||||
return torch.stack(outputs), cross
|
||||
finally:
|
||||
if state is not None:
|
||||
state.close()
|
||||
|
||||
|
||||
def _worker(rank, world_size, sp_size, tp_size, mode, dtype, batch, rendezvous):
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.config.vllm import set_current_vllm_config
|
||||
from vllm.distributed import get_tensor_model_parallel_rank
|
||||
|
||||
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, DiffusionParallelConfig, OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
destroy_distributed_env,
|
||||
destroy_model_parallel,
|
||||
get_sp_group,
|
||||
init_distributed_environment,
|
||||
initialize_model_parallel,
|
||||
)
|
||||
from vllm_omni.diffusion.forward_context import set_forward_context
|
||||
from vllm_omni.diffusion.registry import _apply_sequence_parallel_if_enabled
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
current_omni_platform.set_device(torch.device("cuda", rank))
|
||||
torch.backends.cuda.matmul.allow_tf32 = False
|
||||
torch.backends.cudnn.allow_tf32 = False
|
||||
torch.distributed.init_process_group(
|
||||
"nccl", init_method=f"file://{rendezvous}", world_size=world_size, rank=rank, timeout=timedelta(seconds=90)
|
||||
)
|
||||
init_distributed_environment(world_size=world_size, rank=rank, local_rank=rank, backend="nccl")
|
||||
try:
|
||||
with torch.inference_mode(), set_current_vllm_config(VllmConfig()):
|
||||
for baseline in (True, False):
|
||||
sp, tp = (1, 1) if baseline else (sp_size, tp_size)
|
||||
initialize_model_parallel(
|
||||
data_parallel_size=world_size // (sp * tp),
|
||||
sequence_parallel_size=sp,
|
||||
ulysses_degree=sp,
|
||||
tensor_parallel_size=tp,
|
||||
)
|
||||
config = OmniDiffusionConfig(
|
||||
model=str(Path(rendezvous).parent),
|
||||
dtype=dtype,
|
||||
enforce_eager=True,
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
data_parallel_size=world_size // (sp * tp),
|
||||
sequence_parallel_size=sp,
|
||||
ulysses_degree=sp,
|
||||
tensor_parallel_size=tp,
|
||||
),
|
||||
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="TORCH_SDPA")),
|
||||
)
|
||||
with set_current_diffusion_config(config), set_forward_context(omni_diffusion_config=config):
|
||||
model = _model(dtype)
|
||||
if baseline:
|
||||
weights = _checkpoint(model)
|
||||
else:
|
||||
model.load_weights(iter(weights.items()))
|
||||
_apply_sequence_parallel_if_enabled(SimpleNamespace(transformer=model), config)
|
||||
result, cross = _rollout(model, mode, dtype, batch)
|
||||
if baseline:
|
||||
expected, expected_cross = result, cross
|
||||
else:
|
||||
assert expected.abs().max() > 1e-3
|
||||
bound = 1e-5 if dtype == torch.float32 else 1e-2
|
||||
error = (result - expected).double().norm() / expected.double().norm()
|
||||
assert error <= bound, f"rank={rank}, TP{tp} SP{sp}: relative L2 {error.item():.3g} > {bound}"
|
||||
local_heads = _HEADS // tp // sp
|
||||
first = (
|
||||
get_tensor_model_parallel_rank() * (_HEADS // tp)
|
||||
+ get_sp_group().ulysses_rank * local_heads
|
||||
)
|
||||
for (k, v), (ek, ev) in zip(cross, expected_cross, strict=True):
|
||||
tolerance = 1e-5 if dtype == torch.float32 else 2e-2
|
||||
torch.testing.assert_close(
|
||||
k, ek[:, :, first : first + local_heads], rtol=tolerance, atol=tolerance
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
v, ev[:, :, first : first + local_heads], rtol=tolerance, atol=tolerance
|
||||
)
|
||||
if rank == 0:
|
||||
print(f"{mode} {dtype} TP{tp} SP{sp}: relative L2={error.item():.3g}", flush=True)
|
||||
del model
|
||||
torch.distributed.barrier()
|
||||
destroy_model_parallel()
|
||||
finally:
|
||||
destroy_distributed_env()
|
||||
|
||||
|
||||
def _run(tmp_path: Path, sp, tp, mode, dtype, batch):
|
||||
if torch.accelerator.device_count() < sp * tp:
|
||||
pytest.skip(f"requires {sp * tp} GPUs")
|
||||
torch.multiprocessing.spawn(
|
||||
_worker, args=(sp * tp, sp, tp, mode, dtype, batch, str(tmp_path / "rendezvous")), nprocs=sp * tp
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parallel
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=2)
|
||||
def test_sp2_direct_matches_sp1_fp32(tmp_path):
|
||||
_run(tmp_path, 2, 1, "direct", torch.float32, 2)
|
||||
|
||||
|
||||
@pytest.mark.parallel
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_sp4_direct_matches_sp1_bf16(tmp_path):
|
||||
_run(tmp_path, 4, 1, "direct", torch.bfloat16, 1)
|
||||
|
||||
|
||||
@pytest.mark.parallel
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_tp2_sp2_paged_matches_sp1_bf16(tmp_path):
|
||||
_run(tmp_path, 2, 2, "paged", torch.bfloat16, 1)
|
||||
@@ -126,8 +126,6 @@ class _RecordingTransformer(nn.Module):
|
||||
sink_size=3,
|
||||
)
|
||||
self.blocks = nn.ModuleList([nn.Identity(), nn.Identity()])
|
||||
for block in self.blocks:
|
||||
block.self_attn = SimpleNamespace(num_local_heads=2, head_dim=4)
|
||||
self.calls: list[dict] = []
|
||||
self.cache_allocations: list[dict] = []
|
||||
self.raise_on_call = raise_on_call
|
||||
@@ -387,6 +385,9 @@ def _od_config(**overrides):
|
||||
"parallel_config": SimpleNamespace(
|
||||
pipeline_parallel_size=1,
|
||||
sequence_parallel_size=1,
|
||||
ulysses_degree=1,
|
||||
ring_degree=1,
|
||||
allgather_degree=1,
|
||||
cfg_parallel_size=1,
|
||||
vae_patch_parallel_size=1,
|
||||
use_hsdp=False,
|
||||
@@ -438,15 +439,15 @@ def test_pipeline_respects_loader_managed_component_placement(offload_field: str
|
||||
assert getattr(pipeline.vae, "to_calls", []) == []
|
||||
|
||||
|
||||
def test_ar_diffusion_capability_uses_fixed_tp_local_lingbot_geometry() -> None:
|
||||
def test_ar_diffusion_capability_uses_transformer_local_head_geometry() -> None:
|
||||
module = _load_pipeline_module()
|
||||
module.get_tensor_model_parallel_world_size = lambda: 1
|
||||
pipeline = _pipeline(module)
|
||||
|
||||
# Use the constructed head count even when the config describes a different geometry.
|
||||
pipeline.transformer.blocks[0].self_attn = SimpleNamespace(num_sp_heads=1)
|
||||
spec = pipeline.ar_diffusion_kv_cache_spec()
|
||||
|
||||
assert spec.num_layers == 2
|
||||
assert spec.num_kv_heads == 2
|
||||
assert spec.num_kv_heads == 1
|
||||
assert spec.head_size == 4
|
||||
assert spec.tokens_per_frame == 1
|
||||
assert spec.frames_per_block == 3
|
||||
@@ -604,7 +605,6 @@ def test_component_discovery_uses_official_checkpoint_contract() -> None:
|
||||
("field", "value", "feature"),
|
||||
[
|
||||
("pipeline_parallel_size", 2, "pipeline parallelism"),
|
||||
("sequence_parallel_size", 2, "sequence parallelism"),
|
||||
("cfg_parallel_size", 2, "CFG parallelism"),
|
||||
("vae_patch_parallel_size", 2, "VAE parallelism"),
|
||||
("use_hsdp", True, "HSDP"),
|
||||
@@ -622,6 +622,43 @@ def test_unsupported_parallel_modes_fail_before_component_loading(field: str, va
|
||||
assert module._loader_state.prefetch_calls == []
|
||||
|
||||
|
||||
def test_pure_ulysses_parallel_config_is_supported() -> None:
|
||||
module = _load_pipeline_module()
|
||||
parallel_config = _od_config().parallel_config
|
||||
parallel_config.sequence_parallel_size = 2
|
||||
parallel_config.ulysses_degree = 2
|
||||
|
||||
pipeline = module.LingBotWorldCausalDMDPipeline(od_config=_od_config(parallel_config=parallel_config))
|
||||
|
||||
assert pipeline.transformer is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides",
|
||||
[
|
||||
{"sequence_parallel_size": 4, "ring_degree": 2}, # Normalized hybrid.
|
||||
{"ulysses_degree": 1, "allgather_degree": 2}, # Normalized AllGather-KV.
|
||||
{"ring_degree": 2}, # Isolate each clause from the SP-size mismatch.
|
||||
{"allgather_degree": 2},
|
||||
{"ulysses_mode": "advanced_uaa"},
|
||||
{"ulysses_a2a_permute": True},
|
||||
{"ulysses_degree": None}, # Missing degree must not imply pure Ulysses.
|
||||
],
|
||||
)
|
||||
def test_unsupported_sp_config_fails_before_component_loading(overrides):
|
||||
module = _load_pipeline_module()
|
||||
config = _od_config().parallel_config
|
||||
config.sequence_parallel_size = config.ulysses_degree = 2
|
||||
for name, value in overrides.items():
|
||||
if value is None:
|
||||
delattr(config, name)
|
||||
else:
|
||||
setattr(config, name, value)
|
||||
with pytest.raises(NotImplementedError, match="pure Ulysses"):
|
||||
module.LingBotWorldCausalDMDPipeline(od_config=_od_config(parallel_config=config))
|
||||
assert module._loader_state.prefetch_calls == []
|
||||
|
||||
|
||||
def test_unsupported_quantization_fails_before_component_loading() -> None:
|
||||
module = _load_pipeline_module()
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from examples.offline_inference.diffusion import lingbot_world_v2 as example
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
def test_offline_ulysses_argument(tmp_path):
|
||||
argv = ["--image", "frame.jpg", "--action-dir", "forward", "--prompt", "A lake"]
|
||||
paths = example.LingBotPaths(
|
||||
tmp_path / "frame.jpg", tmp_path / "forward", tmp_path, Path("forward"), 9, tmp_path / "out.mp4"
|
||||
)
|
||||
assert example.parse_args(argv).ulysses_degree == 1
|
||||
args = example.parse_args([*argv, "--ulysses-degree", "4"])
|
||||
kwargs = example.build_omni_kwargs(args, paths)
|
||||
assert kwargs["ulysses_degree"] == 4 and kwargs["tensor_parallel_size"] == 1
|
||||
args.ulysses_degree = 0
|
||||
with pytest.raises(ValueError, match="--ulysses-degree"):
|
||||
example.build_omni_kwargs(args, paths)
|
||||
@@ -18,7 +18,6 @@ import torch.nn.functional as F
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from torch import nn
|
||||
from transformers import AutoTokenizer, UMT5EncoderModel
|
||||
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
@@ -203,7 +202,6 @@ def _validate_parallel_config(od_config: OmniDiffusionConfig) -> None:
|
||||
return
|
||||
unsupported_sizes = {
|
||||
"pipeline_parallel_size": "pipeline parallelism",
|
||||
"sequence_parallel_size": "sequence parallelism",
|
||||
"cfg_parallel_size": "CFG parallelism",
|
||||
"vae_patch_parallel_size": "VAE parallelism",
|
||||
}
|
||||
@@ -211,6 +209,25 @@ def _validate_parallel_config(od_config: OmniDiffusionConfig) -> None:
|
||||
size = getattr(parallel_config, field, 1) or 1
|
||||
if size > 1:
|
||||
raise NotImplementedError(f"LingBot World v1 does not support {feature} ({field}={size}).")
|
||||
sequence_parallel_size = getattr(parallel_config, "sequence_parallel_size", 1) or 1
|
||||
ulysses_degree = getattr(parallel_config, "ulysses_degree", 1) or 1
|
||||
ring_degree = getattr(parallel_config, "ring_degree", 1) or 1
|
||||
allgather_degree = getattr(parallel_config, "allgather_degree", 1) or 1
|
||||
ulysses_mode = getattr(parallel_config, "ulysses_mode", "strict")
|
||||
ulysses_a2a_permute = bool(getattr(parallel_config, "ulysses_a2a_permute", False))
|
||||
if (
|
||||
sequence_parallel_size != ulysses_degree
|
||||
or ring_degree != 1
|
||||
or allgather_degree != 1
|
||||
or ulysses_mode != "strict"
|
||||
or ulysses_a2a_permute
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"LingBot World sequence parallelism requires pure Ulysses with ulysses_a2a_permute disabled: "
|
||||
f"sequence_parallel_size={sequence_parallel_size}, ulysses_degree={ulysses_degree}, "
|
||||
f"ring_degree={ring_degree}, allgather_degree={allgather_degree}, ulysses_mode={ulysses_mode!r}, "
|
||||
f"ulysses_a2a_permute={ulysses_a2a_permute}."
|
||||
)
|
||||
if getattr(parallel_config, "use_hsdp", False):
|
||||
raise NotImplementedError("LingBot World v1 does not support HSDP.")
|
||||
if getattr(parallel_config, "enable_expert_parallel", False):
|
||||
@@ -557,8 +574,6 @@ class LingBotWorldCausalDMDPipeline(
|
||||
latent_height = self._ar_height // spatial
|
||||
latent_width = self._ar_width // spatial
|
||||
tokens_per_frame = (latent_height // patch_height) * (latent_width // patch_width)
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
num_local_heads = int(self.transformer.config.num_attention_heads) // tp_size
|
||||
total_window_frames = (
|
||||
int(self.transformer.config.local_attn_size)
|
||||
if int(self.transformer.config.local_attn_size) != -1
|
||||
@@ -579,7 +594,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
)
|
||||
return ARDiffusionKVCacheSpec(
|
||||
num_layers=int(self.transformer.config.num_layers),
|
||||
num_kv_heads=num_local_heads,
|
||||
num_kv_heads=int(self.transformer.blocks[0].self_attn.num_sp_heads),
|
||||
head_size=int(self.transformer.config.attention_head_dim),
|
||||
tokens_per_frame=tokens_per_frame,
|
||||
frames_per_block=int(self.transformer.config.num_frames_per_block),
|
||||
@@ -960,20 +975,11 @@ class LingBotWorldCausalDMDPipeline(
|
||||
def layer_kv() -> Iterator[tuple[torch.Tensor, torch.Tensor]]:
|
||||
for block in self.transformer.blocks:
|
||||
cross_attention = block.cross_attn
|
||||
key = cross_attention.norm_k(cross_attention.k(projected_text)).unflatten(
|
||||
2,
|
||||
(
|
||||
cross_attention.num_local_heads,
|
||||
cross_attention.head_dim,
|
||||
),
|
||||
)
|
||||
value = cross_attention.v(projected_text).unflatten(
|
||||
2,
|
||||
(
|
||||
cross_attention.num_local_heads,
|
||||
cross_attention.head_dim,
|
||||
),
|
||||
shape = (cross_attention.num_local_heads, cross_attention.head_dim)
|
||||
key = cross_attention.shard_kv_heads(
|
||||
cross_attention.norm_k(cross_attention.k(projected_text)).unflatten(2, shape)
|
||||
)
|
||||
value = cross_attention.shard_kv_heads(cross_attention.v(projected_text).unflatten(2, shape))
|
||||
yield key, value
|
||||
|
||||
state.populate_cross_attention(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Checkpoint-compatible causal DiT for the LingBot World v2 model package."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +24,9 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.model_executor.utils import set_weight_attrs
|
||||
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.distributed.comm import SeqAllToAll4D
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelInput, SequenceParallelOutput
|
||||
from vllm_omni.diffusion.layers.norm import LayerNorm
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbeddingWan
|
||||
from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import (
|
||||
@@ -127,6 +130,37 @@ def _projection_prefix(prefix: str, name: str) -> str:
|
||||
return f"{prefix}.{name}" if prefix else name
|
||||
|
||||
|
||||
def _ulysses_state() -> tuple[int, int, torch.distributed.ProcessGroup | None]:
|
||||
coordinator = get_sp_group()
|
||||
return (
|
||||
int(coordinator.ulysses_world_size),
|
||||
int(coordinator.ulysses_rank),
|
||||
coordinator.ulysses_group,
|
||||
)
|
||||
|
||||
|
||||
class _LingBotSPPrepare(nn.Module):
|
||||
"""Expand frame conditioning only when Ulysses shards the token sequence."""
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
camera_hidden_states: torch.Tensor,
|
||||
timestep_projection: torch.Tensor,
|
||||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
num_frames = timestep_projection.shape[1]
|
||||
if hidden_states.shape[1] % num_frames:
|
||||
raise ValueError("LingBot token count must be divisible by the latent frame count.")
|
||||
tokens_per_frame = hidden_states.shape[1] // num_frames
|
||||
if get_sp_group().ulysses_world_size > 1:
|
||||
timestep_projection = (
|
||||
timestep_projection.unsqueeze(2).expand(-1, -1, tokens_per_frame, -1, -1).flatten(1, 2)
|
||||
)
|
||||
cosine, sine = rotary_emb
|
||||
return hidden_states, camera_hidden_states, timestep_projection, cosine, sine
|
||||
|
||||
|
||||
class LingBotSelfAttention(nn.Module):
|
||||
"""Block-causal self-attention over retained history and one full chunk."""
|
||||
|
||||
@@ -157,6 +191,13 @@ class LingBotSelfAttention(nn.Module):
|
||||
prefix=_projection_prefix(prefix, "qkv"),
|
||||
)
|
||||
self.num_local_heads = self.qkv.num_heads
|
||||
self.ulysses_world_size, self.ulysses_rank, self.ulysses_group = _ulysses_state()
|
||||
if self.num_local_heads % self.ulysses_world_size:
|
||||
raise ValueError(
|
||||
"LingBot local attention heads must be divisible by the Ulysses degree: "
|
||||
f"heads={self.num_local_heads}, ulysses={self.ulysses_world_size}."
|
||||
)
|
||||
self.num_sp_heads = self.num_local_heads // self.ulysses_world_size
|
||||
self.tp_inner_dim = self.num_local_heads * self.head_dim
|
||||
self.o = RowParallelLinear(
|
||||
dim,
|
||||
@@ -170,9 +211,9 @@ class LingBotSelfAttention(nn.Module):
|
||||
self.norm_k = _LingBotRMSNorm(self.tp_inner_dim, eps)
|
||||
self.rotary_embedding = RotaryEmbeddingWan(is_neox_style=False, half_head_dim=True)
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_local_heads,
|
||||
num_heads=self.num_sp_heads,
|
||||
head_size=self.head_dim,
|
||||
num_kv_heads=self.num_local_heads,
|
||||
num_kv_heads=self.num_sp_heads,
|
||||
softmax_scale=self.head_dim**-0.5,
|
||||
causal=False,
|
||||
role="self",
|
||||
@@ -287,6 +328,11 @@ class LingBotSelfAttention(nn.Module):
|
||||
query = self.rotary_embedding(query, cos, sin)
|
||||
key = self.rotary_embedding(key, cos, sin)
|
||||
|
||||
if self.ulysses_world_size > 1:
|
||||
query = SeqAllToAll4D.apply(self.ulysses_group, query, 2, 1, False)
|
||||
key = SeqAllToAll4D.apply(self.ulysses_group, key, 2, 1, False)
|
||||
value = SeqAllToAll4D.apply(self.ulysses_group, value, 2, 1, False)
|
||||
|
||||
if isinstance(cache, ARDiffusionPagedLayerInputs):
|
||||
if query.shape[0] != 1:
|
||||
raise RuntimeError("LingBot AR-Diffusion paged attention requires batch_size=1.")
|
||||
@@ -347,6 +393,8 @@ class LingBotSelfAttention(nn.Module):
|
||||
)
|
||||
else:
|
||||
output = self.attn(query, visible_key, visible_value)
|
||||
if self.ulysses_world_size > 1:
|
||||
output = SeqAllToAll4D.apply(self.ulysses_group, output, 1, 2, False)
|
||||
return self.o(output.flatten(2, 3))
|
||||
|
||||
|
||||
@@ -373,6 +421,13 @@ class LingBotCrossAttention(nn.Module):
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = dim // num_heads
|
||||
self.num_local_heads = num_heads // tp_size
|
||||
self.ulysses_world_size, self.ulysses_rank, self.ulysses_group = _ulysses_state()
|
||||
if self.num_local_heads % self.ulysses_world_size:
|
||||
raise ValueError(
|
||||
"LingBot local cross-attention heads must be divisible by the Ulysses degree: "
|
||||
f"heads={self.num_local_heads}, ulysses={self.ulysses_world_size}."
|
||||
)
|
||||
self.num_sp_heads = self.num_local_heads // self.ulysses_world_size
|
||||
self.tp_inner_dim = self.num_local_heads * self.head_dim
|
||||
|
||||
self.q = ColumnParallelLinear(
|
||||
@@ -410,9 +465,9 @@ class LingBotCrossAttention(nn.Module):
|
||||
self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps)
|
||||
self.norm_k = _LingBotRMSNorm(self.tp_inner_dim, eps)
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_local_heads,
|
||||
num_heads=self.num_sp_heads,
|
||||
head_size=self.head_dim,
|
||||
num_kv_heads=self.num_local_heads,
|
||||
num_kv_heads=self.num_sp_heads,
|
||||
softmax_scale=self.head_dim**-0.5,
|
||||
causal=False,
|
||||
role="cross",
|
||||
@@ -422,6 +477,14 @@ class LingBotCrossAttention(nn.Module):
|
||||
disable_kv_quant=True,
|
||||
)
|
||||
|
||||
def shard_kv_heads(self, value: torch.Tensor) -> torch.Tensor:
|
||||
"""Select this Ulysses rank's projected K/V heads."""
|
||||
|
||||
if self.ulysses_world_size == 1:
|
||||
return value
|
||||
start = self.ulysses_rank * self.num_sp_heads
|
||||
return value[:, :, start : start + self.num_sp_heads].clone(memory_format=torch.contiguous_format)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -431,6 +494,8 @@ class LingBotCrossAttention(nn.Module):
|
||||
) -> tuple[torch.Tensor, LingBotAttentionCache]:
|
||||
query = self.norm_q(self.q(hidden_states))
|
||||
query = query.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
if self.ulysses_world_size > 1:
|
||||
query = SeqAllToAll4D.apply(self.ulysses_group, query, 2, 1, False)
|
||||
|
||||
# Text K/V is constant within a request and is projected once per layer.
|
||||
if cache is None:
|
||||
@@ -438,8 +503,8 @@ class LingBotCrossAttention(nn.Module):
|
||||
raise ValueError("encoder_hidden_states are required when the cross-attention cache is empty.")
|
||||
key = self.norm_k(self.k(encoder_hidden_states))
|
||||
value = self.v(encoder_hidden_states)
|
||||
key = key.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
value = value.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
key = self.shard_kv_heads(key.unflatten(2, (self.num_local_heads, self.head_dim)))
|
||||
value = self.shard_kv_heads(value.unflatten(2, (self.num_local_heads, self.head_dim)))
|
||||
cache = LingBotAttentionCache(
|
||||
key=key,
|
||||
value=value,
|
||||
@@ -452,6 +517,8 @@ class LingBotCrossAttention(nn.Module):
|
||||
value = cache.value[:, : cache.end]
|
||||
|
||||
output = self.attn(query, key, value)
|
||||
if self.ulysses_world_size > 1:
|
||||
output = SeqAllToAll4D.apply(self.ulysses_group, output, 1, 2, False)
|
||||
return self.o(output.flatten(2, 3)), cache
|
||||
|
||||
|
||||
@@ -540,7 +607,18 @@ class LingBotAttentionBlock(nn.Module):
|
||||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
) -> tuple[torch.Tensor, LingBotAttentionCache]:
|
||||
batch_size, token_count, dim = hidden_states.shape
|
||||
if (
|
||||
timestep_projection.ndim != 4
|
||||
or timestep_projection.shape[0] != batch_size
|
||||
or timestep_projection.shape[2:] != (6, dim)
|
||||
):
|
||||
raise ValueError("LingBot timestep projection must have shape (batch, frames or tokens, 6, dim).")
|
||||
num_frames = timestep_projection.shape[1]
|
||||
if num_frames == 0 or token_count % num_frames:
|
||||
raise ValueError("LingBot timestep projection must align with hidden tokens.")
|
||||
if self.self_attn.ulysses_world_size > 1 and num_frames != token_count:
|
||||
raise ValueError("LingBot Ulysses requires one timestep projection per local token.")
|
||||
# Without SP, broadcast per frame. With SP, each entry represents one token.
|
||||
tokens_per_frame = token_count // num_frames
|
||||
# Timestep, camera, and text remain separate conditioning paths.
|
||||
modulation = self.modulation.unsqueeze(1) + timestep_projection.float()
|
||||
@@ -635,13 +713,26 @@ class _LingBotHead(nn.Module):
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
timestep_embedding: torch.Tensor,
|
||||
*,
|
||||
tokens_per_frame: int | None = None,
|
||||
token_offset: int = 0,
|
||||
) -> torch.Tensor:
|
||||
num_frames = timestep_embedding.shape[1]
|
||||
tokens_per_frame = hidden_states.shape[1] // num_frames
|
||||
modulation = self.modulation.unsqueeze(1) + timestep_embedding.unsqueeze(2).float()
|
||||
shift, scale = modulation.chunk(2, dim=2)
|
||||
normalized = self.norm(hidden_states.float()).unflatten(1, (num_frames, tokens_per_frame))
|
||||
normalized = (normalized * (1 + scale) + shift).flatten(1, 2).to(hidden_states.dtype)
|
||||
normalized = self.norm(hidden_states.float())
|
||||
if tokens_per_frame is None:
|
||||
normalized = normalized.unflatten(1, (num_frames, -1))
|
||||
normalized = (normalized * (1 + scale) + shift).flatten(1, 2)
|
||||
else:
|
||||
# A sequence shard can begin/end inside a frame.
|
||||
frames = (
|
||||
torch.arange(hidden_states.shape[1], device=hidden_states.device) + token_offset
|
||||
) // tokens_per_frame
|
||||
scale = scale.squeeze(2).index_select(1, frames)
|
||||
shift = shift.squeeze(2).index_select(1, frames)
|
||||
normalized = normalized * (1 + scale) + shift
|
||||
normalized = normalized.to(hidden_states.dtype)
|
||||
return self.head(normalized)
|
||||
|
||||
|
||||
@@ -678,6 +769,16 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
_repeated_blocks = ["LingBotAttentionBlock"]
|
||||
packed_modules_mapping = {"qkv": ["q", "k", "v"]}
|
||||
_layerwise_offload_blocks_attrs = ["blocks"]
|
||||
_sp_plan = {
|
||||
"sp_prepare": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=3, split_output=True),
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=3, split_output=True),
|
||||
2: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True),
|
||||
3: SequenceParallelInput(split_dim=0, expected_dims=2, split_output=True),
|
||||
4: SequenceParallelInput(split_dim=0, expected_dims=2, split_output=True),
|
||||
},
|
||||
"sp_output_gather": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -768,6 +869,8 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
stride=patch_size,
|
||||
)
|
||||
self.patch_embedding_wancamctrl = _LingBotCameraPatchEmbedding(6 * 8 * 8, dim, patch_size)
|
||||
self.sp_prepare = _LingBotSPPrepare()
|
||||
self.sp_output_gather = nn.Identity()
|
||||
self.c2ws_hidden_states_layer1 = ColumnParallelLinear(
|
||||
dim,
|
||||
dim,
|
||||
@@ -904,8 +1007,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
self.config.local_attn_size if self.config.local_attn_size != -1 else self.config.sliding_window_num_frames
|
||||
)
|
||||
max_tokens = int(window_frames * post_patch_height * post_patch_width)
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
num_local_heads = self.config.num_attention_heads // tp_size
|
||||
num_local_heads = self.blocks[0].self_attn.num_sp_heads
|
||||
return allocate_lingbot_cache(
|
||||
batch_size=batch_size,
|
||||
num_layers=self.config.num_layers,
|
||||
@@ -1138,6 +1240,33 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
query_len=hidden_states.shape[1],
|
||||
)
|
||||
cache.self_attention = [layer_context.to_layer_inputs() for layer_context in cache.self_attention]
|
||||
sp_size = self.blocks[0].self_attn.ulysses_world_size
|
||||
global_tokens = patched_frames * tokens_per_frame
|
||||
hidden_dim = hidden_states.shape[-1]
|
||||
rope_dim = rotary_emb[0].shape[-1]
|
||||
if global_tokens % sp_size:
|
||||
raise ValueError("LingBot Ulysses requires the token count to be divisible by its degree.")
|
||||
hidden_states, camera_hidden_states, timestep_projection, cosine, sine = self.sp_prepare(
|
||||
hidden_states,
|
||||
camera_hidden_states,
|
||||
timestep_projection,
|
||||
rotary_emb,
|
||||
)
|
||||
if sp_size > 1:
|
||||
local_tokens = global_tokens // sp_size
|
||||
expected_hidden = (batch_size, local_tokens, hidden_dim)
|
||||
if (
|
||||
hidden_states.shape != expected_hidden
|
||||
or camera_hidden_states.shape != expected_hidden
|
||||
or timestep_projection.shape != (batch_size, local_tokens, 6, hidden_dim)
|
||||
or cosine.shape != (local_tokens, rope_dim)
|
||||
or sine.shape != (local_tokens, rope_dim)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"LingBot SP input hooks did not shard all conditioning tensors consistently; "
|
||||
f"expected {local_tokens} tokens per rank. Check SP hook registration and input dimensions."
|
||||
)
|
||||
rotary_emb = (cosine, sine)
|
||||
# Phase 3: each layer receives its own cache entry. Text K/V is passed
|
||||
# only when absent; the returned cache is stored for subsequent DMD
|
||||
# steps and causal blocks in this request.
|
||||
@@ -1156,9 +1285,17 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
)
|
||||
cache.cross_attention[index] = cross_cache
|
||||
|
||||
# Phase 4: map tokens to per-patch 16-channel flow values and restore
|
||||
# [B, C, F, H, W] for the Pipeline's sampler update.
|
||||
hidden_states = self.head(hidden_states, timestep_embedding)
|
||||
# Phase 4: project local tokens before gathering the much narrower flow values.
|
||||
hidden_states = self.head(
|
||||
hidden_states,
|
||||
timestep_embedding,
|
||||
tokens_per_frame=tokens_per_frame if sp_size > 1 else None,
|
||||
token_offset=get_sp_group().ulysses_rank * (global_tokens // sp_size) if sp_size > 1 else 0,
|
||||
)
|
||||
hidden_states = self.sp_output_gather(hidden_states)
|
||||
projected_dim = self.config.out_channels * math.prod(self.config.patch_size)
|
||||
if sp_size > 1 and hidden_states.shape != (batch_size, global_tokens, projected_dim):
|
||||
raise RuntimeError("LingBot SP output hook did not gather the full projected token sequence.")
|
||||
return self._unpatchify(
|
||||
hidden_states,
|
||||
batch_size=batch_size,
|
||||
|
||||
Reference in new issue
Block a user