Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Use `--no-instantiate` before GPU work to inspect the resolved runner config wit

- Inspect available runners with `uv run flashdreams-run --help`, then inspect a specific runner with `uv run flashdreams-run --no-instantiate <runner-name>`.
- Prefer CPU checks first: config imports, checkpoint key-remap shape/bijection tests on CPU or meta tensors, docs builds, `pytest -m ci_cpu`, and static assertions about runner names and pipeline wiring.
- For interactive V2 host-side input latency, capture `--profile-path artifacts/<run>.jsonl` and read `docs/source/developer_guides/latency_tuning.rst` for the two measurement boundaries.
- Avoid `ci_gpu`, generation, `torchrun`, Docker GPU tests, large Hugging Face downloads, rollout parity, CUDA graph, WebRTC runtime, and quality-regression tests on CPU-only hosts unless the user requests them or the test explicitly skips cleanly.

## Testing Guidance
Expand Down
38 changes: 38 additions & 0 deletions docs/source/developer_guides/latency_tuning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,44 @@ resolution, and native-acceleration knobs first.
Profiling and validated reference
---------------------------------

V2 host-side input latency
~~~~~~~~~~~~~~~~~~~~~~~~~~

Use ``--profile-path`` with a V2 application to write a JSONL profile:

.. code-block:: bash

uv run flashdreams-run-v2 interactive-drive-omnidreams-perf \
--mode native-window \
--profile-path artifacts/interactive-drive-input-latency.jsonl

Replacement sessions append separate ``session_started`` and summary segments
to the same artifact. Each ``session_started`` record identifies the concrete
client-window type, output layout, UI and model rates, resolution, presentation
mode, backpressure mode, and both measurement endpoints.

The input source publishes the monotonic origin for its session-relative
``UserInputEvent.timestamp`` values. ``run_session`` then records two metrics:

- ``input_to_ui_step_s`` ends when the IUILoop claims the event.
- ``input_to_window_write_s`` ends when the first following client-window
``write`` call returns.

The native-window measurement ends when its window ``write`` returns, normally
after the presenter call. The WebRTC measurement also ends when ``write``
returns. An active video track returns after host materialization and admission
to the bounded two-frame sender queue. A server waiting for a video track
returns after shape validation. Active-peer delivery, RTP transit, browser
decode, compositor scheduling, and physical scanout require matching client
telemetry.
The final ``profile_summary`` records report an exact count and maximum.
Median and p90 use every sample through 1,024 observations, then use a uniform
bounded reservoir. ``quantile_sample_count`` and ``quantiles_approximate`` make
that transition explicit in the artifact.

JSONL writes add host overhead to the measured run. Keep profiling enabled for
every run in a direct comparison. Use Nsight Systems for GPU stage attribution.

Set ``output.profile_world_model: true`` to enable FlashDreams CUDA-event
profiling for the world-model runtime. Set ``output.sync_gpu_timing: true`` only
when you need raster compute
Expand Down
14 changes: 14 additions & 0 deletions flashdreams/flashdreams/api_v2/input_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,17 @@ def get_user_input_events(self) -> UserInputEvents:
Events in timestamp order, empty when nothing arrived.
"""
...


@runtime_checkable
class TimestampedInputSource(InputSource, Protocol):
"""Input source whose event timestamps share the runtime monotonic clock."""

@property
@abstractmethod
def input_timestamp_origin_ns(self) -> int | None:
"""Return the runtime-clock origin for session-relative timestamps."""
...


__all__ = ["InputSource", "TimestampedInputSource"]
8 changes: 8 additions & 0 deletions flashdreams/flashdreams/runtime_v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ The default UI loop, `BlitModelOutputToScreenLoop`, composites every model
channel in list order as if they were image layers and reshapes the result into
the session's layout.

`--profile-path artifacts/<run>.jsonl` records host-side event-to-IUILoop and
event-to-window-write latency. Each input source supplies the monotonic origin
for its session-relative event timestamps. Replacement sessions append distinct
segments with measurement endpoints and runtime settings to the same JSONL
artifact. The
[latency tuning guide](../../../docs/source/developer_guides/latency_tuning.rst)
defines the JSONL records and host-side boundaries.

`SlangPyUILoop` is the alternative for SlangPy's retained
widget subset. `ImGuiUILoop` exposes the complete ImGui API. Both return a `[1, C, H, W]` frame, so an `ISession` using either should declare a `tchw` output layout.

Expand Down
3 changes: 2 additions & 1 deletion flashdreams/flashdreams/runtime_v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

"""Runtime that runs a FlashDreams v2 application against a client window."""

from flashdreams.runtime_v2.runtime_profiler import RuntimeProfiler
from flashdreams.runtime_v2.session_desc import (
BackpressureMode,
PresentationMode,
SessionDesc,
)

__all__ = ["BackpressureMode", "PresentationMode", "SessionDesc"]
__all__ = ["BackpressureMode", "PresentationMode", "RuntimeProfiler", "SessionDesc"]
6 changes: 6 additions & 0 deletions flashdreams/flashdreams/runtime_v2/application_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from flashdreams.api_v2.application import IApplication
from flashdreams.api_v2.client_window import IClientWindow
from flashdreams.runtime_v2.metrics_output_sink import MetricsOutputSink
from flashdreams.runtime_v2.runtime_profiler import RuntimeProfiler
from flashdreams.runtime_v2.session_desc import SessionDesc
from flashdreams.runtime_v2.session_runner import run_session

Expand All @@ -28,17 +29,21 @@ def __init__(
client_window: IClientWindow,
*,
metrics_output_sink: MetricsOutputSink | None = None,
profiler: RuntimeProfiler | None = None,
) -> None:
"""
Args:
application: Long-lived application that creates the session.
client_window: Window that supplies input and presents generated output.
metrics_output_sink: Optional sink for model-step metrics. It is
opened and closed once for each session.
profiler: Optional host-side input-latency profiler. It is opened
and closed once for each session.
"""
self._application = application
self._client_window = client_window
self._metrics_output_sink = metrics_output_sink
self._profiler = profiler

def run(
self,
Expand Down Expand Up @@ -95,6 +100,7 @@ def run(
session,
self._client_window,
metrics_output_sink=self._metrics_output_sink,
profiler=self._profiler,
timeout_seconds=remaining_seconds,
)
except BaseException:
Expand Down
28 changes: 28 additions & 0 deletions flashdreams/flashdreams/runtime_v2/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import sys
from collections.abc import Sequence
from dataclasses import replace
from pathlib import Path
from typing import Any

from flashdreams.api_v2.application import IApplication
Expand All @@ -32,6 +33,7 @@
client_window_mode,
)
from flashdreams.runtime_v2.metrics_output_sink import MetricsOutputSink
from flashdreams.runtime_v2.runtime_profiler import RuntimeProfiler
from flashdreams.runtime_v2.session_desc import (
BackpressureMode,
PresentationMode,
Expand Down Expand Up @@ -71,6 +73,7 @@ def entrypoint(argv: Sequence[str] | None = None) -> None:
if not wants_application_help:
try:
mode.check_arguments(parsed)
_validate_profile_path(parsed)
except ValueError as error:
parser.error(str(error))

Expand All @@ -87,10 +90,14 @@ def entrypoint(argv: Sequence[str] | None = None) -> None:
metrics_output_sink = (
None if parsed.stats_path is None else MetricsOutputSink(parsed.stats_path)
)
profiler = (
None if parsed.profile_path is None else RuntimeProfiler(parsed.profile_path)
)
ApplicationRunner(
application,
window,
metrics_output_sink=metrics_output_sink,
profiler=profiler,
).run(
session_desc,
application_args,
Expand Down Expand Up @@ -154,6 +161,12 @@ def _add_session_arguments(parser: argparse.ArgumentParser) -> None:
Each defaults to asking for nothing, so a run that names none of them gets
what the application generates.
"""
parser.add_argument(
"--profile-path",
type=Path,
default=None,
help="Write host-side input-latency records as JSONL.",
)
parser.add_argument(
"--pixel-width", type=int, default=None, help="Frame width to generate."
)
Expand Down Expand Up @@ -194,6 +207,21 @@ def _add_session_arguments(parser: argparse.ArgumentParser) -> None:
)


def _validate_profile_path(parsed: argparse.Namespace) -> None:
"""Keep the profile separate from artifacts written by the same run."""
if parsed.profile_path is None:
return
profile_path = parsed.profile_path.expanduser().resolve()
other_paths = [parsed.stats_path]
if parsed.mode == "mp4":
other_paths.append(parsed.output_path)
if any(
path is not None and path.expanduser().resolve() == profile_path
for path in other_paths
):
raise ValueError("--profile-path must use a distinct output path.")


def _session_desc(
application: IApplication, parsed_args: argparse.Namespace
) -> SessionDesc:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@
class NativeWindowClientWindow(IClientWindow):
"""Present UI output through a main-thread GLFW window."""

@property
def input_timestamp_origin_ns(self) -> int | None:
"""Return the native session's monotonic input timestamp origin."""
if not self._input_timestamps_share_runtime_clock:
return None
return self._session_started_ns

def __init__(
self,
*,
Expand All @@ -113,6 +120,7 @@ def __init__(
self.title = title
self._presenter_factory = presenter_factory
self._clock_ns = clock_ns
self._input_timestamps_share_runtime_clock = clock_ns is time.monotonic_ns
self._session_started_ns: int | None = None
self._session_desc: SessionDesc | None = None
self._input_events: queue.SimpleQueue[UserInputEvent] = queue.SimpleQueue()
Expand Down Expand Up @@ -158,7 +166,8 @@ def open(self, session_desc: SessionDesc) -> None:
"""
if threading.current_thread() is not threading.main_thread():
raise RuntimeError(
"NativeWindowClientWindow.open() must run on the process main thread for event polling."
"NativeWindowClientWindow.open() must run on the process main "
"thread for event polling."
)
if self._presenter is not None:
raise RuntimeError("NativeWindowClientWindow is already open.")
Expand Down
Loading