Skip to content
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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 @@ -224,6 +224,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.api_v2.output_sink import OutputSink
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: OutputSink | 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 @@ -18,6 +18,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 @@ -31,6 +32,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 @@ -68,6 +70,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 @@ -84,10 +87,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 @@ -151,6 +158,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 @@ -191,6 +204,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