[Model] Enable online FP8 linears for LingBot World (#7549)

Signed-off-by: tsinghua-code <zr-liu25@mails.tsinghua.edu.cn>
This commit is contained in:
tsinghua-code authored and GitHub committed 2026-09-21 12:21:46 +00:00
1 parent 5e28163ba5
commit e5c0afc994
7 files changed
+129 -24

No files matched your search

+42 -1
View File
@@ -213,6 +213,47 @@ accepts:
`s`, `d`, `i`, `j`, `k`, and `l`; `s`, `d`, `i`, `j`, `k`, and `l`;
- `lingbot.camera_trajectory.v1` for explicit pose/intrinsics trajectories. - `lingbot.camera_trajectory.v1` for explicit pose/intrinsics trajectories.
## Online FP8 linear layers (experimental)
Pass `quantization_config={"method": "fp8"}` to the existing `AsyncOmni`
construction to use vLLM's online FP8 linear implementation. On the tested
Hopper-class GPU this dispatches to CUTLASS FP8 GEMM with online per-tensor
activation scaling; no LingBot-specific quantization kernel is introduced.
Only the transformer's existing vLLM parallel linear layers are eligible:
self-attention, cross-attention, FFN, camera injectors, and C2WS projections.
The ordinary PyTorch linear layers (including the output head and
time/text embeddings), normalization, convolutions, VAE, text encoder, and
AR KV cache are not converted to FP8 by this option.
Quality-sensitive projections can remain BF16 through fully qualified
`ignored_layers` names. For example, the following configuration tests
retaining the camera/C2WS path in BF16:
```python
quantization_config = {
"method": "fp8",
"ignored_layers": [
f"transformer.blocks.{i}.cam_injector_layer{j}"
for i in range(40)
for j in (1, 2)
]
+ [
"transformer.c2ws_hidden_states_layer1",
"transformer.c2ws_hidden_states_layer2",
],
}
```
Use `transformer.blocks.<i>.self_attn.qkv` to exclude the fused Q/K/V
projection as a unit. The example exclusion list is an ablation, not a
universal quality guarantee or a default. Validate the intended scene, seed,
camera trajectory, and session length against BF16 before selecting a policy.
An FP8 kernel speedup alone is not an end-to-end performance claim.
The long-session E8 quality gate in
[#7074](https://github.com/vllm-project/vllm-omni/issues/7074) remains required
for Tier-2 acceptance.
## Validation ## Validation
Real-checkpoint validation uses 480x832 output, four DMD steps, and seed 42. Real-checkpoint validation uses 480x832 output, four DMD steps, and seed 42.
@@ -246,7 +287,7 @@ tested commit.
AR blocks in one request. `max_num_seqs` must be one in both cases. AR blocks in one request. `max_num_seqs` must be one in both cases.
- Stateful streaming VAE decode is not implemented; the realtime example emits - Stateful streaming VAE decode is not implemented; the realtime example emits
latent chunks. latent chunks.
- SP/USP, pipeline/CFG parallelism, HSDP, VAE parallelism, quantization, - SP/USP, pipeline/CFG parallelism, HSDP, VAE parallelism, quantization methods other than online FP8,
Cache-DiT, TeaCache, causal-pretrain, and the 1.3B checkpoint are not claimed. Cache-DiT, TeaCache, causal-pretrain, and the 1.3B checkpoint are not claimed.
- No AMD GPU, Ascend NPU, or Intel GPU support is claimed. - No AMD GPU, Ascend NPU, or Intel GPU support is claimed.
@@ -134,6 +134,8 @@ def _install_vllm_stubs() -> None:
**kwargs, **kwargs,
) -> None: ) -> None:
super().__init__() super().__init__()
self.quant_config = kwargs.pop("quant_config", None)
self.prefix = kwargs.pop("prefix", "")
del kwargs del kwargs
self.return_bias = return_bias self.return_bias = return_bias
self.weight = nn.Parameter(torch.empty(output_size, input_size)) self.weight = nn.Parameter(torch.empty(output_size, input_size))
@@ -164,6 +166,8 @@ def _install_vllm_stubs() -> None:
**kwargs, **kwargs,
) -> None: ) -> None:
super().__init__() super().__init__()
self.quant_config = kwargs.pop("quant_config", None)
self.prefix = kwargs.pop("prefix", "")
del kwargs del kwargs
self.num_heads = total_num_heads self.num_heads = total_num_heads
self.num_kv_heads = total_num_kv_heads or total_num_heads self.num_kv_heads = total_num_kv_heads or total_num_heads
@@ -35,6 +35,8 @@ def _tiny_model(
num_layers: int = 2, num_layers: int = 2,
num_frames_per_block: int = 1, num_frames_per_block: int = 1,
sliding_window_num_frames: int = 3, sliding_window_num_frames: int = 3,
quant_config=None,
prefix: str = "",
attention_head_dim: int = 2, attention_head_dim: int = 2,
rope_max_seq_len: int = 16, rope_max_seq_len: int = 16,
): ):
@@ -55,6 +57,8 @@ def _tiny_model(
num_frames_per_block=num_frames_per_block, num_frames_per_block=num_frames_per_block,
sliding_window_num_frames=sliding_window_num_frames, sliding_window_num_frames=sliding_window_num_frames,
local_attn_size=-1, local_attn_size=-1,
quant_config=quant_config,
prefix=prefix,
) )
@@ -485,11 +489,52 @@ def test_constructor_rejects_non_null_image_embedding_fields(field: str) -> None
@pytest.mark.cpu @pytest.mark.cpu
def test_constructor_rejects_unsupported_quantization_with_runtime_error() -> None: @pytest.mark.parametrize("prefix", ["", "transformer"])
@pytest.mark.parametrize("quantized", [False, True])
def test_constructor_propagates_quantization_to_parallel_linears(prefix: str, quantized: bool) -> None:
module = attention_tests._load_module() module = attention_tests._load_module()
quant_config = object() if quantized else None
model = _tiny_model(module, num_layers=1, quant_config=quant_config, prefix=prefix)
block = model.blocks[0]
quantized_linears = [
model.c2ws_hidden_states_layer1,
model.c2ws_hidden_states_layer2,
block.self_attn.qkv,
block.self_attn.o,
block.cross_attn.q,
block.cross_attn.k,
block.cross_attn.v,
block.cross_attn.o,
block.ffn[0],
block.ffn[2],
block.cam_injector_layer1,
block.cam_injector_layer2,
]
with pytest.raises(RuntimeError, match="quant_config.*not supported"): assert len(quantized_linears) == 12
module.CausalLingBotWorldTransformer3DModel(quant_config=object()) assert all(layer.quant_config is quant_config for layer in quantized_linears)
expected_names = {
"c2ws_hidden_states_layer1",
"c2ws_hidden_states_layer2",
*(
f"blocks.0.{name}"
for name in (
"self_attn.qkv",
"self_attn.o",
"cross_attn.q",
"cross_attn.k",
"cross_attn.v",
"cross_attn.o",
"ffn.0",
"ffn.2",
"cam_injector_layer1",
"cam_injector_layer2",
)
),
}
assert {layer.prefix for layer in quantized_linears} == {
f"{prefix}.{name}" if prefix else name for name in expected_names
}
@pytest.mark.cpu @pytest.mark.cpu
@@ -889,13 +889,14 @@ def test_unsupported_sp_config_fails_before_component_loading(overrides):
assert module._loader_state.prefetch_calls == [] assert module._loader_state.prefetch_calls == []
def test_unsupported_quantization_fails_before_component_loading() -> None: def test_quantization_reaches_transformer_factory() -> None:
module = _load_pipeline_module() module = _load_pipeline_module()
quant_config = object()
with pytest.raises(NotImplementedError, match="quantization"): pipeline = module.LingBotWorldCausalDMDPipeline(od_config=_od_config(quantization_config=quant_config))
module.LingBotWorldCausalDMDPipeline(od_config=_od_config(quantization_config=object()))
assert module._loader_state.prefetch_calls == [] assert pipeline.transformer is not None
assert _FakeTransformerFactory.last_call[1:] == (quant_config, "transformer")
def test_official_scheduler_config_matches_fixed_dmd_contract() -> None: def test_official_scheduler_config_matches_fixed_dmd_contract() -> None:
+6 -6
View File
@@ -563,19 +563,19 @@ def test_from_cli_args_preserves_text_encoder_tp_size():
def test_text_encoder_tp_size_reaches_default_diffusion_parallel_config(): def test_text_encoder_tp_size_reaches_default_diffusion_parallel_config():
"""The preserved CLI value must land in DiffusionParallelConfig. """The preserved CLI value must land in DiffusionParallelConfig.
Mirrors the library flow: from_cli_args -> kwargs -> the generic Forward the preserved explicit override to the generic diffusion
diffusion fallback, which resolves the field through fallback, which resolves it through
``DiffusionParallelConfig.from_stage_overrides``. ``DiffusionParallelConfig.from_stage_overrides``. Serializing all engine
defaults would also forward unrelated LLM-only fields to strict diffusion
ingress, unlike the explicit-kwargs library entrypoint.
""" """
from dataclasses import asdict
from vllm_omni.config.config_factory import StageConfigFactory from vllm_omni.config.config_factory import StageConfigFactory
engine_args = OmniEngineArgs.from_cli_args( engine_args = OmniEngineArgs.from_cli_args(
SimpleNamespace(text_encoder_tp_size=2), SimpleNamespace(text_encoder_tp_size=2),
) )
stage_cfg = StageConfigFactory.create_default_diffusion( stage_cfg = StageConfigFactory.create_default_diffusion(
{k: v for k, v in asdict(engine_args).items() if v is not None}, {"text_encoder_tp_size": engine_args.text_encoder_tp_size},
)[0] )[0]
parallel_config = stage_cfg["engine_args"]["parallel_config"] parallel_config = stage_cfg["engine_args"]["parallel_config"]
@@ -241,8 +241,6 @@ def _validate_scheduler_config(config: dict[str, Any]) -> None:
def _validate_parallel_config(od_config: OmniDiffusionConfig) -> None: def _validate_parallel_config(od_config: OmniDiffusionConfig) -> None:
if getattr(od_config, "quantization_config", None) is not None:
raise NotImplementedError("LingBot World v1 does not support quantization.")
parallel_config = getattr(od_config, "parallel_config", None) parallel_config = getattr(od_config, "parallel_config", None)
if parallel_config is None: if parallel_config is None:
return return
@@ -8,7 +8,7 @@ import math
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Self, cast from typing import TYPE_CHECKING, Any, Self, cast
import torch import torch
import torch.nn as nn import torch.nn as nn
@@ -36,6 +36,9 @@ from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import (
paged_write_attn, paged_write_attn,
) )
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
@dataclass @dataclass
class LingBotAttentionCache: class LingBotAttentionCache:
@@ -170,6 +173,7 @@ class LingBotSelfAttention(nn.Module):
num_heads: int, num_heads: int,
*, *,
eps: float = 1e-6, eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
prefix: str = "", prefix: str = "",
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -188,6 +192,7 @@ class LingBotSelfAttention(nn.Module):
head_size=self.head_dim, head_size=self.head_dim,
total_num_heads=num_heads, total_num_heads=num_heads,
bias=True, bias=True,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "qkv"), prefix=_projection_prefix(prefix, "qkv"),
) )
self.num_local_heads = self.qkv.num_heads self.num_local_heads = self.qkv.num_heads
@@ -205,6 +210,7 @@ class LingBotSelfAttention(nn.Module):
bias=True, bias=True,
input_is_parallel=True, input_is_parallel=True,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "o"), prefix=_projection_prefix(prefix, "o"),
) )
self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps) self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps)
@@ -416,6 +422,7 @@ class LingBotCrossAttention(nn.Module):
num_heads: int, num_heads: int,
*, *,
eps: float = 1e-6, eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
prefix: str = "", prefix: str = "",
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -443,6 +450,7 @@ class LingBotCrossAttention(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "q"), prefix=_projection_prefix(prefix, "q"),
) )
self.k = ColumnParallelLinear( self.k = ColumnParallelLinear(
@@ -451,6 +459,7 @@ class LingBotCrossAttention(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "k"), prefix=_projection_prefix(prefix, "k"),
) )
self.v = ColumnParallelLinear( self.v = ColumnParallelLinear(
@@ -459,6 +468,7 @@ class LingBotCrossAttention(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "v"), prefix=_projection_prefix(prefix, "v"),
) )
self.o = RowParallelLinear( self.o = RowParallelLinear(
@@ -467,6 +477,7 @@ class LingBotCrossAttention(nn.Module):
bias=True, bias=True,
input_is_parallel=True, input_is_parallel=True,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "o"), prefix=_projection_prefix(prefix, "o"),
) )
self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps) self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps)
@@ -528,6 +539,7 @@ class LingBotAttentionBlock(nn.Module):
ffn_dim: int | None = None, ffn_dim: int | None = None,
cross_attn_norm: bool = True, cross_attn_norm: bool = True,
eps: float = 1e-6, eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
prefix: str = "", prefix: str = "",
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -538,12 +550,14 @@ class LingBotAttentionBlock(nn.Module):
dim, dim,
num_heads, num_heads,
eps=eps, eps=eps,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "self_attn"), prefix=_projection_prefix(prefix, "self_attn"),
) )
self.cross_attn = LingBotCrossAttention( self.cross_attn = LingBotCrossAttention(
dim, dim,
num_heads, num_heads,
eps=eps, eps=eps,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "cross_attn"), prefix=_projection_prefix(prefix, "cross_attn"),
) )
self.norm2 = LayerNorm(dim, eps=eps, elementwise_affine=False) self.norm2 = LayerNorm(dim, eps=eps, elementwise_affine=False)
@@ -555,6 +569,7 @@ class LingBotAttentionBlock(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "ffn.0"), prefix=_projection_prefix(prefix, "ffn.0"),
), ),
nn.GELU(approximate="tanh"), nn.GELU(approximate="tanh"),
@@ -564,6 +579,7 @@ class LingBotAttentionBlock(nn.Module):
bias=True, bias=True,
input_is_parallel=True, input_is_parallel=True,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "ffn.2"), prefix=_projection_prefix(prefix, "ffn.2"),
), ),
) )
@@ -574,6 +590,7 @@ class LingBotAttentionBlock(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "cam_injector_layer1"), prefix=_projection_prefix(prefix, "cam_injector_layer1"),
) )
self.cam_injector_layer2 = RowParallelLinear( self.cam_injector_layer2 = RowParallelLinear(
@@ -582,6 +599,7 @@ class LingBotAttentionBlock(nn.Module):
bias=True, bias=True,
input_is_parallel=True, input_is_parallel=True,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "cam_injector_layer2"), prefix=_projection_prefix(prefix, "cam_injector_layer2"),
) )
self.cam_scale_layer = nn.Linear(dim, dim) self.cam_scale_layer = nn.Linear(dim, dim)
@@ -823,7 +841,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
num_frames_per_block: int = 3, num_frames_per_block: int = 3,
sliding_window_num_frames: int = 18, sliding_window_num_frames: int = 18,
local_attn_size: int = -1, local_attn_size: int = -1,
quant_config: object | None = None, quant_config: QuantizationConfig | None = None,
prefix: str = "", prefix: str = "",
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -853,11 +871,6 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
): ):
if field_value is not None: if field_value is not None:
raise ValueError(f"{field_name} must be None because LingBot World v2 has no image embedding path.") raise ValueError(f"{field_name} must be None because LingBot World v2 has no image embedding path.")
if quant_config is not None:
raise RuntimeError(
"quant_config is not supported by the LingBot World transformer; construct the unquantized model."
)
dim = num_attention_heads * attention_head_dim dim = num_attention_heads * attention_head_dim
self.dim = dim self.dim = dim
self.config = SimpleNamespace( self.config = SimpleNamespace(
@@ -898,6 +911,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
bias=True, bias=True,
gather_output=False, gather_output=False,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "c2ws_hidden_states_layer1"), prefix=_projection_prefix(prefix, "c2ws_hidden_states_layer1"),
) )
self.c2ws_hidden_states_layer2 = RowParallelLinear( self.c2ws_hidden_states_layer2 = RowParallelLinear(
@@ -906,6 +920,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
bias=True, bias=True,
input_is_parallel=True, input_is_parallel=True,
return_bias=False, return_bias=False,
quant_config=quant_config,
prefix=_projection_prefix(prefix, "c2ws_hidden_states_layer2"), prefix=_projection_prefix(prefix, "c2ws_hidden_states_layer2"),
) )
self.text_embedding = nn.Sequential( self.text_embedding = nn.Sequential(
@@ -930,6 +945,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
ffn_dim=ffn_dim, ffn_dim=ffn_dim,
cross_attn_norm=cross_attn_norm, cross_attn_norm=cross_attn_norm,
eps=eps, eps=eps,
quant_config=quant_config,
prefix=_projection_prefix(prefix, f"blocks.{index}"), prefix=_projection_prefix(prefix, f"blocks.{index}"),
) )
for index in range(num_layers) for index in range(num_layers)
@@ -976,7 +992,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
cls, cls,
config: dict[str, Any], config: dict[str, Any],
*, *,
quant_config: Any | None = None, quant_config: QuantizationConfig | None = None,
prefix: str = "", prefix: str = "",
) -> Self: ) -> Self:
checkpoint_contract = { checkpoint_contract = {