[diffusion] optimization: support cuda graph for Pi-0.5 prefix encoding (#34256)
This commit is contained in:
@@ -46,6 +46,8 @@ class Pi05PipelineConfig(PipelineConfig):
|
||||
image_normalization_std: tuple[float, float, float] = (0.5, 0.5, 0.5)
|
||||
|
||||
enable_global_prefix_cache: bool = False
|
||||
enable_prefix_cuda_graph: bool = True
|
||||
prefix_cuda_graph_max_entries: int = 1
|
||||
enable_action_cuda_graph: bool = True
|
||||
prefix_cache_max_entries: int = 1
|
||||
prefix_cache_layout_version: str = "pi05-prefix-v1"
|
||||
|
||||
@@ -1327,25 +1327,21 @@ class Pi05CoreModel(nn.Module):
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
embs = []
|
||||
pad_masks = []
|
||||
att_masks = []
|
||||
image_embs = self.paligemma_with_expert.embed_images(images)
|
||||
for image_emb, image_mask in zip(image_embs, image_masks, strict=True):
|
||||
batch_size, num_image_embs = image_emb.shape[:2]
|
||||
embs.append(image_emb)
|
||||
pad_masks.append(image_mask[:, None].expand(batch_size, num_image_embs))
|
||||
att_masks += [0] * num_image_embs
|
||||
|
||||
lang_emb = self.paligemma_with_expert.embed_language_tokens(tokens)
|
||||
# Match OpenPI's Pi0.5 prefix embedding semantics.
|
||||
lang_emb = lang_emb * math.sqrt(lang_emb.shape[-1])
|
||||
embs.append(lang_emb)
|
||||
pad_masks.append(token_masks)
|
||||
att_masks += [0] * lang_emb.shape[1]
|
||||
|
||||
embs = torch.cat(embs, dim=1)
|
||||
pad_masks = torch.cat(pad_masks, dim=1)
|
||||
att_masks_t = torch.tensor(att_masks, dtype=torch.bool, device=pad_masks.device)
|
||||
att_masks_t = att_masks_t[None, :].expand(pad_masks.shape[0], len(att_masks))
|
||||
att_masks_t = torch.zeros_like(pad_masks)
|
||||
return embs, pad_masks, att_masks_t
|
||||
|
||||
def embed_suffix(
|
||||
|
||||
@@ -38,9 +38,11 @@ from sglang.multimodal_gen.runtime.models.vlas.pi05_core import Pi05CoreModel
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import maybe_download_model
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.vla.denoise_cuda_graph import (
|
||||
from sglang.multimodal_gen.runtime.vla.cuda_graph import (
|
||||
VLADenoiseGraphRunner,
|
||||
VLADenoiseGraphSignature,
|
||||
VLAPrefixGraphRunner,
|
||||
VLAPrefixGraphSignature,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.vla.observation import (
|
||||
VLAObservationBatch,
|
||||
@@ -161,6 +163,10 @@ class Pi05PolicyModel(nn.Module):
|
||||
if self.runtime_role != "all":
|
||||
logger.info("Pi05 split runtime role on rank: %s", self.runtime_role)
|
||||
self.action_expert = Pi05ActionExpert(config, self.core_model)
|
||||
self.prefix_graph_runner = VLAPrefixGraphRunner(
|
||||
enabled=self._prefix_cuda_graph_enabled(),
|
||||
max_entries=config.prefix_cuda_graph_max_entries,
|
||||
)
|
||||
self.graph_runner = VLADenoiseGraphRunner(
|
||||
enabled=config.enable_action_cuda_graph
|
||||
)
|
||||
@@ -176,6 +182,25 @@ class Pi05PolicyModel(nn.Module):
|
||||
return False
|
||||
return get_tp_world_size() > 1
|
||||
|
||||
def _prefix_cuda_graph_enabled(self) -> bool:
|
||||
if (
|
||||
not self.config.enable_prefix_cuda_graph
|
||||
or self.device.type != "cuda"
|
||||
or self.runtime_role not in ("all", "prefix")
|
||||
or self._prefix_tensor_parallel_enabled()
|
||||
):
|
||||
return False
|
||||
return not any(
|
||||
(
|
||||
self.config.offload_prefix_image_encoder,
|
||||
self.config.offload_prefix_image_encoder_after_embed,
|
||||
self.config.offload_prefix_token_embedding,
|
||||
self.config.offload_prefix_language_layers,
|
||||
self.config.offload_prefix_language_layers_after_prefix,
|
||||
self.config.empty_cache_after_prefix,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _to_empty_preserve_buffers(module: nn.Module, *, device: torch.device) -> None:
|
||||
buffers = {
|
||||
@@ -796,6 +821,10 @@ class Pi05PolicyModel(nn.Module):
|
||||
return None
|
||||
return paligemma.model.language_model
|
||||
|
||||
def _prefix_tensor_parallel_enabled(self) -> bool:
|
||||
language_model = self._prefix_language_model()
|
||||
return language_model is not None and language_model.tensor_parallel
|
||||
|
||||
def _prefix_kv_requires_tp_gather(self) -> bool:
|
||||
language_model = self._prefix_language_model()
|
||||
if language_model is None or not language_model.tensor_parallel:
|
||||
@@ -822,7 +851,12 @@ class Pi05PolicyModel(nn.Module):
|
||||
)
|
||||
)
|
||||
|
||||
def encode_prefix(self, observation: VLAObservationBatch) -> PrefixContext:
|
||||
def encode_prefix(
|
||||
self,
|
||||
observation: VLAObservationBatch,
|
||||
*,
|
||||
use_cuda_graph: bool = True,
|
||||
) -> PrefixContext:
|
||||
camera_order = tuple(observation.metadata.get("camera_order", ()))
|
||||
images = [
|
||||
observation.images[name].to(self.device, dtype=torch.float32)
|
||||
@@ -844,22 +878,57 @@ class Pi05PolicyModel(nn.Module):
|
||||
prefix_full_attention_hint = all(
|
||||
bool(observation.image_masks[name].all().item()) for name in camera_order
|
||||
) and bool(token_masks_cpu.all().item())
|
||||
past_key_values, prefix_pad_masks, full_attention = (
|
||||
self.core_model.encode_prefix(
|
||||
images,
|
||||
image_masks,
|
||||
tokens,
|
||||
token_masks,
|
||||
prefix_full_attention_hint=prefix_full_attention_hint,
|
||||
tokens_trimmed=tokens_trimmed,
|
||||
|
||||
image_count = len(images)
|
||||
graph_inputs = tuple([*images, *image_masks, tokens, token_masks])
|
||||
|
||||
def encode(current_inputs: tuple[torch.Tensor, ...]) -> PrefixContext:
|
||||
current_images = list(current_inputs[:image_count])
|
||||
current_image_masks = list(current_inputs[image_count : 2 * image_count])
|
||||
current_tokens = current_inputs[-2]
|
||||
current_token_masks = current_inputs[-1]
|
||||
past_key_values, prefix_pad_masks, full_attention = (
|
||||
self.core_model.encode_prefix(
|
||||
current_images,
|
||||
current_image_masks,
|
||||
current_tokens,
|
||||
current_token_masks,
|
||||
prefix_full_attention_hint=prefix_full_attention_hint,
|
||||
tokens_trimmed=tokens_trimmed,
|
||||
)
|
||||
)
|
||||
past_key_values = self._materialize_prefix_kv_for_action(past_key_values)
|
||||
return PrefixContext(
|
||||
past_key_values=past_key_values,
|
||||
prefix_pad_masks=prefix_pad_masks,
|
||||
prefix_len=prefix_pad_masks.shape[1],
|
||||
layout={"full_attention": full_attention},
|
||||
)
|
||||
|
||||
if (
|
||||
not use_cuda_graph
|
||||
or not self.prefix_graph_runner.enabled
|
||||
or observation.batch_size != 1
|
||||
):
|
||||
return encode(graph_inputs)
|
||||
|
||||
signature = VLAPrefixGraphSignature(
|
||||
batch_size=observation.batch_size,
|
||||
input_shapes=tuple(tuple(tensor.shape) for tensor in graph_inputs),
|
||||
input_dtypes=tuple(
|
||||
str(tensor.dtype).replace("torch.", "") for tensor in graph_inputs
|
||||
),
|
||||
static_layout=(
|
||||
image_count,
|
||||
prefix_full_attention_hint,
|
||||
tokens_trimmed,
|
||||
),
|
||||
parallel_layout=self.config.parallel_layout_version,
|
||||
)
|
||||
past_key_values = self._materialize_prefix_kv_for_action(past_key_values)
|
||||
return PrefixContext(
|
||||
past_key_values=past_key_values,
|
||||
prefix_pad_masks=prefix_pad_masks,
|
||||
prefix_len=prefix_pad_masks.shape[1],
|
||||
layout={"full_attention": full_attention},
|
||||
return self.prefix_graph_runner.capture_or_run(
|
||||
signature,
|
||||
encode,
|
||||
graph_inputs,
|
||||
)
|
||||
|
||||
def sample_noise(
|
||||
|
||||
@@ -59,13 +59,15 @@ class Pi05Pipeline(ComposedPipelineBase):
|
||||
or bool(server_args.text_encoder_cpu_offload)
|
||||
)
|
||||
logger.info(
|
||||
"Pi05 memory config: prefix_cache=%s/%s, action_cuda_graph=%s, "
|
||||
"Pi05 memory config: prefix_cache=%s/%s, cuda_graph=%s/%s/%s, "
|
||||
"offload_image=%s, offload_image_after_embed=%s, "
|
||||
"offload_tokens=%s, offload_language_layers=%s, "
|
||||
"offload_language_after_prefix=%s/%s, "
|
||||
"offload_action_after_denoise=%s, empty_cache_after_prefix=%s",
|
||||
pipeline_config.enable_global_prefix_cache,
|
||||
pipeline_config.prefix_cache_max_entries,
|
||||
pipeline_config.enable_prefix_cuda_graph,
|
||||
pipeline_config.prefix_cuda_graph_max_entries,
|
||||
pipeline_config.enable_action_cuda_graph,
|
||||
pipeline_config.offload_prefix_image_encoder,
|
||||
pipeline_config.offload_prefix_image_encoder_after_embed,
|
||||
|
||||
@@ -89,6 +89,10 @@ def _effective_prefix_cache_enabled(
|
||||
)
|
||||
|
||||
|
||||
def _cuda_graph_enabled(batch: Req) -> bool:
|
||||
return bool(vla_options(batch).get("enable_cuda_graph", True))
|
||||
|
||||
|
||||
def _grouped_fingerprint(
|
||||
batch: Req,
|
||||
server_args: ServerArgs,
|
||||
@@ -122,6 +126,7 @@ def _grouped_fingerprint(
|
||||
batch.action_horizon,
|
||||
batch.action_dim,
|
||||
batch.num_inference_steps,
|
||||
_cuda_graph_enabled(batch),
|
||||
)
|
||||
|
||||
|
||||
@@ -180,7 +185,10 @@ class VLAPrefixEncodingStage(PipelineStage):
|
||||
vla_state(batch)["observation_batch"] for batch in group_batches
|
||||
]
|
||||
grouped_observation = collate_vla_observation_batches(observations)
|
||||
prefix_context = self.policy_model.encode_prefix(grouped_observation)
|
||||
prefix_context = self.policy_model.encode_prefix(
|
||||
grouped_observation,
|
||||
use_cuda_graph=_cuda_graph_enabled(group_batches[0]),
|
||||
)
|
||||
prefix_ms = (time.perf_counter() - prefix_start) * 1000
|
||||
|
||||
for offset, (index, batch) in enumerate(group):
|
||||
@@ -299,7 +307,10 @@ class VLAPrefixEncodingStage(PipelineStage):
|
||||
prefix_start = time.perf_counter()
|
||||
|
||||
# 3. run encoding
|
||||
prefix_context = self.policy_model.encode_prefix(observation)
|
||||
prefix_context = self.policy_model.encode_prefix(
|
||||
observation,
|
||||
use_cuda_graph=_cuda_graph_enabled(batch) and not cache_enabled,
|
||||
)
|
||||
if cache_key is not None:
|
||||
prefix_context.cache_key_digest = cache_key
|
||||
|
||||
@@ -371,7 +382,7 @@ class VLAActionDenoisingStage(PipelineStage):
|
||||
prefix_context,
|
||||
noise=observation.noise,
|
||||
num_steps=group_batches[0].num_inference_steps,
|
||||
use_cuda_graph=bool(options.get("enable_cuda_graph", True)),
|
||||
use_cuda_graph=_cuda_graph_enabled(group_batches[0]),
|
||||
generator=None,
|
||||
)
|
||||
synchronize_vla_action_tensor(actions)
|
||||
@@ -417,7 +428,7 @@ class VLAActionDenoisingStage(PipelineStage):
|
||||
prefix_context,
|
||||
noise=noise,
|
||||
num_steps=batch.num_inference_steps,
|
||||
use_cuda_graph=bool(options.get("enable_cuda_graph", True)),
|
||||
use_cuda_graph=_cuda_graph_enabled(batch),
|
||||
generator=batch.generator,
|
||||
)
|
||||
synchronize_vla_action_tensor(actions)
|
||||
|
||||
+118
-2
@@ -22,6 +22,22 @@ from sglang.srt.model_executor.runner_utils.pool import (
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VLAPrefixGraphSignature:
|
||||
batch_size: int
|
||||
input_shapes: tuple[tuple[int, ...], ...]
|
||||
input_dtypes: tuple[str, ...]
|
||||
static_layout: tuple[Any, ...]
|
||||
parallel_layout: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CapturedPrefixGraph:
|
||||
graph: torch.cuda.CUDAGraph
|
||||
static_inputs: tuple[torch.Tensor, ...]
|
||||
static_output: PrefixContext
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VLADenoiseGraphSignature:
|
||||
batch_size: int
|
||||
@@ -76,6 +92,103 @@ def _copy_prefix_context_(dst: PrefixContext, src: PrefixContext) -> None:
|
||||
dst.cache_key_digest = src.cache_key_digest
|
||||
|
||||
|
||||
class VLAPrefixGraphRunner:
|
||||
"""Full CUDA graph runner for VLA prefix encoding shape buckets."""
|
||||
|
||||
def __init__(self, enabled: bool = True, max_entries: int = 1):
|
||||
self.max_entries = max(0, max_entries)
|
||||
self.enabled = enabled and self.max_entries > 0
|
||||
self._captured: dict[VLAPrefixGraphSignature, _CapturedPrefixGraph] = {}
|
||||
self._disabled_signatures: set[VLAPrefixGraphSignature] = set()
|
||||
self._capture_stream: torch.cuda.Stream | None = None
|
||||
self._graph_pool: Any = None
|
||||
|
||||
def _capture(
|
||||
self,
|
||||
signature: VLAPrefixGraphSignature,
|
||||
step_fn: Callable[[tuple[torch.Tensor, ...]], PrefixContext],
|
||||
inputs: tuple[torch.Tensor, ...],
|
||||
) -> _CapturedPrefixGraph:
|
||||
static_inputs = tuple(tensor.detach().clone() for tensor in inputs)
|
||||
device_module = torch.get_device_module(inputs[0].device)
|
||||
if self._capture_stream is None:
|
||||
self._capture_stream = device_module.Stream(device=inputs[0].device)
|
||||
if self._graph_pool is None:
|
||||
self._graph_pool = get_or_create_global_graph_memory_pool(device_module)
|
||||
set_graph_pool_id(self._graph_pool)
|
||||
|
||||
device_module.synchronize()
|
||||
with device_module.stream(self._capture_stream), torch.inference_mode():
|
||||
step_fn(static_inputs)
|
||||
self._capture_stream.synchronize()
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with (
|
||||
device_module.graph(
|
||||
cuda_graph=graph,
|
||||
pool=self._graph_pool,
|
||||
stream=self._capture_stream,
|
||||
),
|
||||
torch.inference_mode(),
|
||||
):
|
||||
static_output = step_fn(static_inputs)
|
||||
self._capture_stream.synchronize()
|
||||
static_output.layout["mutable_graph_output"] = True
|
||||
|
||||
captured = _CapturedPrefixGraph(
|
||||
graph=graph,
|
||||
static_inputs=static_inputs,
|
||||
static_output=static_output,
|
||||
)
|
||||
self._captured[signature] = captured
|
||||
logger.info(
|
||||
"Captured VLA prefix CUDA graph: batch=%d inputs=%s",
|
||||
signature.batch_size,
|
||||
signature.input_shapes,
|
||||
)
|
||||
return captured
|
||||
|
||||
def capture_or_run(
|
||||
self,
|
||||
signature: VLAPrefixGraphSignature,
|
||||
step_fn: Callable[[tuple[torch.Tensor, ...]], PrefixContext],
|
||||
inputs: tuple[torch.Tensor, ...],
|
||||
) -> PrefixContext:
|
||||
if (
|
||||
not self.enabled
|
||||
or signature in self._disabled_signatures
|
||||
or not inputs
|
||||
or inputs[0].device.type != "cuda"
|
||||
):
|
||||
return step_fn(inputs)
|
||||
|
||||
captured = self._captured.get(signature)
|
||||
if captured is None and len(self._captured) >= self.max_entries:
|
||||
return step_fn(inputs)
|
||||
try:
|
||||
if captured is None:
|
||||
captured = self._capture(signature, step_fn, inputs)
|
||||
else:
|
||||
for static_input, current_input in zip(
|
||||
captured.static_inputs, inputs, strict=True
|
||||
):
|
||||
static_input.copy_(current_input)
|
||||
captured.graph.replay()
|
||||
torch.get_device_module(inputs[0].device).current_stream(
|
||||
device=inputs[0].device
|
||||
).synchronize()
|
||||
return captured.static_output
|
||||
except Exception:
|
||||
self._disabled_signatures.add(signature)
|
||||
self._captured.pop(signature, None)
|
||||
logger.warning(
|
||||
"VLA prefix CUDA graph disabled for signature %s",
|
||||
signature,
|
||||
exc_info=True,
|
||||
)
|
||||
return step_fn(inputs)
|
||||
|
||||
|
||||
class VLADenoiseGraphRunner:
|
||||
"""Full CUDA graph runner for one VLA action-denoise step.
|
||||
|
||||
@@ -97,13 +210,16 @@ class VLADenoiseGraphRunner:
|
||||
) -> None:
|
||||
context_id = id(prefix_context.past_key_values)
|
||||
context_digest = prefix_context.cache_key_digest
|
||||
if (
|
||||
mutable_graph_output = bool(
|
||||
prefix_context.layout.get("mutable_graph_output", False)
|
||||
)
|
||||
if not mutable_graph_output and (
|
||||
context_digest is not None
|
||||
and captured.current_context_digest == context_digest
|
||||
):
|
||||
captured.current_context_id = context_id
|
||||
return
|
||||
if captured.current_context_id == context_id:
|
||||
if not mutable_graph_output and captured.current_context_id == context_id:
|
||||
return
|
||||
_copy_prefix_context_(captured.static_prefix_context, prefix_context)
|
||||
captured.current_context_id = context_id
|
||||
@@ -21,7 +21,7 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.p
|
||||
_preprocess_image,
|
||||
_resize_with_pad_image_tensor,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.vla.denoise_cuda_graph import (
|
||||
from sglang.multimodal_gen.runtime.vla.cuda_graph import (
|
||||
VLADenoiseGraphRunner,
|
||||
_CapturedDenoiseGraph,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ def test_denoise_graph_skips_prefix_copy_for_same_digest(monkeypatch):
|
||||
raise AssertionError("PrefixContext should not be copied on digest hit")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"sglang.multimodal_gen.runtime.vla.denoise_cuda_graph._copy_prefix_context_",
|
||||
"sglang.multimodal_gen.runtime.vla.cuda_graph._copy_prefix_context_",
|
||||
fail_copy,
|
||||
)
|
||||
|
||||
@@ -83,6 +83,35 @@ def test_denoise_graph_skips_prefix_copy_for_same_digest(monkeypatch):
|
||||
assert captured.static_prefix_context.past_key_values[0][0].eq(1.0).all()
|
||||
|
||||
|
||||
def test_denoise_graph_copies_mutable_prefix_graph_output():
|
||||
runner = VLADenoiseGraphRunner(enabled=True)
|
||||
static_context = _prefix_context(1.0, None)
|
||||
captured = _CapturedDenoiseGraph(
|
||||
graph=object(),
|
||||
static_prefix_context=static_context,
|
||||
static_x_t=torch.empty(1, 2, 4),
|
||||
static_timestep=torch.empty(1),
|
||||
static_output=torch.empty(1, 2, 4),
|
||||
current_context_id=123,
|
||||
)
|
||||
current_context = _prefix_context(2.0, None)
|
||||
current_context.layout["mutable_graph_output"] = True
|
||||
|
||||
runner._sync_context_if_needed(captured, current_context)
|
||||
|
||||
assert captured.static_prefix_context.past_key_values[0][0].eq(2.0).all()
|
||||
|
||||
|
||||
def test_prefix_graph_rejects_tensor_parallel_prefix():
|
||||
model = Pi05PolicyModel.__new__(Pi05PolicyModel)
|
||||
model.config = Pi05PipelineConfig()
|
||||
model.device = torch.device("cuda")
|
||||
model.runtime_role = "all"
|
||||
model._prefix_language_model = lambda: SimpleNamespace(tensor_parallel=True)
|
||||
|
||||
assert not model._prefix_cuda_graph_enabled()
|
||||
|
||||
|
||||
def test_runai_direct_gpu_loader_does_not_reject_split_roles(monkeypatch):
|
||||
class FakeSafeOpen:
|
||||
def __enter__(self):
|
||||
|
||||
Reference in New Issue
Block a user