Add H3 inter node profiling

This commit is contained in:
Satyam Srivastava
2026-09-07 18:49:50 -07:00
parent 8d4e05acef
commit d05b59db37
5 changed files with 387 additions and 55 deletions
@@ -4,7 +4,7 @@ This runbook tests the component-disaggregated MiniMax H3 inference path:
- Spark A: text/multimodal encoder, video VAE, audio VAE, and the driver.
- Spark B: DiT denoising only.
- Ray transports CPU-contiguous conditioning and latent tensors between the two roles.
- Ray transports CUDA-contiguous conditioning and latent tensors between the two roles.
- Each role uses one GPU with tensor parallelism and sequence parallelism disabled.
- All role components remain resident until the generator shuts down.
@@ -510,6 +510,60 @@ For one generator lifetime:
- No request should release or reload the text encoder, DiT, or VAEs.
- Spark B should continue denoising without waiting for Spark A's VAE decode.
### Measure the two payload handoffs
Enable transfer profiling on the **driver before creating the generator**:
~~~bash
FASTVIDEO_H3_PROFILE_TRANSFERS=1 \
fastvideo generate --config /tmp/h3-disaggregated-smoke.yaml
~~~
The driver passes this setting to both actors, including when connecting to an
existing Ray cluster. It also applies to `iter_forward` and its async wrapper.
Look for `[RAY_TRANSFER]`, `[RAY_STAGE]`, and `[RAY_PAYLOAD]` in the worker logs.
Every line includes a `request_id` to correlate concurrent requests.
Each request logs an `A_TO_B` encoded-payload receipt on the DiT worker and a
`B_TO_A` denoised-payload receipt on the encoder/decoder worker:
| Field | What it measures |
|---|---|
| `source_wait_s` | Receiver wait until the producer's result is available somewhere in Ray. Includes any remaining producer work, serialization, and readiness notification. |
| `object_fetch_s` | Wait to make that ready object available in the receiver's local object store. Includes Ray transfer/control overhead and any object-store restore; a cached object can be nearly immediate. |
| `materialize_s` | Local `ray.get` deserialization and tensor reconstruction, including waiting for CUDA copies to complete. |
| `receive_s` | `object_fetch_s + materialize_s`, excluding the upstream wait. |
| `tensor_bytes` | Logical bytes in the conditioning/latent tensors and all five layout tensors. Excludes serialization metadata, backing-storage overhead, and network protocol bytes. |
| `object_fetch_mb_s` | Logical tensor MB divided by `object_fetch_s`; an effective payload rate, not measured NIC bandwidth. |
`[RAY_STAGE]` separately reports synchronized `elapsed_s` for `encode`,
`denoise`, and `decode`. Large `object_fetch_s` points toward the Ray data path;
large `materialize_s` points toward reconstruction/device-copy cost. Large
`source_wait_s` alone does not imply a slow network: compare it with the
producer's stage time. Actor queueing before a receiving method starts is not
part of these receiver timers. Sender serialization occurs after the producer's
stage timer ends and may overlap other work.
The timers use a monotonic clock on the receiving Spark, so they do not require
synchronized clocks between machines. Profiling passes a nested ObjectRef and
uses `ray.wait(fetch_local=False)`, then `ray.wait(fetch_local=True)`, then
`ray.get` to separate the three phases. See Ray's
[object-reference behavior](https://docs.ray.io/en/latest/ray-core/objects.html#passing-object-arguments)
and [wait semantics](https://docs.ray.io/en/latest/ray-core/api/doc/ray.wait.html).
This is an opt-in diagnostic: deferring the fetch until receiver entry changes
prefetch overlap, and CUDA synchronization adds overhead. Compare a short warmed
run with profiling disabled before drawing throughput conclusions. The default
execution keeps Ray's automatic argument fetching. These application timings do
not isolate pure time on the wire, and CUDA payloads alone do not establish that
the transport uses RDMA or GPU-direct transfers.
For an external CUDA trace, set `FASTVIDEO_NVTX_PROFILE=1` in the Ray worker
environment before starting Ray. With transfer profiling enabled, NVTX ranges
include `h3.A_TO_B.source_wait`, `h3.A_TO_B.object_fetch`,
`h3.A_TO_B.materialize` (and the corresponding `B_TO_A` ranges), plus
`h3.encode`, `h3.denoise`, and `h3.decode`.
## 9. Optional FastH3/VSA test after correctness passes
The first smoke test intentionally uses the base MiniMax H3 checkpoint with
+6
View File
@@ -48,6 +48,7 @@ if TYPE_CHECKING:
FASTVIDEO_TRACE_STEPS: str = ""
FASTVIDEO_SERVER_DEV_MODE: bool = False
FASTVIDEO_STAGE_LOGGING: bool = False
FASTVIDEO_H3_PROFILE_TRANSFERS: bool = False
FASTVIDEO_CFG_GATE_STEP: float = 1.0
FASTVIDEO_HOST_IP: str = ""
FASTVIDEO_LOOPBACK_IP: str = ""
@@ -343,6 +344,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
"FASTVIDEO_STAGE_LOGGING":
lambda: bool(int(os.getenv("FASTVIDEO_STAGE_LOGGING", "0"))),
# Opt-in receiver-side Ray fetch/materialization and synchronized stage
# timings for component-disaggregated H3. This changes prefetch overlap.
"FASTVIDEO_H3_PROFILE_TRANSFERS":
lambda: bool(int(os.getenv("FASTVIDEO_H3_PROFILE_TRANSFERS", "0"))),
# CFG gating fraction for stale-uncond reuse (Adaptive Guidance / LinearAG
# variant — Castillo et al. 2023, arXiv:2312.12487). Float in [0, 1].
# Interpretation: for step index `i < len(timesteps) * X`, run both
@@ -481,6 +481,7 @@ def _fake_runtime(monkeypatch):
monkeypatch.setattr(disaggregated_runtime, "ray", fake_ray)
runtime = RayMiniMaxH3DisaggregatedRuntime.__new__(RayMiniMaxH3DisaggregatedRuntime)
runtime._closed = False
runtime._profile_transfers = False
runtime.encoder_decoder = _FakeEncoderActor(events)
runtime.dit = _FakeDiTActor(events)
return runtime, fake_ray, events
@@ -0,0 +1,195 @@
# SPDX-License-Identifier: Apache-2.0
"""CPU regressions for H3 Ray timing; no Ray cluster or CUDA device required."""
from types import SimpleNamespace
import pytest
import torch
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
import fastvideo.worker.minimax_h3_disaggregated as runtime_module
def _state():
return SimpleNamespace(
request_id="request-7",
prompt_embeds=torch.zeros(1, 3, 4),
video_latents=torch.zeros(4, 5),
audio_latents=torch.zeros(2, 6),
layout=SimpleNamespace(
position_ids=torch.zeros(9, 3, dtype=torch.float64),
token_tags=torch.zeros(9, dtype=torch.int64),
text_indices=torch.zeros(3, dtype=torch.int64),
video_indices=torch.zeros(4, dtype=torch.int64),
audio_indices=torch.zeros(2, dtype=torch.int64),
),
)
@pytest.fixture
def timing(monkeypatch):
clock = SimpleNamespace(now=0.0)
events = []
messages = []
result = _state()
def wait(refs, *, num_returns, fetch_local):
assert num_returns == 1
events.append(("wait", fetch_local))
clock.now += 2.0 if fetch_local else 10.0
return refs, []
def get(ref):
events.append(("get", ref))
clock.now += 3.0
return result
def synchronize():
events.append(("sync",))
clock.now += 5.0
monkeypatch.setattr(runtime_module.time, "perf_counter", lambda: clock.now)
monkeypatch.setattr(runtime_module, "ray", SimpleNamespace(wait=wait, get=get))
monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
monkeypatch.setattr(torch.cuda, "synchronize", synchronize)
monkeypatch.setattr(runtime_module.envs, "FASTVIDEO_NVTX_PROFILE", False)
monkeypatch.setattr(runtime_module.logger, "info", lambda fmt, *args: messages.append(fmt % args))
return SimpleNamespace(clock=clock, events=events, messages=messages, result=result)
def test_payload_total_includes_every_layout_tensor(timing):
assert runtime_module._log_h3_state("encoded", timing.result) == 536
assert any("layout.position_ids:" in line for line in timing.messages)
assert any("layout.token_tags:" in line for line in timing.messages)
assert all("request_id=request-7" in line for line in timing.messages)
@pytest.mark.parametrize("direction", ["A_TO_B", "B_TO_A"])
def test_transfer_separates_production_fetch_and_completed_device_copy(timing, direction):
ref = object()
result = runtime_module._receive_h3_state(runtime_module._H3TransferRef(ref, "request-7"), direction)
assert result is timing.result
assert timing.events == [("sync",), ("wait", False), ("wait", True), ("get", ref), ("sync",)]
line, = timing.messages
assert f"direction={direction}" in line
assert "request_id=request-7" in line
assert "tensor_bytes=536" in line
assert "source_wait_s=10.000000" in line
assert "object_fetch_s=2.000000" in line
assert "materialize_s=8.000000" in line
assert "receive_s=10.000000" in line
def test_disabled_profiling_does_not_wait_synchronize_or_read_clock(timing, monkeypatch):
def unexpected_clock():
pytest.fail("profiling disabled must not read the clock")
monkeypatch.setattr(runtime_module.time, "perf_counter", unexpected_clock)
assert runtime_module._receive_h3_state(timing.result, "A_TO_B") is timing.result
with runtime_module._h3_stage_timer("encode", "request-7", False):
pass
assert timing.events == []
assert timing.messages == []
def test_stage_time_includes_gpu_completion_and_excludes_previous_work(timing):
with runtime_module._h3_stage_timer("denoise", "request-7", True):
timing.clock.now += 7.0
assert timing.events == [("sync",), ("sync",)]
assert timing.messages == ["[RAY_STAGE] request_id=request-7 stage=denoise elapsed_s=12.000000"]
def test_failed_transfer_propagates_without_logging_success(timing, monkeypatch):
def fail(ref):
raise RuntimeError("producer failed")
monkeypatch.setattr(runtime_module.ray, "get", fail)
with pytest.raises(RuntimeError, match="producer failed"):
runtime_module._receive_h3_state(runtime_module._H3TransferRef(object(), "request-7"), "A_TO_B")
assert timing.messages == []
def test_zero_fetch_duration_does_not_divide_by_zero(timing, monkeypatch):
monkeypatch.setattr(runtime_module.time, "perf_counter", lambda: 0.0)
runtime_module._receive_h3_state(runtime_module._H3TransferRef(object(), "request-7"), "A_TO_B")
assert "object_fetch_mb_s=n/a" in timing.messages[0]
@pytest.mark.parametrize("stage,direction", [("denoise", "A_TO_B"), ("decode", "B_TO_A")])
def test_actor_receives_before_model_stage(timing, stage, direction):
actor_class = (runtime_module._MiniMaxH3DiTActor if stage == "denoise"
else runtime_module._MiniMaxH3EncoderDecoderActor)
actor = actor_class.__new__(actor_class)
actor._profile_transfers = True
def forward(state):
assert state is timing.result
assert any(f"direction={direction}" in line for line in timing.messages)
timing.clock.now += 7.0
return state
actor.pipeline = SimpleNamespace(**{stage: forward})
assert getattr(actor, stage)(runtime_module._H3TransferRef(object(), "request-7")) is timing.result
assert any(f"stage={stage} elapsed_s=12.000000" in line for line in timing.messages)
def _fake_runtime(monkeypatch, enabled):
events = []
def method(name):
def remote(*args):
ref = SimpleNamespace(kind=name, args=args)
events.append((name, ref))
return ref
return SimpleNamespace(remote=remote)
def get(ref):
# The driver must never materialize encode/denoise intermediates.
assert ref.kind == "decode"
events.append(("get", ref))
return ref
def wait(refs, *, num_returns, fetch_local):
assert refs[0].kind == "denoise"
assert num_returns == 1 and fetch_local is False
events.append(("wait", refs[0]))
return refs, []
runtime = runtime_module.RayMiniMaxH3DisaggregatedRuntime.__new__(
runtime_module.RayMiniMaxH3DisaggregatedRuntime)
runtime._closed = False
runtime._profile_transfers = enabled
runtime.encoder_decoder = SimpleNamespace(encode=method("encode"), decode=method("decode"))
runtime.dit = SimpleNamespace(denoise=method("denoise"))
monkeypatch.setattr(runtime_module, "ray", SimpleNamespace(get=get, wait=wait))
return runtime, events
@pytest.mark.parametrize("enabled", [False, True])
@pytest.mark.parametrize("stream", [False, True])
def test_runtime_preserves_request_ids_and_bounded_driver_dag(monkeypatch, enabled, stream):
runtime, events = _fake_runtime(monkeypatch, enabled)
batches = [ForwardBatch(data_type="video", extra={"request_id": str(i)}) for i in range(3)]
if stream:
assert len(list(runtime.iter_forward(batches))) == 3
assert [name for name, _ in events] == [
"encode", "denoise", "encode", "wait", "decode", "denoise", "get",
"encode", "wait", "decode", "denoise", "get", "decode", "get",
]
else:
runtime.execute_forward(batches[0], request_id="explicit")
assert [name for name, _ in events] == ["encode", "denoise", "decode", "get"]
for name, result_ref in events:
if name not in {"denoise", "decode"}:
continue
arg, = result_ref.args
assert isinstance(arg, runtime_module._H3TransferRef) is enabled
source_ref = arg.ref if enabled else arg
assert source_ref.kind == ("encode" if name == "denoise" else "denoise")
if enabled:
expected_id = source_ref.args[1] if name == "denoise" else source_ref.args[0].request_id
assert arg.request_id == expected_id
if not stream:
assert arg.request_id == "explicit"
+130 -54
View File
@@ -5,15 +5,18 @@ from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator, Iterable, Iterator
from contextlib import suppress
from contextlib import contextmanager, suppress
from copy import deepcopy
from dataclasses import dataclass
import os
from queue import Queue
import time
from typing import Any, cast
from uuid import uuid4
import torch
import fastvideo.envs as envs
from fastvideo.distributed import cleanup_dist_env_and_memory
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
@@ -27,61 +30,118 @@ from fastvideo.pipelines.basic.minimax_h3.disaggregated import (
)
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.lazy_module import is_lazy_module
from fastvideo.profiler import nvtx_range
from fastvideo.utils import get_ip, get_open_port
from fastvideo.worker.executor import Executor
from fastvideo.worker.ray_utils import assert_ray_available, ray
logger = init_logger(__name__)
def _log_h3_state(name: str, state: Any) -> None:
total_bytes = 0
for field_name in (
"prompt_embeds",
"video_latents",
"audio_latents",
):
def _h3_state_tensors(state: Any) -> Iterator[tuple[str, torch.Tensor]]:
for field_name in ("prompt_embeds", "video_latents", "audio_latents"):
tensor = getattr(state, field_name, None)
if tensor is not None:
size = tensor.numel() * tensor.element_size()
total_bytes += size
logger.info(
"[RAY_PAYLOAD] %s.%s: %.2f MB shape=%s dtype=%s device=%s",
name,
field_name,
size / 1e6,
tuple(tensor.shape),
tensor.dtype,
tensor.device,
)
yield field_name, tensor
# layout contains tensors too.
layout = getattr(state, "layout", None)
if layout is not None:
for field_name in (
"text_indices",
"video_indices",
"audio_indices",
):
for field_name in ("position_ids", "token_tags", "text_indices", "video_indices", "audio_indices"):
tensor = getattr(layout, field_name, None)
if tensor is not None:
size = tensor.numel() * tensor.element_size()
total_bytes += size
logger.info(
"[RAY_PAYLOAD] %s.layout.%s: %.2f MB shape=%s dtype=%s device=%s",
name,
field_name,
size / 1e6,
tuple(tensor.shape),
tensor.dtype,
tensor.device,
)
yield f"layout.{field_name}", tensor
def _log_h3_state(name: str, state: Any) -> int:
total_bytes = 0
for field_name, tensor in _h3_state_tensors(state):
size = tensor.numel() * tensor.element_size()
total_bytes += size
logger.info(
"[RAY_PAYLOAD] %s.%s: %.2f MB shape=%s dtype=%s device=%s request_id=%s",
name,
field_name,
size / 1e6,
tuple(tensor.shape),
tensor.dtype,
tensor.device,
state.request_id,
)
logger.info(
"[RAY_PAYLOAD] %s TOTAL: %.2f MB",
"[RAY_PAYLOAD] %s TOTAL: %.2f MB request_id=%s (logical tensor bytes)",
name,
total_bytes / 1e6,
state.request_id,
)
return total_bytes
@dataclass(frozen=True)
class _H3TransferRef:
"""Keep the ref nested so Ray does not fetch it before actor entry."""
ref: Any
request_id: str
def _synchronize_h3_device() -> None:
if torch.cuda.is_available():
torch.cuda.synchronize()
def _receive_h3_state(state: Any, direction: str) -> Any:
if not isinstance(state, _H3TransferRef):
return state
# A receiver-local clock avoids clock skew between the two Sparks. Drain
# earlier device work before timing, then wait for production WITHOUT
# fetching locally so producer compute is not counted as object fetch.
_synchronize_h3_device()
started = time.perf_counter()
with nvtx_range(f"h3.{direction}.source_wait"):
ray.wait([state.ref], num_returns=1, fetch_local=False)
source_ready = time.perf_counter()
with nvtx_range(f"h3.{direction}.object_fetch"):
ray.wait([state.ref], num_returns=1, fetch_local=True)
fetched = time.perf_counter()
with nvtx_range(f"h3.{direction}.materialize"):
result = ray.get(state.ref)
_synchronize_h3_device()
materialized = time.perf_counter()
tensor_bytes = sum(tensor.numel() * tensor.element_size() for _, tensor in _h3_state_tensors(result))
object_fetch_s = fetched - source_ready
# This is an effective logical-payload rate, not measured NIC bandwidth.
rate = f"{tensor_bytes / 1e6 / object_fetch_s:.2f}" if object_fetch_s > 0 else "n/a"
logger.info(
"[RAY_TRANSFER] request_id=%s direction=%s tensor_bytes=%d "
"source_wait_s=%.6f object_fetch_s=%.6f materialize_s=%.6f "
"receive_s=%.6f object_fetch_mb_s=%s",
state.request_id,
direction,
tensor_bytes,
source_ready - started,
object_fetch_s,
materialized - fetched,
materialized - source_ready,
rate,
)
return result
@contextmanager
def _h3_stage_timer(stage: str, request_id: str, enabled: bool) -> Iterator[None]:
if not enabled:
yield
return
_synchronize_h3_device()
started = time.perf_counter()
with nvtx_range(f"h3.{stage}"):
yield
_synchronize_h3_device()
logger.info("[RAY_STAGE] request_id=%s stage=%s elapsed_s=%.6f", request_id, stage, time.perf_counter() - started)
def _node_resource(node_ip: str) -> str:
return f"node:{node_ip}"
@@ -157,7 +217,8 @@ def _next_or_sentinel(iterator: Iterator[ForwardBatch], sentinel: object) -> For
class _MiniMaxH3EncoderDecoderActor:
def __init__(self, fastvideo_args: FastVideoArgs) -> None:
def __init__(self, fastvideo_args: FastVideoArgs, profile_transfers: bool = False) -> None:
self._profile_transfers = profile_transfers
_bind_single_gpu_process()
args = _resident_role_args(fastvideo_args, role="encoder_decoder")
pipeline_cls = MiniMaxH3RefEncoderDecoderPipeline if _is_ref2va(args) else MiniMaxH3EncoderDecoderPipeline
@@ -165,10 +226,13 @@ class _MiniMaxH3EncoderDecoderActor:
self.pipeline.post_init()
def encode(self, batch: ForwardBatch, request_id: str) -> MiniMaxH3EncodedState:
return self.pipeline.encode(batch, request_id=request_id)
with _h3_stage_timer("encode", request_id, self._profile_transfers):
return self.pipeline.encode(batch, request_id=request_id)
def decode(self, state: MiniMaxH3DenoisedState) -> ForwardBatch:
return self.pipeline.decode(state)
def decode(self, state: MiniMaxH3DenoisedState | _H3TransferRef) -> ForwardBatch:
state = _receive_h3_state(state, "B_TO_A")
with _h3_stage_timer("decode", state.request_id, self._profile_transfers):
return self.pipeline.decode(state)
def health(self) -> dict[str, Any]:
modules = tuple(sorted(self.pipeline.modules))
@@ -188,7 +252,8 @@ class _MiniMaxH3EncoderDecoderActor:
class _MiniMaxH3DiTActor:
def __init__(self, fastvideo_args: FastVideoArgs) -> None:
def __init__(self, fastvideo_args: FastVideoArgs, profile_transfers: bool = False) -> None:
self._profile_transfers = profile_transfers
_bind_single_gpu_process()
args = _resident_role_args(fastvideo_args, role="dit")
pipeline_cls = MiniMaxH3RefDiTPipeline if _is_ref2va(args) else MiniMaxH3DiTPipeline
@@ -205,11 +270,15 @@ class _MiniMaxH3DiTActor:
"all_resident": all(not is_lazy_module(module) for module in self.pipeline.modules.values()),
}
def denoise(self, state: MiniMaxH3EncodedState) -> MiniMaxH3DenoisedState:
def denoise(self, state: MiniMaxH3EncodedState | _H3TransferRef) -> MiniMaxH3DenoisedState:
state = _receive_h3_state(state, "A_TO_B")
_log_h3_state("A_TO_B encoded", state)
result = self.pipeline.denoise(state)
with _h3_stage_timer("denoise", state.request_id, self._profile_transfers):
result = self.pipeline.denoise(state)
_log_h3_state("B_TO_A denoised", result)
return result
class RayMiniMaxH3DisaggregatedRuntime:
"""Own one persistent encoder/decoder actor and one persistent DiT actor."""
@@ -227,6 +296,7 @@ class RayMiniMaxH3DisaggregatedRuntime:
ray.init(address=address, runtime_env=fastvideo_args.ray_runtime_env)
_validate_topology(encoder_node_ip, dit_node_ip, ray.cluster_resources())
self._profile_transfers = envs.FASTVIDEO_H3_PROFILE_TRANSFERS
actor_args = deepcopy(fastvideo_args)
actor_args.ray_placement_group = None
actor_args.ray_runtime_env = None
@@ -242,13 +312,13 @@ class RayMiniMaxH3DisaggregatedRuntime:
resources={
_node_resource(encoder_node_ip): 0.001
},
).remote(actor_args)
).remote(actor_args, self._profile_transfers)
self.dit = ray.remote(_MiniMaxH3DiTActor).options(
**common_options,
resources={
_node_resource(dit_node_ip): 0.001
},
).remote(actor_args)
).remote(actor_args, self._profile_transfers)
self._validate_workers()
except Exception:
for actor in (self.encoder_decoder, self.dit):
@@ -279,6 +349,9 @@ class RayMiniMaxH3DisaggregatedRuntime:
raise RuntimeError("MiniMax-H3 disaggregated workers have not been created.")
return ray.get([self.encoder_decoder.health.remote(), self.dit.health.remote()])
def _transfer_arg(self, ref: Any, request_id: str) -> Any:
return _H3TransferRef(ref, request_id) if self._profile_transfers else ref
def submit(self, batch: ForwardBatch, *, request_id: str | None = None):
"""Build a direct actor-to-actor DAG without materializing intermediates on the driver."""
if self._closed:
@@ -287,8 +360,8 @@ class RayMiniMaxH3DisaggregatedRuntime:
raise RuntimeError("MiniMax-H3 disaggregated workers have not been created.")
resolved_request_id = request_id if request_id is not None else _request_id(batch)
encoded_ref = self.encoder_decoder.encode.remote(batch, resolved_request_id)
denoised_ref = self.dit.denoise.remote(encoded_ref)
return self.encoder_decoder.decode.remote(denoised_ref)
denoised_ref = self.dit.denoise.remote(self._transfer_arg(encoded_ref, resolved_request_id))
return self.encoder_decoder.decode.remote(self._transfer_arg(denoised_ref, resolved_request_id))
def execute_forward(self, batch: ForwardBatch, *, request_id: str | None = None) -> ForwardBatch:
return ray.get(self.submit(batch, request_id=request_id))
@@ -312,16 +385,19 @@ class RayMiniMaxH3DisaggregatedRuntime:
if self.encoder_decoder is None or self.dit is None:
raise RuntimeError("MiniMax-H3 disaggregated workers have not been created.")
encoded_ref = self.encoder_decoder.encode.remote(first, _request_id(first))
denoised_ref = self.dit.denoise.remote(encoded_ref)
request_id = _request_id(first)
encoded_ref = self.encoder_decoder.encode.remote(first, request_id)
denoised_ref = self.dit.denoise.remote(self._transfer_arg(encoded_ref, request_id))
for next_batch in iterator:
next_encoded_ref = self.encoder_decoder.encode.remote(next_batch, _request_id(next_batch))
next_request_id = _request_id(next_batch)
next_encoded_ref = self.encoder_decoder.encode.remote(next_batch, next_request_id)
ray.wait([denoised_ref], num_returns=1, fetch_local=False)
decoded_ref = self.encoder_decoder.decode.remote(denoised_ref)
next_denoised_ref = self.dit.denoise.remote(next_encoded_ref)
decoded_ref = self.encoder_decoder.decode.remote(self._transfer_arg(denoised_ref, request_id))
next_denoised_ref = self.dit.denoise.remote(self._transfer_arg(next_encoded_ref, next_request_id))
yield ray.get(decoded_ref)
denoised_ref = next_denoised_ref
yield ray.get(self.encoder_decoder.decode.remote(denoised_ref))
request_id = next_request_id
yield ray.get(self.encoder_decoder.decode.remote(self._transfer_arg(denoised_ref, request_id)))
async def iter_forward_async(self, batches: Iterable[ForwardBatch]) -> AsyncIterator[ForwardBatch]:
"""Asynchronously consume the bounded actor pipeline without blocking the event loop."""