From 672da97bc6f215cc123703e3763123dfa0121af5 Mon Sep 17 00:00:00 2001 From: Jesse Archer Date: Thu, 30 Jul 2026 21:25:10 +0000 Subject: [PATCH 1/8] Add inference runtime API design proposal --- docs/inference_runtime_api_design.md | 640 +++++++++++++++++++++++++++ 1 file changed, 640 insertions(+) create mode 100644 docs/inference_runtime_api_design.md diff --git a/docs/inference_runtime_api_design.md b/docs/inference_runtime_api_design.md new file mode 100644 index 00000000..6a4eea9d --- /dev/null +++ b/docs/inference_runtime_api_design.md @@ -0,0 +1,640 @@ + + +# FlashDreams Inference Runtime API Design Proposal + +Date: July 30, 2026 + +## Summary + +This proposal defines a standard inference runtime API for FlashDreams +integrations. The goal is to make world-model integrations easier to build, +benchmark, and run without forcing every model into the same input shape or +optimization stack. + +The proposed API separates the pieces that are currently mixed together in +integration-specific runner code: + +- `InferenceConfig`: how the model and inference stack should run; +- `UserInputs`: controls or events from an app, replay trace, or benchmark; +- `ModelInputs`: prompts, frames, videos, trajectories, maps, scene data, and + other values required by a specific model; +- input mapping: model/application-specific conversion from user-facing inputs + into model-facing inputs; +- runtime/session execution: model setup, warmup, per-rollout state, and + stepping; +- output targets: WebRTC, native display, MP4, benchmark artifacts, or headless + runs; +- metrics/profiling: timings, memory, traces, NVTX ranges, and benchmark + outputs. + +The API should standardize the envelope and lifecycle. It should not pretend +that all world models have the same inputs, that all models use the same +optimization stack, or that a raw checkpoint can fully describe how to run the +model. + +## Current Implementation Plan + +Implementation should happen on an experimental integration branch. PRs for this +work should target that branch until the API shape, LingBot migration, and +OmniDreams migration are all working well enough to merge to `main` together. + +The experimental branch can temporarily break or simplify command-line options +while the demos are being moved to the new API. The required outcome is that the +LingBot and OmniDreams demos still run through the new runtime path, and that +benchmark tooling can confirm they are at least broadly healthy before the +branch is merged back to `main`. + +Initial scope: + +- define the minimal runtime API envelope; +- migrate LingBot and OmniDreams to use it; +- support selectable output modes such as MP4, JPEG/MJPEG stream, WebRTC, and + headless/null where appropriate; +- use or update benchmark tooling to verify the migrated demos; +- defer broader model migrations, hosted execution, full autotune, and polished + metrics until the first branch proves the API shape. + +## Task Tracker + +| ID | Workstream | Can run in parallel? | Depends on | Done when | +| --- | --- | --- | --- | --- | +| T0 | Create experimental branch and contribution rules. | No, this starts the work. | None. | Branch exists, PR target is agreed, and main merge criteria are written down. | +| T1 | Minimal API envelope and naming. | Partly. | T0. | `InferenceConfig`, `UserInputs`, `ModelInputs`, runtime/session, output target, and mapping boundaries are defined well enough for demos to use. | +| T2 | Event-based `UserInputs`. | Yes, after T1 direction is agreed. | T1. | User inputs are primarily timestamped events; replay traces and derived snapshots are supported where needed. | +| T3 | `ModelInputs`, schemas, and mapping boundary. | Yes, after T1 direction is agreed. | T1. | Models can declare required initial/per-step inputs, and mappings can convert user events into model inputs. | +| T4 | `ModelRunner`, `InferenceRuntime`, and `InferenceSession` skeleton. | Partly. | T1. | A minimal standard loop can initialize a runtime, run at least one sequential session, and close cleanly. | +| T5 | Output mode selection. | Yes, after the result/output shape is agreed. | T1, T4. | A run can choose output behavior such as MP4, JPEG/MJPEG stream, WebRTC, benchmark artifact, or headless/null without changing model code. | +| T6 | LingBot migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | LingBot runs through the new API path with its event inputs mapped into model inputs. | +| T7 | OmniDreams migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | OmniDreams runs through the new API path with its model-specific inputs and mapping preserved. | +| T8 | Benchmark/smoke verification for LingBot and OmniDreams. | Preparation can run early; final gate is late. | T5, T6, T7. | Existing or updated benchmark tooling can run both migrated demos and produce enough evidence that they still work. | +| T9 | Metrics and profiling normalization for the branch. | Yes, but final integration is late. | T4, T5, T8. | Basic canonical metrics are emitted for migrated demos; deeper metrics can remain follow-up work. | +| T10 | CLI compatibility and migration cleanup. | Yes, after demo migrations start. | T6, T7. | Required demo commands are restored or replaced, temporary hacks are removed, and user-facing docs/notes match the branch behavior. | +| T11 | Stabilize and merge experimental branch to `main`. | No, final integration step. | T6-T10. | LingBot and OmniDreams pass agreed smoke/benchmark checks, review feedback is addressed, and the branch can merge as one API transition. | + +Suggested parallel split: + +- one person owns T1/T4, because the API envelope and standard loop are the + critical path; +- one person owns T2/T3, because event inputs, schemas, and mapping need to + stay coherent; +- one person owns T5/T8/T9, because outputs, benchmarks, and metrics are tightly + related; +- LingBot and OmniDreams can be assigned separately once the skeleton is usable; +- one person should track branch health, CLI compatibility, and merge readiness. + +## Architecture + +```text +Optional discovery for CLI, benchmark, hosted, or installed-package flows: + Model/preset registry + -> adapter/preset/default setup/scenario metadata + -> contributes defaults to the app-supplied run setup + +Main runtime flow: +App / integration / benchmark / transport + chooses how the run is driven and where output goes + supplies run setup: + InferenceConfig + UserInputs + ModelInputs + output/metrics options + | + v +ModelRunner / standard loop + orchestrates validation, lifecycle, stepping, output, and metrics + uses input mapping to: + validate that user/app inputs can drive the model + build initial and per-step ModelInputs during the run + | + v +InferenceRuntime + reusable heavyweight lifecycle: distributed init, model load, compile, warmup + load once; create sessions sequentially unless the backend supports concurrency + | + v +InferenceSession + one rollout/stream: prompt/initial inputs, cache/state, current step, reset + keeps per-run state from leaking across prompts, clients, or benchmark repeats + | + v +Model implementation / inference pipeline + hot path: encode -> model step -> decode -> cache/finalize + | + v +Output target + WebRTC | native window | MP4 | benchmark | headless/null + | + v +Metrics / artifacts / logs / reports / traces +``` + +## Example Sequential Session Flow + +The runtime/session split is primarily about reusing expensive model setup while +keeping each rollout's state isolated. The default mental model should be +sequential sessions, not required concurrent sessions. + +```text +ModelRunner / standard loop + | + v +Create InferenceRuntime from InferenceConfig + load checkpoint/model + initialize distributed/backend state + compile/capture/warm up if configured + | + v +Start InferenceSession A + initial ModelInputs: prompt/frame/scene/etc. + per-session state: cache, current step, reset state + step 0 -> step 1 -> ... -> done + outputs -> Output target + metrics -> Metrics recorder + close session A + | + v +Start InferenceSession B + new initial ModelInputs or replay scenario + independent cache/state + step 0 -> step 1 -> ... -> done + outputs -> Output target + metrics -> Metrics recorder + close session B + | + v +Close InferenceRuntime + release model/backend resources +``` + +For v0, an `InferenceRuntime` may support only one active session at a time. +Concurrent sessions should be treated as an optional backend/model capability, +not a baseline API requirement. + +`StreamInferencePipeline` should remain an important local implementation path +for models that already use it, but it should not be treated as the only +possible model boundary. A session may call `StreamInferencePipeline`, another +local model implementation, a Dynamo-like backend, or a hosted service. + +## System Components + +| Component | Role | Boundary | +| --- | --- | --- | +| Model/preset registry | Lists what can run: model/preset slugs, scenarios, capabilities, resource hints, and supported output modes. | Must remain cheap to query and must not load checkpoints. | +| App / integration / benchmark / transport | Owns the user-facing mode: CLI, native integration, WebRTC, benchmark, hosted request, or replay. | Supplies run setup, user inputs, model inputs, and output target selection. | +| User input library | Normalizes live or replayed controls into FlashDreams-supported user input events/windows. | Shared primitives for keyboard, reset, prompt/image updates, traces, and future scalar controls. | +| Input mapping | Converts user/app inputs plus initial model inputs into the model-specific inputs needed by the session. | Owned by model/application code; may be a no-op for simple runs. | +| ModelRunner / standard loop | Orchestrates one run from setup through runtime initialization, stepping, output, metrics, and teardown. | Shared orchestration layer used by CLIs, benchmarks, MP4 runs, and simple realtime flows. | +| InferenceRuntime | Owns heavyweight lifecycle: distributed init, model construction, checkpoint loading, compile/capture, warmup, hosted-service connection, and teardown. | Long-lived reusable runtime created from `InferenceConfig`; lets FlashDreams load/warm once and create sessions sequentially unless the backend supports concurrency. | +| InferenceSession | Owns one rollout or stream: initial inputs, cache state, current step, reset behavior, step requirements, and step execution. | Per-rollout interface consumed by the standard loop; keeps state isolated across prompts, browser clients, replay scenarios, or benchmark repeats. | +| Model implementation / inference pipeline | Implements encode, model step, decode, cache updates, and model-specific optimizations. | FlashDreams wraps this boundary; it should not replace every model implementation. | +| Output target | Consumes generated outputs and handles presentation or persistence. | Separate from model execution so the same session can feed WebRTC, MP4, benchmark, or headless output. | +| Metrics, artifacts, and profiling | Records timings, memory, quality data, logs, reports, traces, and optional NVTX ranges. | Shared observation layer for local runs, benchmarks, CI smoke, and hosted runs. | + +## API Layers + +FlashDreams should expose layered APIs rather than a single all-or-nothing +interface: + +```text +High-level runtime API + run setup -> standard loop -> output targets -> metrics/artifacts + +Adapter/runtime API + model adapter -> InferenceRuntime -> InferenceSession + +Low-level inference API + StreamInferencePipeline -> encoders/decoders -> cache/perf/profiling helpers +``` + +| Layer | Intended user | Provides | +| --- | --- | --- | +| High-level runtime API | Users who want FlashDreams to own the run loop. | Run setup, input mapping, runtime/session lifecycle, output targets, metrics, profiling, and benchmark artifacts. | +| Adapter/runtime API | Model owners who want their model to plug into the standard loop. | Model adapter, input requirements, runtime/session implementation, and model-specific mapping or validation. | +| Low-level inference API | Users who want to own their own loop while reusing FlashDreams building blocks. | `StreamInferencePipeline`, encoders, decoders, cache helpers, profiling tools, and optimization utilities. | + +These layers should remain compatible. The new runtime API sits above the +existing lower-level pieces; it does not replace them. + +## Goals + +- Make FlashDreams easier to use for new world-model integrations. +- Keep model-specific input semantics explicit instead of hiding them in runner + code. +- Avoid a single monolithic inference stack; different models should be able to + validate and use different optimization features. +- Separate model execution from presentation and persistence. +- Support both live input and deterministic replay through the same + runtime/session boundary. +- Make metrics, benchmark artifacts, and profiling first-class without forcing + profiling overhead into normal runs. +- Preserve room for local single-GPU, local distributed, Dynamo-like, and hosted + execution. + +## Non-Goals + +- Do not infer arbitrary model semantics from a raw checkpoint. +- Do not require every model to use the same encoder, decoder, scheduler, + control representation, transport, or optimization set. +- Do not make WebRTC or native display part of the model API. +- Do not make autotuning part of normal inference startup. +- Do not require users to use the high-level standard loop when they only need + lower-level inference building blocks. +- Do not require every existing integration to migrate in one large change. + +## API Placement + +The new API should sit above the existing `flashdreams.infra` layer. Existing +pipelines, encoders, decoders, runner configs, realtime input helpers, WebRTC +code, and quality/benchmark utilities should be reused where possible. + +The exact package layout and class definitions can be decided during +implementation. This document should define responsibilities and boundaries, not +the final Python shape. + +## InferenceConfig + +`InferenceConfig` describes how to run the model/runtime. It should cover: + +- model or preset identity; +- checkpoint or model asset selection; +- execution backend, such as local single GPU, local multi-GPU, Dynamo-like, or + hosted/external execution; +- device placement, precision, and resource hints; +- optimization choices such as compile, CUDA graph capture, attention backend, + cache policy, overlap, prefetch, and native extensions; +- runtime-affecting profiling or tracing options. + +It should not contain prompts, keyboard state, browser settings, MP4 paths, +benchmark output directories, or other app/output settings. Those belong in the +run setup around `InferenceConfig`. + +Existing `StreamInferencePipelineConfig` and `InstantiateConfig` style configs +can remain valid model references behind this layer. The model adapter should +validate which execution and optimization choices are supported. Unsupported +choices should fail clearly or be explicitly handled only when the user selected +an automatic mode. + +## UserInputs + +`UserInputs` describes user-facing controls produced by a live UI, browser, +native app, replay trace, synthetic benchmark driver, or no-op source. + +User inputs should primarily be represented as timestamped events. This gives +live apps, replay traces, and benchmarks the same basic shape, and lets +FlashDreams resample or window those events when a model session asks for the +next chunk of inputs. + +Initial supported user input types should stay close to what FlashDreams already +uses: + +- keyboard keydown/keyup events; +- reset requests; +- prompt update requests; +- image update requests; +- future scalar controls such as throttle, brake, steer, or camera axes once an + integration needs them. + +Snapshot-style inputs, such as current key state, can still be supported when +useful. They should be treated as a derived or compatibility form rather than +the primary user-input abstraction. + +User inputs are not model inputs. A keyboard event does not have one universal +meaning. One model may map it to pose segments, another to steering commands, +and another may ignore it. + +## ModelInputs + +`ModelInputs` describes the data the model or inference pipeline actually +requires. It should distinguish: + +- initial inputs: values needed to start or reset a rollout; +- per-step inputs: values needed for one generated chunk or frame window. + +Examples of initial model inputs include prompt, negative prompt, first frame, +input video, scene id, HD map asset, camera calibration, initial camera pose, +seed, or model-specific fields. + +Examples of per-step model inputs include frame timestamps, pose segments, +camera trajectory chunks, rendered HD map frames, conditioning video windows, +control tensors, event markers, or model-specific fields. + +Model input payloads should use semantic names, not only modality names. For +example, a first frame and an HD map frame should be distinct inputs even if +both are image-like values. + +For interactive runs, most `ModelInputs` will be initial values plus per-step +inputs produced by input mapping. For MP4 generation and benchmarking, the API +should also support fixed per-step model inputs so runs can be deterministic. + +## Schemas + +The API should support lightweight `UserInputSchema` and `ModelInputSchema` +metadata. + +These schemas are not meant to be a rich type system or a replacement for +model-specific validation. They should be just enough to answer: + +- what can this app, transport, trace, or benchmark source provide? +- what does this model require before startup and at each step? +- can this event source drive this model with the selected mapping? + +The purpose is to fail early before expensive model initialization, produce +clearer errors, make fixed scenarios easier to validate, and avoid ambiguous +dict payloads where keys only describe modality. + +For simple CLI text-to-video or image-to-video runs, `UserInputSchema` can be +trivial or omitted because there may be no live controls. `ModelInputSchema` is +more important because each supported model still needs to declare the +model-facing values it expects. + +## Model Requirements + +A raw checkpoint should not be treated as self-describing. It may imply tensor +shapes or architecture details, but it usually does not fully define: + +- required semantic inputs; +- initial versus per-step inputs; +- units for timestamps, poses, or calibration values; +- how user controls become model controls; +- preprocessing, encoder, decoder, mask, prompt, or cache rules. + +Therefore, a FlashDreams-supported model should have an adapter or integration +layer that declares its model input requirements and prepares inputs for the +underlying model implementation. + +Users running an existing FlashDreams-supported model should not need to write +that adapter. Developers bringing a new world model to FlashDreams should expect +to provide one. + +## External Model Usage + +Users should be able to run their own models without adding those models to the +FlashDreams repository. The flow depends on which API layer they use: + +```text +High-level runtime API + user supplies or installs model adapter + FlashDreams owns standard loop, outputs, metrics, benchmarks + +Adapter/runtime API + model owner implements adapter/runtime/session + adapter can be passed directly or registered by an installed package + +Low-level inference API + user owns loop and lifecycle + user reuses pipeline, encoder/decoder, cache, profiling, or optimization tools +``` + +| Flow | Registry needed? | Who provides model-specific code? | Result | +| --- | --- | --- | --- | +| Direct Python | No. | User or model owner passes an adapter/setup directly. | FlashDreams can run the standard loop without the model living in the repo. | +| Installed package | Yes, for discovery. | External or internal package registers adapters/presets. | CLIs, benchmarks, and hosted schedulers can discover the model cheaply. | +| Low-level only | No. | User owns the loop and calls lower-level FlashDreams pieces directly. | Useful when the user wants optimizations or pipeline helpers but not the standard loop. | + +The model adapter is a role/boundary, not necessarily a concrete class. It is +the model-specific code that declares input requirements, validates supported +configs, creates the runtime/session, and connects FlashDreams to the actual +model implementation. + +The registry should not be treated as a central FlashDreams-owned catalog of all +possible models. It is a discovery mechanism for installed adapters. Built-in +public integrations, internal GitLab-only integrations, and third-party packages +can all participate through the same mechanism. + +FlashDreams should not claim to run an arbitrary checkpoint with no adapter +unless the checkpoint already matches a supported generic adapter. + +## Input Mapping + +Input mapping is required whenever `UserInputs` need to become per-step +`ModelInputs`. The exact implementation does not need to be a required top-level +object. It could be: + +- a method on the model adapter; +- a method on an app/runtime adapter; +- a separate mapper object; +- a default no-op or identity mapping for simple T2V/I2V/fixed-input runs. + +There are two separate moments to keep clear: + +- before runtime initialization, FlashDreams should select the mapping and check + obvious compatibility between the app event source and the model; +- during the standard loop, the runner uses the mapping to build initial or + per-step `ModelInputs` from the relevant event window, often after the session + reports what it needs next. + +Examples: + +- T2V mapping validates a prompt and creates no per-step control inputs. +- I2V mapping validates a prompt plus first frame and creates no live controls. +- A keyboard-driven integration maps key events or event windows into pose + segments or steering controls. +- OmniDreams-like integrations may map driving commands into camera poses, HD + map frames, and dynamic actor state. +- Benchmark mapping can read fixed event traces and produce identical step + inputs each run. + +The compatibility check should be treated as early validation, not a guarantee +that the run will succeed. It can catch obvious mismatches, but the model +adapter/runtime still owns deep tensor validation and model semantics. + +## Runtime And Standard Loop + +The standard loop should be shared by CLI generation, headless playback, MP4 +generation, benchmarks, and simple realtime applications. + +A run should: + +1. Discover the model or preset without loading checkpoints. +2. Resolve inference config, user inputs, model inputs, output target, metrics, + profiling, and optional scenario setup. +3. Validate that the event source and mapping can drive the selected model. +4. Initialize the runtime. +5. Start a session from initial model inputs. +6. For each step, ask the session what it needs, gather live or fixed inputs, + build step model inputs, run the session step, route outputs, and record + metrics. +7. Finalize output artifacts, metrics, logs, reports, and traces. + +Realtime transports may need an async variant, backpressure, and explicit flow +control, but the conceptual boundary should remain the same: event/input source, +input mapping, session, output target, metrics. + +The session should expose what it needs for the next step rather than requiring +the app or output layer to guess. This matters because AR step 0 can differ from +steady-state steps, and encoder/decoder temporal compression can produce +different input and output frame windows. + +## Output Targets + +Output handling should be separate from model execution. The model session +returns generated outputs and metadata; the output target decides what to do +with them. + +Expected output targets include: + +- WebRTC streaming; +- native window display; +- MJPEG or lightweight remote preview; +- MP4 writing; +- benchmark artifact writing; +- headless playback; +- null output for pure throughput measurements. + +Display and transport can still affect measured performance through copies, +encoding, queueing, backpressure, and presentation timing. Those costs should be +measured as output-target or end-to-end metrics instead of being mixed into core +model-stage timings. + +## Fixed Inputs, Benchmarks + +The API should support fixed runs as a first-class case. This is needed for MP4 +generation, benchmarks, regression testing, and autotune. + +Two replay levels should be supported: + +- user-event replay: records timestamped key events, prompt updates, image + updates, reset events, and timing, then runs normal input mapping; +- model-input replay: records or defines already-mapped per-step model inputs + for stricter model-level regression tests. + +User-event replay tests more of the application stack. Model-input replay is +better for isolating model runtime performance and reproducibility. + +## Metrics And Profiling + +Metrics should have a small canonical baseline plus optional extras. + +The baseline should cover: + +- lifecycle timing: startup, load, warmup, first-step latency; +- model-stage timing: encode, model step, decode, finalize/cache update; +- memory: allocated, reserved, peak, and per-rank where applicable; +- throughput: frames per second, chunks per second, real-time factor. + +Realtime runs may add input-to-present latency, jitter, missed deadlines, queue +depth, dropped frames, WebRTC stats, encoder bitrate, and client stats. +Benchmark runs may add quality metrics, logs, MP4/image previews, and reports. + +Persisted timing metrics should use seconds as the canonical unit because +seconds compose cleanly across Python timers, traces, and long-running +durations. Reports and UIs can display milliseconds for short latencies. + +Profiling should be optional and controlled separately from normal metrics. +NVTX ranges should be supported for Nsight profiling, but profiling should not +be required for normal inference or benchmark runs. + +## Autotune + +Autotune should be a separate harness that evaluates candidate +`InferenceConfig` variants against fixed scenarios. It should not be part of +normal startup. + +Autotune may search over compile, CUDA graph capture, attention backend, +precision, cache policy, overlap, prefetch, native extensions, and chunk size +when the model supports those knobs. + +Results are only valid for a specific model, checkpoint, hardware, driver, +FlashDreams commit, and scenario. First-run compile/capture cost should be +separated from steady-state metrics. Agent assistance could help propose search +spaces or summarize results, but the measured selection process should be +deterministic code. + +## Distributed And Hosted Execution + +The API should leave room for local single-GPU, local multi-GPU, Dynamo-like +execution, and hosted execution such as a Reactor-style platform. + +At this stage, the proposal should not define Reactor- or Dynamo-specific +contracts in detail. It should preserve the right boundary: execution backend +selection belongs in `InferenceConfig`, while backend-specific scheduling, +authentication, asset access, output streaming, artifact handling, and failure +behavior belong behind the runtime/backend implementation. + +The practical order should be local first, then local distributed, then +hosted/distributed backends once concrete backend owners can validate the +requirements. + +## Existing Code And Migration + +The new API should reuse existing code instead of replacing everything: + +- keep `flashdreams.infra.pipeline` as the common local encode/model/decode + implementation path; +- keep existing encoder and decoder contracts and reuse temporal size helpers; +- keep existing runner configs and CLI compatibility during migration; +- reuse `KeyboardResampler` and realtime input helpers behind the new input + boundary; +- treat WebRTC as a transport/output adapter and bridge it gradually; +- reuse existing quality and benchmark utilities where applicable; +- keep internal-only integrations registered only in the GitLab/internal + workspace. + +The task tracker near the start of this document is the source of truth for the +first implementation branch. The first milestone is intentionally narrower than +the full design: prove the API with LingBot and OmniDreams, selectable output +modes, and enough benchmark/smoke coverage to merge the experimental branch +back to `main` safely. + +## Design Risks + +- `InferenceConfig` could become too broad if prompts, controls, output paths, + browser settings, and benchmark settings are added to it. Keep it focused on + model/runtime execution. +- Dict-like model inputs are flexible but can fail late. Keep dict payloads for + flexibility, but require lightweight schemas and adapter validation for + supported models. +- Schemas could become too heavy. Keep them minimal and role-oriented. +- User inputs are not model inputs. Keep input mapping explicit and + model/application-owned. +- Per-frame, per-chunk, and AR-step clocks are easy to confuse. The session + should expose step requirements instead of making app code guess. +- Output separation is necessary but not free. Measure output and transport + costs separately from core model timings. +- Hosted/distributed execution is still under-specified. Keep the API boundary + open until backend owners validate concrete requirements. +- Existing WebRTC behavior is nontrivial. Bridge it gradually to avoid + regressions. +- Public/internal boundaries must remain clean. Internal adapters, slugs, and + scenarios should not leak into the public repo. + +## Decisions To Make Before Implementation + +- What should the top-level package/API be called? +- Should the main registered object be called an adapter, integration, runtime + factory, or something else? +- What direct-Python API should let users pass an external adapter without + registering it? +- What package registration mechanism should third-party and internal adapters + use for CLI discovery and benchmarks? +- How lightweight should `UserInputSchema` and `ModelInputSchema` be? +- Where should input mapping live: model adapter, app adapter, separate object, + or a mix? +- What should the output abstraction be called? +- What is the minimum v0 set of supported user input events? +- What is the first public model to migrate? +- What metrics are required for every benchmark run? +- What metadata must be discoverable without loading checkpoints? +- What requirements do Dynamo/Reactor-style backends need before we commit to + hosted execution details? + +The document currently uses "integration" for model-specific packages and app +entrypoints. If the team prefers "model" as the public term, that can be changed +later without changing the architecture. + +## Recommendation + +Proceed with the proposed split: + +- `InferenceConfig` for model/runtime execution; +- `UserInputs` for app-facing controls and replay traces; +- `ModelInputs` for model-facing initial and per-step inputs; +- input mapping for model/application-specific conversion; +- runtime/session boundaries for lifecycle and stepping; +- output targets for display, streaming, files, and benchmarks; +- shared metrics and optional profiling. + +The main constraint is that arbitrary world-model inputs cannot be standardized +away. FlashDreams can provide the shared envelope, loop, metrics, replay, and +output tools, but each supported model still needs an adapter that declares and +validates its own input contract. From 0c089c20c711f291b95589d8af26ada18028fb54 Mon Sep 17 00:00:00 2001 From: jarcherNV Date: Tue, 4 Aug 2026 02:11:54 -0700 Subject: [PATCH 2/8] Add experimental inference runtime API envelope (#403) Define the initial flashdreams.runtime package with minimal T1 boundaries for runtime config, user/model inputs, schemas, input mapping, model adapters, runtime/session protocols, output targets, and metrics. Add focused CPU tests for the new API surface without migrating existing runners. --- docs/inference_runtime_api_design.md | 112 ++-- flashdreams/flashdreams/runtime/__init__.py | 60 +++ flashdreams/flashdreams/runtime/_utils.py | 17 + flashdreams/flashdreams/runtime/config.py | 76 +++ flashdreams/flashdreams/runtime/inputs.py | 200 +++++++ flashdreams/flashdreams/runtime/interfaces.py | 90 ++++ flashdreams/flashdreams/runtime/mapping.py | 85 +++ flashdreams/flashdreams/runtime/metrics.py | 124 +++++ flashdreams/flashdreams/runtime/output.py | 78 +++ flashdreams/flashdreams/runtime/types.py | 56 ++ .../tests/test_inference_runtime_api.py | 492 ++++++++++++++++++ 11 files changed, 1350 insertions(+), 40 deletions(-) create mode 100644 flashdreams/flashdreams/runtime/__init__.py create mode 100644 flashdreams/flashdreams/runtime/_utils.py create mode 100644 flashdreams/flashdreams/runtime/config.py create mode 100644 flashdreams/flashdreams/runtime/inputs.py create mode 100644 flashdreams/flashdreams/runtime/interfaces.py create mode 100644 flashdreams/flashdreams/runtime/mapping.py create mode 100644 flashdreams/flashdreams/runtime/metrics.py create mode 100644 flashdreams/flashdreams/runtime/output.py create mode 100644 flashdreams/flashdreams/runtime/types.py create mode 100644 flashdreams/tests/test_inference_runtime_api.py diff --git a/docs/inference_runtime_api_design.md b/docs/inference_runtime_api_design.md index 6a4eea9d..2f0ba19f 100644 --- a/docs/inference_runtime_api_design.md +++ b/docs/inference_runtime_api_design.md @@ -59,25 +59,25 @@ Initial scope: ## Task Tracker -| ID | Workstream | Can run in parallel? | Depends on | Done when | -| --- | --- | --- | --- | --- | -| T0 | Create experimental branch and contribution rules. | No, this starts the work. | None. | Branch exists, PR target is agreed, and main merge criteria are written down. | -| T1 | Minimal API envelope and naming. | Partly. | T0. | `InferenceConfig`, `UserInputs`, `ModelInputs`, runtime/session, output target, and mapping boundaries are defined well enough for demos to use. | -| T2 | Event-based `UserInputs`. | Yes, after T1 direction is agreed. | T1. | User inputs are primarily timestamped events; replay traces and derived snapshots are supported where needed. | -| T3 | `ModelInputs`, schemas, and mapping boundary. | Yes, after T1 direction is agreed. | T1. | Models can declare required initial/per-step inputs, and mappings can convert user events into model inputs. | -| T4 | `ModelRunner`, `InferenceRuntime`, and `InferenceSession` skeleton. | Partly. | T1. | A minimal standard loop can initialize a runtime, run at least one sequential session, and close cleanly. | -| T5 | Output mode selection. | Yes, after the result/output shape is agreed. | T1, T4. | A run can choose output behavior such as MP4, JPEG/MJPEG stream, WebRTC, benchmark artifact, or headless/null without changing model code. | -| T6 | LingBot migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | LingBot runs through the new API path with its event inputs mapped into model inputs. | -| T7 | OmniDreams migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | OmniDreams runs through the new API path with its model-specific inputs and mapping preserved. | -| T8 | Benchmark/smoke verification for LingBot and OmniDreams. | Preparation can run early; final gate is late. | T5, T6, T7. | Existing or updated benchmark tooling can run both migrated demos and produce enough evidence that they still work. | -| T9 | Metrics and profiling normalization for the branch. | Yes, but final integration is late. | T4, T5, T8. | Basic canonical metrics are emitted for migrated demos; deeper metrics can remain follow-up work. | -| T10 | CLI compatibility and migration cleanup. | Yes, after demo migrations start. | T6, T7. | Required demo commands are restored or replaced, temporary hacks are removed, and user-facing docs/notes match the branch behavior. | -| T11 | Stabilize and merge experimental branch to `main`. | No, final integration step. | T6-T10. | LingBot and OmniDreams pass agreed smoke/benchmark checks, review feedback is addressed, and the branch can merge as one API transition. | +| ID | Status | Workstream | Can run in parallel? | Depends on | Done when | +| --- | --- | --- | --- | --- | --- | +| T0 | Complete | Create experimental branch and contribution rules. | No, this starts the work. | None. | Branch exists, PR target is agreed, and main merge criteria are written down. | +| T1 | Complete | Minimal API envelope and naming. | Partly. | T0. | `InferenceConfig`, `UserInputs`, `ModelInputs`, runtime/session, output target, and mapping boundaries are defined well enough for demos to use. | +| T2 | Planned | Event-based `UserInputs`. | Yes, after T1 direction is agreed. | T1. | User inputs are primarily timestamped events; replay traces and derived snapshots are supported where needed. | +| T3 | Planned | `ModelInputs`, schemas, and mapping boundary. | Yes, after T1 direction is agreed. | T1. | Models can declare required initial/per-step inputs, and mappings can convert user events into model inputs. | +| T4 | Planned | `ModelRunner`, `InferenceRuntime`, and `InferenceSession` skeleton. | Partly. | T1. | A minimal standard loop can initialize a runtime, run at least one sequential session, and close cleanly. | +| T5 | Planned | Output mode selection. | Yes, after the result/output shape is agreed. | T1, T4. | A run can choose output behavior such as MP4, JPEG/MJPEG stream, WebRTC, benchmark artifact, or headless/null without changing model code. | +| T6 | Planned | LingBot migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | LingBot runs through the new API path with its event inputs mapped into model inputs. | +| T7 | Planned | OmniDreams migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | OmniDreams runs through the new API path with its model-specific inputs and mapping preserved. | +| T8 | Planned | Benchmark/smoke verification for LingBot and OmniDreams. | Preparation can run early; final gate is late. | T5, T6, T7. | Existing or updated benchmark tooling can run both migrated demos and produce enough evidence that they still work. | +| T9 | Planned | Metrics and profiling normalization for the branch. | Yes, but final integration is late. | T4, T5, T8. | Basic canonical metrics are emitted for migrated demos; deeper metrics can remain follow-up work. | +| T10 | Planned | CLI compatibility and migration cleanup. | Yes, after demo migrations start. | T6, T7. | Required demo commands are restored or replaced, temporary hacks are removed, and user-facing docs/notes match the branch behavior. | +| T11 | Planned | Stabilize and merge experimental branch to `main`. | No, final integration step. | T6-T10. | LingBot and OmniDreams pass agreed smoke/benchmark checks, review feedback is addressed, and the branch can merge as one API transition. | Suggested parallel split: -- one person owns T1/T4, because the API envelope and standard loop are the - critical path; +- one person owns T4 and keeps it aligned with the completed T1 envelope, + because the standard loop is now the critical path; - one person owns T2/T3, because event inputs, schemas, and mapping need to stay coherent; - one person owns T5/T8/T9, because outputs, benchmarks, and metrics are tightly @@ -182,7 +182,7 @@ local model implementation, a Dynamo-like backend, or a hosted service. | Model/preset registry | Lists what can run: model/preset slugs, scenarios, capabilities, resource hints, and supported output modes. | Must remain cheap to query and must not load checkpoints. | | App / integration / benchmark / transport | Owns the user-facing mode: CLI, native integration, WebRTC, benchmark, hosted request, or replay. | Supplies run setup, user inputs, model inputs, and output target selection. | | User input library | Normalizes live or replayed controls into FlashDreams-supported user input events/windows. | Shared primitives for keyboard, reset, prompt/image updates, traces, and future scalar controls. | -| Input mapping | Converts user/app inputs plus initial model inputs into the model-specific inputs needed by the session. | Owned by model/application code; may be a no-op for simple runs. | +| Input mapping | Converts user/app inputs plus initial model inputs into the model-specific inputs needed by the session. | A model adapter may provide a default mapping; runtimes, applications, benchmarks, and replay tools may override it without changing the model step. | | ModelRunner / standard loop | Orchestrates one run from setup through runtime initialization, stepping, output, metrics, and teardown. | Shared orchestration layer used by CLIs, benchmarks, MP4 runs, and simple realtime flows. | | InferenceRuntime | Owns heavyweight lifecycle: distributed init, model construction, checkpoint loading, compile/capture, warmup, hosted-service connection, and teardown. | Long-lived reusable runtime created from `InferenceConfig`; lets FlashDreams load/warm once and create sessions sequentially unless the backend supports concurrency. | | InferenceSession | Owns one rollout or stream: initial inputs, cache state, current step, reset behavior, step requirements, and step execution. | Per-rollout interface consumed by the standard loop; keeps state isolated across prompts, browser clients, replay scenarios, or benchmark repeats. | @@ -281,8 +281,11 @@ native app, replay trace, synthetic benchmark driver, or no-op source. User inputs should primarily be represented as timestamped events. This gives live apps, replay traces, and benchmarks the same basic shape, and lets -FlashDreams resample or window those events when a model session asks for the -next chunk of inputs. +FlashDreams route, drain, or window those events when a model session asks for +the next chunk of inputs. Resampling and interpolation should remain +input-specific mapping or helper behavior, because controls such as rotations, +poses, or controller state may need semantics that generic runtime code cannot +infer safely. Initial supported user input types should stay close to what FlashDreams already uses: @@ -359,8 +362,8 @@ shapes or architecture details, but it usually does not fully define: - preprocessing, encoder, decoder, mask, prompt, or cache rules. Therefore, a FlashDreams-supported model should have an adapter or integration -layer that declares its model input requirements and prepares inputs for the -underlying model implementation. +layer that declares its model input requirements, declares any user inputs it can +map by default, and prepares inputs for the underlying model implementation. Users running an existing FlashDreams-supported model should not need to write that adapter. Developers bringing a new world model to FlashDreams should expect @@ -407,21 +410,25 @@ unless the checkpoint already matches a supported generic adapter. ## Input Mapping Input mapping is required whenever `UserInputs` need to become per-step -`ModelInputs`. The exact implementation does not need to be a required top-level -object. It could be: - -- a method on the model adapter; -- a method on an app/runtime adapter; -- a separate mapper object; -- a default no-op or identity mapping for simple T2V/I2V/fixed-input runs. +`ModelInputs`. In the T1 envelope this boundary is represented by a separate +`InputMapping` protocol. A model adapter may provide the default mapper because +it knows how its supported user controls affect model-facing inputs. Applications, +benchmarks, replay tools, or hosted runtimes may replace that mapper when they +need a different wire surface or aggregation policy. There are two separate moments to keep clear: - before runtime initialization, FlashDreams should select the mapping and check obvious compatibility between the app event source and the model; -- during the standard loop, the runner uses the mapping to build initial or - per-step `ModelInputs` from the relevant event window, often after the session - reports what it needs next. +- during the standard loop, the runtime or runner queues and timestamps user + events, then uses the selected mapping to build initial or per-step + `ModelInputs` from the relevant event window, often after the session reports + what it needs next. + +This keeps the Reactor-style contract intact: the model-side integration can +declare user inputs, declare model inputs, and provide a default mapping, while +the runtime owns transport, event validation, timestamping, input queue/window +selection, output delivery, and optional overrides. Examples: @@ -465,6 +472,11 @@ the app or output layer to guess. This matters because AR step 0 can differ from steady-state steps, and encoder/decoder temporal compression can produce different input and output frame windows. +Input and output timing should share a session timeline even when raw capture +rates and presentation rates differ. A session can request a user-input window +for mapping, then return an output window or equivalent metadata so an output +target can present the generated chunk at the intended cadence. + ## Output Targets Output handling should be separate from model execution. The model session @@ -598,20 +610,40 @@ back to `main` safely. - Public/internal boundaries must remain clean. Internal adapters, slugs, and scenarios should not leak into the public repo. -## Decisions To Make Before Implementation +## Decisions Made In T1 + +Task T1 settles the initial package and naming envelope without committing to a +registry, standard loop, concrete output modes, or model migrations: + +- The experimental API lives under `flashdreams.runtime`. +- The model-specific integration boundary is named `ModelAdapter`. +- Heavyweight lifecycle is split into `InferenceRuntime` and + `InferenceSession`. +- Step data carriers are named `StepRequest` and `StepResult`; a session returns + `None` from `next_step_request()` when the rollout is complete. +- User-facing inputs use `UserInputs`; model-facing inputs use `ModelInputs`. + Both remain lightweight payload envelopes with shallow read-only mappings. +- `UserInputSchema` and `ModelInputSchema` stay intentionally small: they + declare supported event types and required named fields for early validation, + not a full type system. +- Input mapping is represented by a separate `InputMapping` protocol. Model + adapters may provide a default mapping; runtimes and applications may override + it while preserving the `UserInputs` to `ModelInputs` boundary. Simple + fixed-input runs can use `IdentityInputMapping`. +- Output handling is represented by `OutputTarget`; `NullOutputTarget` is the + initial headless implementation. +- Metrics collection is represented by `MetricsRecorder`; timing samples use + seconds as the canonical unit. +- The minimum v0 user input shape is timestamped `UserInputEvent` records plus + optional snapshot data. Concrete event-type catalogs are left to T2 and demo + migrations. + +## Remaining Decisions -- What should the top-level package/API be called? -- Should the main registered object be called an adapter, integration, runtime - factory, or something else? - What direct-Python API should let users pass an external adapter without registering it? - What package registration mechanism should third-party and internal adapters use for CLI discovery and benchmarks? -- How lightweight should `UserInputSchema` and `ModelInputSchema` be? -- Where should input mapping live: model adapter, app adapter, separate object, - or a mix? -- What should the output abstraction be called? -- What is the minimum v0 set of supported user input events? - What is the first public model to migrate? - What metrics are required for every benchmark run? - What metadata must be discoverable without loading checkpoints? diff --git a/flashdreams/flashdreams/runtime/__init__.py b/flashdreams/flashdreams/runtime/__init__.py new file mode 100644 index 00000000..03e6202b --- /dev/null +++ b/flashdreams/flashdreams/runtime/__init__.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Experimental inference runtime API envelope. + +This package defines the small v0 boundary above ``flashdreams.infra``. It is +intentionally additive while integrations migrate onto it. +""" + +from flashdreams.runtime.config import ExecutionBackend, InferenceConfig, Precision +from flashdreams.runtime.inputs import ( + InputField, + ModelInputs, + ModelInputSchema, + TimeWindow, + UserInputEvent, + UserInputs, + UserInputSchema, +) +from flashdreams.runtime.interfaces import ( + InferenceRuntime, + InferenceSession, + ModelAdapter, +) +from flashdreams.runtime.mapping import IdentityInputMapping, InputMapping +from flashdreams.runtime.metrics import ( + InMemoryMetricsRecorder, + MetricsRecorder, + NullMetricsRecorder, + RuntimeMetricSample, +) +from flashdreams.runtime.output import NullOutputTarget, OutputArtifact, OutputTarget +from flashdreams.runtime.types import StepRequest, StepResult + +__all__ = [ + "ExecutionBackend", + "IdentityInputMapping", + "InferenceConfig", + "InferenceRuntime", + "InferenceSession", + "InMemoryMetricsRecorder", + "InputField", + "InputMapping", + "MetricsRecorder", + "ModelAdapter", + "ModelInputs", + "ModelInputSchema", + "NullMetricsRecorder", + "NullOutputTarget", + "OutputArtifact", + "OutputTarget", + "Precision", + "RuntimeMetricSample", + "StepRequest", + "StepResult", + "TimeWindow", + "UserInputEvent", + "UserInputs", + "UserInputSchema", +] diff --git a/flashdreams/flashdreams/runtime/_utils.py b/flashdreams/flashdreams/runtime/_utils.py new file mode 100644 index 00000000..d8016c6b --- /dev/null +++ b/flashdreams/flashdreams/runtime/_utils.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Small helpers shared by the experimental runtime API.""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import MappingProxyType +from typing import TypeVar + +ValueT = TypeVar("ValueT") + + +def freeze_mapping(value: Mapping[str, ValueT]) -> Mapping[str, ValueT]: + """Return a read-only shallow copy of ``value``.""" + return MappingProxyType(dict(value)) diff --git a/flashdreams/flashdreams/runtime/config.py b/flashdreams/flashdreams/runtime/config.py new file mode 100644 index 00000000..4b8752f1 --- /dev/null +++ b/flashdreams/flashdreams/runtime/config.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Runtime-facing configuration envelope.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Literal + +from flashdreams.runtime._utils import freeze_mapping + +ExecutionBackend = Literal["local", "local-distributed", "external", "hosted"] +"""Where and how inference compute is run.""" + +Precision = Literal["auto", "fp32", "fp16", "bf16"] +"""Coarse runtime precision choices.""" + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceConfig: + """Runtime settings that affect model execution. + + Prompts, user controls, browser settings, output paths, and benchmark + directories intentionally live outside this object. The typed optimization + fields cover common cross-backend knobs; open-ended adapter-specific choices + can use :attr:`runtime_options`. + """ + + __hash__ = None + + model_id: str + """Stable identity for the model adapter or runtime integration.""" + + preset_id: str | None = None + """Optional preset identity under :attr:`model_id`.""" + + checkpoint: str | Path | None = None + """Optional checkpoint or model-asset selector understood by the adapter.""" + + backend: ExecutionBackend = "local" + """Execution placement and backend family for inference compute.""" + + device: str | None = None + """Optional device selector such as ``cuda`` or ``cuda:0``; ``None`` leaves placement to the adapter/backend.""" + + precision: Precision = "auto" + """Preferred compute precision.""" + + compile: bool | None = None + """Optional - Whether model compilation is requested or disabled. `None` means left to the adapter to decide.""" + + cuda_graph: bool | None = None + """Optional - Whether CUDA graph capture is requested or disabled. `None` means left to the adapter to decide.""" + + attention_backend: str | None = None + """Optional attention implementation selector; ``None`` leaves the choice to the adapter.""" + + cache_policy: str | None = None + """Optional cache policy selector; ``None`` leaves the choice to the adapter.""" + + runtime_options: Mapping[str, Any] = field(default_factory=dict) + """Adapter/backend-specific runtime options.""" + + resource_hints: Mapping[str, Any] = field(default_factory=dict) + """Resource hints for launchers, schedulers, or hosted backends.""" + + def __post_init__(self) -> None: + if not self.model_id.strip(): + raise ValueError("InferenceConfig.model_id must be non-empty.") + object.__setattr__( + self, "runtime_options", freeze_mapping(self.runtime_options) + ) + object.__setattr__(self, "resource_hints", freeze_mapping(self.resource_hints)) diff --git a/flashdreams/flashdreams/runtime/inputs.py b/flashdreams/flashdreams/runtime/inputs.py new file mode 100644 index 00000000..e14b3572 --- /dev/null +++ b/flashdreams/flashdreams/runtime/inputs.py @@ -0,0 +1,200 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""User- and model-input envelopes for the experimental runtime API.""" + +from __future__ import annotations + +import math +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any + +from flashdreams.runtime._utils import freeze_mapping + + +@dataclass(frozen=True, kw_only=True, slots=True) +class TimeWindow: + """Half-open time window in seconds since session start.""" + + start_s: float + end_s: float + + def __post_init__(self) -> None: + if not math.isfinite(self.start_s) or not math.isfinite(self.end_s): + raise ValueError("TimeWindow bounds must be finite seconds.") + if self.start_s < 0 or self.end_s < 0: + raise ValueError("TimeWindow bounds must be non-negative.") + if self.end_s < self.start_s: + raise ValueError("TimeWindow.end_s must be >= start_s.") + + def contains(self, timestamp_s: float) -> bool: + """Return whether ``timestamp_s`` falls within this half-open window.""" + return self.start_s <= timestamp_s < self.end_s + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InputField: + """Lightweight schema field for user snapshots or model inputs.""" + + name: str + required: bool = True + semantic_type: str | None = None + description: str = "" + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("InputField.name must be non-empty.") + + +@dataclass(frozen=True, kw_only=True, slots=True) +class UserInputSchema: + """Minimal metadata for user events a source or mapping can provide.""" + + event_types: frozenset[str] = field(default_factory=frozenset) + snapshot_fields: tuple[InputField, ...] = () + description: str = "" + + def supports_event_types(self, event_types: Iterable[str]) -> bool: + """Return whether every requested event type is declared supported.""" + requested = frozenset(event_types) + if not requested: + return True + return requested.issubset(self.event_types) + + def missing_snapshot(self, inputs: "UserInputs") -> tuple[str, ...]: + """Return required snapshot fields absent from ``inputs``.""" + return _missing_required(self.snapshot_fields, inputs.snapshot) + + def require_snapshot(self, inputs: "UserInputs") -> None: + """Raise if required snapshot fields are absent.""" + missing = self.missing_snapshot(inputs) + if missing: + raise ValueError(f"Missing required user snapshot field(s): {missing}") + + +@dataclass(frozen=True, kw_only=True, slots=True) +class ModelInputSchema: + """Minimal metadata for model-facing initial and per-step inputs.""" + + initial_fields: tuple[InputField, ...] = () + """Model inputs required before starting the initial generation/session.""" + + step_fields: tuple[InputField, ...] = () + """Per-step model inputs required after the session starts.""" + + description: str = "" + + def missing_initial(self, inputs: "ModelInputs") -> tuple[str, ...]: + """Return required initial fields absent from ``inputs``.""" + return _missing_required(self.initial_fields, inputs.initial) + + def missing_step(self, inputs: "ModelInputs") -> tuple[str, ...]: + """Return required per-step fields absent from ``inputs``.""" + return _missing_required(self.step_fields, inputs.step) + + def require_initial(self, inputs: "ModelInputs") -> None: + """Raise if required initial fields are absent.""" + missing = self.missing_initial(inputs) + if missing: + raise ValueError(f"Missing required initial model input(s): {missing}") + + def require_step(self, inputs: "ModelInputs") -> None: + """Raise if required per-step fields are absent.""" + missing = self.missing_step(inputs) + if missing: + raise ValueError(f"Missing required step model input(s): {missing}") + + +@dataclass(frozen=True, kw_only=True, slots=True) +class UserInputEvent: + """User-facing input event timestamped in seconds since session start. + + Live runtimes, transports, replay loaders, or benchmark drivers stamp events + before queuing them for input mapping. Payload schema is intentionally minimal + in T1; concrete event catalogs belong to follow-up input-mapping work. + """ + + __hash__ = None + + timestamp_s: float + event_type: str + payload: Mapping[str, Any] = field(default_factory=dict) + source: str | None = None + source_event_id: str | None = None + + def __post_init__(self) -> None: + if not math.isfinite(self.timestamp_s) or self.timestamp_s < 0: + raise ValueError("UserInputEvent.timestamp_s must be finite and >= 0.") + if not self.event_type.strip(): + raise ValueError("UserInputEvent.event_type must be non-empty.") + object.__setattr__(self, "payload", freeze_mapping(self.payload)) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class UserInputs: + """Transport-neutral user input batch or window. + + Events must be in non-decreasing timestamp order. Runtimes can pass the full + input history, a drained queue batch, or a session-requested time window to an + ``InputMapping``. + """ + + __hash__ = None + + events: tuple[UserInputEvent, ...] = () + snapshot: Mapping[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + previous_timestamp_s = -math.inf + for event in self.events: + if event.timestamp_s < previous_timestamp_s: + raise ValueError( + "UserInputs.events must be sorted by non-decreasing timestamp_s." + ) + previous_timestamp_s = event.timestamp_s + object.__setattr__(self, "snapshot", freeze_mapping(self.snapshot)) + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + def window(self, time_window: TimeWindow) -> "UserInputs": + """Return inputs with events filtered to ``time_window``.""" + return UserInputs( + events=tuple( + event + for event in self.events + if time_window.contains(event.timestamp_s) + ), + snapshot=self.snapshot, + metadata=self.metadata, + ) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class ModelInputs: + """Model-facing payloads split by initial and per-step use.""" + + __hash__ = None + + initial: Mapping[str, Any] = field(default_factory=dict) + step: Mapping[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "initial", freeze_mapping(self.initial)) + object.__setattr__(self, "step", freeze_mapping(self.step)) + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + def with_step(self, step: Mapping[str, Any]) -> "ModelInputs": + """Return a copy with replaced per-step payload.""" + return ModelInputs(initial=self.initial, step=step, metadata=self.metadata) + + +def _missing_required( + fields: tuple[InputField, ...], payload: Mapping[str, Any] +) -> tuple[str, ...]: + return tuple( + input_field.name + for input_field in fields + if input_field.required and input_field.name not in payload + ) diff --git a/flashdreams/flashdreams/runtime/interfaces.py b/flashdreams/flashdreams/runtime/interfaces.py new file mode 100644 index 00000000..9b6a064f --- /dev/null +++ b/flashdreams/flashdreams/runtime/interfaces.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Protocols for model adapters, reusable runtimes, and sessions.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from flashdreams.runtime.config import InferenceConfig +from flashdreams.runtime.inputs import ( + ModelInputs, + ModelInputSchema, + UserInputSchema, +) +from flashdreams.runtime.mapping import InputMapping +from flashdreams.runtime.types import StepRequest, StepResult + + +@runtime_checkable +class InferenceSession(Protocol): + """One rollout or stream with isolated model/cache state.""" + + def next_step_request(self) -> StepRequest | None: + """Describe the next step's inputs, or return ``None`` when complete.""" + ... + + def step(self, inputs: ModelInputs) -> StepResult: + """Run one sequential inference step.""" + ... + + def reset(self, inputs: ModelInputs | None = None) -> None: + """Reset this session's rollout state when the backend supports it.""" + ... + + def close(self) -> None: + """Release per-session resources.""" + ... + + +@runtime_checkable +class InferenceRuntime(Protocol): + """Heavyweight reusable runtime created from :class:`InferenceConfig`.""" + + def start_session(self, inputs: ModelInputs) -> InferenceSession: + """Create an isolated session from initial model inputs.""" + ... + + def close(self) -> None: + """Release model/backend resources.""" + ... + + +# Do not mark ModelAdapter runtime-checkable: properties make issubclass() +# unreliable, and isinstance() would only verify attribute presence. +class ModelAdapter(Protocol): + """Model-specific boundary that declares defaults and creates runtimes. + + Adapters declare model-facing input requirements, optional user-input + capabilities, and an optional default mapping between the two. Runtime, + application, or benchmark code may override that mapping while preserving the + same ``UserInputs`` to ``ModelInputs`` boundary. + """ + + @property + def model_id(self) -> str: + """Stable identity for the model adapter or runtime integration.""" + ... + + @property + def model_input_schema(self) -> ModelInputSchema: + """Model-facing initial and per-step input requirements.""" + ... + + @property + def user_input_schema(self) -> UserInputSchema | None: + """User inputs supported by the adapter's default mapping, if any.""" + ... + + def default_input_mapping(self) -> InputMapping | None: + """Return the model-provided default user-to-model mapping, if any.""" + ... + + def validate_config(self, config: InferenceConfig) -> None: + """Fail early for unsupported runtime settings.""" + ... + + def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: + """Initialize and return the heavyweight runtime.""" + ... diff --git a/flashdreams/flashdreams/runtime/mapping.py b/flashdreams/flashdreams/runtime/mapping.py new file mode 100644 index 00000000..75635108 --- /dev/null +++ b/flashdreams/flashdreams/runtime/mapping.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Input mapping boundary from user input windows to model inputs.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from flashdreams.runtime.inputs import ( + ModelInputs, + ModelInputSchema, + UserInputs, + UserInputSchema, +) +from flashdreams.runtime.types import StepRequest + + +@runtime_checkable +class InputMapping(Protocol): + """Convert user-facing inputs into model-facing inputs. + + A mapping may be supplied by the model adapter as a default or by an + application/runtime override. Step mappings usually receive a timestamped + event window selected by the runner for the current model step or chunk. + """ + + def validate( + self, + *, + user_schema: UserInputSchema | None = None, + model_schema: ModelInputSchema | None = None, + ) -> None: + """Fail early for obvious app, event-source, and model mismatches.""" + ... + + def map_initial_inputs( + self, + *, + user_inputs: UserInputs, + model_inputs: ModelInputs, + ) -> ModelInputs: + """Build initial model inputs before a session starts.""" + ... + + def map_step_inputs( + self, + *, + user_inputs: UserInputs, + model_inputs: ModelInputs, + request: StepRequest, + ) -> ModelInputs: + """Build model inputs for one session step from the current input window.""" + ... + + +class IdentityInputMapping: + """No-op mapper for fixed model-input or simple generation flows.""" + + def validate( + self, + *, + user_schema: UserInputSchema | None = None, + model_schema: ModelInputSchema | None = None, + ) -> None: + del user_schema, model_schema + + def map_initial_inputs( + self, + *, + user_inputs: UserInputs, + model_inputs: ModelInputs, + ) -> ModelInputs: + del user_inputs + return model_inputs + + def map_step_inputs( + self, + *, + user_inputs: UserInputs, + model_inputs: ModelInputs, + request: StepRequest, + ) -> ModelInputs: + del user_inputs, request + return model_inputs diff --git a/flashdreams/flashdreams/runtime/metrics.py b/flashdreams/flashdreams/runtime/metrics.py new file mode 100644 index 00000000..4286204f --- /dev/null +++ b/flashdreams/flashdreams/runtime/metrics.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Runtime metrics boundary for inference sessions.""" + +from __future__ import annotations + +import math +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any, Protocol, runtime_checkable + +from flashdreams.runtime._utils import freeze_mapping + + +@dataclass(frozen=True, kw_only=True, slots=True) +class RuntimeMetricSample: + """One runtime metric sample. + + Timing samples should use seconds as their canonical unit. + """ + + __hash__ = None + + name: str + value: float | int + unit: str = "s" + step_index: int | None = None + category: str = "runtime" + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("RuntimeMetricSample.name must be non-empty.") + if isinstance(self.value, bool) or not isinstance(self.value, (int, float)): + raise TypeError("RuntimeMetricSample.value must be numeric.") + if not math.isfinite(float(self.value)): + raise ValueError("RuntimeMetricSample.value must be finite.") + if self.step_index is not None and self.step_index < 0: + raise ValueError("RuntimeMetricSample.step_index must be >= 0.") + if not self.unit.strip(): + raise ValueError("RuntimeMetricSample.unit must be non-empty.") + if self.category == "timing" and self.unit != "s": + raise ValueError("Timing metric samples must use unit='s'.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@runtime_checkable +class MetricsRecorder(Protocol): + """Collector for runtime metrics.""" + + def record(self, sample: RuntimeMetricSample) -> None: + """Record one metric sample.""" + ... + + def record_timing( + self, + name: str, + duration_s: float, + *, + step_index: int | None = None, + metadata: Mapping[str, Any] | None = None, + ) -> None: + """Record one timing sample in seconds.""" + ... + + def close(self) -> None: + """Finalize metric collection.""" + ... + + +@dataclass(slots=True) +class InMemoryMetricsRecorder: + """Simple metrics recorder useful for tests, smoke runs, and adapters.""" + + samples: list[RuntimeMetricSample] = field(default_factory=list) + closed: bool = False + + def record(self, sample: RuntimeMetricSample) -> None: + if self.closed: + raise RuntimeError("Cannot record metrics after close().") + self.samples.append(sample) + + def record_timing( + self, + name: str, + duration_s: float, + *, + step_index: int | None = None, + metadata: Mapping[str, Any] | None = None, + ) -> None: + self.record( + RuntimeMetricSample( + name=name, + value=duration_s, + unit="s", + step_index=step_index, + category="timing", + metadata={} if metadata is None else metadata, + ) + ) + + def close(self) -> None: + self.closed = True + + +class NullMetricsRecorder: + """Metrics recorder that intentionally drops all samples.""" + + def record(self, sample: RuntimeMetricSample) -> None: + del sample + + def record_timing( + self, + name: str, + duration_s: float, + *, + step_index: int | None = None, + metadata: Mapping[str, Any] | None = None, + ) -> None: + del name, duration_s, step_index, metadata + + def close(self) -> None: + return None diff --git a/flashdreams/flashdreams/runtime/output.py b/flashdreams/flashdreams/runtime/output.py new file mode 100644 index 00000000..aac341ee --- /dev/null +++ b/flashdreams/flashdreams/runtime/output.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Output target boundary for generated inference results.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Protocol, runtime_checkable + +from flashdreams.runtime._utils import freeze_mapping +from flashdreams.runtime.types import StepResult + + +@dataclass(frozen=True, kw_only=True, slots=True) +class OutputArtifact: + """Artifact produced by an output target.""" + + __hash__ = None + + kind: str + uri: str + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.kind.strip(): + raise ValueError("OutputArtifact.kind must be non-empty.") + if not self.uri.strip(): + raise ValueError("OutputArtifact.uri must be non-empty.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@runtime_checkable +class OutputTarget(Protocol): + """Consumes generated session outputs for presentation or persistence.""" + + def open(self) -> None: + """Prepare the target for a new run.""" + ... + + def write(self, result: StepResult) -> None: + """Consume one generated step result.""" + ... + + def close(self) -> Sequence[OutputArtifact]: + """Finalize and return any produced artifacts.""" + ... + + +@dataclass(slots=True) +class NullOutputTarget: + """Output target for headless runs and throughput measurements.""" + + store_results: bool = False + output_count: int = field(default=0, init=False) + results: list[StepResult] = field(default_factory=list, init=False) + _opened: bool = field(default=False, init=False, repr=False) + + @property + def closed(self) -> bool: + return not self._opened + + def open(self) -> None: + self._opened = True + self.output_count = 0 + self.results.clear() + + def write(self, result: StepResult) -> None: + if not self._opened: + raise RuntimeError("Cannot write to a closed output target.") + self.output_count += 1 + if self.store_results: + self.results.append(result) + + def close(self) -> Sequence[OutputArtifact]: + self._opened = False + return () diff --git a/flashdreams/flashdreams/runtime/types.py b/flashdreams/flashdreams/runtime/types.py new file mode 100644 index 00000000..52bf8216 --- /dev/null +++ b/flashdreams/flashdreams/runtime/types.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Plain data carriers shared by runtime protocols and adapters.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + +from flashdreams.runtime._utils import freeze_mapping +from flashdreams.runtime.inputs import ModelInputSchema, TimeWindow + + +@dataclass(frozen=True, kw_only=True, slots=True) +class StepRequest: + """Model-session request for the next step's inputs. + + ``user_input_window`` lets a runner drain or slice timestamped user events for + the current step before invoking the selected ``InputMapping``. + """ + + __hash__ = None + + step_index: int + model_input_schema: ModelInputSchema | None = None + user_input_window: TimeWindow | None = None + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if self.step_index < 0: + raise ValueError("StepRequest.step_index must be >= 0.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class StepResult: + """Generated output and metadata for one inference step.""" + + __hash__ = None + + step_index: int + output: Any = None + frame_count: int | None = None + output_window: TimeWindow | None = None + metadata: Mapping[str, Any] = field(default_factory=dict) + metrics: Mapping[str, float | int] = field(default_factory=dict) + + def __post_init__(self) -> None: + if self.step_index < 0: + raise ValueError("StepResult.step_index must be >= 0.") + if self.frame_count is not None and self.frame_count < 0: + raise ValueError("StepResult.frame_count must be >= 0.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + object.__setattr__(self, "metrics", freeze_mapping(self.metrics)) diff --git a/flashdreams/tests/test_inference_runtime_api.py b/flashdreams/tests/test_inference_runtime_api.py new file mode 100644 index 00000000..1474383a --- /dev/null +++ b/flashdreams/tests/test_inference_runtime_api.py @@ -0,0 +1,492 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import fields +from typing import Any, cast + +import pytest + +from flashdreams.runtime import ( + IdentityInputMapping, + InferenceConfig, + InferenceRuntime, + InferenceSession, + InMemoryMetricsRecorder, + InputField, + InputMapping, + MetricsRecorder, + ModelAdapter, + ModelInputs, + ModelInputSchema, + NullOutputTarget, + OutputArtifact, + OutputTarget, + RuntimeMetricSample, + StepRequest, + StepResult, + TimeWindow, + UserInputEvent, + UserInputs, + UserInputSchema, +) + +pytestmark = pytest.mark.ci_cpu + + +def test_inference_config_keeps_runtime_settings_separate() -> None: + denied_app_fields = {"prompt", "output_dir", "browser_settings"} + config = InferenceConfig( + model_id="lingbot-world", + preset_id="fast-taehv", + backend="local", + precision="bf16", + compile=False, + runtime_options={"chunk_size": 3}, + ) + + assert config.model_id == "lingbot-world" + assert config.preset_id == "fast-taehv" + assert config.runtime_options["chunk_size"] == 3 + assert denied_app_fields.isdisjoint(field.name for field in fields(InferenceConfig)) + with pytest.raises(TypeError): + cast(Any, config.runtime_options)["chunk_size"] = 4 + + +def test_inference_config_rejects_empty_model_id() -> None: + with pytest.raises(ValueError, match="model_id"): + InferenceConfig(model_id=" ") + + +@pytest.mark.parametrize( + ("factory", "match"), + [ + (lambda: InputField(name=" "), "InputField.name"), + (lambda: TimeWindow(start_s=1.0, end_s=0.0), "end_s"), + (lambda: TimeWindow(start_s=-1.0, end_s=0.0), "non-negative"), + (lambda: TimeWindow(start_s=0.0, end_s=float("nan")), "finite"), + ( + lambda: UserInputEvent(timestamp_s=-1.0, event_type="keydown"), + "timestamp_s", + ), + (lambda: UserInputEvent(timestamp_s=0.0, event_type=" "), "event_type"), + (lambda: StepRequest(step_index=-1), "step_index"), + (lambda: StepResult(step_index=-1), "step_index"), + (lambda: StepResult(step_index=0, frame_count=-1), "frame_count"), + (lambda: RuntimeMetricSample(name=" ", value=1.0), "name"), + (lambda: RuntimeMetricSample(name="sample", value=float("nan")), "finite"), + (lambda: OutputArtifact(kind=" ", uri="artifact://demo"), "kind"), + (lambda: OutputArtifact(kind="mp4", uri=" "), "uri"), + ], +) +def test_runtime_envelopes_reject_invalid_values(factory: object, match: str) -> None: + with pytest.raises(ValueError, match=match): + cast(Any, factory)() + + +def test_runtime_metric_sample_rejects_bool_values() -> None: + with pytest.raises(TypeError, match="numeric"): + RuntimeMetricSample(name="sample", value=True) + + +def test_model_input_schema_validates_initial_and_step_payloads() -> None: + schema = ModelInputSchema( + initial_fields=( + InputField(name="prompt"), + InputField(name="first_frame"), + ), + step_fields=(InputField(name="camera_poses"),), + ) + inputs = ModelInputs(initial={"prompt": "drive", "first_frame": object()}) + + schema.require_initial(inputs) + assert schema.missing_step(inputs) == ("camera_poses",) + + with pytest.raises(ValueError, match="camera_poses"): + schema.require_step(inputs) + + +def test_user_inputs_filter_timestamped_event_windows() -> None: + inputs = UserInputs( + events=( + UserInputEvent( + timestamp_s=0.1, + event_type="keyboard.keydown", + payload={"key": "w"}, + ), + UserInputEvent( + timestamp_s=0.4, + event_type="keyboard.keyup", + payload={"key": "w"}, + ), + UserInputEvent(timestamp_s=0.8, event_type="reset"), + ) + ) + + windowed = inputs.window(TimeWindow(start_s=0.25, end_s=0.75)) + + assert [event.event_type for event in windowed.events] == ["keyboard.keyup"] + + +def test_user_inputs_require_sorted_events() -> None: + with pytest.raises(ValueError, match="non-decreasing"): + UserInputs( + events=( + UserInputEvent(timestamp_s=1.0, event_type="late"), + UserInputEvent(timestamp_s=0.5, event_type="early"), + ) + ) + + +def test_user_input_schema_declares_event_capabilities() -> None: + schema = UserInputSchema( + event_types=frozenset({"keyboard.keydown", "keyboard.keyup", "reset"}) + ) + + assert schema.supports_event_types(["keyboard.keydown", "reset"]) + assert not schema.supports_event_types(["prompt.update"]) + + +def test_user_input_schema_validates_required_snapshot_fields() -> None: + schema = UserInputSchema( + snapshot_fields=( + InputField(name="pressed_keys"), + InputField(name="prompt", required=False), + ) + ) + inputs = UserInputs(snapshot={"pressed_keys": frozenset({"w"})}) + + schema.require_snapshot(inputs) + assert schema.missing_snapshot(UserInputs()) == ("pressed_keys",) + + with pytest.raises(ValueError, match="pressed_keys"): + schema.require_snapshot(UserInputs()) + + +def test_identity_input_mapping_leaves_model_inputs_unchanged() -> None: + mapping = IdentityInputMapping() + model_inputs = ModelInputs(initial={"prompt": "fixed"}, step={"hdmap": object()}) + request = StepRequest(step_index=0) + + assert ( + mapping.map_initial_inputs( + user_inputs=UserInputs(), + model_inputs=model_inputs, + ) + is model_inputs + ) + assert ( + mapping.map_step_inputs( + user_inputs=UserInputs(), + model_inputs=model_inputs, + request=request, + ) + is model_inputs + ) + + +def test_null_output_target_counts_and_optionally_stores_results() -> None: + target = NullOutputTarget(store_results=True) + result = StepResult(step_index=0, output=b"frame") + + assert target.closed + with pytest.raises(RuntimeError, match="closed output target"): + target.write(result) + + target.open() + assert not target.closed + target.write(result) + artifacts = target.close() + + assert target.closed + assert artifacts == () + assert target.output_count == 1 + assert target.results == [result] + with pytest.raises(RuntimeError, match="closed output target"): + target.write(StepResult(step_index=1)) + + +def test_null_output_target_open_resets_per_run_state() -> None: + target = NullOutputTarget(store_results=True) + + target.open() + target.write(StepResult(step_index=0, output=b"first")) + target.close() + target.open() + + assert target.output_count == 0 + assert target.results == [] + target.write(StepResult(step_index=0, output=b"second")) + assert target.output_count == 1 + assert target.results == [StepResult(step_index=0, output=b"second")] + + +def test_in_memory_metrics_recorder_uses_seconds_for_timing() -> None: + recorder = InMemoryMetricsRecorder() + + recorder.record_timing("model_step", 0.125, step_index=2) + + assert len(recorder.samples) == 1 + sample = recorder.samples[0] + assert sample.name == "model_step" + assert sample.value == pytest.approx(0.125) + assert sample.unit == "s" + assert sample.category == "timing" + assert sample.step_index == 2 + + +def test_timing_metric_samples_must_use_seconds() -> None: + with pytest.raises(ValueError, match="unit='s'"): + RuntimeMetricSample( + name="model_step", + value=12.5, + unit="ms", + category="timing", + ) + + +def test_runtime_api_components_compose_for_sequential_session() -> None: + adapter = _FakeAdapter() + config = InferenceConfig(model_id="fake-model") + user_inputs = UserInputs( + events=( + UserInputEvent( + timestamp_s=0.25, + event_type="keyboard.keydown", + payload={"key": "w"}, + ), + ) + ) + model_inputs = ModelInputs(initial={"prompt": "drive forward"}) + output = NullOutputTarget(store_results=True) + metrics = InMemoryMetricsRecorder() + + adapter.validate_config(config) + mapping = adapter.default_input_mapping() + assert mapping is not None + _drive_two_step_session( + adapter=adapter, + config=config, + mapping=mapping, + user_inputs=user_inputs, + model_inputs=model_inputs, + output=output, + metrics=metrics, + ) + + assert output.output_count == 2 + assert [result.output for result in output.results] == ["chunk-0", "chunk-1"] + assert [result.frame_count for result in output.results] == [3, 3] + assert output.results[0].output_window == TimeWindow(start_s=0.0, end_s=0.5) + assert [sample.step_index for sample in metrics.samples] == [0, 1] + assert metrics.closed + + +def test_reference_loop_validates_mapping_before_runtime_creation() -> None: + mapping = _OrderCheckingMapping() + adapter = _OrderCheckingAdapter(mapping=mapping) + + _drive_two_step_session( + adapter=adapter, + config=InferenceConfig(model_id="fake-model"), + mapping=mapping, + user_inputs=UserInputs(), + model_inputs=ModelInputs(initial={"prompt": "drive forward"}), + output=NullOutputTarget(), + metrics=InMemoryMetricsRecorder(), + ) + + assert mapping.validated + assert adapter.created_runtime_after_validate + + +def test_reference_loop_closes_runtime_when_session_start_fails() -> None: + adapter = _FailingStartAdapter() + output = NullOutputTarget() + metrics = InMemoryMetricsRecorder() + + with pytest.raises(RuntimeError, match="start failed"): + _drive_two_step_session( + adapter=adapter, + config=InferenceConfig(model_id="fake-model"), + mapping=IdentityInputMapping(), + user_inputs=UserInputs(), + model_inputs=ModelInputs(initial={"prompt": "drive forward"}), + output=output, + metrics=metrics, + ) + + assert adapter.runtime is not None + assert adapter.runtime.closed + assert output.closed + assert metrics.closed + + +def _drive_two_step_session( + *, + adapter: ModelAdapter, + config: InferenceConfig, + mapping: InputMapping, + user_inputs: UserInputs, + model_inputs: ModelInputs, + output: OutputTarget, + metrics: MetricsRecorder, +) -> None: + mapping.validate( + user_schema=adapter.user_input_schema, + model_schema=adapter.model_input_schema, + ) + initial_inputs = mapping.map_initial_inputs( + user_inputs=user_inputs, + model_inputs=model_inputs, + ) + runtime = adapter.create_runtime(config) + session: InferenceSession | None = None + output_opened = False + try: + session = runtime.start_session(initial_inputs) + output.open() + output_opened = True + while (request := session.next_step_request()) is not None: + step_inputs = mapping.map_step_inputs( + user_inputs=( + user_inputs.window(request.user_input_window) + if request.user_input_window is not None + else user_inputs + ), + model_inputs=ModelInputs( + initial=initial_inputs.initial, + step={"chunk_index": request.step_index}, + ), + request=request, + ) + result = session.step(step_inputs) + output.write(result) + metrics.record_timing( + "model_step", + float(result.metrics["model_step_s"]), + step_index=result.step_index, + ) + finally: + if output_opened: + output.close() + if session is not None: + session.close() + runtime.close() + metrics.close() + + +class _FakeAdapter: + model_id = "fake-model" + model_input_schema = ModelInputSchema( + initial_fields=(InputField(name="prompt"),), + step_fields=(InputField(name="chunk_index"),), + ) + user_input_schema = UserInputSchema(event_types=frozenset({"keyboard.keydown"})) + + def default_input_mapping(self) -> InputMapping: + return IdentityInputMapping() + + def validate_config(self, config: InferenceConfig) -> None: + if config.model_id != self.model_id: + raise ValueError(f"Unsupported model_id={config.model_id!r}.") + + def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: + self.validate_config(config) + return _FakeRuntime(model_input_schema=self.model_input_schema) + + +class _FakeRuntime: + def __init__(self, *, model_input_schema: ModelInputSchema) -> None: + self._model_input_schema = model_input_schema + self.closed = False + + def start_session(self, inputs: ModelInputs) -> InferenceSession: + self._model_input_schema.require_initial(inputs) + return _FakeSession(model_input_schema=self._model_input_schema) + + def close(self) -> None: + self.closed = True + + +class _FailingRuntime(_FakeRuntime): + def start_session(self, inputs: ModelInputs) -> InferenceSession: + del inputs + raise RuntimeError("start failed") + + +class _FakeSession: + def __init__(self, *, model_input_schema: ModelInputSchema) -> None: + self._model_input_schema = model_input_schema + self.step_index = 0 + self.closed = False + + def next_step_request(self) -> StepRequest | None: + if self.step_index >= 2: + return None + return StepRequest( + step_index=self.step_index, + model_input_schema=self._model_input_schema, + user_input_window=TimeWindow( + start_s=0.5 * self.step_index, + end_s=0.5 * (self.step_index + 1), + ), + ) + + def step(self, inputs: ModelInputs) -> StepResult: + self._model_input_schema.require_step(inputs) + result = StepResult( + step_index=self.step_index, + output=f"chunk-{self.step_index}", + frame_count=3, + output_window=TimeWindow( + start_s=0.5 * self.step_index, + end_s=0.5 * (self.step_index + 1), + ), + metrics={"model_step_s": 0.01}, + ) + self.step_index += 1 + return result + + def reset(self, inputs: ModelInputs | None = None) -> None: + del inputs + self.step_index = 0 + + def close(self) -> None: + self.closed = True + + +class _OrderCheckingMapping(IdentityInputMapping): + def __init__(self) -> None: + self.validated = False + + def validate( + self, + *, + user_schema: UserInputSchema | None = None, + model_schema: ModelInputSchema | None = None, + ) -> None: + super().validate(user_schema=user_schema, model_schema=model_schema) + self.validated = True + + +class _OrderCheckingAdapter(_FakeAdapter): + def __init__(self, *, mapping: _OrderCheckingMapping) -> None: + self._mapping = mapping + self.created_runtime_after_validate = False + + def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: + self.validate_config(config) + self.created_runtime_after_validate = self._mapping.validated + return _FakeRuntime(model_input_schema=self.model_input_schema) + + +class _FailingStartAdapter(_FakeAdapter): + def __init__(self) -> None: + self.runtime: _FailingRuntime | None = None + + def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: + self.validate_config(config) + self.runtime = _FailingRuntime(model_input_schema=self.model_input_schema) + return self.runtime From d0c4a4ca3fc576f766d5432adf86f8d2b33f3701 Mon Sep 17 00:00:00 2001 From: Fangjun Zhou Date: Tue, 4 Aug 2026 16:50:55 -0700 Subject: [PATCH 3/8] Add flashdreams.runtime documentation --- .../flashdreams-runtime-data-flow.png | Bin 0 -> 197049 bytes .../_static/diagrams/flashdreams-runtime.png | Bin 0 -> 101726 bytes .../developer_guides/flashdreams_runtime.rst | 307 ++++++++++++++++++ docs/source/developer_guides/index.rst | 1 + 4 files changed, 308 insertions(+) create mode 100644 docs/source/_static/diagrams/flashdreams-runtime-data-flow.png create mode 100644 docs/source/_static/diagrams/flashdreams-runtime.png create mode 100644 docs/source/developer_guides/flashdreams_runtime.rst diff --git a/docs/source/_static/diagrams/flashdreams-runtime-data-flow.png b/docs/source/_static/diagrams/flashdreams-runtime-data-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..d94ac73573783e6223238aefab7e70791d601248 GIT binary patch literal 197049 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%0AS!bSEmf_*kjOp5MySmVz#lfVujvKYbq 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/diagrams/flashdreams-runtime.png b/docs/source/_static/diagrams/flashdreams-runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..7178cc6af97deb4dc0e240605da38d4fde7b830f GIT binary patch literal 101726 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 z�xaCEJ0^R?DWl*xkYuqkju5k-aQ;iK;oR8EY<=b<&7W+w^vEK}?J~ZEfTvb*(mN z0&whW{LXX}h+>8YHw*=51g?=nAGa3FIq9hj@UT2}Z9KOeTTaS1Af8mwZ9A@c+TgjV z;&Su^?G%>B#*2g2R+O>6LR0M0rc<03-y8~a7HD7nJnJGd)-l%qWWHW?u09M(0C~OB z>uS6ztxX4${b5uem+uDkq?>TNt7`i%M%{!Jh4vo{Wj&{fa?KC68l7_yX372o`NMF; zW{d2nCCC9{Gw*l!Dz1F4`U3xhg=f~7iy_%4*rsiseyNI66>Ey~7d2D4py9gD&*S6P zX=7N(BOd$3%f(lp&rz6~on1%0I`8iFu9?QHG`rciHq523t-hFO z8DTLB3aAqP(YcnYYzO4G37I=PwC~8t4?Jha9kzvT!_Si{p_8JMZA4IX8f{-FPpP>* zS01%yyecU7L5o~!`8h`0OsnG5S}_7WGUK(Q)bsjX?(Dd5SIy5+dNrda>^##omb)_a z65L~T0S)To7P=0xA5eC;?DD{>_i@VPE@NUcqt+;zaxWQcOp@a>S-eoI*3Bm;UX)fm zaAasZuXG2(YNL)6n`GYnuxr_QWv}2Bvb9eatKY?(#9OJxjeNURYtJ!uuN1wQ%+da0 zKx}YXj>p+?bOrJm_`P=FrJYpsXrC1<7dS=^cFz#RCJyQ}G`lU*>f(0fFBQ;41)mA8 ztu&asa`|{C_6@3i(V_0d<<|{4H_-QWy;}Wj5$k-Yr6SS)W#c@%0Q)dsUA)&+`1|;& z14~y!(jmQrzmwJTtEY}$crG=U({;OIAosA^f>)|_yepqh{~?#M7NU{ShLzibOCm|3 zZF59bTsM86j86Yqt)^}75B?S4It}G>d%$^RgKcJ)&c0a}O{J6fonQaLf~npgfkGQM zQ0ZDQv8-77sx}UD;)8f4nL8M3Q?nrg3Vo~FR|s7MvqR`)Js|zqmF!

^S0JYkf@R z{v9mQx`_m2H1YQd)}mZ7T^$&Ty)9lV?XDqVy7`*Z?Ky8QOskLg7H!r0FD0Y6PPb@; zjI!=4Yi%O4e=_f~XM2$vqN2#^0V!BR+gG9`zgkX-PxmU@vi~|m1z_Z0f7DVA<*aJ@ zT%pQSlQq9I&aPtU1dhq4v#S?b%&5b42YX3)4dZ_PKGB)oC%g0Q#x6YDn>G2m!4

    !45v2stwEgp|aBPEt}?#FT)qcH|p0a z1D?Vo6$t%u&T6vhNkGw;rTqMH_VsyjG3+OfzVZY;c(*B*Z?r7pl+`YR-BpI(SjXtz zmwA8=(_S6DMzU)?{bH+I>yEp^3BHuQf+eh5gRAxnd)a-n!`ND1!m-;sA zw*OQQ(wXB#&(WG_hacE;cwo1rc)qf~Xj#dP@i)o+P)kqgzR`@b;@aT^{D%>9(>JU>1mXwwYoyNs ztY1#WBweE|FFL(S!7u-AoKC;$)OQURU+PIa8i zhCpNM5my+Dh(Ata(&zbjnhiTvrsdkvpIblI1(m;Au6^AfY?YsdUUn*zG0~grla|>* zVvVEstxypM>y7GC;{GS0Q9k$oIlH`cJ-0w9-XWZH7@Na zHyx3BT>kW()rsdBzX9TZ+UUNkO4I5r7!8P-p4RD4o$IB6XJ@YR%%A7!urJ<*b~&Y7 zi#as1e4FJckMQcQY=E{b+er4GENgxF2OT$8qVlEYmKeG|184 zer7xEnHsAje3D#u1)(5jX9o!+js!xCHcX_fr*$`YrGBhU@+#+RN*f{mrouLDeNFe{ zE&R=lyS5cKBO2J*x@bsHgGoi;DnYhe&g6sETUnwi2k!k;V2q7gNpbe8ND5i;Zc}s?^CR+X7{T4-#-E9X_4(sDF zr1F~?9Bl+ko#oi$;*loB>oLo$vI60gIrD;fFY@HzZV{fSq@NOmGBwLbLbAfiNL?V5 zy%=Vm$7m(VpZ(<|hZ9R@yVt#@DcvdRzm_zHToQ?P78!1;nbeWC%|G)k2OyE(7a!SL z9(XpHH74~Cdv!!gp>(yK!D3`Wm-(>Cxuw@fR9HCA#(|a)vY`8Ty&+~wsJMW^Npq)#y*!F(I-Bc(iZ4|jdI zW;M5~<=l`?>m}{vtC4=t+K{Wn^=cKUp1{^r zfhzefS6s$_I9>@!h!fc#E}YISxAbFovTyDhkrdC4sZNN4y6xgF=F(1eOD&xCRLPXA z!96k%Y~UVybH3E_Gp_GyXOm*l&ohi4-jIhK!yhC?Jk zQDZvcM|jH~@^z?N5}w|xT&_|~x(gq;^9F1c!IwJZ0gwN44-lFph72a(!a=dIKT_n&ym^nPpI z|B_e7&(N-*s$4Qkk~74>-K6rs*ob zA@Ks~S^`srOJieuc>?!?2De}h7km6d`fBr;RJ)clBt~0h>dQ%F%zjbvuQJdlNj7@v z`qJc7;?{Ui2o{Fggx?IcId{@=r}VUZTWn9Zd zhp6iJkciPc-UQHg@mHtAg(H)@bXeSR_hiA{LdYZCbTn}}4fie*EW}=; zpjnlM%}2fT;kB2FUlRSTN$mM?Bby<$>ItzlD0^(tfXWE@DP3OT} zMj@neOccMz?AaXgETfB%XxLA7VQVUi$FCLhrB$H4WOlVDdwtp&n~BkCu7*a@p}&1* z2=y+vSauzY=i!1TG%)4@-b zpjgvVe{nUfT_>2Zp%9A;GXpeOyGWjD<66H0d>f&6WbQXy-!%5NIaDm!k#yr_+d!Tp z)Ji^k!tN>SDH)z4CTyo=Ed{ZIG*@Z?2maAFWBm2$d{RSvTEmkenJsnG+8+$D1>dz09wzh{&Dzh!4X z9=`vC)@UiES*wcU)aFwno@3D*AfERlw1yF4P8r`7;W0_n9+anfk?JTWvFiLve(J8J zr+y<;KRNaK^JVu>bzD>Q!4TPJ@r^pfui;`g_L)Bd4%g$q{URq`j7i3<~(WAAXHy98;N4mxU_vpCxRvRnkq{E+U&b7ZDIX&`fOc$+WuLNcAVem=G85cl+?2~n+vm-3f2tY zpXz%fQaMJ(F~&Pv6l&JWA4HLz^{P6=tN{(`XxL!cDdd9K5$VuOV-oZIXF1;#ymxd!_6`# zy~`O!CHX4$%?Ei*WQ^>JCZ)!r()*O2i@QYXL_{x@ZG3zg4de^nTeaO|Jxo=zYI}8a zDb7&HX`;!_|ArK2k$sSe23|P2`20o1_pS!w7mBx!CVk#!=BkmmO4EIWkKVJxZ?{YQ zB_400)sI}E5Y{kPXAA~=2u-;;1YzID&`=nLs$zpYtfzk_=JIYiFn=02qhF->Il$=e zD5k@?(@zDrU2Cs7fJyncuH4y`I^quUoD*C3Sl^4ESNy6n@w6(?)1<=Z^#b%Adsyr^ zVrDqp+7gsho)L}_-qsbm)TX7)82j`PeRVjx)Cn>y$LI|2A#N7Oxy1zD2wNSanNx3d zYAM5sP zeK=0H0TPnV+!nrX1<>h^#rR=?%7o0!nyaq+R3%3bdfAIW?;1aQbtahp{Kug%F|QU5$tn`%2+vu9_mQ&G zNIOZl66)Bac-tWhjuUXAY`izlNs7naHO6e-^7DqeFAtNmWr6ntQvhrqp5yejN{rxv z-N`e;gzbcwrcF1T^#Fj-(|gxan2PNbuua7}k6OOD68hbRBCmMYr9z&9hA-WLwhN4A z>l4_+wW(c^eI|F?Jm}Y&hvQ9?Ob5?pujyL(5LCpjE~iY;sdLh}fac*?)A**uz>0ex zV8Wk|Ed<}bps0umlAt5$Sem1Ay2f)ZOGJT7e-%LbTBAc#7t;warnSU=4SSLCAZ^7Uz zZunBZAl58Sxq;+Mp6y#Vux*OD>lcpG(3U-maM!_-u1x_aNjKt2-)a3p3MHf@fgPi_Ls;;Ko^{@b1gmY+aH4}V8HyG4saf?4{6zBLga7XqtdwP zWZu5Smlmyi%Z7`(;ay({$qJGBuEtsqjV^~;cJ3kYwb>SDCu~i% zj-dB>=SL-9y=OjR^)T|~sM?XM_s>b1hi@U7#*41_Ed0)An`Dc3y_Y-wh_&wo;S;?( ze#l0*B5yzOLyq6Q%=U-mG5=AXrxRyDM3Fo zLZnN>-}<~iwf68HF8P+>#9yLmg(Gj&EmDB-%Q+_vuFDh7s)<*FUw+$3oV*9GU#Wa%BI@aa+EU>{&6fKbcrC0b0RJU{9cl zck`h9o0s3(7Se0t@170_5L}_mta-Th`3Fw{fV36p;eYVc4Bl}hP{&36cyM_7or_v$ zmHo7E9j#8=v6VCnKi9m2es{p$C4c3KLUcGGwbRe@3V#_aV)`6|+C{*Pwx?XOej&IJ?N^35FK}H{pEccan9Xz2>d&9G5q=Uk zYjVAB2S^^6F^lpr8^?rP-0h_FZ143|ca{BZpCgxy))$lXHM-Jhm8`}8~CskPi!r8!E0kcF7fgVycbe_gW_Z4cY$8W zo{2wC)_V-tXSi_u0qhnNkj8ONw(dH$mPatl1C)QXTfpeqW7Swq z0u7O>CUkEO6ev-ha0>I$0aYcm%_OcV7~a`hg#ykIhkd`~rJiR@?CaXZW`DGmTDviwU%S>#avp?U6#MV`o+eVr=CP_ic>3; ztigI(`~baV5fBM)-giE85D2&!dD$jlwqPEB8{nTK`1cRs|fcmk;}vo3e0o$y}kdW*;V))G)w2)Vpb@ht$c zv_0}c1S8r&-dT>l+C$wiYm)R73hg&1#1YJTVIQFWLLq`7owbNM*RM@b@_)Tb>5n

    t1Zt}HEe0Mpp`_0_GugpK=mS~t4Zg`Z0{P?B3N2@46;y;$7Q11x% zN*=eE^$R~&`k7@r!ejkf@vEWU93#|qXSjE#$(7mjI-wDRb49cT(k_be$zIvV<`QIu zTwoBLOlIuy@qSqP$#R#-eoQbsP(3l2uV8kz|0bgCT07_Za8+oi^QQY3!N$4=o+(Tm zui*(Zy}tFgkWFzMgg6EMS*e#dF9oM4ynrci059XZvY*{xtd1W);W#*-_U8ppAtLYk zld2MIhQE)Ij05oca%vCuj^*35Hvot2J*q{yg*Kc4Uia3imq3cF?m<-LwKw*QTz>Mn z4C?Xs?;MY+YmScGf$jDk(I?A-swy_HZp(}&{N@4pA7>jOue18;9s{vH z5u@Tl(11DjIwTGkSb#@V28{EY2M2yHT2kfjbNQ;XzR!-rT;jO47ypi2SF(1RG1^jV z1iog?v(m&1*s1SaU#K%JXM#s{Ka6+g2zx}@^SygijvF9|^%~Z{K3N+H{$KggUH$(# zDD2q&H}40Lz z6u~NP#BFN47i%nX)_`C^$aDfXwG3!R$D&4i0N9%nDISRn*glPaKx*FyUg%ni?V5Xa zLahhieg;ZsH6nCR{w>S2kZjMj(j*^P3G!L_-3}I{HxG9H=uY!507uf-gfSUdW8MRs zUgPR2vA^)a5MBE$1I#A$84<73K^w{Z7BWZKMEsf|ph0Q)4@(obe|$JH!5GM0m)s6x z+IT7O&;I<-#Cgj!N%I2h;TzaVohq%+m25!@9$#wON3Dmw@h2NpCQ?N=480U8!H)eQvN=y%A~EN9 z)`J^$4L-2&< zld(BVTdSRR3gNB=%R`(!q1wdHV~?=@%_(@~{e_@O?8|qAf)dT<=lg(IAB4D8aqVEp zl^2{g?+m9rYxNo5b9ne8WnrQw_F$X_M5V#FXje$UW}vA&0c?7&@~SLgyk`5|pcg@{ zQ<7f>o=t&)0a0GyFI)olD)k^zCiaq(+!4(jc#ee-h`M>eb=MvW3@0!q&cfC%5sBha z3g592?ZDIEfxZRBS)|V_AeE3j)3EQmYJt^NjM)0H2&`k6LiC0Y+$e}wX1ui3adu^S z%z18D#S-LKOulzwRyXW8KvcDdq3}d;xwB2^F3je;ld2=-VU|wWlROxCfq!Bg{~bf- zfz_#R=>{Ru6?rk_CSoPZ|KylI=H2b511g+rU)5!5HD77tZXym`Y~5sUi2*^2#fpH_ zxvTww?uzRmgzB&yC*0Qgs_u7*U26Q8JILd8S=xXcd7-ggR^agpkXZ|XXRDuR2;nk- zFaxW;OIe7Ca5^|IQuAvBE0gMS>$^n!+zF04ltYM4M|eT->qJ7=EX%gE*DSwv=h+lY zN~~X=N+)HHR^`67^a8V8(chwl~q*rVvpK%gTVH9x5a8yU{U^8B+cvBq?-G^!Yxq6k6z2z zKMSXfw=4L9Y0{~HDcJMA$Ai+zR;a}wWfUk)e2la(Q zG6SmZK~FV_)Ea*(xP|BIRXF}Z5M=NTX#pd8wJW@d46vQyTx`?kMP>{=5PR@OwKri= zQU<&S#nrP%bC)6T7s3*nV(=Eg(euA+{>Quj2lbR_(c0AdTH`AGhXQUm-WH~*5z5!Q z0T3%jQ4U#JHxEX{#T7W3S{`aecFyzb2$cyMzN}c$4_LJ!$X)EJwSH{DJ^hD@qx{?8s@APhm@;P#7J*LeLXc@o(?19$ zfuj0%QU8x(x{7b4kz2Nk6;a;?#hgQf)$lp(2bQ_C)G;B-B`4MAaV1s2YPr^bE zC&KxxELL8TWwrXAo3kTFVY#Vv|1sPO>(0cRIl}$4+W(*k|pCrx17a zogYtD`~Kqdb&I55L)3*5;C|L2m|3r^A2d$AU5HF}smD9SicuFGwZ1mWC&RS<2hu3dc!! zJx<8UK#G{u;;^La8t>%@=3~4h(+A zxM{5v7YMcJ4kP}dJ#3&sH*x+B%SD_Eb(sjfxJjTc7kqJ8o}Uobm9}Wuw{_=O7T8uc z5K=X&qsY_KsJMl^)dTh0_#!q^1jgQoi!k}=WTOkJ3d@x66#-zK>x88<*wA)n3kW3MZ3Lz;nPrH;PicrZ=nKGooJSRh%6qz$C zBpEVK_wP7wwRiR1dq2yL5M7*L7a!xz@4PI@Yl!r-%T_;iDGOK4*t6 zT=$Ru&IM4)P2Bv*=U#paBS-YC3oq`pqKRE}2cHAGM-bblgC>VfoXd@Qn*-Rm-5B(Fi=*1GtsQOSHFNMNdpjTpug2Xa1m3R*B@Do*$#wP z4<)y*JH1i#+0e#Faa>Fz`x8mBj&zW^PJeGk9`0?{-lw6dVLYh*M3dDXbAv@}ArlJH zOs{|lA@DN!ecIa6QgKRywVL#CyeEADbnlS>dMVKEz?dKq9?NxHyw5`2qlNTSOaX>S z>&9pdA7athh|#Jr@s%9XP6flOZWvy?_O#siNdSg)Oub2ih|7D9{cp6IwHSOM;gJFI zkTa5F_W?cM8o5IAD?!2XQ%d_zy2Bn38hu*LdZn%@2;yxp=`cnzuyb+>Z9H|Qwc9*= zTPv=z#%or5e8>3IDnpA^TEE)*=cYl5m+D`ZUe0a4jOX<{%5==_HI!(0Z|4_u`W+u; zRbQ?Jsk05kZ+CO16HHAyjIx33H7hh(=4tTidYmG|Kt;cGGN%#f4Wv?gItxF3L7xr_ z`DoGog~hRc_gw^)15O1a>d-I++1m{5#EJQLm2Qe07=B58&eBxT16hW3-Q(AabE9IL z1w>ZaehbKSwY8qKqw=TAdRU`VgR<=l8sbuAdJrkC@VKkdt<9Xs8mkdmw^x|q_~Q@b z^06}URbKl|G69FaKwrTc8fYG5ZJY-6G>kPrG#%_bxXeFzfnm058j98mAZ1g-NS+8iwX)+L{ zl!Ky3>yJ+o&j?9|_fuI!V1X*Bkq~hB>G|{p?BcS`)##YagjU9x`o~i*W}g{sL^H&) zNeRmaiBxp@$d-Nu9Ri&LJuXmV$|RJM{KE9!#2a61o!?Z<_G8_k`CzO5QBh@9M|d|!gr^XcWU`D zhg{gR+93@(%iR9`n+7-%bH-vFvE%ANIbI^^bNOc(oF9)OP|A;a27%E03t|SGhcpW7knh?A?VlU^ z`_*P-p}Xb}kKl4iEeu$-t2@JBXS1peV^klsMjJ`OjFJPF3ip2O6uY0Ze z=wy{K3Mz>mR*4D$0z*9=qPe4&CvSC5P8gHs`vc3iYmsm&M6@;(lPCcgT7Onn_z^Jt zm<1H5&Gm|Qi=t0vG$XnaO9R>AY)Flr)j<_>%TiU?{Ng zf)TMv8AM&!;UK87{IpK)$XddhH9~7TtFY!W;!;S+?8!mJ;PIpG`!!N4hT6G^c zG?wmwY9YZO?dZg8A-&5TeS3wo!*&)j${2tUO`3Htag+uM?1;;Moc|o1oL(HoMR$J! z+!WPwe_}HXJ)aFdg4N&8jEcr#Xwi3ms)%bRa1WWE4q{ryGm)@M|M)7nu94Q&9};Wc z)oF|N`|dOKHd@2*Yt_{3t!9;JBn)SF9(cR-k1nvV83^_>6l=x=ks?WqQo_LRUWS9u~x#Bc* z%m_P3Xdp}TIc?oq=Vz-g$ogKQFNDhoivxj<>?R^9Q5X4UXr!+-!S7LYZ2s7R@qQpZ zovnv0{1Zhsh)0wDXeRaI;pHpQW&1By>4$vGhYLH?{rQw0Q9dHY*cz*q$T3+DJy1uc zlLF_qJC9k%39KX&V0An+JEA*fQgn%TpVQP zW{dp67h*0S09Ay2#*hm^QCK6I-~|p&1_J-u6lWw)Q!b025#Qwy>azQ`ara$;rZb z!x8?9WIXP^sg+ zEN=br+(FRe2J4<7^~;(SXRsBI_w3I+W5LuaFme!xy~93MHqI!*Eh)AVaI=C$4#YVo zahc!RnG$<0o@!<=kRGEzgb>>>etWeoh6rVlsn`xK;v|8a&8anu_W)?F#<|aM|tVOA3?40 zJD^#(dOvFuLhpBgxUWejR?cxPRCG5FBYeQSUacE z1Xlf&H(+q%z6RYSO4|&g^s&!eoE_umAeb z=KVWJME7XAM?B}%3|H?kcedD$L{@>WjKMS$_+fMvN-0xp^;%c27r*yNocRaes z!1E&xXD2a{8ag`19re}JmUJwpEE1T`Y5x1`{xuN&R|A7b?t}1gIhm23^7Om@i`b5Z zTz3DB*Y^L;KK}*t{@Zr@C4%}l3-n(U;(jzBZmI!+fwlL8u3bClFz5flNx4HLCXg|! zy^m=P^{DEsFChE>2RQU!Y?edVA$*W9rOA%`7nX~pxw(!+Mn=(aU*G3HrRV>BE30d|4I}BcN$H)r!j|4aX>rPT%SL&#&))+0Vq6K`1-wJ#ZOFWof zwh?otUQX6R`#=w|5Wc_Uqs^n5Q1hYn!L9zUtQsJE`yO*;*(Yx)Z>!rMr@1i|L<(8y z=*Qa)b{Fp|j@bZ2bDGpE2TqG4NgT9(-MWh|%KX7q_hL%d2UnFGa}+q9`^cW15@Y^% zbuR55n$~*z1fRmc%0m4xp@Vy%Nqx_B7%vT$TQcL9$gN8h#6`C1E*sv6Ld4d zK-nd2EDVTOg=AbuyE+jFpjg_I`~aZyipg67b=@eocoRwQJ7KMWtsfvi*y4f;Rma1< zgn#CJSdP7sWZ3i*B|fl|m@PX<1zZ! zG#jj5Z?jW$OEo8d(CiIFj~LHv!sz<*fQ_!FtX@Afp|Ni^@d1mLo;U=3XH(OA-436Y z^piUGIFbacc=zB&2y03X=3wTqDCWHg#~ng5T0IH1-j;ScA#BogPmM20sma1F@qgu# zp#3nPFkzADGa)Ycb?4OK>k){&JPYniu)D9nF;CHV~n%q7~LQ21znnu+Z|`?(B}BBaQjrQ1vXNV`MY z&)tZ2B_3?Xb@E5IMUZZ!{hWe2NN@0$YSJu1VpB|5h&ZWOLd9UJE7bc~Ife6!^QM+HLu?WtY ztKeDkmPPR3-tu>9s{3!RVW#f&XDJw!He){~y}A2FO~*?eNA29kuOn%dwZRzZ>{vRS zFznbkxluKb>FAzA^fFRBo9Vc8TMQ`AE?_3GjwCPR~r~ zU+q4ZQnQ4Le^8K1xsrhVBR^#OZcMqN7Snfoq4V0h8F&xEEWJCIFS!mv+T6XAXMy;} z{?!=CYfeO(tY9~y1RbDnUW6gKi?(jvTEl_atRi>a*q&d>o9X66ZYPC&`$Ei)J<`=> zKy@HK{`;zOqq?1@oH_8hcLNuPI9e{_;}mz6HJsEV$EAEUB`9D?jVQk zA^`z=Mx5_wA%FiAAvkZMME|jti44UQ!pP+^aSEmFH#)&@2;$$rI@R+0;h$|g zkv&)`29<$wwmKv}j4%bD4;h}QE;5^b6~sYnR?Wfu*xNWXZn5NzQDhc-y3%p2gnq(F z36QM%uKGIhyhiCQ$!!>On)hLRHxq4$HoRTnD|dW;Ia#on6IP$+BZ@mj32pyI(D zVj(I7>QOavq~(~IJX-n?t~BBWp4I#on0TGSfA_4#afyk($WZb@CG^5|2t^(j6vG79 z>zFA|N~cyxx(24;nt}gu&3M$WhWo;(qhX!>%Z+a>ZX78R)s*UYrOot!0944PM(c7V zp+k9Y0ls+RGuX3C@PaItFJFEO&5M|O_tsg=rilB@A=`?)v0`!1##)e(Q3~S3 zLdPT|=#4(39v}l+Fil4(Xd!%{7JE_}eV5~e_F_>-TI}-UGf8q1r2IA7jSCh)Y z+ctvA)G=U>wC;g-g~}3tcwrL-UQy!gQEj z2?oS26pzc>A?W5q#z7a7spnM(PrrIDiKX54_vXNd#2;J8PIxa9;wJP!>v1wU z0xG_p?ArX8vq4Cd#?2@I96X2%cAiGLdL?EC>zY`=c0?h@#~Prqy5Bl*&TotJ@7*CDk7P__Y_{l>~X>4djV+7@H_=KD0jiK2(E)Qc{;v7pxFji*ebUVWTgN4N-?)PaKZ51VfhUE;G7Sls8sCy@uLG z+O!rUU&*o$mQJlJt)O!r?-VgO(F3~P3N`~Ls|*R}v4Lt3@XC);C>5`_&Tt}=sGzrXlbpZZ`?2qmmgdn>*muKf zJmQ7zb%Fs1pRA~=D#ieNW-=RiZ}7H+B+0j$6OLr+#~NRWB}5ST<;|^eN$eDgxF3w= z-P^ZUfBN*v5b(;>O;cBQC9HL1F`b*wPj28%4oSpzZLge#m0Cuw4UuJFrSUW=`v4YGnEQmE6~im7JW4-MtsRaGT!8~+lEO4^7R zNUDZ$37FCakmPgcFN0joeWDp~haxZgn>x)n!_LANQ)DXO1A(^Yz%$^#UoVUw{c7j% zBkUT6{ZV@_vaZ)Gz!a>z(0YpOrpOdu&70}=85z8_D^{)~yLjLJ{r;0cCUh5Ru!{J8 zjM?S-)YWB#H!Osm*ijzs%Y&mNL$*!DwC^*WKxH%@mW~xiYu5Ms@(?CNoyj0KL=DBL z%osK0xxYnRQr3zq6Gv{2t8j4=NTGSE{RU=wL`dt9mYw&+n-SkiHqq$MpR9zx<^XX< zzDH&YTtx?}yeR-}l%s!dmuhap4S28EDv&*+iwHrxQm2kJGf=o}|K2VTR6$W+gTs2B zvfx)NL*N6YVh_15w;8{p5Mo+nwyI&l<}`s2{WIJE9HOXRCN>Vi=OrTX+5h2%QU1BP zZ*yI#1g!i87ieIGED6CxZpR~ld9=yQK@5#V_FySk%)8Bja3YJbTh0}sJd>&U)bLHm{LPMM*<{Fh2$CgsXvJkt>~6qke?208{7#D6RJEZy_pkckX5Z|bRlpZb9kgr9*8YjScj?8|avVbOG9GP38FZPXgEe^tC$jpAVBTc_Z+Pp@VmBT8)$SeX z`VRwM$D<6`uKdd`lNo~6{9?5Gc#e%ZJnor(VL#(jypmt>E`Idi-P1{je%_R2XTJIG zKm8f)`)_+s@4!ol;R_*Ec_$@Bb1}{*&DMT*(fA}W%aEnsQRKax86_BBFyY9A!Mkud z|LJ?LB(%Mb1B+Xp-{RAM+qUM3|Nqdo{<(?G&?F=TJyz`;C;PqEH?PK+R!+SP>yjZZ zSQ8RZZ5%a#<+=JZei54XVV7Yv{_;>8^)f_|GP_lj``z-AT3?8Zo9nTUn{JE-yv z2Ry4J6Wk#F*U4`3ZO6LpctfFF+41Y%sUhr3Bj^+3lV})W!Q7s07ZK3?FWA-}4&mLX zs3?HF4l#0IvkAV3Q8&aOueZoL{$g8yz=Ge_Qlo&0vYh6>kwhgZqI|>_0|DMbV2=2m zKi$AuBQ&uH;SiEH=8ega;I1sg8(Sy2fMQL9?|)?ocy0Cd5KN8Ttw760$>hITSEwFA z$L9(xF_^NK(FtJeNKVf`r7DnpEX3HYvK@#NSZuX5lAH2yD8?#~$snuUJz z$1l6~AB`^(ud>e0&Lj?xQVHV#*uD6ZE%=UB@wLRCBLm=rfffY>%0ca4=wwsg&rpt7 z8d`=b4w)SY&ig6Xz5|Cho!Fi~{yx!^fZh;E(D?is++l$4y@e4l2Y{hGCnlfZw_mbw zTP+*1mO(QX5jOg6WT{h=ukx{kUs!t8_F#^J;z20aTOmj(1{-+9vOU~y=0_6t8;96v zdb97?Mu==!j;3t?JfLw60#nUsr4o{TPHH&F+t&mA^e~^JmcS*E7L+{4HGo58t3_TR zT{t61y{!lvdl%gTTHmYOkh2Q(X%W3-ZsJ9yZ8bch$l1`oK8^=SH7QI1j{MLop2AEr z90g^E1wxSNEgRBEW~FYsEX`kAoAKPREkJ zzR{!%gY;WWgxWW8f+I#IT*f=iq}WNwjw8@b`*cZsddYRGvgjNnuOhqSj<260 zjgqy}{NwUP;keB1KQBX?ocC#I1?3E5k|H;XDyg;hChOhCOH^Ez1RXpj|7Q?RvNl1||jaV9MXG zc$odvv(=pm#ZvuOeIw}VVrvzC!2Mw<9 z9&Z*bkQ!;iMB>>be)Ixn4M-6+-8fZ`WMwN#3YRccb330(=)u|rlq_3|4~Xcv7Bbqc zi`TWx-U{Mp+rT6h5xF-F^rEzOZ&-7AW9~?H^6ryEU={!tSix! znq$Evo+pXE1!7HZw9f;!(tK!8zJlL1G8u#9i$Q2xid8Q;-fjro)X8NEG^kjZc@aZ? z3PIrt+r+lVhWV$4~!7@7>od#NO&CuvzJC`q@Vr9!|$C5eSf#2~H6;Oki{ z%@?UiPgqJzeUcIR3D z5xphSAO^`}dz=BSx&RHDp`mBdk+2y#`C^| z3=MuyY`OJ%QXj7ymF29W#Ozh`B(3#Pg}*WLEAPE@I{4ZpSCHartXn(_pnuN_LwsO# z4&@Dp-;S5RI10t)t*=aa!3L7Lj}{lr(qhq^zRT84BI!qmLF(I``+TO`)+F>e7GnL_ zzo(HSyOo~vZVsLxo4*IsraNDb$_p)~yh(vNCNGj%Wgos9f&Qz{C))U>FG7%jM^E`! zuhOx828uw?`%p)WDx_|kI=E>{5?iQj>I^zIeQ`(wkZ7h*9-L9jQ*6fZv+u*e1_{Z9 z3nd3sQ5Xo6Ja+3YL``KF%lrno5(YhVSX`r<&{6U#|H%hb{T)ZCYR zB^zW*%w^?WCtzqZu%ssSw=Zz4Amh@}EQ2S z|8Ow!Zq}fT(27!)qJN-#|0#`E%-5A}9(zefww6n+q{O_(;sg|BB7jnH%>(^9D3HVK z6(;p4q>d@KG=G%N!a$YBXwE7G(Tts#COE|{^_3D+7Asu8z>g4dh5A{zr@UI!Ut}Xv zH!Gev8hBmY=D3cw$kFH5NT!cu&9n77kn8rBav=pR(A7G!=<*z|4crBzR@WMtb`A_5 zC>xUfij_nkd?6YJRv1jQu$AxB$E&<`GJmd+X<3Bzwp>D5-&TF-Y?)D95hhm-RSnz? z=HbZsc+-rSu&rAT=bsz+VhPlIkxk!jHMvo2e2Z3WLwQ%3(-;{YQHPtQXlIfYw8QhZ zoDLg5=&;F0qW>8Qs>Rp$JUaTq5cs^a`-KbI8KZouL#er!iR82W47Xtuj9nrnpr{um z8?$ZU^9<^cFK&%Nxe7u4H~RgT&%~Yhzn4FTImDf(`e#(mSSYbsPkF3HqqPwY zI1#B|l*itq@kQMEHD+K1E*++)^ympkO@OgcjJFju_25{=$Z%}jeJ|v#r7I5@ha_2& zB@SQ4(wndFXL0JSuZu`_NC!-LS=}WvNuDHGt&g3Hs9t+RXc5KE6{olbZHN~ohpN|f zn+0E6-=ySq!H@e+TS|RNw=BF^6cT|CkzghTlK05gbdpJfiA$+`zPyiTaM--tXZl+Y zdH}A!t`0VzL21a0>T4%7bPlK9ClxJ?sEch0b)B+rEv3A1N9xq5#E-YrSJOZ9>BZB4 zptBh54o5<+fEQfiYu2MEGhBi-DmJCxlAe4(c>K2((#(%?-UgHx8ZrqDIHZ2+cg`_Y z31~w^c7I^B^IvY;h`SPl2vIAX+j^YU%9jf<_dN|lUF9Fpp(EXG`+~JM2NnK@FszZK zieFSu6P)ydIAQQJA}5b?k6L|#D4E};(lBXhQh8g-ss|l$T1^c*W@r$V?_%g9l%j)6fYOY)d{#Pzl6HjZi5&^N$O zuLj-;Mk6*8NJ~z;kaq#-5WQaV@Y|N~v^|o_f03s}>Q!3I+;4Y1(+ikX5qK z%x_9`^r2GuLLF?T9?~F6W&;rzlypTD1qfg5EIX^uxzwq?$6=G^AGTn*H8)LZ5TMY2 ztt@ct^{wRw~A27p*e`Aw{-@H)1voREz@ig@Iw4!rDX|}upDiyTbBEEG-EZXsP zoW2G(?xo!pIPvUQ*@a~v3_yW0mCig)e;PhvZG8qhT&B+4Lc=iXrM**IMMMfnFBgiI zkrqmR4?B5g%|gPGmU~48#N-(4&?V|*BUQ<`$zsKx`Xl~IWxAIEqPv*GMr5;~=MYJM zTZ73i5p(xOriOFFrecmEiAu^^y)GM2I>=Ts;U1+kb9SCQ&X7gwFE2J_Zg3`Z29(E- zQ^F_TRa6wNPs~!&QQ>*q%jdiqB`{j1j^x9)$lq2~+nf~3b7S-==hTCw1(YOTFpk7H z;AN`Ot=fuS{DTQ@w%~dZ)|!^KCQ18OJX^XU5TBM5y5~e+y~yuC z11=npKoCPIy!=K_`^y{t4GJPxSr%A;e$#0_B0XMHYDmVJjtWxThS#rqNwh}FJ^=A; z-N>WBXjgoBB^xQVXH3G=$}>6R^{2ktR!5R2WgmDrT+o4CMxBA;8_7h{N(xus?;JoG zOg>0@=%H^RCfZq2SSjc)L(>`=L+qWAAGN}(^!b)Pv#?ylV5CG0d)@&T(^${ zO-BPKPL?1=hSEkgK3#r&84M9AejtS8H+*(wb*Dup*@4wyJ1O|mA;d%Bk-X%xu~?G4 z4FgV7B(F3LmEsnJmWo0CoDWYVz1?Mn7NUr*Z`ec(7FWCos}k%Z4SABUh-qbtNo`)7 z$we&>s+jLafSo=qh@Z|9Cmq6^-A?NGU{~rXCfq1VOA+A-Q^~6@^U=Ti9_))ZIkV<1 zTZF2iH>xmTOW>?|q1iXhTG5ht+u$r%?9PQIApJ0*;PR~y()Q)smb6Z8UAE(lISCX& z1_^f#ah)2qLf20~B{Qj<;e}lwBoXr8w=WczYDG<5sc0r`id~50Oj$6_!D#h9>Tx|RlYDyoa7nbGgNW&bz0bvxODemy4IsG1)p}@ zrZ3SPNYZ}|_{2uufMVANQUyiV5x%0`KWyR%a9aXCfUbQu(om?-^?`T?S`_TC9h?>O z4F?`YB^;Udy|Y_B4fnL9JbG&KcB#Q{H{dG&pA2DZ&Lk~L_k)e!fO@YV*i7|{8b$ZqvMTK zn)U>4!MdkG5^i3P_R#(XN94^bZz?os@wTV?O=bZo^r6-7TICCQ!$&;Z%wDau%vwxh zTH?1OEnZnXD&c#GECyCae&{RwkDQT_k*V(KfK@8I?b~1U9OfPx7BFM>sajy5FtIjK5+2oTsV9q^g|;#U9bk9i3aRdN2b|6%j_m4PV%U!jyy zL^wT&)UjhP6S0v>fbp`FQ?<1!k;VJvIUdyJNOuS9zR#0;pk(x3+jzNtsn*%Son^nr zXLQiTl?2H47mt&{OV!tT#%nOZOR;OCshPAqojzzjrrEqDL7xx@nDi;0kgmBEHZ=Br zeZ7iqm}8m!XLz&7!9b^`8JwQdQ+>VwRDb=>1wbmp7m;{irLNzq-KQi< ztOB->!X$_dLU#kB(v6E(Gee{Y)+LCNe&dt_s<+ZUKYF2aIlw6pP)S`TXK9;ybA-0a^f0k$tuzJbgV*7mLt*b4C$6|x{gEroP{K23C z+l}eiI7Z;V;X^!DmP@E9N01@zgRDzxQiESxaU6JDDyG@#3SWa?eLeAtSDK|B)OCff zjq?2$(&YSbq44h_rNzL4Ok8q@|hb?tsd;q(NIfQ|XGzMx)3< zfmc+H;JBoE2xTV>OdPS1KumrTWQo{;jE(YAiESBikyE_u1nSX6Pp;QBK|*ViH^a`U z9eYpYBrsQ8Fd;P}q03z%_k#T4aexWMu2;N9QJ8nFj^(hxQL9|X>rqmYZe9YnxD4Ii zyU5S=Y&B}BDK^<62wTM_2g-3omZO^c9^fK!@S|U`GKL`IGB;DDLn5gfd6{auu4w^F zTaqho*Qji3k1kt~l7Lm3Od3pI-()dKpHRjW_i?_(>3~^L77S?tXT0TS*DYqqiqqdy zPn1+av%rq-=cM*=9>`{q(IB!)Y5~QCwQ*&1?_|w9+|AV7YV;YLf7rQNW*%NF{wZEF+HToJXZfO5;L5!uE;%~Ou3CXT@~1B2qnl*a@-iK62f{QZ z$W!Zd>J=jyVl66c)N56eHJ%+X4UyON@DHeDNUor2I=DO%=U~W6s5+}LCaIoRmyvQm zsDD#a_l8N!ilx1L0ymaq`K?Q=gY6J%_~ya<)_uUAOQdRV{uZ_6OLX>YnmPLnnO)Pw zdxdp3g(dfuunY(6U1xCOc;82X8>ggSOseR-h+4;3`>)cH# zoNF-x-9DD*%Sd&x)Kb*wBL`3UUB8>KT+zR-DpftJE&@m^gf439KRY3LFE9T(v*akp z;V+$u)w<>SQ=0^~hkoonKN6IcJ~hKjVSIO}z(v>KcBSVf z!abVuQZd>63IVsm6o$(6Q_m?MV>{73J6`^w)+te+Bbq&xNy%XfzR`XXhmxILYjIlS zT^{>=-Cr^sot<-m_?fl|^;4mB;-L}M5BzjfQ}R{jy;Vd8%&7`0*|~zpfI3+@WHi1)rlkE-zx~ zDy~S?1)`zIy*nVjs8WAculpSg_$8$0wpW9ks_Kxkq7j51RUH^Jz~?lg9$Rz8XU}Vz zAr}cTe-Numg1e-{W1#j)V?7^9d_0f~XcfEHxKl>kae3ZstmSON)Akdsa+TR2l86pr@DQ=36z3g!ynqv_2hdnm8;xON!d28 zvWSa6Byz^#?xWaJ0@zYFFoHlHx!B{K&ro*^AS`S zMI-kstkci(?9zmws(OEJTPJk%yvN4I1{RUP9l5jl{SvF(URnoEr52fe4-Fyiz>Tc_XuRkj&1s3?AaMnXY|rZU8wzj|_S;~~oaKXqgPpp$$ZQ&(oxg2u ziz(OcklSwa!?SGyIXA1@&zf#2 zgnv8?gdC>WF#TF+#}kWA^J`41M}OSs%ax)l7psq3++_f zpQddq+VRA8u?d{g!p#PBZeX8dCOBi{&r{w1acm(uk<$#dnyw9#iDZ|S|JSCaF?Lgv z9$Iq7N-8QVk5T8+w?+J5XV$1d|5(|;1xm{H9J6Gi{xVMeJX9u@t9X)5Z1_*p&J03Y zVq=3{K`yos=wb#xdu_|`oVDRU*csofm*3yl1aQP%#A^2Jj51MvLb;Hoj7WrXa6y?i z*O9{ShXfZ1m=Lrt1B9zMFxD8m5ss68QFwN`8=vL(BSQ`>q2iJhg!A~8V=p~XLClSb z{^M5Kv_sLyM%gz?C;jbfJ|!YKrM^1#0hh}6ulVKEt$l(EP-s#gB8;L{{Qn8|X zB##N>*;y03!}ozJH9Uz*7^w95{*+3s`PSN@$BNa$K$=| ziX2K%&IIPrwYh?BN$T7XkcvzcIsNfv2Z|}vNBQ?j4RVylpAy5&DE6meCP($^%q8D8 zysyMXY%2$p-0tS&NI7@eLdM4Y!IN2X67$=h!7yXT4QgrV%G zWK#)>lTBsbh6G9HhSt*mX%{K*zhnqfW0~yuVOCvE5&t{IJoB0S4~~C#=+aFGuje>f zSXhK!pu4gTz;@b{Os^~f1(SlRvM^8ywe$^;1}hH#?QB`k>u-lrZi0i3;YVO{H3(|l zyOFCY3kus}tc*>vCh>Q=XV?BTWxHvI44}k+ifwHyfewh4jkmi~x?eGfe6y@H?|A1g zK=SO;2-{i#=IX2bxEJ-1qZ0>U+*nUBG4&t$%a5_#0LuP`49PXFDxnhYJdTH6-*3t_ zo91Vqjy(P!b)v4_Whi!%N&>>pO?{Arb4{>2Z7^0ARMtbZFKa%IlZ+xM{pIurBo^4_ zd<`i(vv&75an2v1#bqS~m~&mq*#Xqm50acOksM#brf0p6gy|3AJ4Y%KU06|)-QC%D z=O$>*x6r>qvbdxy;pMQxP2j^n85Xl3B1Dyfq9thhDr9uH#_-1mn7(S%SB=nv>Wi+4 z#8<73$W0rff~OP&L9$N+jVT`?E#!+jO2N0T93|5o*X{y9%u@YolSenZ0K&01rc#l4 z$&Eg13fMym_=IG(X1CAY@dhZ77o%9F?V!AmSDH&b!U88I~9q=q6<<3t2vjmc>iiM(Q*j}qtCTka;Lg-TR^Ikw;ZoewQLlF24>lEt6-<$%*M-*Ta!3}=9iH^ zU)NP2VOf^tSEKeWz5wO*GT-}4O6H{_Xt#ChR5b9~ue?1nNs{R26`&Dl1;kN*M^noV zqUk+?nvteMRtdlKXvx4>;;pFVcEjLIkN~ZPBxFFIX=*&NFO)^~GG--^G`$!!&8Voj zkTNT(4qGLoawk3rc5WXn;Fnt5I5m`7t{W~Ye~dv2?3^-cDT?JJF=L0h!bY&%D#qFp zq)c~~bK+3vsPzdz{72tmV^G+%r-2b!7jcRqOTKX!+p`#co0A+9&|oB?wmI(Fhewtm z6t-njPIhBiq+)zO<5R5b4ofnA2gq)_o<8X02T>*ALh?cZ&t(yaBF5;o+L*m<&sY@*{!cqyRFG^hb0G!50SDe1l8|H}JhZV; zLg^TgVh>5KQGQ6X(DLnWXyUMXOmZ8??eU4FF zyVdTyrJCvr%Y?GL;gd5Wy-GV7s@2pHy_@akszKe;S9;%AhWk2rvwm(Kmt?ZJA-_Dz| z{p=I(+oe+b4H8aN&^NR-@_Ou`RZH`m_~eDY;F6puJ|ECobdBYch+xwsmYoxp?6^*R zFpCUt&-(PP-%m;Tar;Hle~h|wmvjYh98+TS^y7|+c)-8zI0uUUV=G{l)(`JBj+$Jf z5^|Vw*h1sJn`q4tGT)Jtzm*O1Owy9o1I3*3(l=Qy@=Y50IiVBn444_YdyLP6B~xC0 zr|L-5NZU=Rnn0F!e3P=mTb#-oCBMGhLgS1Pa^litKEDE+vM}CBCAETV*xq7N&m)5& zoZ8lKUL?#pIES7HX*YS7aKEe~Sn8mmI{AHHv~tHPDu(k;>RWX4ph8|7i1>{V|}n*5C` znZGIAnewB4J#dVDJhl7uYpE^mpPgS@x=NLPbv$;gbhP{NP_1Kbk3-|2UQg|zvvwQi z1}}O-h&P9Qwtw_2zL&3uSRLOII$$W`HGS8Xq%9{*VZAySsm5)o^V_Irrk7 zSX9H!Uk*MqxjV^h%GW(JYhoqch5%##p1ZMK;3#lzt2yF-5((TC@p+AXaR!qDUJCch zLi2*SBaXgh%&(ex>CmD!?kG9+VAN>G7OV@nm zI<7PoTDZ~E*p#VnmejZ_b=+XUcSKg;W$tix&*dKx8fwKhs|6$;r|Tgy^l?pU@@#MWh>Kn8u=D+C z!k5~%bgtbcQsy@L@FadjY_4|>oXiEnpxYunx=OuC4|5Rjc#eX;X~rJ(R&JcXY_IrS zJ3%nXwC_ohp4{Tthoc?>a=4GGmav$Um!wPb|F|Za)ja>Q>D<;UL^fk^;Jlr;D$fs!F?&;Kw!ndfh*Dr+bK?~nH0+GNAzcA z9`I99G;B;VF$Si%OyB%{jXuWB|NWEW%q(>}<*Fo7oDHGx=i0f;djjQ4Db0S3yd65r zs0#IBQ_NSR?o2W-UVLw9tHI*MS3fOhMe^f6_EhYZkj5v$GOnb%9Yxt=dd_-IPV8noB3pO3obDzqjxJ1s;y>|zHxzZq?-M8qkP28nOPfD4*-to6f=(`qJvvp^R`m^liYohQi}R{!L?*M z%I>#ji?a{@z60Bo%(6Q_oHLwF_A*QZ@oA)duN$VQIzNnId;QETQcGUN{6>rL7rN{+ z9XD1SnpkhsI-(+b(`!gYRyIP%+f@!{(O7)~Nu9-4R~xCX^7J;?+vF=awysGMUh1hp zT)gmC|5En{gT#~t49DaJy}s;eix%dD(kJK@Uj^x4@m+a}6?IiPL&-+9RTsJG1POXS zOqfACxj%P+tI#l(o8>lKEInn#vUN56&Ux4D&`#dE?Y$g*10*%cnntJW=S|!3|zvN{41mlgJDBR2Tzk++jH(4g#YF=aF^49UGNSYonTc8>U!q^ zCby5hrR|yl#XQ}2ugO>~ALEhtv@;ZWdQHnwcNGFi7sTDqIKGPQ2t(G$DJO9aCxf9! zLCHRfTmRd;$c52v@#|Zsp07K6KmDUuF~(0`)eczN+{LPOaB-lv;YGVui$4>)WB$&q2focQx-{BAI+Q#4TuJmMt1M?KYh&IM7e8FI(Vw@p09hdpl2F zbz|$R#N#a22ZAqZO*-q+l94-@U@6z<>n&9)ef5~|S~|gaAmU~qy7{CkHW9MHDOBN$ zQ;xqIo3!H5aVTeqZ1QO?oyl}K=6r6^3N!D;nSqOY>0KsB59q+Xk;($-^B;WUd_Bdnd##mD!$7(D3m6W}?fIRS8jG5BSx{Je9wc z$u50}Sg0X}J8gtB>2)J!j}vL0wRk~Hn0&~41i5`({K~hqjkl~Q;bO?b0B*0nqvuXP{CAGHe~KS zb5&eVDu1Z}E0wvJEh*r5wtDkXC*X-PTlKauXq6ahILp3pn%&5Mws=JAQ&C@-rwD6z z7`#C;*}&IbmtN%)7MQX%VaP%p@4J)f(4$$0Jv{d?!?U)EJX`mj3oz|vnFL<1-gR4e zW4_Lw;zeO!fbEPnEu-rqF@wj}drJ(KlyT8i{hxw8$3}!0)`pTv5KsAn&4s_FhRwLz zy?FPk#U#+x-g>398|0~nhFtlFVn?`Lp1Xv8N}Y3i(abe6JKsF*Vg0RcaofrJ!dy#T z5cP=eI^r$X31zi8q{AS-;RW@pW$nc`*PlCIVO>VLP?`xZGWSI?m*&o_TdZeFfPwro z@`iXHLYBjC(Y{!^CmZNX(QNP_nd_|42Jz8CK^LK@#U&R=6lZiUg;dlJ#{Ut{^42sE z)ZfhjCXI86770r|cYdVKB&<=Nrp;KJ>o8twieWV&s&BeNO7C|$KW;zlUZSaM!RpL* zG8Fr7^JhrEJv?<{scas}+=vl>Hm8ti$L(7DMMF!Em92gQ0A}B+krAwts7ut#tx0El zA|*+u)WLK9K5v&6F)D6Me)m#EYWC#wWZ?z~q1^D0@)n9&v9~U5A-;T^>0A%#o)?3* zaLb5C$c#k;WvM{;Ml2I*R??snkuLKne*2T+r!t$@s z-3qSyYV#0p+r3SSs6{i{1^+Rm@+p62>>lJ-d|cn>@B38nZG*n^0Avwa-tHQ?X&{%o z?8vUwG{h$>TXcxtpnq+XMqV9DlB*gUpQ;>kB3iPuF1AW9Hig70xk=*A-%`Ci=ZP}f z!DStp$JfhS3M9iyvI!?&Z*NcX+3FEC`PsOcA zsQ`rU=OMiUb)R`=3CWtDvq@LZOXT;S*86jFJGY^;=aQ@PEitQ)i%q$S{}3b42Mrz^ z1p25CNqoBmbIemJ2rz5nG5x1+9FmA*n-AyDv^?)Aavq{QMP?z$FdE%Ipg(77cxB_r zuJu!Th8KBtsG@tmA=SA@S{so_OkG0VfkvFo_Wf-mlL#kG!LIca{T=Tyk7lm6FpkCD zX6^ODg3o$$?~_=?GuD$LK_vItOvcFCXtniQ>X@VinwaIGyjuoF3=eC9Q zWsfxB+@|8^X>N+LH=mKb!vk>MiWPI4{$QH5qTQ3_@YDhPMa+gnsx-8n=HbB;<3?gC z52RF*ZRRanxSE^$hF45Xqxy}!Tu^88xi4+FqpBx+{ttofuz7+bjf4x>O!70=cRjM3 zMSUUrk!!*-mqaJT#Vz7Dl#@Nr-fAG9bjcO=<#pf{lUG}-%-MU`zP;Ur-_Z+XaJ)QIbg z5)eAlG+TOEL!XcBIT9S%h#J4&BCtR#=AOc-dUgK$70IYC69fW(xgkL??2GvP?f3EdRAX(DnO!o zo0DM5I^C;m)wkCc=2|+|)#(%)&n|X7>pmfx!g)9!(d4JfKK*n&^Ha3dakxOkt+;O) zcTa_;Zo(i{$=%jHs~_5j9OE+^Rir#eye)c+qkYFca$E-9OvwilXEvsP>Ww{h{JB<+ z@R)wfh18tEgoIY%oV?sC<7y;a{30oW(uVG6A%8o~#%3iBU{zpe7+ zsFgOIZ6C`*YzNhXeYUx3f9?jJD}BE|SJ{M)a1UYx?t&(HrhwAlqfhx}H?@c@}3K`J>vJh&R|iN(;LlcHLUj z$YwHO-pze|AQr8soc(%H#fe+_(F4|e%(fBU)ucd~08^SrbJ560(cIHqe;N6?6Y0J9!M7X{OnIqCN2B2TN*WvB6A&Pg)0BxPo>aBPQe=Kdp34~R*i zWZ`2!@Mf`J5aP+anyLBI_BM>8qetxX&aRJ<-WN zRZkv_IhgtmsbutlB%RXIh>GAN8;PqEf_;Lm>dP(E{tsvG85Py`to=%mEICSUa!v|J z28og+35aA65Cjn=2sAk~IR}XXN)RQAft;EsASeQoL0c4rCU!#ucP{sTpL522?-}DA z_tXC3*z{U+&6-s;>-jzKj1ubwTj!Z)Pj*04k%bBQAlbtR{|qc~93-YY(~BVcsnf=j z(T}mz)JCime`CP%a4djW=1il{xCF`YU!DKH_}{A)KCA;ScS37Fj4Yv^dcG#>c#Jd2 zefONzRUnsPhcX5I?@l>32>g{_H>__iFnO z?`>wHziYsCAhiiE`}w{m(r)ps<909H1iW^1FQiSHcDV~@6>{phlCHN~BisU1iI4*! zG9e~vhT*yJ{;LHc&)yOEYeXR+!~pxW-&OGd>fO+daES$d;aL#ADdb|=)P!E zvH-gB<-ipd5H#;F4BkpKzvA-p6G$z@A<)ySaeND#4p4~>>{Zry#DDMy-at||i(Dkm z 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 WtsO%Av|uzeCi@Q`_ +provides similar concepts for configuring input devices and actions. In +FlashDreams, users can configure arbitrary key bindings through +``InputSystem``, which converts device signals into canonicalized user input. + +.. admonition:: Example + :class: note + + Either WASD or HJKL can be bound to movement directions and mapped into a + 2D character-movement vector. + +.. admonition:: Queued events between pulls + :class: note + + A call to ``InferenceSession.step()`` can take approximately 100--1000 ms + because it runs a latent-diffusion step. ``InputSystem`` must therefore + preserve every input change that occurs while inference is running rather + than returning only the most recent device state. It queues all events since + the previous ``InputSystem`` pull and returns them as an ordered list of + timestamped, canonicalized user-input events. + + For example, assume positive *x* means right and positive *y* means forward. + The user presses W at 5 ms, presses D at 10 ms, releases W at 50 ms, presses + S at 60 ms, releases D at 70 ms, and releases S at 80 ms. The next pull + returns the following canonical movement states: + + .. code-block:: text + + [ + ( 5 ms, vec2(0, 1)), # W pressed + (10 ms, vec2(1, 1)), # D pressed; W remains pressed + (50 ms, vec2(1, 0)), # W released + (60 ms, vec2(1, -1)), # S pressed; D remains pressed + (70 ms, vec2(0, -1)), # D released + (80 ms, vec2(0, 0)), # S released + ] + + The timestamps are the original event times, not the time at which the + application eventually calls ``pull()``. + +This layer handles device-facing concerns such as key bindings, dead zones, +axis conventions, and event sampling. Its output describes the user's intent +in a stable, device-independent form. It does not create model embeddings or +know how a particular inference pipeline represents conditioning. + +InputMapping +~~~~~~~~~~~~ + +``InputMapping`` consumes the ordered list of timestamped, canonicalized +user-input events returned by ``InputSystem`` and produces the model-ready, +per-step inference conditioning expected by an ``InferenceSession``. Depending +on the model, this conversion can include embedding control values, rendering +a control representation, changing layouts, or assembling tensors. + +.. admonition:: Example + :class: note + + ``integrations/omnidreams`` represents canonicalized driving input as a + floating-point steering-wheel angle and a floating-point paddle/brake value. + Its ``InputMapping`` runs the vehicle-dynamics simulation, renders the + resulting HD map with the Ludus renderer, and produces the rendered RGB HD + map as the per-step user-input condition. The resulting tensor has shape + ``(3, H, W)``. + +The ``(3, H, W)`` output is specific to OmniDreams, not a universal +``InputMapping`` contract. Another ``InferenceSession`` might expect an image +embedding as its condition. In that case, ``InputMapping`` can use an image +encoder to encode the frame and return the resulting embedding instead. + +This is the boundary between application-level control semantics and +model-specific conditioning. Replacing a keyboard with a controller should +usually affect the ``InputSystem``; replacing the model or its control encoder +should usually affect the ``InputMapping``. + +OutputTarget +~~~~~~~~~~~~ + +``OutputTarget`` consumes the ``FrameStream`` produced by the inference +session. A target can present frames in a native window, send them to a video +encoder, publish them through a WebRTC host, or adapt them for another output +system. + +The output target owns presentation and transport concerns. It must not be +responsible for interpreting user controls or advancing model inference. +Buffering and backpressure policies belong at this output boundary so that a +slow consumer does not silently redefine inference behavior. + +InferenceSession +~~~~~~~~~~~~~~~~ + +``InferenceSession`` is the execution boundary for the main inference +pipeline. It accepts inference input, maintains the state required across +autoregressive steps, runs the pipeline, and exposes generated output as a +``FrameStream``. + +The session receives model-ready data only. It does not poll devices, +canonicalize user intent, or present generated frames. After accepting global +inference conditioning, it retains the active global condition across later +steps until the application supplies an update or the session ends. + +Input data flow +--------------- + +The input path deliberately separates physical device readings, semantic +controls, and model-ready conditioning. This separation allows devices and +models to evolve independently. + +User conditioning +~~~~~~~~~~~~~~~~~ + +Raw user input +^^^^^^^^^^^^^^ + +**Raw user input** is a reading or event in the vocabulary of a physical input +source. Examples include: + +* WASD key presses and releases; +* digital wheel input; +* controller joystick readings; and +* Meta Quest hand-tracking readings. + +Raw values can depend on a particular device, driver, sampling rate, or key +binding. They are consumed by ``InputSystem`` and must not be passed directly +to ``InferenceSession``. + +Canonicalized user input +^^^^^^^^^^^^^^^^^^^^^^^^ + +**Canonicalized user input** expresses user intent in the vocabulary of the +interaction, independent of the device that produced it. Typical structures +include: + +* a 2D character-movement vector and a 2D camera-movement vector for + character-control games; +* a floating-point wheel value and a floating-point paddle value for driving; + and +* hand-tracking positions, correction vectors, or another agreed semantic hand + control for Cosmos-style interaction models. + +The exact structure depends on the interaction type and can evolve as its +semantics become clearer. The important invariant is that equivalent intent +from different devices has the same canonical representation. + +Per-step inference conditioning +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Per-step inference conditioning** is the model-ready encoding of +canonicalized user input for one inference step. ``InputMapping`` produces it by +performing whatever embedding, rendering, or tensor conversion the selected +model requires. + +This term distinguishes the changing user control for one step from global +conditioning, which normally remains stable across many steps. + +Inference input +^^^^^^^^^^^^^^^ + +**Inference input** is the complete input delivered to ``InferenceSession``. +It is the runtime boundary object, not another name for a raw or canonicalized +control. It can carry: + +* the per-step inference conditioning; and +* optional global inference conditioning. + +The first inference step generally carries both. On later steps, the global +condition remains active inside the session, so the application normally sends +only new per-step inference conditioning. Omitting global conditioning means +"continue using the active global condition"; it must not mean "clear the +global condition." + +Global conditioning +~~~~~~~~~~~~~~~~~~~ + +Global conditioning establishes the scene-level context for generation and can +contain model-specific data. Two of the most common examples are: + +* a **global conditioning frame**, sometimes called an initial frame by a + model; and +* a **global conditioning prompt**, containing the text description for the + run. + +The runtime uses *global conditioning frame* instead of *initial frame* +because the condition is not inherently limited to initialization. A future +runtime can replace it while a session is already running. + +Raw and canonicalized global conditioning +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Raw global conditioning** is the application-facing representation. For +example, the prompt is text and the conditioning frame is image data. + +**Canonicalized global conditioning** is the model-ready representation sent +through inference input as **global inference conditioning**. A text prompt is +typically converted into embedded tokens. A conditioning frame is +model-dependent: one model might convert it into CLIP embeddings, while +another might retain a frame or spatial representation such as an HD-map +condition. + +For that reason, *canonicalized* is preferred over *embedded* for the combined +global condition. It does not incorrectly imply that every part of the global +condition must become an embedding. + +Updating global conditioning during a run +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Although mid-run global-conditioning updates are not implemented yet, the +runtime contracts must leave room for them. An application could, for example, +submit a new conditioning frame and prompt to make an OmniDreams driving scene +transition suddenly to rainy weather. + +A later inference input can therefore carry new global inference conditioning +alongside its per-step conditioning. The session then treats it as a change to +the active global context. When no update is present, the session reuses the +previous context. The exact effects on model history, caches, and transition +behavior are model-specific and must be defined by the corresponding pipeline; +the application-level contract must not assume that global conditioning is +initialization-only. + +End-to-end runtime loop +----------------------- + +At a conceptual level, one runtime iteration follows these steps: + +#. ``Application`` pulls ``InputSystem`` for the events accumulated since the + previous pull. +#. ``InputSystem`` returns an ordered list of timestamped, canonicalized + user-input events. +#. ``InputMapping`` consumes this list of timestamped, canonicalized events and + produces per-step inference conditioning. +#. ``Application`` packages that data as inference input, adding canonicalized + global conditioning on the first step or whenever it changes. +#. ``InferenceSession`` advances the pipeline and emits generated frames + through ``FrameStream``. +#. ``OutputTarget`` consumes the stream for display, encoding, transport, or + another presentation path. + +These boundaries are the central architectural constraint: input devices +produce semantic controls, the input map produces model-ready conditioning, +the inference session runs the model, and the output target delivers the +result. diff --git a/docs/source/developer_guides/index.rst b/docs/source/developer_guides/index.rst index 7cc2bac8..7b7b5057 100644 --- a/docs/source/developer_guides/index.rst +++ b/docs/source/developer_guides/index.rst @@ -60,6 +60,7 @@ generated clip, see :doc:`/quickstart/index`. :hidden: :maxdepth: 1 + flashdreams_runtime inference_pipeline_overview config_system new_integration From 1e8fe779c7fbb6a454c0cb81640b485364e6a239 Mon Sep 17 00:00:00 2001 From: Fangjun Zhou Date: Tue, 4 Aug 2026 17:52:17 -0700 Subject: [PATCH 4/8] Update InputSystem doc --- .../developer_guides/flashdreams_runtime.rst | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/source/developer_guides/flashdreams_runtime.rst b/docs/source/developer_guides/flashdreams_runtime.rst index 58e97489..85159ec2 100644 --- a/docs/source/developer_guides/flashdreams_runtime.rst +++ b/docs/source/developer_guides/flashdreams_runtime.rst @@ -45,7 +45,7 @@ Application layer ----------------- ``Application`` is the composition and lifecycle boundary for an interactive -FlashDreams runtime. It owns one ``InputSystem``, one ``InputMapping``, one +FlashDreams runtime. It owns ``InputSystem``, ``InputMapping``, ``OutputTarget``, and the main ``InferenceSession`` that runs the inference pipeline. These components are passed to the application through dependency injection. The application connects them, drives the runtime loop, and shuts @@ -59,37 +59,33 @@ the inference session. InputSystem ~~~~~~~~~~~ -``InputSystem`` owns the interaction with input devices and converts their -events into a canonical control representation. Raw input can come from many -sources, including keyboard events, a digital steering wheel, a controller -joystick, or Meta Quest hand tracking. +``InputSystem`` accepts an ordered list of timestamped raw input events +supplied by the application or an upstream input framework. It converts that +list into an ordered stream of timestamped, canonicalized user-input events. +Raw events can include keyboard key-down and key-up events, digital wheel +readings, controller joystick readings, or Meta Quest hand-tracking readings. `Unity's Input System `_ -provides similar concepts for configuring input devices and actions. In -FlashDreams, users can configure arbitrary key bindings through -``InputSystem``, which converts device signals into canonicalized user input. +provides similar concepts for devices and actions. FlashDreams has a narrower +boundary: it does not poll devices or configure key bindings. Device polling, +event collection, and binding configuration are handled upstream. -.. admonition:: Example - :class: note - - Either WASD or HJKL can be bound to movement directions and mapped into a - 2D character-movement vector. - -.. admonition:: Queued events between pulls +.. admonition:: Preserving events during slow inference :class: note A call to ``InferenceSession.step()`` can take approximately 100--1000 ms - because it runs a latent-diffusion step. ``InputSystem`` must therefore - preserve every input change that occurs while inference is running rather - than returning only the most recent device state. It queues all events since - the previous ``InputSystem`` pull and returns them as an ordered list of - timestamped, canonicalized user-input events. + because it runs a latent-diffusion step. The upstream input source must + preserve every raw input change that occurs while inference is running + rather than retain only the most recent device state. On the next runtime + iteration, the application passes the complete ordered raw-event list to + ``InputSystem``, which converts every event without collapsing intermediate + states. For example, assume positive *x* means right and positive *y* means forward. - The user presses W at 5 ms, presses D at 10 ms, releases W at 50 ms, presses - S at 60 ms, releases D at 70 ms, and releases S at 80 ms. The next pull - returns the following canonical movement states: + The raw-event list contains a W key-down at 5 ms, a D key-down at 10 ms, a W + key-up at 50 ms, an S key-down at 60 ms, a D key-up at 70 ms, and an S + key-up at 80 ms. ``InputSystem`` produces this canonical event stream: .. code-block:: text @@ -102,13 +98,14 @@ FlashDreams, users can configure arbitrary key bindings through (80 ms, vec2(0, 0)), # S released ] - The timestamps are the original event times, not the time at which the - application eventually calls ``pull()``. + The timestamps are the original raw-event times, not the time at which + ``InputSystem`` processes the list. -This layer handles device-facing concerns such as key bindings, dead zones, -axis conventions, and event sampling. Its output describes the user's intent -in a stable, device-independent form. It does not create model embeddings or -know how a particular inference pipeline represents conditioning. +This layer handles raw-to-canonical conversion concerns such as device-value +normalization, dead zones, axis conventions, and event ordering. Its output +describes the user's intent in a stable, device-independent form. It does not +create model embeddings or know how a particular inference pipeline represents +conditioning. InputMapping ~~~~~~~~~~~~ @@ -186,9 +183,10 @@ source. Examples include: * controller joystick readings; and * Meta Quest hand-tracking readings. -Raw values can depend on a particular device, driver, sampling rate, or key -binding. They are consumed by ``InputSystem`` and must not be passed directly -to ``InferenceSession``. +Raw events can depend on a particular device, driver, and upstream input +framework. They are collected and timestamped before entering FlashDreams. The +application passes the ordered raw-event list to ``InputSystem``; raw events +must not be passed directly to ``InferenceSession``. Canonicalized user input ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -205,8 +203,10 @@ include: control for Cosmos-style interaction models. The exact structure depends on the interaction type and can evolve as its -semantics become clearer. The important invariant is that equivalent intent -from different devices has the same canonical representation. +semantics become clearer. ``InputSystem`` emits these values as an ordered +stream of timestamped, canonicalized events. The important invariant is that +equivalent intent from different devices has the same canonical +representation. Per-step inference conditioning ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -288,12 +288,12 @@ End-to-end runtime loop At a conceptual level, one runtime iteration follows these steps: -#. ``Application`` pulls ``InputSystem`` for the events accumulated since the - previous pull. -#. ``InputSystem`` returns an ordered list of timestamped, canonicalized - user-input events. -#. ``InputMapping`` consumes this list of timestamped, canonicalized events and - produces per-step inference conditioning. +#. ``Application`` passes ``InputSystem`` the ordered list of timestamped + raw input events accumulated upstream since the previous runtime iteration. +#. ``InputSystem`` converts that list into an ordered stream of timestamped, + canonicalized user-input events. +#. ``InputMapping`` consumes this canonical event stream and produces per-step + inference conditioning. #. ``Application`` packages that data as inference input, adding canonicalized global conditioning on the first step or whenever it changes. #. ``InferenceSession`` advances the pipeline and emits generated frames @@ -301,7 +301,7 @@ At a conceptual level, one runtime iteration follows these steps: #. ``OutputTarget`` consumes the stream for display, encoding, transport, or another presentation path. -These boundaries are the central architectural constraint: input devices -produce semantic controls, the input map produces model-ready conditioning, -the inference session runs the model, and the output target delivers the -result. +These boundaries are the central architectural constraint: an upstream input +source collects timestamped raw events, ``InputSystem`` produces canonical +events, ``InputMapping`` produces model-ready conditioning, +``InferenceSession`` runs the model, and ``OutputTarget`` delivers the result. From ddae9749a04247b4ecf80eab12cc95facfbf9f03 Mon Sep 17 00:00:00 2001 From: aidanfnv Date: Wed, 5 Aug 2026 09:58:16 -0700 Subject: [PATCH 5/8] WIP Implement T2, T3, and part of T4 from API refactor plan (#413) * WIP implementation of T2, T3, partial T4 * Fix issues found by Claude * Rewrite based on discussion, port after merge * doc update * doc updates * Update based on new diagrams * Align closer to diagrams --- docs/inference_runtime_api_design.md | 111 +++- ...inference_runtime_inputs_implementation.md | 287 +++++++++ ...ence_runtime_supported_inputs_inventory.md | 321 ++++++++++ flashdreams/flashdreams/runtime/__init__.py | 59 +- flashdreams/flashdreams/runtime/canonical.py | 387 ++++++++++++ flashdreams/flashdreams/runtime/inputs.py | 359 ++++++++++- flashdreams/flashdreams/runtime/interfaces.py | 30 +- flashdreams/flashdreams/runtime/mapping.py | 356 ++++++++++- flashdreams/flashdreams/runtime/types.py | 4 +- .../tests/test_inference_runtime_api.py | 154 +++-- flashdreams/tests/test_runtime_canonical.py | 590 ++++++++++++++++++ .../tests/test_runtime_input_mapping.py | 573 +++++++++++++++++ 12 files changed, 3076 insertions(+), 155 deletions(-) create mode 100644 docs/inference_runtime_inputs_implementation.md create mode 100644 docs/inference_runtime_supported_inputs_inventory.md create mode 100644 flashdreams/flashdreams/runtime/canonical.py create mode 100644 flashdreams/tests/test_runtime_canonical.py create mode 100644 flashdreams/tests/test_runtime_input_mapping.py diff --git a/docs/inference_runtime_api_design.md b/docs/inference_runtime_api_design.md index 2f0ba19f..f70fbd89 100644 --- a/docs/inference_runtime_api_design.md +++ b/docs/inference_runtime_api_design.md @@ -19,7 +19,7 @@ integration-specific runner code: - `InferenceConfig`: how the model and inference stack should run; - `UserInputs`: controls or events from an app, replay trace, or benchmark; -- `ModelInputs`: prompts, frames, videos, trajectories, maps, scene data, and +- `InferenceInput`: prompts, frames, videos, trajectories, maps, scene data, and other values required by a specific model; - input mapping: model/application-specific conversion from user-facing inputs into model-facing inputs; @@ -30,6 +30,12 @@ integration-specific runner code: - metrics/profiling: timings, memory, traces, NVTX ranges, and benchmark outputs. +Current T2/T3 implementation notes are in +`docs/inference_runtime_inputs_implementation.md`. + +The supported-model input inventory used to revisit T2/T3 is in +`docs/inference_runtime_supported_inputs_inventory.md`. + The API should standardize the envelope and lifecycle. It should not pretend that all world models have the same inputs, that all models use the same optimization stack, or that a raw checkpoint can fully describe how to run the @@ -62,9 +68,9 @@ Initial scope: | ID | Status | Workstream | Can run in parallel? | Depends on | Done when | | --- | --- | --- | --- | --- | --- | | T0 | Complete | Create experimental branch and contribution rules. | No, this starts the work. | None. | Branch exists, PR target is agreed, and main merge criteria are written down. | -| T1 | Complete | Minimal API envelope and naming. | Partly. | T0. | `InferenceConfig`, `UserInputs`, `ModelInputs`, runtime/session, output target, and mapping boundaries are defined well enough for demos to use. | -| T2 | Planned | Event-based `UserInputs`. | Yes, after T1 direction is agreed. | T1. | User inputs are primarily timestamped events; replay traces and derived snapshots are supported where needed. | -| T3 | Planned | `ModelInputs`, schemas, and mapping boundary. | Yes, after T1 direction is agreed. | T1. | Models can declare required initial/per-step inputs, and mappings can convert user events into model inputs. | +| T1 | Complete | Minimal API envelope and naming. | Partly. | T0. | `InferenceConfig`, `UserInputs`, `InferenceInput`, runtime/session, output target, and mapping boundaries are defined well enough for demos to use. | +| T2 | Complete | Event-based `UserInputs`. | Yes, after T1 direction is agreed. | T1. | User inputs are primarily timestamped events; replay traces and derived snapshots are supported where needed. | +| T3 | Complete | `CanonicalInputs`, `InferenceInput`, schemas, and mapping boundary. | Yes, after T1 direction is agreed. | T1. | Models can declare required global/per-step inputs, and mappings can convert canonical inputs into inference inputs. | | T4 | Planned | `ModelRunner`, `InferenceRuntime`, and `InferenceSession` skeleton. | Partly. | T1. | A minimal standard loop can initialize a runtime, run at least one sequential session, and close cleanly. | | T5 | Planned | Output mode selection. | Yes, after the result/output shape is agreed. | T1, T4. | A run can choose output behavior such as MP4, JPEG/MJPEG stream, WebRTC, benchmark artifact, or headless/null without changing model code. | | T6 | Planned | LingBot migration. | Yes, once T2-T4 have a usable skeleton. | T2, T3, T4. | LingBot runs through the new API path with its event inputs mapped into model inputs. | @@ -97,14 +103,14 @@ Main runtime flow: App / integration / benchmark / transport chooses how the run is driven and where output goes supplies run setup: - InferenceConfig + UserInputs + ModelInputs + output/metrics options + InferenceConfig + UserInputs + InferenceInput + output/metrics options | v ModelRunner / standard loop orchestrates validation, lifecycle, stepping, output, and metrics uses input mapping to: validate that user/app inputs can drive the model - build initial and per-step ModelInputs during the run + build global and per-step InferenceInput during the run | v InferenceRuntime @@ -145,7 +151,7 @@ Create InferenceRuntime from InferenceConfig | v Start InferenceSession A - initial ModelInputs: prompt/frame/scene/etc. + global conditioning: prompt/frame/scene/etc. per-session state: cache, current step, reset state step 0 -> step 1 -> ... -> done outputs -> Output target @@ -154,7 +160,7 @@ Start InferenceSession A | v Start InferenceSession B - new initial ModelInputs or replay scenario + new global conditioning or replay scenario independent cache/state step 0 -> step 1 -> ... -> done outputs -> Output target @@ -305,33 +311,63 @@ User inputs are not model inputs. A keyboard event does not have one universal meaning. One model may map it to pose segments, another to steering commands, and another may ignore it. -## ModelInputs +## CanonicalInputs And InferenceInput + +Inputs move through three layers: + +```text +UserInputs -> CanonicalInputs -> InferenceInput + raw canonicalized encoded +``` + +Raw device events are canonicalized into device-independent modalities before an +application sees them, so adding a keyboard, gamepad, or wheel is a converter +registration rather than an application change. `InferenceInput` is what an +`InferenceSession` actually receives. + +`InferenceInput` describes the data the model or inference pipeline actually +requires. Both it and `CanonicalInputs` distinguish two conditioning slots: -`ModelInputs` describes the data the model or inference pipeline actually -requires. It should distinguish: +- global conditioning: values that condition the whole rollout; +- per-step conditioning: values needed for one generated chunk or frame window. -- initial inputs: values needed to start or reset a rollout; -- per-step inputs: values needed for one generated chunk or frame window. +Examples of global conditioning include prompt, negative prompt, conditioning +frame, input video, scene id, HD map asset, camera calibration, initial camera +pose, seed, or model-specific fields. -Examples of initial model inputs include prompt, negative prompt, first frame, -input video, scene id, HD map asset, camera calibration, initial camera pose, -seed, or model-specific fields. +Global conditioning is normally supplied when a session starts, but a non-empty +global slot on a mid-rollout input is an update request rather than a reset; +resetting rollout state is a separate `InferenceSession.reset()` call. Whether a +given value can be swapped mid-rollout is declared per field by +`InputField.update_policy`. -Examples of per-step model inputs include frame timestamps, pose segments, +Examples of per-step conditioning include frame timestamps, pose segments, camera trajectory chunks, rendered HD map frames, conditioning video windows, control tensors, event markers, or model-specific fields. -Model input payloads should use semantic names, not only modality names. For +Inference input payloads should use semantic names, not only modality names. For example, a first frame and an HD map frame should be distinct inputs even if both are image-like values. -For interactive runs, most `ModelInputs` will be initial values plus per-step -inputs produced by input mapping. For MP4 generation and benchmarking, the API +Model input metadata may also include a lightweight lifecycle label, such as +runtime config, cache initialization, rollout binding, per-step input, or +session update. This should remain query metadata, not model-specific tensor +validation. + +Model input names, payload kinds, lifecycle labels, and schema metadata should +be open-ended. Supported integrations such as SANA-WM, LingBot, Omnidreams, and +future external adapters may need different semantic fields. Adding a new model +should usually mean adding adapter-owned schema declarations and mappings, not +changing a central FlashDreams enum. + +For interactive runs, most `InferenceInput` values will be global conditioning +plus per-step inputs produced by input mapping. For MP4 generation and benchmarking, the API should also support fixed per-step model inputs so runs can be deterministic. ## Schemas -The API should support lightweight `UserInputSchema` and `ModelInputSchema` +The API should support lightweight `UserInputSchema`, `CanonicalInputSchema`, +and `InferenceInputSchema` metadata. These schemas are not meant to be a rich type system or a replacement for @@ -345,8 +381,15 @@ The purpose is to fail early before expensive model initialization, produce clearer errors, make fixed scenarios easier to validate, and avoid ambiguous dict payloads where keys only describe modality. +Schema objects may carry open-ended metadata for query-time hints such as +coordinate frame, units, rough shape summary, accepted file suffixes, schema +URI, model family, or source/transport details. Metadata should help humans and +adapter selection code, but compatibility should still be based on the declared +event capabilities, semantic model fields, payload representation hints, and +lifecycle labels. + For simple CLI text-to-video or image-to-video runs, `UserInputSchema` can be -trivial or omitted because there may be no live controls. `ModelInputSchema` is +trivial or omitted because there may be no live controls. `InferenceInputSchema` is more important because each supported model still needs to declare the model-facing values it expects. @@ -410,19 +453,24 @@ unless the checkpoint already matches a supported generic adapter. ## Input Mapping Input mapping is required whenever `UserInputs` need to become per-step -`ModelInputs`. In the T1 envelope this boundary is represented by a separate +`InferenceInput`. In the T1 envelope this boundary is represented by a separate `InputMapping` protocol. A model adapter may provide the default mapper because it knows how its supported user controls affect model-facing inputs. Applications, benchmarks, replay tools, or hosted runtimes may replace that mapper when they need a different wire surface or aggregation policy. +The selected mapping may be a single mapper or a composed set of mappers, so one +run can combine separate prompt, first-frame, and live-control mappings instead +of routing everything through one object. + There are two separate moments to keep clear: -- before runtime initialization, FlashDreams should select the mapping and check - obvious compatibility between the app event source and the model; +- before runtime initialization, FlashDreams should select the mapping or mapper + set and check obvious compatibility between the app event source and the + model; - during the standard loop, the runtime or runner queues and timestamps user events, then uses the selected mapping to build initial or per-step - `ModelInputs` from the relevant event window, often after the session reports + `InferenceInput` from the relevant event window, often after the session reports what it needs next. This keeps the Reactor-style contract intact: the model-side integration can @@ -621,14 +669,16 @@ registry, standard loop, concrete output modes, or model migrations: `InferenceSession`. - Step data carriers are named `StepRequest` and `StepResult`; a session returns `None` from `next_step_request()` when the rollout is complete. -- User-facing inputs use `UserInputs`; model-facing inputs use `ModelInputs`. +- Raw inputs use `UserInputs`, canonicalized inputs use `CanonicalInputs`, and + model-facing inputs use `InferenceInput`. Both remain lightweight payload envelopes with shallow read-only mappings. -- `UserInputSchema` and `ModelInputSchema` stay intentionally small: they +- `UserInputSchema`, `CanonicalInputSchema`, and `InferenceInputSchema` stay + intentionally small: they declare supported event types and required named fields for early validation, not a full type system. - Input mapping is represented by a separate `InputMapping` protocol. Model adapters may provide a default mapping; runtimes and applications may override - it while preserving the `UserInputs` to `ModelInputs` boundary. Simple + it while preserving the `CanonicalInputs` to `InferenceInput` boundary. Simple fixed-input runs can use `IdentityInputMapping`. - Output handling is represented by `OutputTarget`; `NullOutputTarget` is the initial headless implementation. @@ -660,7 +710,8 @@ Proceed with the proposed split: - `InferenceConfig` for model/runtime execution; - `UserInputs` for app-facing controls and replay traces; -- `ModelInputs` for model-facing initial and per-step inputs; +- `CanonicalInputs` for device-independent application-facing inputs; +- `InferenceInput` for model-facing global and per-step conditioning; - input mapping for model/application-specific conversion; - runtime/session boundaries for lifecycle and stepping; - output targets for display, streaming, files, and benchmarks; diff --git a/docs/inference_runtime_inputs_implementation.md b/docs/inference_runtime_inputs_implementation.md new file mode 100644 index 00000000..8d485768 --- /dev/null +++ b/docs/inference_runtime_inputs_implementation.md @@ -0,0 +1,287 @@ + + +# Inference Runtime Inputs Implementation Notes + +This note documents the input layers of the experimental runtime API: what +exists, how the pieces fit together, what the compatibility query answers, and +what is intentionally still outside this layer. + +Implementation lives in `flashdreams.runtime`: + +- `flashdreams/flashdreams/runtime/inputs.py` — the input types and schemas +- `flashdreams/flashdreams/runtime/canonical.py` — raw device to canonical + modality conversion +- `flashdreams/flashdreams/runtime/mapping.py` — canonical to encoded mapping + and compatibility +- `flashdreams/tests/test_runtime_canonical.py` +- `flashdreams/tests/test_runtime_input_mapping.py` +- `flashdreams/tests/test_inference_runtime_api.py` — the T1 envelope tests, + including a reference loop that exercises all three layers + +The supported-model input inventory that informed this work is in +`docs/inference_runtime_supported_inputs_inventory.md`. + +## The Three Layers + +```text +UserInputs ──InputCanonicalizer──▶ CanonicalInputs ──InputMapping──▶ InferenceInput + raw canonicalized encoded +(device events) (device-independent) (what the session gets) +``` + +| Layer | Type | Owner | Example | +| --- | --- | --- | --- | +| raw | `UserInputs` / `UserInputEvent` | transport, replay loader, benchmark driver | `key_down {"key": "w"}`, wheel axis reading | +| canonicalized | `CanonicalInputs` | device converters registered on `InputCanonicalizer` | `driver_command {throttle, brake, steer, ...}` | +| encoded | `InferenceInput` | the selected `InputMapping` | whatever the model's session consumes | + +Applications and mappings consume `CanonicalInputs`. They never read raw device +events: `InputMapping.map_step_inputs` takes `canonical_inputs`, not +`user_inputs`, so this is enforced by the signature rather than by convention. +Adding a keyboard, gamepad, or wheel is an `InputCanonicalizer.register` call +that touches no application, mapping, or model code. + +This path covers **live user control only**. Global conditioning is +application-owned data and reaches `InferenceInput` directly, without passing +through canonicalization or a device converter. An application that wants a +trigger key to swap the prompt reads that as ordinary canonical control input +and updates its own global conditioning in response. + +## Conditioning Slots + +Both the canonical and encoded layers split into two slots, and the split means +the same thing at each: + +- **global conditioning** — conditions the whole rollout: prompt, conditioning + frame, scene. Normally supplied at session start. +- **per-step conditioning** — needed to generate the next chunk or frame: + steering, HD map frames, camera trajectory. + +`InputPhase` is `Literal["global", "step"]`. The axis names *which slot*, not +*when the value may arrive* — see the next section. + +## Global Conditioning Updates Are Not Resets + +A non-empty global slot on a mid-rollout `InferenceInput` is an **update +request**. The session should apply it when the model supports doing so. +Resetting rollout state is a separate, explicit `InferenceSession.reset()` call. +The motivating case is changing prompt and conditioning frame mid-run to change +the weather in an Omnidreams rollout. + +```python +from flashdreams.runtime import InferenceInput + +steady_state = InferenceInput(step={"steering": 0.25}) +assert not steady_state.requests_global_update + +changed_weather = steady_state.with_global_update({"prompt": "heavy rain"}) +assert changed_weather.requests_global_update +``` + +Because `with_step()` carries the global slot through unchanged, use +`without_global_update()` for the steady-state case; otherwise every step looks +like an update request. + +Whether a value can actually be swapped mid-rollout is declared per field: + +```python +from flashdreams.runtime import SESSION_START_ONLY, InferenceInputSchema, InputField + +schema = InferenceInputSchema( + global_fields=( + InputField(name="prompt", update_policy="step_boundary"), + InputField(name="scene_id", update_policy=SESSION_START_ONLY), + ) +) +schema.unsupported_global_updates( + InferenceInput(global_conditioning={"prompt": "heavy rain", "scene_id": "town_02"}) +) +# ("scene_id",) +``` + +`SESSION_START_ONLY` is the one reserved `update_policy` token. Everything else +in that vocabulary, and all of `lifecycle`, is open and adapter-owned; this layer +only carries it as queryable metadata. + +Steady-state steps must leave the global slot empty; otherwise every step reads +as an update request. Converters emit every window, because live control is +level-triggered: a key held across a step emits no events but still means full +throttle. + +## Raw Inputs + +`UserInputEvent` carries `timestamp_s`, `event_type`, `payload`, `source`, and +`source_event_id`. `UserInputs` holds an ordered batch plus a `snapshot` and +`metadata`, and slices to a half-open `TimeWindow`: + +```python +from flashdreams.runtime import TimeWindow, UserInputEvent, UserInputs + +inputs = UserInputs( + events=( + UserInputEvent(timestamp_s=0.0, event_type="prompt_set", + payload={"prompt": "drive forward"}), + UserInputEvent(timestamp_s=0.5, event_type="key_down", payload={"key": "w"}), + ) +) +step_window = inputs.window(TimeWindow(start_s=0.0, end_s=1.0)) +``` + +`UserInputSchema` describes what a transport, replay trace, or benchmark driver +can provide. `event_types` declares only that an event type exists; +`UserInputCapability` additionally pins the payload fields it carries, so a +converter can require `key_down` events that actually have a `key`. A bare +`event_types` entry still satisfies any consumer needing no specific payload +fields, so schemas written before capabilities existed keep working. + +## Canonical Modalities + +A `CanonicalModality` is a device-independent input: a name and the payload +fields it guarantees. Converters implement `DeviceConverter`, declaring +what raw capabilities they consume and which modality they produce. + +```python +from flashdreams.runtime import ( + DRIVER_COMMAND, InputCanonicalizer, KeyboardToDriverCommand, TimeWindow, +) + +canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) +canonicalizer.register(WheelToDriverCommand()) # a wheel is one call + +canonical = canonicalizer.canonicalize( + user_inputs, window=TimeWindow(start_s=0.0, end_s=1.0), source_schema=browser +) +canonical.values["driver_command"]["throttle"] +``` + +`DRIVER_COMMAND` is the one shipped modality. `KeyboardToDriverCommand` reuses +`KeyboardState`/`normalize_key` from `flashdreams.serving.realtime.input` and +mirrors the semantics the Omnidreams interactive-drive keyboard backend already +has. Its key bindings are data (`DEFAULT_DRIVING_BINDINGS`), and the set of +tracked keys is derived from them, so a rebound layout cannot leave an action +unreachable. + +`ScriptedModality` is the mock/replay converter. It consumes no raw +capabilities, so a benchmark or test can author a scenario at the canonical +level without knowing any device vocabulary: + +```python +canonicalizer = InputCanonicalizer([ + ScriptedModality(modality=DRIVER_COMMAND, timeline=[(0.0, full_throttle)]), +]) +canonicalizer.canonicalize( + UserInputs(), window=step_window, source_schema=UserInputSchema() +) +``` + +Application code is identical between a real run and a scripted one. + +Converters are stateful, so feed windows in session order and call +`InputCanonicalizer.reset()` at a rollout boundary. Replaying the same window +sequence reproduces the same `CanonicalInputs`. + +When several devices produce the same modality, the highest-priority one that +returned a value wins; `CanonicalInputs.metadata["canonical_sources"]` records +which device supplied each. Every feedable converter still sees each window, so +a preempted device's state stays current and unplugging the higher-priority +device does not resume from stale state. + +## Mapping And Compatibility + +`InputMapping` is the canonical-to-encoded boundary. `InputMappingSchema` is its +declarative surface: `consumes` names canonical modalities; `produces_global` +and `produces_step` name the `InferenceInput` fields it can build. + +`InputMapping.validate()` raises, which fails a run late and cannot say *which* +optional model input a source would enable or *which* missing modality makes a +required one unreachable. `check_mapping_compatibility` answers those before +expensive runtime initialization: + +```python +from flashdreams.runtime import check_mapping_set_compatibility + +compatibility = check_mapping_set_compatibility( + canonical_schema=canonicalizer.canonical_schema(browser), + inference_input_schema=adapter.inference_input_schema, + mapping_schemas=(prompt_mapping, frame_mapping, steering_mapping), +) +if not compatibility.can_drive: + compatibility.raise_if_incompatible() +``` + +`MappingCompatibility` reports `missing_modalities`, +`missing_required_model_fields`, `satisfied_required_model_fields`, +`available_optional_model_fields`, and `unavailable_mapping_schemas`. + +Compatibility is evaluated per mapping rather than over a flattened bag, so each +mapping keeps its own consumes/produces link. A mapping the source cannot feed +is dropped and reported, costing only the inputs it produced. So a dropped +mapping that fed only optional fields degrades the run instead of vetoing it, +and those fields are correctly absent from `available_optional_model_fields`; a +dropped mapping that was the only producer of a required field still blocks. + +Because a mapping consumes modalities rather than raw events, one mapping +written against `driver_command` works for a keyboard, a wheel, or any device +registered later, with no change to the mapping or the model schema. + +`undeclared_inference_inputs()` reports payload keys a mapping produced but did +not declare, which keeps hand-written schemas honest as the code drifts. + +## What This Does Not Validate + +The schemas intentionally avoid becoming a rich type system. These remain the +responsibility of the model adapter, runtime, session, or mapping: + +- tensor shape and dtype, image decode details; +- camera coordinate systems, pose and timestamp units; +- prompt-embedding swap mechanics; +- whether a model can actually apply a declared update policy at runtime; +- deep validation of scene, HD map, or actor-state data. + +The layer answers "can this source plausibly drive this model through this +mapping?" It does not replace model-owned validation. + +## Open Questions + +Tracked against the runtime API discussion, not yet settled: + +- **Alternative valid input combinations.** `InferenceInputSchema` has one flat + required set, so "accepts `{prompt}` OR `{prompt, conditioning_frame}`" cannot + be expressed. `MappingCompatibility.missing_required_model_fields` assumes a + single required set too. +- **`step()` returning a future**, for models with a dependency on their own + output. `InferenceSession.step()` is currently synchronous. +- **`Input System` ownership.** The diagrams show it pulling events, so the + Application owns an input system. `InputCanonicalizer` is currently a pure + function over a supplied window and owns no source. Whether it needs to grow + one depends on the loop-ownership decision. Mock input and key binding are + handled (`ScriptedModality`, `DEFAULT_DRIVING_BINDINGS`). + +## Owned Elsewhere + +Named here only so the boundary is explicit; these are not gaps in the input +layer: + +- **`FrameStream`**, which the architecture diagrams place between + `InferenceSession` and `Output Target`. The code writes `StepResult` straight + to `OutputTarget.write()`. Output shape is T5. +- **Declared output modalities**, so an output target or quality-eval can state + what it requires and be matched the way inputs now are. T5/T8. +- **`Application`**, the class that has-a input system, input map, global + conditioning, session, and output target. T4. +- **Loop ownership** — whether the application or the runtime/session drives the + main event loop, and whether inputs are queued and batched. + +## Validation + +```bash +.venv/bin/pytest flashdreams/tests/test_runtime_canonical.py \ + flashdreams/tests/test_runtime_input_mapping.py \ + flashdreams/tests/test_inference_runtime_api.py -q +.venv/bin/ty check flashdreams/flashdreams/runtime +``` + +At the time of writing these pass: 87 tests, and `ty` is clean. diff --git a/docs/inference_runtime_supported_inputs_inventory.md b/docs/inference_runtime_supported_inputs_inventory.md new file mode 100644 index 00000000..ebe9d853 --- /dev/null +++ b/docs/inference_runtime_supported_inputs_inventory.md @@ -0,0 +1,321 @@ + + +# Supported Model Input Inventory + +This note inventories the inputs used by the currently supported FlashDreams +runners and interactive runtimes, plus the SANA-WM input surface on `main`, then +records the T2/T3 API implications. It is intentionally about input contracts, +not tensor shape validation or model quality. + +## Inventory + +WAN 2.1 T2V, Self-Forcing WAN 2.1 T2V, Causal-Forcing T2V, +FastVideo Causal WAN 2.2 T2V, and Cosmos Predict2 T2V: + +- Source/app inputs: prompt text or prompt text file, pixel height/width, and + fps or block count depending on runner. +- Model-facing initial inputs: prompt text plus latent/output height and width + derived from run config. +- Model-facing step/update inputs: no live controls; AR loop steps with fixed + session state. + +WAN 2.1 I2V, Causal-Forcing I2V, and Cosmos Predict2 I2V: + +- Source/app inputs: prompt text or prompt file, first-frame image path or URL, + and pixel height/width. +- Model-facing initial inputs: prompt text and decoded first-frame tensor. +- Model-facing step/update inputs: no live controls. + +FlashVSR: + +- Source/app inputs: input video path or URL, chunk size, crop region, sparse + ratio, and optional output FPS. +- Model-facing initial inputs: no explicit prompt at runner time; the prompt + tensor is configured in the pipeline. Input video dimensions affect + per-video runtime/pipeline setup. +- Model-facing step/update inputs: video chunks passed to + `pipeline.generate(input=clip)`. + +LingBot CLI: + +- Source/app inputs: prompt or prompt path, first-frame image path, pose path, + intrinsics path, total blocks, dimensions, and fps. +- Model-facing initial inputs: prompt text and first-frame tensor. +- Model-facing step/update inputs: `CamCtrlInput` with intrinsics, camera poses, + and world scale. + +LingBot WebRTC: + +- Source/app inputs: session prompt, uploaded/remote/default first-frame image, + keyboard events, reset requests, text-event catalog, and trigger events. +- Model-facing initial inputs: prompt text, first-frame tensor, base text + embeddings, precomputed text-event embeddings, base intrinsics, and world + scale. +- Model-facing step/update inputs: keyboard event windows become pose segments + and camera trajectories. Text-event triggers can replace rollout text + embeddings when the model supports it. + +HY-WorldPlay WAN I2V: + +- Source/app inputs: prompt or prompt path, first-frame image path or example + image, pose string or pose JSON, memory-selection settings, dimensions, fps, + and seed. +- Model-facing initial inputs: prompt text and first-frame tensor for cache + initialization. +- Model-facing step/update inputs: pose data is bound for the rollout as action + labels, view matrices, intrinsics, and memory-selection state before AR steps. + +Omnidreams CLI: + +- Source/app inputs: shared prompt or per-camera prompts, HDMap video paths, + first-frame image/video paths, camera names, example-data UUID, and optional + embedding save/load paths. +- Model-facing initial inputs: prompt list, first-frame tensor, view names; or + precomputed text/image/negative-text embeddings. +- Model-facing step/update inputs: HDMap video chunks passed per AR step. + +Omnidreams WebRTC: + +- Source/app inputs: scene directory or scene UUID, scene variant, camera name, + prompt/first-frame assets resolved from the scene, keyboard events, reset + requests, and optional postprocess preset. +- Model-facing initial inputs: scene data, renderer, first-frame tensor, prompt, + camera calibration/extrinsics, initial ego pose, and initial timestamp. +- Model-facing step/update inputs: keyboard event windows become ego poses, + camera poses per view, and frame timestamps. The wrapper renders HDMap + conditioning internally for each step. + +Omnidreams interactive drive: + +- Source/app inputs: scene bundle, keyboard events or wheel/controller samples, + view-mode/reset/scene-exit controls, and vehicle/chunk config. +- Model-facing initial inputs: scene bundle, selected camera, prompt, initial + RGB frame, initial rig pose, and initial timestamp. +- Model-facing step/update inputs: `DriverCommand` samples become trajectory + chunks, rendered frames, and world-model conditioning. + +Template recipe: + +- Source/app inputs: synthetic runner config: batch size, height, width, context + tokens, AR steps, and seed. +- Model-facing initial inputs: synthetic transformer context, optional negative + context, height, and width. +- Model-facing step/update inputs: optional synthetic control tensor. + +WAN 2.2 TI2V pipeline config: + +- Source/app inputs: downstream runners use this rather than a standalone runner + in this tree. +- Model-facing initial inputs: prompt text and first-frame image for TI2V-style + cache initialization. +- Model-facing step/update inputs: downstream runners decide controls; + HY-WorldPlay currently binds action/camera state around it. + +SANA-WM bidirectional and streaming on `main`: + +- Source/app inputs: first-frame image path, prompt or prompt path, optional + negative prompt, camera trajectory path or action DSL, optional intrinsics + path or derived intrinsics, frame count, fps, Stage-1 sampling knobs, seed, + precision/refiner options, and streaming chunk/block settings. +- Model-facing initial inputs: decoder context such as prompt, fps, + `save_stage1`, refiner seed, sink size, and streaming refiner window/block + parameters. +- Model-facing step/update inputs: bidirectional passes one + `SanaWMI2VConditioningRequest` into the single generation step. Streaming + passes one `SanaWMStreamingI2VConditioningRequest` repeatedly; the + conditioning encoder caches rollout-wide prompt, first-frame, camera, latent + shape, and chunk-boundary state, then slices per AR chunk. +- Model-facing semantic fields include prompt, negative prompt, first frame, + camera-to-world trajectory, intrinsics vec4 sequence, frame count, fps, + sampling parameters, seed, and streaming chunking parameters. + +## API Implications + +The inventory changes the T2/T3 shape in four concrete ways. + +First, a selected mapping is often a composition. A LingBot-like run needs prompt +mapping, first-frame mapping, and keyboard-to-camera mapping. Omnidreams may add +scene selection, camera selection, and HDMap mapping. The implementation should +support checking a set of mapping schemas as one compatibility surface, while +still allowing a single mapping object when that is simpler. + +Second, `InferenceInputSchema` needs a lightweight lifecycle tag in addition to the +`initial` versus `step` phase. The phase answers when the value is needed at the +standard-loop level. The lifecycle tag distinguishes where the model adapter +uses it, such as: + +- `runtime_config`: values that affect setup before model/runtime construction, + such as FlashVSR input-video dimensions; +- `cache_init`: values passed when initializing or resetting a rollout cache, + such as prompts, first frames, view names, and precomputed embeddings; +- `rollout_binding`: values bound after cache initialization but before AR + steps, such as HY-WorldPlay action labels, camera tensors, and memory state; +- `step_input`: values consumed for one generated chunk, such as HDMap frames, + camera trajectories, driver commands, video chunks, and timestamps; +- `session_update`: values that can update an active session when supported, + such as LingBot text-event embedding swaps. + +The lifecycle tag is metadata, not a new deep type system. If both a model field +and mapping output specify lifecycle, compatibility should require them to agree. +If either side omits it, matching stays permissive for simple schemas. + +Third, `semantic_type` should be treated as a representation hint rather than a +universal semantic type. For example, `prompt` may arrive as inline text or a +path but become prompt text or text embeddings; the global conditioning frame +may arrive as a path, URL, bytes, or decoded tensor; camera motion may arrive as keys, pose JSON, +Numpy arrays, or integrated tensors. The semantic input name is still the main +contract. + +Fourth, schema objects need open-ended metadata for future adapters. This lets a +SANA-WM-like adapter advertise that `camera_trajectory_c2w` uses an +`[F,4,4]` OpenCV camera-to-world sequence, or lets another model advertise a +schema URI, units, coordinate frame, accepted file suffixes, cardinality hints, +or update notes. Metadata should remain query information and should not become +the compatibility type system. + +Fifth, `UserInputSchema` describes raw source capabilities, `CanonicalModality` +describes what an application consumes, and mapping schemas describe derived +model-facing semantics. A browser may provide `key_down`, `key_up`, +`prompt_set`, and `initial_frame_set` events. Those become canonical modalities +such as `driver_command` or `conditioning_prompt`; whether they can then drive +`steering`, `camera_trajectory`, or text embedding updates depends on the +selected mapping and model schema. + +## Implemented T2/T3 Shape + +The implementation that came out of this inventory is: + +1. Keep `UserInputEvent` and `UserInputs` as the raw event API, sliced by a + half-open `TimeWindow`. Static startup values remain timestamp-zero events. +2. Keep `UserInputSchema` lightweight and source-facing. `event_types` declares + that an event type exists; `UserInputCapability` additionally pins the + payload fields it carries. +3. Add a canonical layer between raw and encoded. `CanonicalModality` names a + device-independent input and its conditioning phase; `InputCanonicalizer` + registers per-device converters and produces `CanonicalInputs`. Applications + and mappings consume canonical inputs and never read raw device events. +4. Split `InferenceInput` (formerly `ModelInputs`) into `global_conditioning` + and `step`. A non-empty global slot mid-rollout is an update request, not a + reset; `InputField.update_policy` declares whether the model can apply it. +5. Extend `InputField` with `update_policy`, `lifecycle`, and `metadata` so + models can distinguish runtime config, cache initialization, rollout binding, + per-step inputs, and supported active-session updates. +6. Keep `InputMappingSchema` as the canonical-to-encoded boundary, with + mapping-set compatibility helpers for composed mappings. +7. Keep input names, semantic types, lifecycle labels, and metadata open-ended. + Adding a new model should usually mean adding adapter-owned schema + declarations and mappings, not changing the core input dataclasses. +8. Leave deep validation to model adapters, sessions, and mappings. The schema + layer catches obvious source/mapping/model mismatches before expensive + runtime initialization; it does not validate every tensor and coordinate + convention. + +See `docs/inference_runtime_inputs_implementation.md` for the resulting API. + +## Extensibility Contract + +The inventory above is not a vocabulary freeze. The core API does not contain a +closed enum of allowed input names. New adapters can introduce semantic field +names that match the model boundary they own. + +Use these conventions when adding future model schemas: + +- Prefer semantic names over modality names, such as `camera_trajectory_c2w` + instead of `array`, or `hdmap_frames` instead of `image`. +- Use `semantic_type` for a coarse representation hint, such as `path`, + `decoded_tensor`, `c2w_sequence`, `intrinsics_vec4_sequence`, or `embedding`. +- Use `lifecycle` to say where the adapter consumes the value, such as + `runtime_config`, `cache_init`, `rollout_binding`, `step_input`, or + `session_update`. +- Use `update_policy` to say when a value may change. `SESSION_START_ONLY` is + the one reserved token, meaning the value cannot be swapped mid-rollout. +- Use `metadata` for query hints: units, coordinate frame, shape summary, + accepted suffixes, schema URI, model family, value ranges, or cardinality. +- Keep deep validation in the adapter/mapping. The lightweight schemas answer + whether the selected source and mapping can plausibly drive the model before + expensive initialization. + +## Representative Schema Sketches + +These are not migration work for T4+, but they show that the current primitives +can describe the supported input surfaces. All use +`flashdreams.runtime.InferenceInputSchema` and `InputField`. + +```python +lingbot_model = InferenceInputSchema( + description="lingbot-world", + global_fields=( + InputField(name="prompt", lifecycle="cache_init"), + InputField(name="global_conditioning_frame", lifecycle="cache_init"), + ), + step_fields=( + InputField(name="camera_trajectory", lifecycle="step_input"), + InputField( + name="text_embeddings", + required=False, + update_policy="step_boundary", + lifecycle="session_update", + ), + ), +) +``` + +```python +omnidreams_model = InferenceInputSchema( + description="omnidreams", + global_fields=( + InputField(name="prompts", lifecycle="cache_init"), + InputField(name="global_conditioning_frames", lifecycle="cache_init"), + InputField(name="view_names", lifecycle="cache_init"), + InputField(name="text_embeddings", required=False, lifecycle="cache_init"), + InputField(name="image_embeddings", required=False, lifecycle="cache_init"), + ), + step_fields=(InputField(name="hdmap_frames", lifecycle="step_input"),), +) +``` + +```python +hy_worldplay_model = InferenceInputSchema( + description="hy-worldplay", + global_fields=( + InputField(name="prompt", lifecycle="cache_init"), + InputField(name="global_conditioning_frame", lifecycle="cache_init"), + InputField(name="action_labels", lifecycle="rollout_binding"), + InputField(name="camera_viewmats", lifecycle="rollout_binding"), + InputField(name="camera_intrinsics", lifecycle="rollout_binding"), + InputField(name="memory_config", lifecycle="rollout_binding"), + ), +) +``` + +```python +sana_wm_model = InferenceInputSchema( + description="sana-wm", + global_fields=( + InputField(name="prompt", lifecycle="cache_init"), + InputField(name="negative_prompt", required=False, lifecycle="cache_init"), + InputField(name="global_conditioning_frame", lifecycle="cache_init"), + InputField( + name="camera_trajectory_c2w", + semantic_type="c2w_sequence", + lifecycle="rollout_binding", + metadata={"shape": "[F,4,4]", "coordinates": "opencv_c2w"}, + ), + InputField( + name="camera_intrinsics_vec4", + required=False, + semantic_type="intrinsics_vec4_sequence", + lifecycle="rollout_binding", + metadata={"shape": "[F,4]"}, + ), + ), +) +``` + +SANA-WM's `stage1_sampling` and `streaming_chunking` are deliberately absent +above. They describe how to run the model rather than what conditions it, so +they belong in `InferenceConfig`, not in an input schema. Flagged here because +the runner currently threads them alongside the conditioning inputs. diff --git a/flashdreams/flashdreams/runtime/__init__.py b/flashdreams/flashdreams/runtime/__init__.py index 03e6202b..ab303c74 100644 --- a/flashdreams/flashdreams/runtime/__init__.py +++ b/flashdreams/flashdreams/runtime/__init__.py @@ -7,22 +7,49 @@ intentionally additive while integrations migrate onto it. """ +from flashdreams.runtime.canonical import ( + DEFAULT_DRIVING_BINDINGS, + DRIVER_COMMAND, + DeviceConverter, + DeviceConverterSchema, + InputCanonicalizer, + KeyboardToDriverCommand, + ScriptedModality, +) from flashdreams.runtime.config import ExecutionBackend, InferenceConfig, Precision from flashdreams.runtime.inputs import ( + INPUT_PHASES, + SESSION_START_ONLY, + CanonicalInputs, + CanonicalInputSchema, + CanonicalModality, + InferenceInput, + InferenceInputSchema, InputField, - ModelInputs, - ModelInputSchema, + InputPhase, TimeWindow, + UserInputCapability, UserInputEvent, UserInputs, UserInputSchema, + validate_phase, ) from flashdreams.runtime.interfaces import ( InferenceRuntime, InferenceSession, ModelAdapter, ) -from flashdreams.runtime.mapping import IdentityInputMapping, InputMapping +from flashdreams.runtime.mapping import ( + DeclaresMappingSchema, + IdentityInputMapping, + InputMapping, + InputMappingSchema, + MappingCompatibility, + check_mapping_compatibility, + check_mapping_set_compatibility, + combine_mapping_schemas, + undeclared_inference_inputs, +) from flashdreams.runtime.metrics import ( InMemoryMetricsRecorder, MetricsRecorder, @@ -33,28 +60,50 @@ from flashdreams.runtime.types import StepRequest, StepResult __all__ = [ + "CanonicalInputs", + "CanonicalInputSchema", + "CanonicalModality", + "check_mapping_compatibility", + "check_mapping_set_compatibility", + "combine_mapping_schemas", + "DeclaresMappingSchema", + "DEFAULT_DRIVING_BINDINGS", + "DeviceConverter", + "DeviceConverterSchema", + "DRIVER_COMMAND", "ExecutionBackend", "IdentityInputMapping", "InferenceConfig", + "InferenceInput", + "InferenceInputSchema", "InferenceRuntime", "InferenceSession", "InMemoryMetricsRecorder", + "INPUT_PHASES", + "InputCanonicalizer", "InputField", "InputMapping", + "InputMappingSchema", + "InputPhase", + "KeyboardToDriverCommand", + "MappingCompatibility", "MetricsRecorder", "ModelAdapter", - "ModelInputs", - "ModelInputSchema", "NullMetricsRecorder", "NullOutputTarget", "OutputArtifact", "OutputTarget", "Precision", "RuntimeMetricSample", + "ScriptedModality", + "SESSION_START_ONLY", "StepRequest", "StepResult", "TimeWindow", + "undeclared_inference_inputs", + "UserInputCapability", "UserInputEvent", "UserInputs", "UserInputSchema", + "validate_phase", ] diff --git a/flashdreams/flashdreams/runtime/canonical.py b/flashdreams/flashdreams/runtime/canonical.py new file mode 100644 index 00000000..55f333ce --- /dev/null +++ b/flashdreams/flashdreams/runtime/canonical.py @@ -0,0 +1,387 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Raw device input to canonical modality conversion. + +This is the ``raw input -> canonicalized input`` leg. Applications consume +:class:`~flashdreams.runtime.inputs.CanonicalInputs`; they never read raw device +events. Adding a keyboard, gamepad, or force-feedback wheel is therefore a +:meth:`InputCanonicalizer.register` call that touches no application, mapping, +or model code. + +Converters are stateful, because HID input is edge-triggered while per-step +conditioning is level-triggered: a key held across a step emits no events yet +still means full throttle. Feed windows in session order and call +:meth:`InputCanonicalizer.reset` at a rollout boundary; replaying the same +window sequence then reproduces the same canonical inputs. + +This layer covers live user control only. Global conditioning such as a prompt +or conditioning frame is application-owned and reaches ``InferenceInput`` +directly, without passing through canonicalization. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Any, Protocol, runtime_checkable + +from flashdreams.runtime._utils import freeze_mapping +from flashdreams.runtime.inputs import ( + CanonicalInputs, + CanonicalInputSchema, + CanonicalModality, + TimeWindow, + UserInputCapability, + UserInputs, + UserInputSchema, +) +from flashdreams.serving.realtime.input import KeyboardState, normalize_key + +DriverBindings = Mapping[str, frozenset[str]] + +DEFAULT_DRIVING_BINDINGS: DriverBindings = MappingProxyType( + { + "throttle": frozenset({"w", "up"}), + "brake": frozenset({"s", "down"}), + "steer_left": frozenset({"a", "left"}), + "steer_right": frozenset({"d", "right"}), + "stop": frozenset({"space"}), + "reverse": frozenset(), + } +) +"""Default key bindings for :class:`KeyboardToDriverCommand`. + +Bindings are data so a layout can be rebound without editing the converter, and +so the set of tracked keys is derived from them rather than declared twice. +""" + +_DRIVER_ACTIONS = frozenset(DEFAULT_DRIVING_BINDINGS) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class DeviceConverterSchema: + """Metadata for one device-to-canonical-modality converter.""" + + name: str + produces: CanonicalModality + consumes: tuple[UserInputCapability, ...] = () + device_kind: str | None = None + priority: int = 0 + metadata: Mapping[str, Any] = field( + default_factory=dict, + compare=False, + hash=False, + ) + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("DeviceConverterSchema.name must be non-empty.") + if not isinstance(self.produces, CanonicalModality): + raise TypeError("produces must be a CanonicalModality object.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@runtime_checkable +class DeviceConverter(Protocol): + """Contract for turning one device's raw events into a canonical modality.""" + + @property + def schema(self) -> DeviceConverterSchema: + """Return converter metadata used for source selection.""" + ... + + def reset(self) -> None: + """Drop accumulated device state at a session or rollout boundary.""" + ... + + def convert( + self, + user_inputs: UserInputs, + window: TimeWindow, + ) -> Mapping[str, Any] | None: + """Return the modality value for ``window``, or ``None`` if inactive. + + ``user_inputs`` is already filtered to ``window``. Returning ``None`` + lets a present-but-idle device yield to a lower-priority one. + """ + ... + + +DRIVER_COMMAND = CanonicalModality( + name="driver_command", + payload_fields=frozenset({"throttle", "brake", "steer", "stop", "reverse"}), + description=( + "Normalized driving intent. throttle/brake are in [0, 1], steer is in " + "[-1, 1] with positive meaning left." + ), +) + + +class KeyboardToDriverCommand: + """Convert keyboard edges into :data:`DRIVER_COMMAND` level state. + + Mirrors the mapping the Omnidreams interactive-drive keyboard backend + already uses, so a keyboard reaches a model through the shared layer with + the same semantics it has today. + """ + + def __init__( + self, + *, + name: str = "keyboard-to-driver-command", + bindings: DriverBindings = DEFAULT_DRIVING_BINDINGS, + priority: int = 0, + ) -> None: + unknown = sorted(set(bindings) - _DRIVER_ACTIONS) + if unknown: + raise ValueError( + f"Unknown driver actions in bindings: {unknown}. " + f"Supported actions: {sorted(_DRIVER_ACTIONS)}." + ) + self._bindings = { + action: frozenset(normalize_key(key) for key in bindings.get(action, ())) + for action in _DRIVER_ACTIONS + } + # Tracked keys are derived, so they cannot drift from the bindings and + # silently make an action unreachable. + self._supported_keys = frozenset( + key for keys in self._bindings.values() for key in keys + ) + self._state = KeyboardState(supported_keys=self._supported_keys) + self._schema = DeviceConverterSchema( + name=name, + produces=DRIVER_COMMAND, + device_kind="keyboard", + priority=priority, + consumes=( + UserInputCapability( + event_type="key_down", + payload_fields=frozenset({"key"}), + ), + UserInputCapability( + event_type="key_up", + payload_fields=frozenset({"key"}), + ), + ), + ) + + @property + def schema(self) -> DeviceConverterSchema: + return self._schema + + def reset(self) -> None: + self._state = KeyboardState(supported_keys=self._supported_keys) + + def convert( + self, + user_inputs: UserInputs, + window: TimeWindow, + ) -> Mapping[str, Any] | None: + del window + for event in user_inputs.events: + if event.event_type not in {"key_down", "key_up"}: + continue + key = event.payload.get("key") + if not isinstance(key, str): + continue + self._state.apply_event( + event="keydown" if event.event_type == "key_down" else "keyup", + key=key, + ) + + pressed = {normalize_key(key) for key in self._state.snapshot()} + + def held(action: str) -> bool: + return bool(self._bindings[action] & pressed) + + steer = 0.0 + if held("steer_left"): + steer += 1.0 + if held("steer_right"): + steer -= 1.0 + return DRIVER_COMMAND.value( + { + "throttle": 1.0 if held("throttle") else 0.0, + "brake": 1.0 if held("brake") else 0.0, + "steer": steer, + "stop": held("stop"), + "reverse": held("reverse"), + } + ) + + +class ScriptedModality: + """Emit pre-authored canonical values, for benchmarks, replay, and tests. + + Mocking input should not require knowing the raw device vocabulary. This + converter consumes no raw capabilities, so it is feedable by any source + -- including an empty :class:`UserInputSchema` -- and application code is + identical between a real run and a scripted one. + + ``timeline`` is ``(start_s, value)`` pairs. Values are level-triggered and + held until the next entry begins, matching how live converters behave. An + entry applies to a window once it has begun by the window's end, and + ``None`` is returned for windows before the first entry. + """ + + def __init__( + self, + *, + modality: CanonicalModality, + timeline: Sequence[tuple[float, Mapping[str, Any]]], + name: str | None = None, + device_kind: str | None = "scripted", + priority: int = 0, + ) -> None: + entries = tuple(sorted(timeline, key=lambda entry: entry[0])) + for start_s, value in entries: + if start_s < 0: + raise ValueError("timeline start_s must be >= 0.") + modality.value(value) + self._entries = tuple( + (start_s, modality.value(value)) for start_s, value in entries + ) + self._modality = modality + self._schema = DeviceConverterSchema( + name=name or f"scripted-{modality.name}", + produces=modality, + device_kind=device_kind, + priority=priority, + ) + + @property + def schema(self) -> DeviceConverterSchema: + return self._schema + + def reset(self) -> None: + # The timeline is a pure function of the window, so replay is + # deterministic without any state to clear. + return None + + def convert( + self, + user_inputs: UserInputs, + window: TimeWindow, + ) -> Mapping[str, Any] | None: + del user_inputs + current: Mapping[str, Any] | None = None + for start_s, value in self._entries: + if start_s < window.end_s: + current = value + else: + break + return current + + +class InputCanonicalizer: + """Registry of device converters plus the raw-to-canonical rewrite. + + Registration is the whole extension point: a new device is a converter + registered against an existing modality, and a new modality is a converter + registered with a new :class:`CanonicalModality`. + """ + + def __init__(self, converters: Iterable[DeviceConverter] = ()) -> None: + self._converters: list[DeviceConverter] = [] + for converter in converters: + self.register(converter) + + def register(self, converter: DeviceConverter) -> None: + """Register one device converter.""" + if not isinstance(converter, DeviceConverter): + raise TypeError("converter must implement the DeviceConverter protocol.") + name = converter.schema.name + if any(existing.schema.name == name for existing in self._converters): + raise ValueError( + f"A device converter named {name!r} is already registered." + ) + self._converters.append(converter) + + @property + def converters(self) -> tuple[DeviceConverter, ...]: + """Return every registered converter.""" + return tuple(self._converters) + + def reset(self) -> None: + """Reset every registered converter's device state.""" + for converter in self._converters: + converter.reset() + + def converters_for( + self, + source_schema: UserInputSchema, + ) -> tuple[DeviceConverter, ...]: + """Return converters this source can feed, highest priority first.""" + feedable = [ + converter + for converter in self._converters + if all( + source_schema.supports(capability) + for capability in converter.schema.consumes + ) + ] + # Sort is stable, so equal-priority converters keep registration order. + return tuple(sorted(feedable, key=lambda each: -each.schema.priority)) + + def unavailable_converters( + self, + source_schema: UserInputSchema, + ) -> tuple[DeviceConverter, ...]: + """Return converters this source cannot feed, for diagnostics.""" + feedable = {id(converter) for converter in self.converters_for(source_schema)} + return tuple( + converter for converter in self._converters if id(converter) not in feedable + ) + + def canonical_schema( + self, + source_schema: UserInputSchema, + ) -> CanonicalInputSchema: + """Return the canonical modalities this raw source can supply. + + This is the boundary an application declares against. A mapping that + consumes ``driver_command`` then matches a keyboard source, a wheel + source, or any device registered later. + """ + modalities: list[CanonicalModality] = [] + for converter in self.converters_for(source_schema): + modality = converter.schema.produces + if modality not in modalities: + modalities.append(modality) + return CanonicalInputSchema( + modalities=tuple(modalities), + description=source_schema.description, + ) + + def canonicalize( + self, + user_inputs: UserInputs, + *, + window: TimeWindow, + source_schema: UserInputSchema, + ) -> CanonicalInputs: + """Convert one raw window into canonical inputs. + + Every feedable converter sees the window so its device state stays + current even while another device has precedence; that way unplugging + the higher-priority device does not resume from stale state. Among + converters producing the same modality, the highest-priority one that + returned a value wins. + """ + windowed = user_inputs.window(window) + values: dict[str, Any] = {} + sources: dict[str, str] = {} + for converter in self.converters_for(source_schema): + value = converter.convert(windowed, window) + modality = converter.schema.produces + if value is not None and modality.name not in values: + values[modality.name] = value + if converter.schema.device_kind is not None: + sources[modality.name] = converter.schema.device_kind + + metadata: dict[str, Any] = {} + if sources: + metadata["canonical_sources"] = freeze_mapping(sources) + return CanonicalInputs(values=values, metadata=metadata) diff --git a/flashdreams/flashdreams/runtime/inputs.py b/flashdreams/flashdreams/runtime/inputs.py index e14b3572..f0be31be 100644 --- a/flashdreams/flashdreams/runtime/inputs.py +++ b/flashdreams/flashdreams/runtime/inputs.py @@ -8,10 +8,29 @@ import math from collections.abc import Iterable, Mapping from dataclasses import dataclass, field -from typing import Any +from typing import Any, Literal, cast from flashdreams.runtime._utils import freeze_mapping +InputPhase = Literal["global", "step"] + +INPUT_PHASES: tuple[InputPhase, ...] = ("global", "step") + +SESSION_START_ONLY = "session_start" +"""``InputField.update_policy`` value meaning "supply at session start only". + +``update_policy`` is otherwise an open, adapter-owned vocabulary. This is the +one reserved token, because the runtime needs to distinguish a conditioning +value that can be swapped mid-rollout from one that cannot. +""" + + +def validate_phase(value: str) -> InputPhase: + """Return ``value`` as a validated :data:`InputPhase`.""" + if value not in INPUT_PHASES: + raise ValueError(f"phase must be 'global' or 'step', got {value!r}.") + return cast(InputPhase, value) + @dataclass(frozen=True, kw_only=True, slots=True) class TimeWindow: @@ -35,16 +54,70 @@ def contains(self, timestamp_s: float) -> bool: @dataclass(frozen=True, kw_only=True, slots=True) class InputField: - """Lightweight schema field for user snapshots or model inputs.""" + """Lightweight schema field for user snapshots or model inputs. + + ``update_policy`` and ``lifecycle`` are plain query metadata. They let a + model advertise facts such as "prompt updates land at step boundaries" or + "this value is consumed at cache init" without making this layer + responsible for implementing or deeply validating that behavior. + """ name: str required: bool = True semantic_type: str | None = None + update_policy: str | None = None + lifecycle: str | None = None + metadata: Mapping[str, Any] = field( + default_factory=dict, + compare=False, + hash=False, + ) description: str = "" def __post_init__(self) -> None: if not self.name.strip(): raise ValueError("InputField.name must be non-empty.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class UserInputCapability: + """One user event a source or mapping can provide, at payload granularity. + + ``UserInputSchema.event_types`` declares only that an event type exists. A + capability additionally pins the payload fields carried by that event, so a + mapping can state that it needs ``key_down`` events that actually carry a + ``key``. + """ + + event_type: str + semantic_type: str | None = None + payload_fields: frozenset[str] = field(default_factory=frozenset) + metadata: Mapping[str, Any] = field( + default_factory=dict, + compare=False, + hash=False, + ) + description: str = "" + + def __post_init__(self) -> None: + if not self.event_type.strip(): + raise ValueError("UserInputCapability.event_type must be non-empty.") + for payload_field in self.payload_fields: + if not payload_field.strip(): + raise ValueError("payload field names must be non-empty.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + def is_satisfied_by(self, provider: "UserInputCapability") -> bool: + """Return whether ``provider`` can satisfy this consumed capability.""" + if self.event_type != provider.event_type: + return False + semantic_ok = ( + self.semantic_type is None + or provider.semantic_type is None + or self.semantic_type == provider.semantic_type + ) + return semantic_ok and self.payload_fields.issubset(provider.payload_fields) @dataclass(frozen=True, kw_only=True, slots=True) @@ -53,6 +126,7 @@ class UserInputSchema: event_types: frozenset[str] = field(default_factory=frozenset) snapshot_fields: tuple[InputField, ...] = () + capabilities: tuple[UserInputCapability, ...] = () description: str = "" def supports_event_types(self, event_types: Iterable[str]) -> bool: @@ -60,7 +134,63 @@ def supports_event_types(self, event_types: Iterable[str]) -> bool: requested = frozenset(event_types) if not requested: return True - return requested.issubset(self.event_types) + return requested.issubset(self.declared_event_types()) + + def declared_event_types(self) -> frozenset[str]: + """Return event types from ``event_types`` and from ``capabilities``.""" + return self.event_types | frozenset( + capability.event_type for capability in self.capabilities + ) + + def declared_capabilities(self) -> tuple[UserInputCapability, ...]: + """Return capabilities, widened with bare ``event_types`` entries. + + A plain ``event_types`` entry carries no payload promise, so it is + modeled as a capability with no payload fields. Coarse schemas written + before capabilities existed therefore still satisfy any consumer that + does not require specific payload fields. + """ + declared = list(self.capabilities) + covered = {capability.event_type for capability in declared} + declared.extend( + UserInputCapability(event_type=event_type) + for event_type in sorted(self.event_types - covered) + ) + return tuple(declared) + + def supports(self, capability: UserInputCapability) -> bool: + """Return whether this source can satisfy ``capability``.""" + return any( + capability.is_satisfied_by(provider) + for provider in self.declared_capabilities() + ) + + def validate_event(self, event: "UserInputEvent") -> None: + """Validate one event against the event types this source declares.""" + matching = [ + capability + for capability in self.declared_capabilities() + if capability.event_type == event.event_type + ] + if not matching: + raise ValueError( + f"User input source does not provide event type {event.event_type!r}." + ) + payload_keys = set(event.payload) + if not any( + capability.payload_fields.issubset(payload_keys) for capability in matching + ): + expected = sorted( + { + payload_field + for capability in matching + for payload_field in capability.payload_fields + } + ) + raise ValueError( + f"Event {event.event_type!r} payload is missing required " + f"fields: {expected}." + ) def missing_snapshot(self, inputs: "UserInputs") -> tuple[str, ...]: """Return required snapshot fields absent from ``inputs``.""" @@ -74,10 +204,10 @@ def require_snapshot(self, inputs: "UserInputs") -> None: @dataclass(frozen=True, kw_only=True, slots=True) -class ModelInputSchema: +class InferenceInputSchema: """Minimal metadata for model-facing initial and per-step inputs.""" - initial_fields: tuple[InputField, ...] = () + global_fields: tuple[InputField, ...] = () """Model inputs required before starting the initial generation/session.""" step_fields: tuple[InputField, ...] = () @@ -85,21 +215,81 @@ class ModelInputSchema: description: str = "" - def missing_initial(self, inputs: "ModelInputs") -> tuple[str, ...]: + def fields_for(self, phase: InputPhase) -> tuple[InputField, ...]: + """Return every declared field for ``phase``.""" + return ( + self.global_fields + if validate_phase(phase) == "global" + else self.step_fields + ) + + def required_fields( + self, + phase: InputPhase | None = None, + ) -> tuple[tuple[InputPhase, InputField], ...]: + """Return required fields as ``(phase, field)``, optionally filtered.""" + return self._select(phase, required=True) + + def optional_fields( + self, + phase: InputPhase | None = None, + ) -> tuple[tuple[InputPhase, InputField], ...]: + """Return optional fields as ``(phase, field)``, optionally filtered.""" + return self._select(phase, required=False) + + def field_for(self, *, name: str, phase: InputPhase) -> InputField | None: + """Return one declared field, if present.""" + for input_field in self.fields_for(phase): + if input_field.name == name: + return input_field + return None + + def _select( + self, + phase: InputPhase | None, + *, + required: bool, + ) -> tuple[tuple[InputPhase, InputField], ...]: + phases = INPUT_PHASES if phase is None else (validate_phase(phase),) + return tuple( + (each_phase, input_field) + for each_phase in phases + for input_field in self.fields_for(each_phase) + if input_field.required is required + ) + + def unsupported_global_updates(self, inputs: "InferenceInput") -> tuple[str, ...]: + """Return requested conditioning updates this model cannot apply. + + A field whose ``update_policy`` is :data:`SESSION_START_ONLY` can be + supplied when the session starts but not changed mid-rollout. Any other + policy, including ``None``, is treated as permissive here; the adapter + still owns whether the swap actually succeeds. + """ + return tuple( + name + for name in inputs.global_conditioning + if (declared := self.field_for(name=name, phase="global")) is not None + and declared.update_policy == SESSION_START_ONLY + ) + + def missing_global(self, inputs: "InferenceInput") -> tuple[str, ...]: """Return required initial fields absent from ``inputs``.""" - return _missing_required(self.initial_fields, inputs.initial) + return _missing_required(self.global_fields, inputs.global_conditioning) - def missing_step(self, inputs: "ModelInputs") -> tuple[str, ...]: + def missing_step(self, inputs: "InferenceInput") -> tuple[str, ...]: """Return required per-step fields absent from ``inputs``.""" return _missing_required(self.step_fields, inputs.step) - def require_initial(self, inputs: "ModelInputs") -> None: + def require_global(self, inputs: "InferenceInput") -> None: """Raise if required initial fields are absent.""" - missing = self.missing_initial(inputs) + missing = self.missing_global(inputs) if missing: - raise ValueError(f"Missing required initial model input(s): {missing}") + raise ValueError( + f"Missing required global conditioning input(s): {missing}" + ) - def require_step(self, inputs: "ModelInputs") -> None: + def require_step(self, inputs: "InferenceInput") -> None: """Raise if required per-step fields are absent.""" missing = self.missing_step(inputs) if missing: @@ -171,23 +361,154 @@ def window(self, time_window: TimeWindow) -> "UserInputs": @dataclass(frozen=True, kw_only=True, slots=True) -class ModelInputs: - """Model-facing payloads split by initial and per-step use.""" +class CanonicalModality: + """A device-independent user input an application consumes. + + This is the middle layer of ``raw input -> canonicalized input -> encoded + inference input``. Applications and benchmarks declare and consume + modalities; they never read raw device events, so adding a new device is a + converter registration rather than an application change. + + Modalities describe live user control only. Global conditioning such as a + prompt or conditioning frame is application-owned and reaches + :class:`InferenceInput` directly, without passing through this layer. + """ + + name: str + payload_fields: frozenset[str] = field(default_factory=frozenset) + metadata: Mapping[str, Any] = field( + default_factory=dict, + compare=False, + hash=False, + ) + description: str = "" + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("CanonicalModality.name must be non-empty.") + for payload_field in self.payload_fields: + if not payload_field.strip(): + raise ValueError("payload field names must be non-empty.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + def is_satisfied_by(self, provider: "CanonicalModality") -> bool: + """Return whether ``provider`` can satisfy this consumed modality.""" + return self.name == provider.name and self.payload_fields.issubset( + provider.payload_fields + ) + + def value(self, payload: Mapping[str, Any]) -> Mapping[str, Any]: + """Return ``payload`` frozen, checking it covers this modality.""" + missing = sorted(self.payload_fields - set(payload)) + if missing: + raise ValueError( + f"Canonical modality {self.name!r} requires payload fields " + f"{missing}, which the converter did not produce." + ) + return freeze_mapping(payload) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class CanonicalInputSchema: + """Canonical modalities an application can be fed by a given source.""" + + modalities: tuple[CanonicalModality, ...] = () + description: str = "" + + def supports(self, modality: CanonicalModality) -> bool: + """Return whether this source can supply ``modality``.""" + return any(modality.is_satisfied_by(provided) for provided in self.modalities) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class CanonicalInputs: + """Canonicalized user input for one step, keyed by modality name. + + Values are level-triggered and normally present every step: a key held down + emits no events but still means full throttle. Global conditioning does not + appear here; it is application-owned and reaches :class:`InferenceInput` + directly. + """ __hash__ = None - initial: Mapping[str, Any] = field(default_factory=dict) + values: Mapping[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "values", freeze_mapping(self.values)) + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceInput: + """Encoded inputs for one :class:`InferenceSession` call. + + Two conditioning slots: + + - ``global_conditioning``: values that condition the whole rollout, such as + the conditioning frame or prompt. Normally supplied when the session + starts. + - ``step``: values needed to generate the next chunk or frame. + + A non-empty ``global_conditioning`` on a mid-rollout input is an *update + request*, not a reset. The session should apply it when the model supports + that; resetting rollout state is a separate, explicit + :meth:`InferenceSession.reset` call. Whether a given value can be updated + mid-rollout is declared per field by ``InputField.update_policy``; see + :meth:`InferenceInputSchema.unsupported_global_updates`. + """ + + __hash__ = None + + global_conditioning: Mapping[str, Any] = field(default_factory=dict) step: Mapping[str, Any] = field(default_factory=dict) metadata: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: - object.__setattr__(self, "initial", freeze_mapping(self.initial)) + object.__setattr__( + self, "global_conditioning", freeze_mapping(self.global_conditioning) + ) object.__setattr__(self, "step", freeze_mapping(self.step)) object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) - def with_step(self, step: Mapping[str, Any]) -> "ModelInputs": - """Return a copy with replaced per-step payload.""" - return ModelInputs(initial=self.initial, step=step, metadata=self.metadata) + @property + def requests_global_update(self) -> bool: + """Return whether this input asks the session to update conditioning.""" + return bool(self.global_conditioning) + + def with_step(self, step: Mapping[str, Any]) -> "InferenceInput": + """Return a copy with replaced per-step payload. + + The global slot is carried through unchanged, so a mid-rollout input + built this way keeps whatever update request it already had. Use + :meth:`without_global_update` for the common steady-state case. + """ + return InferenceInput( + global_conditioning=self.global_conditioning, + step=step, + metadata=self.metadata, + ) + + def with_global_update( + self, global_conditioning: Mapping[str, Any] + ) -> "InferenceInput": + """Return a copy requesting a mid-rollout conditioning update.""" + return InferenceInput( + global_conditioning=global_conditioning, + step=self.step, + metadata=self.metadata, + ) + + def without_global_update(self) -> "InferenceInput": + """Return a copy that requests no conditioning update.""" + return InferenceInput(step=self.step, metadata=self.metadata) + + def for_phase(self, phase: InputPhase) -> Mapping[str, Any]: + """Return the payload mapping for ``phase``.""" + return ( + self.global_conditioning if validate_phase(phase) == "global" else self.step + ) def _missing_required( diff --git a/flashdreams/flashdreams/runtime/interfaces.py b/flashdreams/flashdreams/runtime/interfaces.py index 9b6a064f..852a77f1 100644 --- a/flashdreams/flashdreams/runtime/interfaces.py +++ b/flashdreams/flashdreams/runtime/interfaces.py @@ -9,9 +9,9 @@ from flashdreams.runtime.config import InferenceConfig from flashdreams.runtime.inputs import ( - ModelInputs, - ModelInputSchema, - UserInputSchema, + CanonicalInputSchema, + InferenceInput, + InferenceInputSchema, ) from flashdreams.runtime.mapping import InputMapping from flashdreams.runtime.types import StepRequest, StepResult @@ -25,11 +25,11 @@ def next_step_request(self) -> StepRequest | None: """Describe the next step's inputs, or return ``None`` when complete.""" ... - def step(self, inputs: ModelInputs) -> StepResult: + def step(self, inputs: InferenceInput) -> StepResult: """Run one sequential inference step.""" ... - def reset(self, inputs: ModelInputs | None = None) -> None: + def reset(self, inputs: InferenceInput | None = None) -> None: """Reset this session's rollout state when the backend supports it.""" ... @@ -42,8 +42,8 @@ def close(self) -> None: class InferenceRuntime(Protocol): """Heavyweight reusable runtime created from :class:`InferenceConfig`.""" - def start_session(self, inputs: ModelInputs) -> InferenceSession: - """Create an isolated session from initial model inputs.""" + def start_session(self, inputs: InferenceInput) -> InferenceSession: + """Create an isolated session from global conditioning inputs.""" ... def close(self) -> None: @@ -56,10 +56,10 @@ def close(self) -> None: class ModelAdapter(Protocol): """Model-specific boundary that declares defaults and creates runtimes. - Adapters declare model-facing input requirements, optional user-input - capabilities, and an optional default mapping between the two. Runtime, - application, or benchmark code may override that mapping while preserving the - same ``UserInputs`` to ``ModelInputs`` boundary. + Adapters declare model-facing input requirements, the canonical modalities + their default mapping consumes, and an optional default mapping between the + two. Runtime, application, or benchmark code may override that mapping while + preserving the same ``CanonicalInputs`` to ``InferenceInput`` boundary. """ @property @@ -68,17 +68,17 @@ def model_id(self) -> str: ... @property - def model_input_schema(self) -> ModelInputSchema: + def inference_input_schema(self) -> InferenceInputSchema: """Model-facing initial and per-step input requirements.""" ... @property - def user_input_schema(self) -> UserInputSchema | None: - """User inputs supported by the adapter's default mapping, if any.""" + def canonical_input_schema(self) -> CanonicalInputSchema | None: + """Canonical modalities the adapter's default mapping consumes.""" ... def default_input_mapping(self) -> InputMapping | None: - """Return the model-provided default user-to-model mapping, if any.""" + """Return the model-provided default canonical-to-model mapping.""" ... def validate_config(self, config: InferenceConfig) -> None: diff --git a/flashdreams/flashdreams/runtime/mapping.py b/flashdreams/flashdreams/runtime/mapping.py index 75635108..94f48140 100644 --- a/flashdreams/flashdreams/runtime/mapping.py +++ b/flashdreams/flashdreams/runtime/mapping.py @@ -1,17 +1,24 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Input mapping boundary from user input windows to model inputs.""" +"""Input mapping boundary from canonical inputs to encoded inference inputs.""" from __future__ import annotations -from typing import Protocol, runtime_checkable +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field, replace +from typing import Any, Protocol, runtime_checkable +from flashdreams.runtime._utils import freeze_mapping from flashdreams.runtime.inputs import ( - ModelInputs, - ModelInputSchema, - UserInputs, - UserInputSchema, + INPUT_PHASES, + CanonicalInputs, + CanonicalInputSchema, + CanonicalModality, + InferenceInput, + InferenceInputSchema, + InputField, + InputPhase, ) from flashdreams.runtime.types import StepRequest @@ -28,28 +35,28 @@ class InputMapping(Protocol): def validate( self, *, - user_schema: UserInputSchema | None = None, - model_schema: ModelInputSchema | None = None, + canonical_schema: CanonicalInputSchema | None = None, + inference_input_schema: InferenceInputSchema | None = None, ) -> None: """Fail early for obvious app, event-source, and model mismatches.""" ... - def map_initial_inputs( + def map_global_inputs( self, *, - user_inputs: UserInputs, - model_inputs: ModelInputs, - ) -> ModelInputs: - """Build initial model inputs before a session starts.""" + canonical_inputs: CanonicalInputs, + inference_input: InferenceInput, + ) -> InferenceInput: + """Build global conditioning inputs before a session starts.""" ... def map_step_inputs( self, *, - user_inputs: UserInputs, - model_inputs: ModelInputs, + canonical_inputs: CanonicalInputs, + inference_input: InferenceInput, request: StepRequest, - ) -> ModelInputs: + ) -> InferenceInput: """Build model inputs for one session step from the current input window.""" ... @@ -60,26 +67,315 @@ class IdentityInputMapping: def validate( self, *, - user_schema: UserInputSchema | None = None, - model_schema: ModelInputSchema | None = None, + canonical_schema: CanonicalInputSchema | None = None, + inference_input_schema: InferenceInputSchema | None = None, ) -> None: - del user_schema, model_schema + del canonical_schema, inference_input_schema - def map_initial_inputs( + def map_global_inputs( self, *, - user_inputs: UserInputs, - model_inputs: ModelInputs, - ) -> ModelInputs: - del user_inputs - return model_inputs + canonical_inputs: CanonicalInputs, + inference_input: InferenceInput, + ) -> InferenceInput: + del canonical_inputs + return inference_input def map_step_inputs( self, *, - user_inputs: UserInputs, - model_inputs: ModelInputs, + canonical_inputs: CanonicalInputs, + inference_input: InferenceInput, request: StepRequest, - ) -> ModelInputs: - del user_inputs, request - return model_inputs + ) -> InferenceInput: + del canonical_inputs, request + return inference_input + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InputMappingSchema: + """Declarative compatibility surface for one mapping. + + ``InputMapping.validate`` fails a run late and opaquely: it raises, but it + cannot answer which optional model inputs a source would enable, or which + missing user capability is responsible for an unreachable model input. This + schema makes those questions answerable before runtime initialization. + """ + + name: str = "input-mapping" + consumes: tuple[CanonicalModality, ...] = () + produces_global: tuple[InputField, ...] = () + produces_step: tuple[InputField, ...] = () + metadata: Mapping[str, Any] = field( + default_factory=dict, + compare=False, + hash=False, + ) + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("InputMappingSchema.name must be non-empty.") + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + def produces_for(self, phase: InputPhase) -> tuple[InputField, ...]: + """Return the fields this mapping produces for ``phase``.""" + return self.produces_global if phase == "global" else self.produces_step + + def can_produce(self, phase: InputPhase, required: InputField) -> bool: + """Return whether this mapping can produce ``required`` in ``phase``.""" + return any( + _field_matches(produced, required) for produced in self.produces_for(phase) + ) + + +def _field_matches(produced: InputField, required: InputField) -> bool: + if produced.name != required.name: + return False + semantic_ok = ( + produced.semantic_type is None + or required.semantic_type is None + or produced.semantic_type == required.semantic_type + ) + lifecycle_ok = ( + produced.lifecycle is None + or required.lifecycle is None + or produced.lifecycle == required.lifecycle + ) + return semantic_ok and lifecycle_ok + + +@dataclass(frozen=True, kw_only=True, slots=True) +class MappingCompatibility: + """Compatibility report for one source, model schema, and mapping set. + + Mappings whose consumed capabilities the source cannot provide are reported + in ``unavailable_mapping_schemas`` and excluded from the satisfied/available + reports, so those lists only name model inputs that can really be produced. + """ + + __hash__ = None + + canonical_schema: CanonicalInputSchema + inference_input_schema: InferenceInputSchema + mapping_schema: InputMappingSchema + missing_modalities: tuple[CanonicalModality, ...] = () + missing_required_model_fields: tuple[tuple[InputPhase, InputField], ...] = () + satisfied_required_model_fields: tuple[tuple[InputPhase, InputField], ...] = () + available_optional_model_fields: tuple[tuple[InputPhase, InputField], ...] = () + unavailable_mapping_schemas: tuple[InputMappingSchema, ...] = () + + @property + def can_drive(self) -> bool: + """Return whether this source can drive this model through the mapping. + + A mapping the source cannot feed does not block the run unless it was + the only way to produce a required model input. + """ + return not (self.missing_required_model_fields or self.missing_modalities) + + @property + def unavailable_mapping_names(self) -> tuple[str, ...]: + """Return names of mappings dropped because the source cannot feed them.""" + return tuple(schema.name for schema in self.unavailable_mapping_schemas) + + def raise_if_incompatible(self) -> None: + """Raise a compact error when this mapping cannot drive the model.""" + if self.can_drive: + return + problems: list[str] = [] + if self.missing_modalities: + missing = ", ".join(modality.name for modality in self.missing_modalities) + problems.append(f"missing canonical modalities: {missing}") + if self.missing_required_model_fields: + missing = ", ".join( + f"{phase}:{input_field.name}" + for phase, input_field in self.missing_required_model_fields + ) + problems.append(f"missing required model inputs: {missing}") + if self.unavailable_mapping_schemas: + problems.append( + "unavailable mappings: " + ", ".join(self.unavailable_mapping_names) + ) + raise ValueError( + f"Input mapping {self.mapping_schema.name!r} cannot drive this model " + f"from the selected source: " + "; ".join(problems) + ) + + +def _source_can_feed( + canonical_schema: CanonicalInputSchema, + mapping_schema: InputMappingSchema, +) -> bool: + return all( + canonical_schema.supports(modality) for modality in mapping_schema.consumes + ) + + +def combine_mapping_schemas( + mapping_schemas: Sequence[InputMappingSchema], + *, + name: str = "input-mapping-set", +) -> InputMappingSchema: + """Combine independently declared mappings into one compatibility surface. + + Duplicates are collapsed. Because ``metadata`` is excluded from equality, + the metadata of collapsed duplicates is merged rather than dropped, with the + first declaration winning on conflicting keys. + """ + consumes: list[CanonicalModality] = [] + produces: dict[InputPhase, list[InputField]] = {"global": [], "step": []} + + def _merge(target: list[Any], value: Any) -> None: + for index, existing in enumerate(target): + if existing == value: + if value.metadata: + target[index] = replace( + existing, + metadata={**dict(value.metadata), **dict(existing.metadata)}, + ) + return + target.append(value) + + for mapping_schema in mapping_schemas: + if not isinstance(mapping_schema, InputMappingSchema): + raise TypeError("mapping_schemas must contain InputMappingSchema objects.") + for modality in mapping_schema.consumes: + _merge(consumes, modality) + for phase in INPUT_PHASES: + for input_field in mapping_schema.produces_for(phase): + _merge(produces[phase], input_field) + + return InputMappingSchema( + name=name, + consumes=tuple(consumes), + produces_global=tuple(produces["global"]), + produces_step=tuple(produces["step"]), + ) + + +def _build_compatibility( + *, + canonical_schema: CanonicalInputSchema, + inference_input_schema: InferenceInputSchema, + mapping_schemas: Sequence[InputMappingSchema], + reported_schema: InputMappingSchema, +) -> MappingCompatibility: + feedable: list[InputMappingSchema] = [] + unavailable: list[InputMappingSchema] = [] + for mapping_schema in mapping_schemas: + if _source_can_feed(canonical_schema, mapping_schema): + feedable.append(mapping_schema) + else: + unavailable.append(mapping_schema) + + usable = combine_mapping_schemas(feedable, name=reported_schema.name) + required = inference_input_schema.required_fields() + missing_required = tuple( + (phase, input_field) + for phase, input_field in required + if not usable.can_produce(phase, input_field) + ) + satisfied_required = tuple( + (phase, input_field) + for phase, input_field in required + if usable.can_produce(phase, input_field) + ) + available_optional = tuple( + (phase, input_field) + for phase, input_field in inference_input_schema.optional_fields() + if usable.can_produce(phase, input_field) + ) + + # Only capabilities that block a required model input make the mapping + # unusable. A dropped mapping that fed nothing but optional fields degrades + # the run instead of vetoing it. + missing_modalities: list[CanonicalModality] = [] + for mapping_schema in unavailable: + if not any( + mapping_schema.can_produce(phase, input_field) + for phase, input_field in missing_required + ): + continue + for modality in mapping_schema.consumes: + if canonical_schema.supports(modality) or modality in missing_modalities: + continue + missing_modalities.append(modality) + + return MappingCompatibility( + canonical_schema=canonical_schema, + inference_input_schema=inference_input_schema, + mapping_schema=reported_schema, + missing_modalities=tuple(missing_modalities), + missing_required_model_fields=missing_required, + satisfied_required_model_fields=satisfied_required, + available_optional_model_fields=available_optional, + unavailable_mapping_schemas=tuple(unavailable), + ) + + +def check_mapping_compatibility( + *, + canonical_schema: CanonicalInputSchema, + inference_input_schema: InferenceInputSchema, + mapping_schema: InputMappingSchema, +) -> MappingCompatibility: + """Check whether a user-input source can drive a model through a mapping.""" + if not isinstance(mapping_schema, InputMappingSchema): + raise TypeError("mapping_schema must be an InputMappingSchema object.") + return _build_compatibility( + canonical_schema=canonical_schema, + inference_input_schema=inference_input_schema, + mapping_schemas=(mapping_schema,), + reported_schema=mapping_schema, + ) + + +def check_mapping_set_compatibility( + *, + canonical_schema: CanonicalInputSchema, + inference_input_schema: InferenceInputSchema, + mapping_schemas: Sequence[InputMappingSchema], + name: str = "input-mapping-set", +) -> MappingCompatibility: + """Check compatibility for a composed set of mappings. + + Each mapping keeps its own consumes/produces link, so a mapping the source + cannot feed only costs the model inputs that mapping produced. + """ + mapping_schemas = tuple(mapping_schemas) + return _build_compatibility( + canonical_schema=canonical_schema, + inference_input_schema=inference_input_schema, + mapping_schemas=mapping_schemas, + reported_schema=combine_mapping_schemas(mapping_schemas, name=name), + ) + + +def undeclared_inference_inputs( + inputs: InferenceInput, + mapping_schema: InputMappingSchema, +) -> tuple[tuple[InputPhase, str], ...]: + """Return payload keys a mapping produced but did not declare. + + Mapping schemas are hand-written, so they drift from what + ``map_global_inputs``/``map_step_inputs`` actually return. Mapping tests + can use this to keep the declared compatibility surface honest. + """ + return tuple( + (phase, key) + for phase in INPUT_PHASES + for key in inputs.for_phase(phase) + if not any( + declared.name == key for declared in mapping_schema.produces_for(phase) + ) + ) + + +@runtime_checkable +class DeclaresMappingSchema(Protocol): + """Optional refinement of :class:`InputMapping` that declares its surface.""" + + @property + def mapping_schema(self) -> InputMappingSchema: + """Return the declarative compatibility surface for this mapping.""" + ... diff --git a/flashdreams/flashdreams/runtime/types.py b/flashdreams/flashdreams/runtime/types.py index 52bf8216..46775302 100644 --- a/flashdreams/flashdreams/runtime/types.py +++ b/flashdreams/flashdreams/runtime/types.py @@ -10,7 +10,7 @@ from typing import Any from flashdreams.runtime._utils import freeze_mapping -from flashdreams.runtime.inputs import ModelInputSchema, TimeWindow +from flashdreams.runtime.inputs import InferenceInputSchema, TimeWindow @dataclass(frozen=True, kw_only=True, slots=True) @@ -24,7 +24,7 @@ class StepRequest: __hash__ = None step_index: int - model_input_schema: ModelInputSchema | None = None + inference_input_schema: InferenceInputSchema | None = None user_input_window: TimeWindow | None = None metadata: Mapping[str, Any] = field(default_factory=dict) diff --git a/flashdreams/tests/test_inference_runtime_api.py b/flashdreams/tests/test_inference_runtime_api.py index 1474383a..edfafa63 100644 --- a/flashdreams/tests/test_inference_runtime_api.py +++ b/flashdreams/tests/test_inference_runtime_api.py @@ -9,17 +9,20 @@ import pytest from flashdreams.runtime import ( + CanonicalInputs, + CanonicalInputSchema, IdentityInputMapping, InferenceConfig, + InferenceInput, + InferenceInputSchema, InferenceRuntime, InferenceSession, InMemoryMetricsRecorder, + InputCanonicalizer, InputField, InputMapping, MetricsRecorder, ModelAdapter, - ModelInputs, - ModelInputSchema, NullOutputTarget, OutputArtifact, OutputTarget, @@ -27,6 +30,7 @@ StepRequest, StepResult, TimeWindow, + UserInputCapability, UserInputEvent, UserInputs, UserInputSchema, @@ -35,6 +39,18 @@ pytestmark = pytest.mark.ci_cpu +_SESSION_HORIZON_S = 3600.0 + +_KEYBOARD_SOURCE = UserInputSchema( + capabilities=( + UserInputCapability( + event_type="keyboard.keydown", payload_fields=frozenset({"key"}) + ), + ) +) +_KEYBOARD_CANONICALIZER = InputCanonicalizer() + + def test_inference_config_keeps_runtime_settings_separate() -> None: denied_app_fields = {"prompt", "output_dir", "browser_settings"} config = InferenceConfig( @@ -90,17 +106,19 @@ def test_runtime_metric_sample_rejects_bool_values() -> None: RuntimeMetricSample(name="sample", value=True) -def test_model_input_schema_validates_initial_and_step_payloads() -> None: - schema = ModelInputSchema( - initial_fields=( +def test_inference_input_schema_validates_initial_and_step_payloads() -> None: + schema = InferenceInputSchema( + global_fields=( InputField(name="prompt"), - InputField(name="first_frame"), + InputField(name="global_conditioning_frame"), ), step_fields=(InputField(name="camera_poses"),), ) - inputs = ModelInputs(initial={"prompt": "drive", "first_frame": object()}) + inputs = InferenceInput( + global_conditioning={"prompt": "drive", "global_conditioning_frame": object()} + ) - schema.require_initial(inputs) + schema.require_global(inputs) assert schema.missing_step(inputs) == ("camera_poses",) with pytest.raises(ValueError, match="camera_poses"): @@ -164,25 +182,27 @@ def test_user_input_schema_validates_required_snapshot_fields() -> None: schema.require_snapshot(UserInputs()) -def test_identity_input_mapping_leaves_model_inputs_unchanged() -> None: +def test_identity_input_mapping_leaves_inference_input_unchanged() -> None: mapping = IdentityInputMapping() - model_inputs = ModelInputs(initial={"prompt": "fixed"}, step={"hdmap": object()}) + inference_input = InferenceInput( + global_conditioning={"prompt": "fixed"}, step={"hdmap": object()} + ) request = StepRequest(step_index=0) assert ( - mapping.map_initial_inputs( - user_inputs=UserInputs(), - model_inputs=model_inputs, + mapping.map_global_inputs( + canonical_inputs=CanonicalInputs(), + inference_input=inference_input, ) - is model_inputs + is inference_input ) assert ( mapping.map_step_inputs( - user_inputs=UserInputs(), - model_inputs=model_inputs, + canonical_inputs=CanonicalInputs(), + inference_input=inference_input, request=request, ) - is model_inputs + is inference_input ) @@ -258,7 +278,7 @@ def test_runtime_api_components_compose_for_sequential_session() -> None: ), ) ) - model_inputs = ModelInputs(initial={"prompt": "drive forward"}) + inference_input = InferenceInput(global_conditioning={"prompt": "drive forward"}) output = NullOutputTarget(store_results=True) metrics = InMemoryMetricsRecorder() @@ -269,8 +289,10 @@ def test_runtime_api_components_compose_for_sequential_session() -> None: adapter=adapter, config=config, mapping=mapping, + canonicalizer=_KEYBOARD_CANONICALIZER, + source_schema=_KEYBOARD_SOURCE, user_inputs=user_inputs, - model_inputs=model_inputs, + inference_input=inference_input, output=output, metrics=metrics, ) @@ -291,8 +313,10 @@ def test_reference_loop_validates_mapping_before_runtime_creation() -> None: adapter=adapter, config=InferenceConfig(model_id="fake-model"), mapping=mapping, + canonicalizer=_KEYBOARD_CANONICALIZER, + source_schema=_KEYBOARD_SOURCE, user_inputs=UserInputs(), - model_inputs=ModelInputs(initial={"prompt": "drive forward"}), + inference_input=InferenceInput(global_conditioning={"prompt": "drive forward"}), output=NullOutputTarget(), metrics=InMemoryMetricsRecorder(), ) @@ -311,8 +335,12 @@ def test_reference_loop_closes_runtime_when_session_start_fails() -> None: adapter=adapter, config=InferenceConfig(model_id="fake-model"), mapping=IdentityInputMapping(), + canonicalizer=_KEYBOARD_CANONICALIZER, + source_schema=_KEYBOARD_SOURCE, user_inputs=UserInputs(), - model_inputs=ModelInputs(initial={"prompt": "drive forward"}), + inference_input=InferenceInput( + global_conditioning={"prompt": "drive forward"} + ), output=output, metrics=metrics, ) @@ -328,18 +356,24 @@ def _drive_two_step_session( adapter: ModelAdapter, config: InferenceConfig, mapping: InputMapping, + canonicalizer: InputCanonicalizer, + source_schema: UserInputSchema, user_inputs: UserInputs, - model_inputs: ModelInputs, + inference_input: InferenceInput, output: OutputTarget, metrics: MetricsRecorder, ) -> None: mapping.validate( - user_schema=adapter.user_input_schema, - model_schema=adapter.model_input_schema, + canonical_schema=adapter.canonical_input_schema, + inference_input_schema=adapter.inference_input_schema, ) - initial_inputs = mapping.map_initial_inputs( - user_inputs=user_inputs, - model_inputs=model_inputs, + initial_inputs = mapping.map_global_inputs( + canonical_inputs=canonicalizer.canonicalize( + user_inputs, + window=TimeWindow(start_s=0.0, end_s=_SESSION_HORIZON_S), + source_schema=source_schema, + ), + inference_input=inference_input, ) runtime = adapter.create_runtime(config) session: InferenceSession | None = None @@ -350,13 +384,16 @@ def _drive_two_step_session( output_opened = True while (request := session.next_step_request()) is not None: step_inputs = mapping.map_step_inputs( - user_inputs=( - user_inputs.window(request.user_input_window) - if request.user_input_window is not None - else user_inputs + canonical_inputs=canonicalizer.canonicalize( + user_inputs, + window=request.user_input_window + or TimeWindow(start_s=0.0, end_s=_SESSION_HORIZON_S), + source_schema=source_schema, ), - model_inputs=ModelInputs( - initial=initial_inputs.initial, + # The global slot stays empty in steady state. A mapping that + # sees ``canonical_inputs.has_global_change`` fills it via + # ``with_global_update`` to request a mid-rollout swap. + inference_input=InferenceInput( step={"chunk_index": request.step_index}, ), request=request, @@ -379,11 +416,11 @@ def _drive_two_step_session( class _FakeAdapter: model_id = "fake-model" - model_input_schema = ModelInputSchema( - initial_fields=(InputField(name="prompt"),), + inference_input_schema = InferenceInputSchema( + global_fields=(InputField(name="prompt"),), step_fields=(InputField(name="chunk_index"),), ) - user_input_schema = UserInputSchema(event_types=frozenset({"keyboard.keydown"})) + canonical_input_schema = CanonicalInputSchema() def default_input_mapping(self) -> InputMapping: return IdentityInputMapping() @@ -394,31 +431,31 @@ def validate_config(self, config: InferenceConfig) -> None: def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: self.validate_config(config) - return _FakeRuntime(model_input_schema=self.model_input_schema) + return _FakeRuntime(inference_input_schema=self.inference_input_schema) class _FakeRuntime: - def __init__(self, *, model_input_schema: ModelInputSchema) -> None: - self._model_input_schema = model_input_schema + def __init__(self, *, inference_input_schema: InferenceInputSchema) -> None: + self._inference_input_schema = inference_input_schema self.closed = False - def start_session(self, inputs: ModelInputs) -> InferenceSession: - self._model_input_schema.require_initial(inputs) - return _FakeSession(model_input_schema=self._model_input_schema) + def start_session(self, inputs: InferenceInput) -> InferenceSession: + self._inference_input_schema.require_global(inputs) + return _FakeSession(inference_input_schema=self._inference_input_schema) def close(self) -> None: self.closed = True class _FailingRuntime(_FakeRuntime): - def start_session(self, inputs: ModelInputs) -> InferenceSession: + def start_session(self, inputs: InferenceInput) -> InferenceSession: del inputs raise RuntimeError("start failed") class _FakeSession: - def __init__(self, *, model_input_schema: ModelInputSchema) -> None: - self._model_input_schema = model_input_schema + def __init__(self, *, inference_input_schema: InferenceInputSchema) -> None: + self._inference_input_schema = inference_input_schema self.step_index = 0 self.closed = False @@ -427,15 +464,15 @@ def next_step_request(self) -> StepRequest | None: return None return StepRequest( step_index=self.step_index, - model_input_schema=self._model_input_schema, + inference_input_schema=self._inference_input_schema, user_input_window=TimeWindow( start_s=0.5 * self.step_index, end_s=0.5 * (self.step_index + 1), ), ) - def step(self, inputs: ModelInputs) -> StepResult: - self._model_input_schema.require_step(inputs) + def step(self, inputs: InferenceInput) -> StepResult: + self._inference_input_schema.require_step(inputs) result = StepResult( step_index=self.step_index, output=f"chunk-{self.step_index}", @@ -449,7 +486,7 @@ def step(self, inputs: ModelInputs) -> StepResult: self.step_index += 1 return result - def reset(self, inputs: ModelInputs | None = None) -> None: + def reset(self, inputs: InferenceInput | None = None) -> None: del inputs self.step_index = 0 @@ -464,14 +501,19 @@ def __init__(self) -> None: def validate( self, *, - user_schema: UserInputSchema | None = None, - model_schema: ModelInputSchema | None = None, + canonical_schema: CanonicalInputSchema | None = None, + inference_input_schema: InferenceInputSchema | None = None, ) -> None: - super().validate(user_schema=user_schema, model_schema=model_schema) + super().validate( + canonical_schema=canonical_schema, + inference_input_schema=inference_input_schema, + ) self.validated = True class _OrderCheckingAdapter(_FakeAdapter): + canonical_input_schema = CanonicalInputSchema() + def __init__(self, *, mapping: _OrderCheckingMapping) -> None: self._mapping = mapping self.created_runtime_after_validate = False @@ -479,14 +521,18 @@ def __init__(self, *, mapping: _OrderCheckingMapping) -> None: def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: self.validate_config(config) self.created_runtime_after_validate = self._mapping.validated - return _FakeRuntime(model_input_schema=self.model_input_schema) + return _FakeRuntime(inference_input_schema=self.inference_input_schema) class _FailingStartAdapter(_FakeAdapter): + canonical_input_schema = CanonicalInputSchema() + def __init__(self) -> None: self.runtime: _FailingRuntime | None = None def create_runtime(self, config: InferenceConfig) -> InferenceRuntime: self.validate_config(config) - self.runtime = _FailingRuntime(model_input_schema=self.model_input_schema) + self.runtime = _FailingRuntime( + inference_input_schema=self.inference_input_schema + ) return self.runtime diff --git a/flashdreams/tests/test_runtime_canonical.py b/flashdreams/tests/test_runtime_canonical.py new file mode 100644 index 00000000..1ad48d39 --- /dev/null +++ b/flashdreams/tests/test_runtime_canonical.py @@ -0,0 +1,590 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the raw-input to canonical-modality layer. + +These cover the middle leg of ``raw input -> canonicalized input -> encoded +inference input``: applications consume canonical modalities, never raw device +events, so adding a device is a registration rather than an application change. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import pytest + +from flashdreams.runtime import ( + DRIVER_COMMAND, + CanonicalInputs, + CanonicalModality, + DeviceConverterSchema, + InferenceInput, + InferenceInputSchema, + InputCanonicalizer, + InputField, + InputMappingSchema, + KeyboardToDriverCommand, + ScriptedModality, + TimeWindow, + UserInputCapability, + UserInputEvent, + UserInputs, + UserInputSchema, + check_mapping_compatibility, +) + +pytestmark = pytest.mark.ci_cpu + +KEYBOARD_SOURCE = UserInputSchema( + capabilities=( + UserInputCapability(event_type="key_down", payload_fields=frozenset({"key"})), + UserInputCapability(event_type="key_up", payload_fields=frozenset({"key"})), + ) +) +WHEEL_SOURCE = UserInputSchema( + capabilities=( + UserInputCapability( + event_type="wheel_axis", payload_fields=frozenset({"axis", "value"}) + ), + ) +) +PROMPT_SOURCE = UserInputSchema( + capabilities=( + UserInputCapability( + event_type="prompt_set", payload_fields=frozenset({"prompt"}) + ), + ) +) + +# Written once against the canonical modality. It names no key and no axis. +STEERING_MAPPING = InputMappingSchema( + name="driver-command-to-steering", + consumes=(DRIVER_COMMAND,), + produces_step=(InputField(name="steering"),), +) +STEERING_MODEL = InferenceInputSchema(step_fields=(InputField(name="steering"),)) + +WINDOW = TimeWindow(start_s=0.0, end_s=1.0) +NEXT_WINDOW = TimeWindow(start_s=1.0, end_s=2.0) + + +class WheelToDriverCommand: + """Minimal wheel converter standing in for a real evdev profile.""" + + def __init__(self, *, priority: int = 10) -> None: + self._steer = 0.0 + self._seen = False + self._schema = DeviceConverterSchema( + name="wheel-to-driver-command", + produces=DRIVER_COMMAND, + device_kind="wheel", + priority=priority, + consumes=( + UserInputCapability( + event_type="wheel_axis", + payload_fields=frozenset({"axis", "value"}), + ), + ), + ) + + @property + def schema(self) -> DeviceConverterSchema: + return self._schema + + def reset(self) -> None: + self._steer = 0.0 + self._seen = False + + def convert( + self, user_inputs: UserInputs, window: TimeWindow + ) -> Mapping[str, Any] | None: + del window + for event in user_inputs.events: + if event.event_type == "wheel_axis" and event.payload["axis"] == "steer": + self._seen = True + self._steer = float(event.payload["value"]) + if not self._seen: + return None + return DRIVER_COMMAND.value( + { + "throttle": 0.0, + "brake": 0.0, + "steer": self._steer, + "stop": False, + "reverse": False, + } + ) + + +def _key(event_type: str, key: str, timestamp_s: float) -> UserInputEvent: + return UserInputEvent( + timestamp_s=timestamp_s, event_type=event_type, payload={"key": key} + ) + + +def _command(canonical: CanonicalInputs) -> Mapping[str, Any]: + assert DRIVER_COMMAND.name in canonical.values + return canonical.values[DRIVER_COMMAND.name] + + +# --- per-step conditioning ---------------------------------------------- + + +def test_keyboard_edges_become_canonical_driver_command() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs(events=(_key("key_down", "w", 0.1),)) + + canonical = canonicalizer.canonicalize( + inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert _command(canonical)["throttle"] == 1.0 + assert _command(canonical)["steer"] == 0.0 + assert canonical.metadata["canonical_sources"]["driver_command"] == "keyboard" + + +def test_key_aliases_are_normalized() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs(events=(_key("key_down", "ArrowLeft", 0.1),)) + + canonical = canonicalizer.canonicalize( + inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert _command(canonical)["steer"] == 1.0 + + +def test_held_key_still_emits_in_a_window_with_no_events() -> None: + """Edge-triggered HID must become level-triggered per-step conditioning.""" + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs(events=(_key("key_down", "w", 0.1),)) + canonicalizer.canonicalize(inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE) + + quiet = canonicalizer.canonicalize( + inputs, window=NEXT_WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert _command(quiet)["throttle"] == 1.0 + + +def test_key_release_returns_to_neutral() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs(events=(_key("key_down", "a", 0.1), _key("key_up", "a", 1.5))) + canonicalizer.canonicalize(inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE) + + released = canonicalizer.canonicalize( + inputs, window=NEXT_WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert _command(released)["steer"] == 0.0 + + +def test_reset_drops_device_state() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs(events=(_key("key_down", "w", 0.1),)) + canonicalizer.canonicalize(inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE) + + canonicalizer.reset() + after = canonicalizer.canonicalize( + UserInputs(), window=NEXT_WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert _command(after)["throttle"] == 0.0 + + +# --- boundary: global conditioning is not canonicalized ----------------- + + +def test_canonical_inputs_carry_live_control_only() -> None: + """Global conditioning is application-owned and bypasses this layer.""" + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + inputs = UserInputs( + events=( + _key("key_down", "w", 0.1), + UserInputEvent( + timestamp_s=0.2, event_type="prompt_set", payload={"prompt": "rain"} + ), + ) + ) + + canonical = canonicalizer.canonicalize( + inputs, window=WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert set(canonical.values) == {"driver_command"} + + +def test_application_supplies_global_conditioning_directly() -> None: + """A prompt swap reaches the session without touching canonicalization.""" + update = InferenceInput(step={"steering": 0.0}).with_global_update( + {"prompt": "heavy rain"} + ) + + assert update.requests_global_update + assert update.global_conditioning["prompt"] == "heavy rain" + + +# --- device independence ------------------------------------------------ + + +def test_mapping_written_against_a_modality_accepts_a_keyboard() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + + compatibility = check_mapping_compatibility( + canonical_schema=canonicalizer.canonical_schema(KEYBOARD_SOURCE), + inference_input_schema=STEERING_MODEL, + mapping_schema=STEERING_MAPPING, + ) + + assert compatibility.can_drive + + +def test_adding_a_device_needs_no_application_or_model_change() -> None: + """A wheel is one register() call; mapping and model schemas are untouched.""" + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + canonicalizer.register(WheelToDriverCommand()) + + compatibility = check_mapping_compatibility( + canonical_schema=canonicalizer.canonical_schema(WHEEL_SOURCE), + inference_input_schema=STEERING_MODEL, + mapping_schema=STEERING_MAPPING, + ) + assert compatibility.can_drive + + canonical = canonicalizer.canonicalize( + UserInputs( + events=( + UserInputEvent( + timestamp_s=0.5, + event_type="wheel_axis", + payload={"axis": "steer", "value": -0.4}, + ), + ) + ), + window=WINDOW, + source_schema=WHEEL_SOURCE, + ) + assert _command(canonical)["steer"] == pytest.approx(-0.4) + + +def test_source_with_no_feedable_converter_supplies_no_modalities() -> None: + canonicalizer = InputCanonicalizer([WheelToDriverCommand()]) + + schema = canonicalizer.canonical_schema(KEYBOARD_SOURCE) + + assert schema.modalities == () + assert not schema.supports(DRIVER_COMMAND) + assert canonicalizer.unavailable_converters(KEYBOARD_SOURCE) + + +def test_highest_priority_device_wins_when_both_are_present() -> None: + canonicalizer = InputCanonicalizer( + [KeyboardToDriverCommand(), WheelToDriverCommand()] + ) + both = UserInputSchema( + capabilities=KEYBOARD_SOURCE.capabilities + WHEEL_SOURCE.capabilities + ) + + canonical = canonicalizer.canonicalize( + UserInputs( + events=( + _key("key_down", "a", 0.2), + UserInputEvent( + timestamp_s=0.5, + event_type="wheel_axis", + payload={"axis": "steer", "value": -0.4}, + ), + ) + ), + window=WINDOW, + source_schema=both, + ) + + assert canonical.metadata["canonical_sources"]["driver_command"] == "wheel" + assert _command(canonical)["steer"] == pytest.approx(-0.4) + + +def test_preempted_device_keeps_its_state_current() -> None: + """Keyboard state must not be stale when the wheel disappears.""" + canonicalizer = InputCanonicalizer( + [KeyboardToDriverCommand(), WheelToDriverCommand()] + ) + both = UserInputSchema( + capabilities=KEYBOARD_SOURCE.capabilities + WHEEL_SOURCE.capabilities + ) + inputs = UserInputs( + events=( + _key("key_down", "w", 0.2), + UserInputEvent( + timestamp_s=0.5, + event_type="wheel_axis", + payload={"axis": "steer", "value": -0.4}, + ), + ) + ) + preempted = canonicalizer.canonicalize(inputs, window=WINDOW, source_schema=both) + assert preempted.metadata["canonical_sources"]["driver_command"] == "wheel" + + keyboard_only = canonicalizer.canonicalize( + inputs, window=NEXT_WINDOW, source_schema=KEYBOARD_SOURCE + ) + + assert keyboard_only.metadata["canonical_sources"]["driver_command"] == "keyboard" + assert _command(keyboard_only)["throttle"] == 1.0 + + +# --- registry ----------------------------------------------------------- + + +def test_duplicate_converter_names_are_rejected() -> None: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + + with pytest.raises(ValueError, match="already registered"): + canonicalizer.register(KeyboardToDriverCommand()) + + +def test_converter_must_fill_the_declared_modality_payload() -> None: + modality = CanonicalModality( + name="steering_wheel", payload_fields=frozenset({"steer", "throttle"}) + ) + + with pytest.raises(ValueError, match="requires payload fields"): + modality.value({"steer": 0.0}) + + +def test_new_modality_is_a_registration_not_a_core_change() -> None: + pedals = CanonicalModality( + name="pedal_state", payload_fields=frozenset({"throttle"}) + ) + + class PedalsConverter: + schema = DeviceConverterSchema( + name="pedals", + produces=pedals, + device_kind="pedals", + consumes=( + UserInputCapability( + event_type="pedal_axis", + payload_fields=frozenset({"value"}), + ), + ), + ) + + def reset(self) -> None: + return None + + def convert( + self, user_inputs: UserInputs, window: TimeWindow + ) -> Mapping[str, Any] | None: + del window + if not user_inputs.events: + return None + return pedals.value( + {"throttle": float(user_inputs.events[-1].payload["value"])} + ) + + source = UserInputSchema( + capabilities=( + UserInputCapability( + event_type="pedal_axis", payload_fields=frozenset({"value"}) + ), + ) + ) + canonicalizer = InputCanonicalizer([PedalsConverter()]) + + assert canonicalizer.canonical_schema(source).modalities == (pedals,) + canonical = canonicalizer.canonicalize( + UserInputs( + events=( + UserInputEvent( + timestamp_s=0.5, event_type="pedal_axis", payload={"value": 0.75} + ), + ) + ), + window=WINDOW, + source_schema=source, + ) + assert canonical.values["pedal_state"]["throttle"] == pytest.approx(0.75) + + +def test_replaying_the_same_windows_reproduces_the_same_canonical_inputs() -> None: + inputs = UserInputs(events=(_key("key_down", "w", 0.1), _key("key_down", "a", 1.2))) + + def run() -> list[dict[str, Any]]: + canonicalizer = InputCanonicalizer([KeyboardToDriverCommand()]) + return [ + dict( + _command( + canonicalizer.canonicalize( + inputs, window=window, source_schema=KEYBOARD_SOURCE + ) + ) + ) + for window in (WINDOW, NEXT_WINDOW) + ] + + assert run() == run() + + +# --- key bindings ------------------------------------------------------- + + +def test_bindings_are_data_and_can_be_rebound() -> None: + """A layout change must not require editing the converter.""" + azerty = InputCanonicalizer( + [ + KeyboardToDriverCommand( + bindings={ + "throttle": frozenset({"z"}), + "brake": frozenset({"s"}), + "steer_left": frozenset({"q"}), + "steer_right": frozenset({"d"}), + "stop": frozenset({"space"}), + } + ) + ] + ) + + canonical = azerty.canonicalize( + UserInputs(events=(_key("key_down", "z", 0.1),)), + window=WINDOW, + source_schema=KEYBOARD_SOURCE, + ) + + assert _command(canonical)["throttle"] == 1.0 + + +def test_tracked_keys_are_derived_so_an_action_cannot_go_unreachable() -> None: + """Declaring bindings and tracked keys separately used to disagree.""" + converter = KeyboardToDriverCommand( + bindings={"stop": frozenset({"escape"}), "throttle": frozenset({"w"})} + ) + canonicalizer = InputCanonicalizer([converter]) + + canonical = canonicalizer.canonicalize( + UserInputs(events=(_key("key_down", "escape", 0.1),)), + window=WINDOW, + source_schema=KEYBOARD_SOURCE, + ) + + assert _command(canonical)["stop"] is True + + +def test_unknown_driver_action_is_rejected() -> None: + with pytest.raises(ValueError, match="Unknown driver actions"): + KeyboardToDriverCommand(bindings={"turbo": frozenset({"t"})}) + + +def test_reverse_is_bindable() -> None: + canonicalizer = InputCanonicalizer( + [KeyboardToDriverCommand(bindings={"reverse": frozenset({"r"})})] + ) + + canonical = canonicalizer.canonicalize( + UserInputs(events=(_key("key_down", "r", 0.1),)), + window=WINDOW, + source_schema=KEYBOARD_SOURCE, + ) + + assert _command(canonical)["reverse"] is True + + +# --- scripted / mock input ---------------------------------------------- + + +def _scripted() -> InputCanonicalizer: + return InputCanonicalizer( + [ + ScriptedModality( + modality=DRIVER_COMMAND, + timeline=[ + ( + 0.0, + { + "throttle": 1.0, + "brake": 0.0, + "steer": 0.0, + "stop": False, + "reverse": False, + }, + ), + ( + 2.0, + { + "throttle": 0.0, + "brake": 0.0, + "steer": 1.0, + "stop": False, + "reverse": False, + }, + ), + ], + ) + ] + ) + + +def test_mock_input_needs_no_raw_events_or_source_schema() -> None: + """Authoring a benchmark scenario must not require raw device vocabulary.""" + canonical = _scripted().canonicalize( + UserInputs(), window=WINDOW, source_schema=UserInputSchema() + ) + + assert _command(canonical)["throttle"] == 1.0 + + +def test_scripted_values_hold_until_the_next_entry() -> None: + canonicalizer = _scripted() + windows = [TimeWindow(start_s=t, end_s=t + 1.0) for t in (0.0, 1.0, 2.0)] + + steer = [ + _command( + canonicalizer.canonicalize( + UserInputs(), window=w, source_schema=UserInputSchema() + ) + )["steer"] + for w in windows + ] + + assert steer == [0.0, 0.0, 1.0] + + +def test_scripted_converter_is_silent_before_its_first_entry() -> None: + canonicalizer = InputCanonicalizer( + [ + ScriptedModality( + modality=CanonicalModality(name="late", payload_fields=frozenset()), + timeline=[(5.0, {})], + ) + ] + ) + + canonical = canonicalizer.canonicalize( + UserInputs(), window=WINDOW, source_schema=UserInputSchema() + ) + + assert canonical.values == {} + + +def test_scripted_timeline_is_validated_against_the_modality() -> None: + with pytest.raises(ValueError, match="requires payload fields"): + ScriptedModality(modality=DRIVER_COMMAND, timeline=[(0.0, {"throttle": 1.0})]) + + +def test_scripted_replay_is_deterministic() -> None: + def run() -> list[float]: + canonicalizer = _scripted() + return [ + _command( + canonicalizer.canonicalize( + UserInputs(), + window=TimeWindow(start_s=t, end_s=t + 1.0), + source_schema=UserInputSchema(), + ) + )["steer"] + for t in (0.0, 1.0, 2.0) + ] + + assert run() == run() diff --git a/flashdreams/tests/test_runtime_input_mapping.py b/flashdreams/tests/test_runtime_input_mapping.py new file mode 100644 index 00000000..00cd9758 --- /dev/null +++ b/flashdreams/tests/test_runtime_input_mapping.py @@ -0,0 +1,573 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for declarative input-mapping compatibility in the runtime API. + +These cover the T2/T3 contract: sources declare what user events they can +provide at payload granularity, models declare required and optional +initial/per-step inputs, and a mapping declares what it consumes and produces so +compatibility can be answered before expensive runtime initialization. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from flashdreams.runtime import ( + DRIVER_COMMAND, + SESSION_START_ONLY, + CanonicalInputs, + CanonicalInputSchema, + CanonicalModality, + IdentityInputMapping, + InferenceInput, + InferenceInputSchema, + InputField, + InputMappingSchema, + StepRequest, + TimeWindow, + UserInputCapability, + UserInputEvent, + UserInputs, + UserInputSchema, + check_mapping_compatibility, + check_mapping_set_compatibility, + combine_mapping_schemas, + undeclared_inference_inputs, +) + +pytestmark = pytest.mark.ci_cpu + +KEY_DOWN = UserInputCapability(event_type="key_down", payload_fields=frozenset({"key"})) +KEY_UP = UserInputCapability(event_type="key_up", payload_fields=frozenset({"key"})) +PROMPT_SET = UserInputCapability( + event_type="prompt_set", + semantic_type="text", + payload_fields=frozenset({"prompt"}), +) +FRAME_SET = UserInputCapability( + event_type="initial_frame_set", payload_fields=frozenset({"image"}) +) + +BROWSER_SOURCE = UserInputSchema( + capabilities=(KEY_DOWN, KEY_UP, PROMPT_SET, FRAME_SET), + description="browser webrtc client", +) + +CAMERA_LOOK = CanonicalModality( + name="camera_look", payload_fields=frozenset({"yaw", "pitch"}) +) + +CANONICAL_ALL = CanonicalInputSchema(modalities=(DRIVER_COMMAND, CAMERA_LOOK)) + +# Global conditioning is application-owned and does not come from a canonical +# modality, so this mapping consumes nothing and only declares what it produces. +PROMPT_MAPPING = InputMappingSchema( + name="prompt", + produces_global=(InputField(name="prompt", semantic_type="text"),), +) +FRAME_MAPPING = InputMappingSchema( + name="conditioning-frame", + produces_global=(InputField(name="global_conditioning_frame", required=False),), +) +STEERING_MAPPING = InputMappingSchema( + name="driver-command-to-steering", + consumes=(DRIVER_COMMAND,), + produces_step=(InputField(name="steering"),), +) +LOOK_MAPPING = InputMappingSchema( + name="camera-look", + consumes=(CAMERA_LOOK,), + produces_step=(InputField(name="camera_delta", required=False),), +) + +DRIVING_MODEL = InferenceInputSchema( + global_fields=( + InputField(name="prompt", semantic_type="text", lifecycle="cache_init"), + ), + step_fields=( + InputField(name="steering", lifecycle="step_input"), + InputField(name="camera_delta", required=False, lifecycle="step_input"), + ), +) + + +# --- user input events and windowing ------------------------------------ + + +def test_startup_values_are_represented_as_events() -> None: + inputs = UserInputs( + events=( + UserInputEvent( + timestamp_s=0.0, event_type="prompt_set", payload={"prompt": "drive"} + ), + UserInputEvent( + timestamp_s=0.5, event_type="key_down", payload={"key": "w"} + ), + ) + ) + + assert inputs.events[0].event_type == "prompt_set" + assert inputs.events[0].payload["prompt"] == "drive" + + +def test_windowing_is_half_open_and_deterministic() -> None: + inputs = UserInputs( + events=tuple( + UserInputEvent(timestamp_s=t, event_type="key_down", payload={"key": "w"}) + for t in (0.0, 0.5, 1.0, 1.5) + ) + ) + + windowed = inputs.window(TimeWindow(start_s=0.5, end_s=1.5)) + + assert [event.timestamp_s for event in windowed.events] == [0.5, 1.0] + + +def test_out_of_order_events_are_rejected() -> None: + with pytest.raises(ValueError, match="non-decreasing"): + UserInputs( + events=( + UserInputEvent(timestamp_s=1.0, event_type="key_down"), + UserInputEvent(timestamp_s=0.5, event_type="key_up"), + ) + ) + + +# --- user input schemas ------------------------------------------------- + + +def test_source_declares_capabilities_at_payload_granularity() -> None: + assert BROWSER_SOURCE.supports(KEY_DOWN) + assert not BROWSER_SOURCE.supports( + UserInputCapability( + event_type="key_down", payload_fields=frozenset({"key", "modifiers"}) + ) + ) + + +def test_bare_event_types_still_satisfy_payload_free_consumers() -> None: + """Coarse pre-capability schemas keep working against the finer query.""" + coarse = UserInputSchema(event_types=frozenset({"reset"})) + + assert coarse.supports(UserInputCapability(event_type="reset")) + assert not coarse.supports( + UserInputCapability(event_type="reset", payload_fields=frozenset({"reason"})) + ) + assert coarse.supports_event_types({"reset"}) + + +def test_capabilities_widen_declared_event_types() -> None: + assert "key_down" in BROWSER_SOURCE.declared_event_types() + assert BROWSER_SOURCE.supports_event_types({"key_down", "prompt_set"}) + + +def test_semantic_type_mismatch_blocks_capability_match() -> None: + source = UserInputSchema( + capabilities=( + UserInputCapability(event_type="prompt_set", semantic_type="embedding"), + ) + ) + + assert not source.supports( + UserInputCapability(event_type="prompt_set", semantic_type="text") + ) + + +def test_event_validation_reports_missing_payload_fields() -> None: + event = UserInputEvent(timestamp_s=0.0, event_type="key_down", payload={}) + + with pytest.raises(ValueError, match="missing required"): + BROWSER_SOURCE.validate_event(event) + + +def test_event_validation_rejects_undeclared_event_type() -> None: + event = UserInputEvent(timestamp_s=0.0, event_type="wheel_axis") + + with pytest.raises(ValueError, match="does not provide event type"): + BROWSER_SOURCE.validate_event(event) + + +# --- model input schemas ------------------------------------------------ + + +def test_model_declares_required_and_optional_fields_per_phase() -> None: + required = DRIVING_MODEL.required_fields() + optional = DRIVING_MODEL.optional_fields() + + assert {(phase, f.name) for phase, f in required} == { + ("global", "prompt"), + ("step", "steering"), + } + assert {(phase, f.name) for phase, f in optional} == {("step", "camera_delta")} + + +def test_required_fields_can_be_filtered_by_phase() -> None: + step_only = DRIVING_MODEL.required_fields("step") + + assert [f.name for _, f in step_only] == ["steering"] + + +def test_field_lookup_is_phase_scoped() -> None: + assert DRIVING_MODEL.field_for(name="prompt", phase="global") is not None + assert DRIVING_MODEL.field_for(name="prompt", phase="step") is None + + +def test_invalid_phase_is_rejected() -> None: + bad_phase: Any = "final" + + with pytest.raises(ValueError, match="phase must be"): + DRIVING_MODEL.fields_for(bad_phase) + + +def test_inference_input_expose_payload_per_phase() -> None: + inputs = InferenceInput( + global_conditioning={"prompt": "drive"}, step={"steering": 0.25} + ) + + assert inputs.for_phase("global")["prompt"] == "drive" + assert inputs.for_phase("step")["steering"] == 0.25 + + +def test_lifecycle_and_update_policy_are_queryable_metadata() -> None: + field = InputField( + name="prompt", + update_policy="step_boundary", + lifecycle="cache_init", + metadata={"coordinates": "opencv_c2w"}, + ) + + assert field.update_policy == "step_boundary" + assert field.lifecycle == "cache_init" + assert field.metadata["coordinates"] == "opencv_c2w" + + +def test_metadata_is_excluded_from_field_equality() -> None: + plain = InputField(name="prompt") + annotated = InputField(name="prompt", metadata={"note": "hint"}) + + assert plain == annotated + + +# --- mapping compatibility ---------------------------------------------- + + +def test_compatible_source_model_and_mapping_can_drive() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, STEERING_MAPPING, LOOK_MAPPING), + ) + + assert compatibility.can_drive + assert {(p, f.name) for p, f in compatibility.satisfied_required_model_fields} == { + ("global", "prompt"), + ("step", "steering"), + } + assert {(p, f.name) for p, f in compatibility.available_optional_model_fields} == { + ("step", "camera_delta") + } + + +def test_missing_required_model_field_blocks_the_run() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING,), + ) + + assert not compatibility.can_drive + assert [f.name for _, f in compatibility.missing_required_model_fields] == [ + "steering" + ] + + +def test_missing_source_capability_is_reported_when_it_blocks() -> None: + no_wheel = CanonicalInputSchema(modalities=(CAMERA_LOOK,)) + + compatibility = check_mapping_set_compatibility( + canonical_schema=no_wheel, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, STEERING_MAPPING), + ) + + assert not compatibility.can_drive + assert compatibility.unavailable_mapping_names == ("driver-command-to-steering",) + assert {m.name for m in compatibility.missing_modalities} == {"driver_command"} + + +def test_unfeedable_optional_mapping_degrades_instead_of_vetoing() -> None: + """Losing a mapping that fed only optional fields must not block the run.""" + no_look = CanonicalInputSchema(modalities=(DRIVER_COMMAND,)) + + compatibility = check_mapping_set_compatibility( + canonical_schema=no_look, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, STEERING_MAPPING, LOOK_MAPPING), + ) + + assert compatibility.can_drive + assert compatibility.unavailable_mapping_names == ("camera-look",) + # The dropped mapping's field must not be advertised as available. + assert compatibility.available_optional_model_fields == () + + +def test_optional_field_needs_mapping_support_to_be_available() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, STEERING_MAPPING), + ) + + assert compatibility.can_drive + assert compatibility.available_optional_model_fields == () + + +def test_lifecycle_disagreement_blocks_a_field_match() -> None: + model = InferenceInputSchema( + global_fields=(InputField(name="prompt", lifecycle="rollout_binding"),) + ) + mapping = InputMappingSchema( + name="prompt", + produces_global=(InputField(name="prompt", lifecycle="cache_init"),), + ) + + compatibility = check_mapping_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=model, + mapping_schema=mapping, + ) + + assert not compatibility.can_drive + + +def test_unspecified_lifecycle_stays_permissive() -> None: + model = InferenceInputSchema(global_fields=(InputField(name="prompt"),)) + + compatibility = check_mapping_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=model, + mapping_schema=PROMPT_MAPPING, + ) + + assert compatibility.can_drive + + +def test_raise_if_incompatible_names_both_failure_kinds() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CanonicalInputSchema(modalities=(CAMERA_LOOK,)), + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, STEERING_MAPPING), + ) + + with pytest.raises(ValueError) as excinfo: + compatibility.raise_if_incompatible() + + message = str(excinfo.value) + assert "missing canonical modalities" in message + assert "missing required model inputs" in message + + +def test_raise_if_incompatible_is_a_no_op_when_compatible() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(PROMPT_MAPPING, FRAME_MAPPING, STEERING_MAPPING), + ) + + compatibility.raise_if_incompatible() + + +def test_check_mapping_compatibility_rejects_a_non_schema() -> None: + not_a_schema: Any = object() + + with pytest.raises(TypeError, match="InputMappingSchema"): + check_mapping_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schema=not_a_schema, + ) + + +# --- mapping schema composition ----------------------------------------- + + +def test_combining_mappings_unions_their_surfaces() -> None: + combined = combine_mapping_schemas((PROMPT_MAPPING, STEERING_MAPPING)) + + assert {m.name for m in combined.consumes} == {"driver_command"} + assert [f.name for f in combined.produces_global] == ["prompt"] + assert [f.name for f in combined.produces_step] == ["steering"] + + +def test_duplicate_declarations_collapse_and_merge_metadata() -> None: + first = InputMappingSchema( + name="a", + produces_global=(InputField(name="prompt", metadata={"source": "a"}),), + ) + second = InputMappingSchema( + name="b", + produces_global=( + InputField(name="prompt", metadata={"source": "b", "extra": "kept"}), + ), + ) + + combined = combine_mapping_schemas((first, second)) + + assert len(combined.produces_global) == 1 + metadata = combined.produces_global[0].metadata + assert metadata["source"] == "a" + assert metadata["extra"] == "kept" + + +def test_combine_rejects_non_schema_entries() -> None: + not_a_schema: Any = object() + + with pytest.raises(TypeError, match="InputMappingSchema"): + combine_mapping_schemas((PROMPT_MAPPING, not_a_schema)) + + +# --- declaration drift -------------------------------------------------- + + +def test_undeclared_inference_input_catches_schema_drift() -> None: + produced = InferenceInput( + global_conditioning={"prompt": "drive"}, step={"steering": 0.0} + ) + + undeclared = undeclared_inference_inputs(produced, PROMPT_MAPPING) + + assert undeclared == (("step", "steering"),) + + +def test_declared_outputs_report_no_drift() -> None: + combined = combine_mapping_schemas((PROMPT_MAPPING, STEERING_MAPPING)) + produced = InferenceInput( + global_conditioning={"prompt": "drive"}, step={"steering": 0.0} + ) + + assert undeclared_inference_inputs(produced, combined) == () + + +# --- interoperability with the T1 envelope ------------------------------ + + +def test_identity_mapping_needs_no_declared_surface() -> None: + """Fixed-input runs stay possible without any schema declaration.""" + mapping = IdentityInputMapping() + fixed = InferenceInput( + global_conditioning={"prompt": "fixed"}, step={"steering": 0.0} + ) + + mapped = mapping.map_step_inputs( + canonical_inputs=CanonicalInputs(), + inference_input=fixed, + request=StepRequest(step_index=0), + ) + + assert mapped.step["steering"] == 0.0 + + +def test_empty_mapping_set_cannot_satisfy_a_required_field() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CANONICAL_ALL, + inference_input_schema=DRIVING_MODEL, + mapping_schemas=(), + ) + + assert not compatibility.can_drive + assert len(compatibility.missing_required_model_fields) == 2 + + +def test_model_with_no_requirements_is_always_drivable() -> None: + compatibility = check_mapping_set_compatibility( + canonical_schema=CanonicalInputSchema(), + inference_input_schema=InferenceInputSchema(), + mapping_schemas=(), + ) + + assert compatibility.can_drive + + +# --- global conditioning updates vs reset ------------------------------- + + +def test_empty_global_slot_requests_no_update() -> None: + steady_state = InferenceInput(step={"steering": 0.25}) + + assert not steady_state.requests_global_update + + +def test_non_empty_global_slot_mid_rollout_is_an_update_request() -> None: + """Changing weather mid-run updates conditioning; it is not a reset.""" + updated = InferenceInput(step={"steering": 0.0}).with_global_update( + {"prompt": "heavy rain"} + ) + + assert updated.requests_global_update + assert updated.global_conditioning["prompt"] == "heavy rain" + assert updated.step["steering"] == 0.0 + + +def test_with_step_carries_the_global_slot_through() -> None: + started = InferenceInput(global_conditioning={"prompt": "drive"}) + + stepped = started.with_step({"steering": 0.5}) + + assert stepped.global_conditioning["prompt"] == "drive" + + +def test_without_global_update_clears_the_request() -> None: + started = InferenceInput( + global_conditioning={"prompt": "drive"}, step={"steering": 0.5} + ) + + steady_state = started.without_global_update() + + assert not steady_state.requests_global_update + assert steady_state.step["steering"] == 0.5 + + +def test_model_can_declare_conditioning_it_cannot_swap_mid_rollout() -> None: + schema = InferenceInputSchema( + global_fields=( + InputField(name="prompt", update_policy="step_boundary"), + InputField(name="scene_id", update_policy=SESSION_START_ONLY), + ) + ) + update = InferenceInput( + global_conditioning={"prompt": "heavy rain", "scene_id": "town_02"} + ) + + assert schema.unsupported_global_updates(update) == ("scene_id",) + + +def test_permissive_when_no_update_policy_is_declared() -> None: + schema = InferenceInputSchema(global_fields=(InputField(name="prompt"),)) + update = InferenceInput(global_conditioning={"prompt": "heavy rain"}) + + assert schema.unsupported_global_updates(update) == () + + +def test_undeclared_global_values_are_left_to_the_adapter() -> None: + schema = InferenceInputSchema(global_fields=(InputField(name="prompt"),)) + update = InferenceInput(global_conditioning={"mystery": 1}) + + assert schema.unsupported_global_updates(update) == () + + +def test_steady_state_steps_do_not_request_a_global_update() -> None: + """Carrying session-start conditioning forward would look like an update.""" + started = InferenceInput(global_conditioning={"prompt": "drive"}) + + steady_state = InferenceInput(step={"chunk_index": 1}) + + assert started.requests_global_update + assert not steady_state.requests_global_update + assert ( + not started.with_step({"chunk_index": 1}) + .without_global_update() + .requests_global_update + ) From 637b38cf3d0a0cc6d400919347a86394c467b7ae Mon Sep 17 00:00:00 2001 From: Fangjun Zhou Date: Wed, 5 Aug 2026 11:22:40 -0700 Subject: [PATCH 6/8] Create InferenceSession and moved InferenceInput to inference_session --- ...inference_runtime_inputs_implementation.md | 5 +- flashdreams/flashdreams/runtime/__init__.py | 2 +- .../flashdreams/runtime/inference_session.py | 135 ++++++++++++++++++ flashdreams/flashdreams/runtime/inputs.py | 95 ++---------- flashdreams/flashdreams/runtime/interfaces.py | 7 +- flashdreams/flashdreams/runtime/mapping.py | 2 +- 6 files changed, 157 insertions(+), 89 deletions(-) create mode 100644 flashdreams/flashdreams/runtime/inference_session.py diff --git a/docs/inference_runtime_inputs_implementation.md b/docs/inference_runtime_inputs_implementation.md index 8d485768..f7db0564 100644 --- a/docs/inference_runtime_inputs_implementation.md +++ b/docs/inference_runtime_inputs_implementation.md @@ -11,7 +11,10 @@ what is intentionally still outside this layer. Implementation lives in `flashdreams.runtime`: -- `flashdreams/flashdreams/runtime/inputs.py` — the input types and schemas +- `flashdreams/flashdreams/runtime/inputs.py` — user/canonical input types + and schemas +- `flashdreams/flashdreams/runtime/inference_session.py` — model-ready + `InferenceInput` and session lifecycle - `flashdreams/flashdreams/runtime/canonical.py` — raw device to canonical modality conversion - `flashdreams/flashdreams/runtime/mapping.py` — canonical to encoded mapping diff --git a/flashdreams/flashdreams/runtime/__init__.py b/flashdreams/flashdreams/runtime/__init__.py index ab303c74..9ce9d4ee 100644 --- a/flashdreams/flashdreams/runtime/__init__.py +++ b/flashdreams/flashdreams/runtime/__init__.py @@ -17,13 +17,13 @@ ScriptedModality, ) from flashdreams.runtime.config import ExecutionBackend, InferenceConfig, Precision +from flashdreams.runtime.inference_session import InferenceInput from flashdreams.runtime.inputs import ( INPUT_PHASES, SESSION_START_ONLY, CanonicalInputs, CanonicalInputSchema, CanonicalModality, - InferenceInput, InferenceInputSchema, InputField, InputPhase, diff --git a/flashdreams/flashdreams/runtime/inference_session.py b/flashdreams/flashdreams/runtime/inference_session.py new file mode 100644 index 00000000..6ce8b48d --- /dev/null +++ b/flashdreams/flashdreams/runtime/inference_session.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inference session lifecycle and model-input envelope.""" + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any, TypedDict + +from typing_extensions import Unpack + +from flashdreams.infra.pipeline import ( + StreamInferencePipeline, + StreamInferencePipelineConfig, +) +from flashdreams.runtime._utils import freeze_mapping +from flashdreams.runtime.inputs import InputPhase, validate_phase + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceInput: + """Encoded inputs for one :class:`InferenceSession` call. + + Two conditioning slots: + + - ``global_conditioning``: values that condition the whole rollout, such as + the conditioning frame or prompt. Normally supplied when the session + starts. + - ``step``: values needed to generate the next chunk or frame. + + A non-empty ``global_conditioning`` on a mid-rollout input is an *update + request*, not a reset. The session should apply it when the model supports + that; resetting rollout state is a separate, explicit + :meth:`InferenceSession.reset` call. Whether a given value can be updated + mid-rollout is declared by ``InputField.update_policy``; see + ``InferenceInputSchema.unsupported_global_updates``. + """ + + __hash__ = None + + global_conditioning: Mapping[str, Any] = field(default_factory=dict) + step: Mapping[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, "global_conditioning", freeze_mapping(self.global_conditioning) + ) + object.__setattr__(self, "step", freeze_mapping(self.step)) + object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + + @property + def requests_global_update(self) -> bool: + """Return whether this input asks the session to update conditioning.""" + return bool(self.global_conditioning) + + def with_step(self, step: Mapping[str, Any]) -> "InferenceInput": + """Return a copy with replaced per-step payload. + + The global slot is carried through unchanged, so a mid-rollout input + built this way keeps whatever update request it already had. Use + :meth:`without_global_update` for the common steady-state case. + """ + return InferenceInput( + global_conditioning=self.global_conditioning, + step=step, + metadata=self.metadata, + ) + + def with_global_update( + self, global_conditioning: Mapping[str, Any] + ) -> "InferenceInput": + """Return a copy requesting a mid-rollout conditioning update.""" + return InferenceInput( + global_conditioning=global_conditioning, + step=self.step, + metadata=self.metadata, + ) + + def without_global_update(self) -> "InferenceInput": + """Return a copy that requests no conditioning update.""" + return InferenceInput(step=self.step, metadata=self.metadata) + + def for_phase(self, phase: InputPhase) -> Mapping[str, Any]: + """Return the payload mapping for ``phase``.""" + return ( + self.global_conditioning if validate_phase(phase) == "global" else self.step + ) + + +class InferenceSessionConfig(TypedDict): + """Configuration for constructing an inference session.""" + + pipeline: StreamInferencePipelineConfig + """Pipeline configuration to instantiate.""" + + +class InferenceSession: + """Stateful inference pipeline session.""" + + def __init__(self, **kwargs: Unpack[InferenceSessionConfig]) -> None: + """Initialize the inference pipeline. + + Args: + **kwargs: Session construction keyword arguments. + """ + # Initialize the inference pipeline from the provided configuration. + self.pipeline: StreamInferencePipeline = kwargs["pipeline"].setup() + + def __del__(self) -> None: + """Release session resources.""" + if hasattr(self, "pipeline"): + del self.pipeline + + def reset(self) -> None: + """Reset the inference session.""" + + def step(self, inference_input: InferenceInput) -> None: + """Run one inference step. + + Args: + inference_input: Model-ready inputs for the step. + """ diff --git a/flashdreams/flashdreams/runtime/inputs.py b/flashdreams/flashdreams/runtime/inputs.py index f0be31be..66a4432c 100644 --- a/flashdreams/flashdreams/runtime/inputs.py +++ b/flashdreams/flashdreams/runtime/inputs.py @@ -1,17 +1,20 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""User- and model-input envelopes for the experimental runtime API.""" +"""User, canonical, and model-input schemas for the experimental runtime API.""" from __future__ import annotations import math from collections.abc import Iterable, Mapping from dataclasses import dataclass, field -from typing import Any, Literal, cast +from typing import TYPE_CHECKING, Any, Literal, cast from flashdreams.runtime._utils import freeze_mapping +if TYPE_CHECKING: + from flashdreams.runtime.inference_session import InferenceInput + InputPhase = Literal["global", "step"] INPUT_PHASES: tuple[InputPhase, ...] = ("global", "step") @@ -258,7 +261,7 @@ def _select( if input_field.required is required ) - def unsupported_global_updates(self, inputs: "InferenceInput") -> tuple[str, ...]: + def unsupported_global_updates(self, inputs: InferenceInput) -> tuple[str, ...]: """Return requested conditioning updates this model cannot apply. A field whose ``update_policy`` is :data:`SESSION_START_ONLY` can be @@ -273,15 +276,15 @@ def unsupported_global_updates(self, inputs: "InferenceInput") -> tuple[str, ... and declared.update_policy == SESSION_START_ONLY ) - def missing_global(self, inputs: "InferenceInput") -> tuple[str, ...]: + def missing_global(self, inputs: InferenceInput) -> tuple[str, ...]: """Return required initial fields absent from ``inputs``.""" return _missing_required(self.global_fields, inputs.global_conditioning) - def missing_step(self, inputs: "InferenceInput") -> tuple[str, ...]: + def missing_step(self, inputs: InferenceInput) -> tuple[str, ...]: """Return required per-step fields absent from ``inputs``.""" return _missing_required(self.step_fields, inputs.step) - def require_global(self, inputs: "InferenceInput") -> None: + def require_global(self, inputs: InferenceInput) -> None: """Raise if required initial fields are absent.""" missing = self.missing_global(inputs) if missing: @@ -289,7 +292,7 @@ def require_global(self, inputs: "InferenceInput") -> None: f"Missing required global conditioning input(s): {missing}" ) - def require_step(self, inputs: "InferenceInput") -> None: + def require_step(self, inputs: InferenceInput) -> None: """Raise if required per-step fields are absent.""" missing = self.missing_step(inputs) if missing: @@ -371,7 +374,8 @@ class CanonicalModality: Modalities describe live user control only. Global conditioning such as a prompt or conditioning frame is application-owned and reaches - :class:`InferenceInput` directly, without passing through this layer. + :class:`flashdreams.runtime.inference_session.InferenceInput` directly, + without passing through this layer. """ name: str @@ -426,8 +430,8 @@ class CanonicalInputs: Values are level-triggered and normally present every step: a key held down emits no events but still means full throttle. Global conditioning does not - appear here; it is application-owned and reaches :class:`InferenceInput` - directly. + appear here; it is application-owned and reaches + :class:`flashdreams.runtime.inference_session.InferenceInput` directly. """ __hash__ = None @@ -440,77 +444,6 @@ def __post_init__(self) -> None: object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) -@dataclass(frozen=True, kw_only=True, slots=True) -class InferenceInput: - """Encoded inputs for one :class:`InferenceSession` call. - - Two conditioning slots: - - - ``global_conditioning``: values that condition the whole rollout, such as - the conditioning frame or prompt. Normally supplied when the session - starts. - - ``step``: values needed to generate the next chunk or frame. - - A non-empty ``global_conditioning`` on a mid-rollout input is an *update - request*, not a reset. The session should apply it when the model supports - that; resetting rollout state is a separate, explicit - :meth:`InferenceSession.reset` call. Whether a given value can be updated - mid-rollout is declared per field by ``InputField.update_policy``; see - :meth:`InferenceInputSchema.unsupported_global_updates`. - """ - - __hash__ = None - - global_conditioning: Mapping[str, Any] = field(default_factory=dict) - step: Mapping[str, Any] = field(default_factory=dict) - metadata: Mapping[str, Any] = field(default_factory=dict) - - def __post_init__(self) -> None: - object.__setattr__( - self, "global_conditioning", freeze_mapping(self.global_conditioning) - ) - object.__setattr__(self, "step", freeze_mapping(self.step)) - object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) - - @property - def requests_global_update(self) -> bool: - """Return whether this input asks the session to update conditioning.""" - return bool(self.global_conditioning) - - def with_step(self, step: Mapping[str, Any]) -> "InferenceInput": - """Return a copy with replaced per-step payload. - - The global slot is carried through unchanged, so a mid-rollout input - built this way keeps whatever update request it already had. Use - :meth:`without_global_update` for the common steady-state case. - """ - return InferenceInput( - global_conditioning=self.global_conditioning, - step=step, - metadata=self.metadata, - ) - - def with_global_update( - self, global_conditioning: Mapping[str, Any] - ) -> "InferenceInput": - """Return a copy requesting a mid-rollout conditioning update.""" - return InferenceInput( - global_conditioning=global_conditioning, - step=self.step, - metadata=self.metadata, - ) - - def without_global_update(self) -> "InferenceInput": - """Return a copy that requests no conditioning update.""" - return InferenceInput(step=self.step, metadata=self.metadata) - - def for_phase(self, phase: InputPhase) -> Mapping[str, Any]: - """Return the payload mapping for ``phase``.""" - return ( - self.global_conditioning if validate_phase(phase) == "global" else self.step - ) - - def _missing_required( fields: tuple[InputField, ...], payload: Mapping[str, Any] ) -> tuple[str, ...]: diff --git a/flashdreams/flashdreams/runtime/interfaces.py b/flashdreams/flashdreams/runtime/interfaces.py index 852a77f1..0ff276e4 100644 --- a/flashdreams/flashdreams/runtime/interfaces.py +++ b/flashdreams/flashdreams/runtime/interfaces.py @@ -8,11 +8,8 @@ from typing import Protocol, runtime_checkable from flashdreams.runtime.config import InferenceConfig -from flashdreams.runtime.inputs import ( - CanonicalInputSchema, - InferenceInput, - InferenceInputSchema, -) +from flashdreams.runtime.inference_session import InferenceInput +from flashdreams.runtime.inputs import CanonicalInputSchema, InferenceInputSchema from flashdreams.runtime.mapping import InputMapping from flashdreams.runtime.types import StepRequest, StepResult diff --git a/flashdreams/flashdreams/runtime/mapping.py b/flashdreams/flashdreams/runtime/mapping.py index 94f48140..4527c3fd 100644 --- a/flashdreams/flashdreams/runtime/mapping.py +++ b/flashdreams/flashdreams/runtime/mapping.py @@ -10,12 +10,12 @@ from typing import Any, Protocol, runtime_checkable from flashdreams.runtime._utils import freeze_mapping +from flashdreams.runtime.inference_session import InferenceInput from flashdreams.runtime.inputs import ( INPUT_PHASES, CanonicalInputs, CanonicalInputSchema, CanonicalModality, - InferenceInput, InferenceInputSchema, InputField, InputPhase, From 4083c344a05f8dd07c5b1382c60bca2e11759d3e Mon Sep 17 00:00:00 2001 From: Fangjun Zhou Date: Wed, 5 Aug 2026 15:35:57 -0700 Subject: [PATCH 7/8] WIP InferenceSession implementation --- docs/inference_runtime_api_design.md | 2 +- ...inference_runtime_inputs_implementation.md | 61 ++---- ...ence_runtime_supported_inputs_inventory.md | 10 +- flashdreams/flashdreams/runtime/__init__.py | 10 +- .../flashdreams/runtime/inference_session.py | 194 +++++++++++------ flashdreams/flashdreams/runtime/inputs.py | 115 ++-------- flashdreams/flashdreams/runtime/interfaces.py | 12 +- flashdreams/flashdreams/runtime/mapping.py | 24 ++- flashdreams/flashdreams/runtime/output.py | 8 +- flashdreams/flashdreams/runtime/types.py | 7 +- .../tests/test_inference_runtime_api.py | 81 +++++-- flashdreams/tests/test_inference_session.py | 198 ++++++++++++++++++ flashdreams/tests/test_runtime_canonical.py | 13 +- .../tests/test_runtime_input_mapping.py | 137 ++---------- 14 files changed, 493 insertions(+), 379 deletions(-) create mode 100644 flashdreams/tests/test_inference_session.py diff --git a/docs/inference_runtime_api_design.md b/docs/inference_runtime_api_design.md index f70fbd89..a753742d 100644 --- a/docs/inference_runtime_api_design.md +++ b/docs/inference_runtime_api_design.md @@ -667,7 +667,7 @@ registry, standard loop, concrete output modes, or model migrations: - The model-specific integration boundary is named `ModelAdapter`. - Heavyweight lifecycle is split into `InferenceRuntime` and `InferenceSession`. -- Step data carriers are named `StepRequest` and `StepResult`; a session returns +- Step data carriers are named `StepRequest` and `InferenceOutput`; a session returns `None` from `next_step_request()` when the rollout is complete. - Raw inputs use `UserInputs`, canonicalized inputs use `CanonicalInputs`, and model-facing inputs use `InferenceInput`. diff --git a/docs/inference_runtime_inputs_implementation.md b/docs/inference_runtime_inputs_implementation.md index f7db0564..9ad8a4a3 100644 --- a/docs/inference_runtime_inputs_implementation.md +++ b/docs/inference_runtime_inputs_implementation.md @@ -14,7 +14,7 @@ Implementation lives in `flashdreams.runtime`: - `flashdreams/flashdreams/runtime/inputs.py` — user/canonical input types and schemas - `flashdreams/flashdreams/runtime/inference_session.py` — model-ready - `InferenceInput` and session lifecycle + `InferenceInput`, `InferenceInputSchema`, and session lifecycle - `flashdreams/flashdreams/runtime/canonical.py` — raw device to canonical modality conversion - `flashdreams/flashdreams/runtime/mapping.py` — canonical to encoded mapping @@ -63,56 +63,41 @@ the same thing at each: - **per-step conditioning** — needed to generate the next chunk or frame: steering, HD map frames, camera trajectory. -`InputPhase` is `Literal["global", "step"]`. The axis names *which slot*, not -*when the value may arrive* — see the next section. - -## Global Conditioning Updates Are Not Resets - -A non-empty global slot on a mid-rollout `InferenceInput` is an **update -request**. The session should apply it when the model supports doing so. -Resetting rollout state is a separate, explicit `InferenceSession.reset()` call. -The motivating case is changing prompt and conditioning frame mid-run to change -the weather in an Omnidreams rollout. +``InferenceInput`` exposes exactly those two mappings: ```python from flashdreams.runtime import InferenceInput -steady_state = InferenceInput(step={"steering": 0.25}) -assert not steady_state.requests_global_update - -changed_weather = steady_state.with_global_update({"prompt": "heavy rain"}) -assert changed_weather.requests_global_update +inputs = InferenceInput( + global_conditioning={"prompt": "drive"}, + per_step_conditioning={"steering": 0.25}, +) ``` -Because `with_step()` carries the global slot through unchanged, use -`without_global_update()` for the steady-state case; otherwise every step looks -like an update request. +``InputPhase`` remains ``Literal["global", "step"]`` for mapping schemas. The +inference envelope uses the more explicit ``global_conditioning`` and +``per_step_conditioning`` names directly. -Whether a value can actually be swapped mid-rollout is declared per field: +## Payload Validation + +``InferenceInputSchema`` declares ``global_fields`` and +``per_step_fields``. Its two checks validate the corresponding payload and +raise ``ValueError`` when a required field is absent: ```python -from flashdreams.runtime import SESSION_START_ONLY, InferenceInputSchema, InputField +from flashdreams.runtime import InferenceInputSchema, InputField schema = InferenceInputSchema( - global_fields=( - InputField(name="prompt", update_policy="step_boundary"), - InputField(name="scene_id", update_policy=SESSION_START_ONLY), - ) + global_fields=(InputField(name="prompt"),), + per_step_fields=(InputField(name="steering"),), ) -schema.unsupported_global_updates( - InferenceInput(global_conditioning={"prompt": "heavy rain", "scene_id": "town_02"}) -) -# ("scene_id",) +schema.check_global_payload(inputs) +schema.check_per_step_payload(inputs) ``` -`SESSION_START_ONLY` is the one reserved `update_policy` token. Everything else -in that vocabulary, and all of `lifecycle`, is open and adapter-owned; this layer -only carries it as queryable metadata. - -Steady-state steps must leave the global slot empty; otherwise every step reads -as an update request. Converters emit every window, because live control is -level-triggered: a key held across a step emits no events but still means full -throttle. +Optional fields do not block either check. ``update_policy``, ``lifecycle``, and +other ``InputField`` metadata remain adapter-owned query hints; deep payload +validation stays in the adapter or mapping. ## Raw Inputs @@ -269,7 +254,7 @@ Named here only so the boundary is explicit; these are not gaps in the input layer: - **`FrameStream`**, which the architecture diagrams place between - `InferenceSession` and `Output Target`. The code writes `StepResult` straight + `InferenceSession` and `Output Target`. The code writes `InferenceOutput` straight to `OutputTarget.write()`. Output shape is T5. - **Declared output modalities**, so an output target or quality-eval can state what it requires and be matched the way inputs now are. T5/T8. diff --git a/docs/inference_runtime_supported_inputs_inventory.md b/docs/inference_runtime_supported_inputs_inventory.md index ebe9d853..9b87b382 100644 --- a/docs/inference_runtime_supported_inputs_inventory.md +++ b/docs/inference_runtime_supported_inputs_inventory.md @@ -198,7 +198,7 @@ The implementation that came out of this inventory is: registers per-device converters and produces `CanonicalInputs`. Applications and mappings consume canonical inputs and never read raw device events. 4. Split `InferenceInput` (formerly `ModelInputs`) into `global_conditioning` - and `step`. A non-empty global slot mid-rollout is an update request, not a + and `per_step_conditioning`. A non-empty global slot mid-rollout is an update request, not a reset; `InputField.update_policy` declares whether the model can apply it. 5. Extend `InputField` with `update_policy`, `lifecycle`, and `metadata` so models can distinguish runtime config, cache initialization, rollout binding, @@ -246,12 +246,11 @@ can describe the supported input surfaces. All use ```python lingbot_model = InferenceInputSchema( - description="lingbot-world", global_fields=( InputField(name="prompt", lifecycle="cache_init"), InputField(name="global_conditioning_frame", lifecycle="cache_init"), ), - step_fields=( + per_step_fields=( InputField(name="camera_trajectory", lifecycle="step_input"), InputField( name="text_embeddings", @@ -265,7 +264,6 @@ lingbot_model = InferenceInputSchema( ```python omnidreams_model = InferenceInputSchema( - description="omnidreams", global_fields=( InputField(name="prompts", lifecycle="cache_init"), InputField(name="global_conditioning_frames", lifecycle="cache_init"), @@ -273,13 +271,12 @@ omnidreams_model = InferenceInputSchema( InputField(name="text_embeddings", required=False, lifecycle="cache_init"), InputField(name="image_embeddings", required=False, lifecycle="cache_init"), ), - step_fields=(InputField(name="hdmap_frames", lifecycle="step_input"),), + per_step_fields=(InputField(name="hdmap_frames", lifecycle="step_input"),), ) ``` ```python hy_worldplay_model = InferenceInputSchema( - description="hy-worldplay", global_fields=( InputField(name="prompt", lifecycle="cache_init"), InputField(name="global_conditioning_frame", lifecycle="cache_init"), @@ -293,7 +290,6 @@ hy_worldplay_model = InferenceInputSchema( ```python sana_wm_model = InferenceInputSchema( - description="sana-wm", global_fields=( InputField(name="prompt", lifecycle="cache_init"), InputField(name="negative_prompt", required=False, lifecycle="cache_init"), diff --git a/flashdreams/flashdreams/runtime/__init__.py b/flashdreams/flashdreams/runtime/__init__.py index 9ce9d4ee..427e5d14 100644 --- a/flashdreams/flashdreams/runtime/__init__.py +++ b/flashdreams/flashdreams/runtime/__init__.py @@ -17,14 +17,18 @@ ScriptedModality, ) from flashdreams.runtime.config import ExecutionBackend, InferenceConfig, Precision -from flashdreams.runtime.inference_session import InferenceInput +from flashdreams.runtime.inference_session import ( + InferenceInput, + InferenceInputSchema, + InferenceOutput, + InferenceSessionConfig, +) from flashdreams.runtime.inputs import ( INPUT_PHASES, SESSION_START_ONLY, CanonicalInputs, CanonicalInputSchema, CanonicalModality, - InferenceInputSchema, InputField, InputPhase, TimeWindow, @@ -76,8 +80,10 @@ "InferenceConfig", "InferenceInput", "InferenceInputSchema", + "InferenceOutput", "InferenceRuntime", "InferenceSession", + "InferenceSessionConfig", "InMemoryMetricsRecorder", "INPUT_PHASES", "InputCanonicalizer", diff --git a/flashdreams/flashdreams/runtime/inference_session.py b/flashdreams/flashdreams/runtime/inference_session.py index 6ce8b48d..50a07acc 100644 --- a/flashdreams/flashdreams/runtime/inference_session.py +++ b/flashdreams/flashdreams/runtime/inference_session.py @@ -13,123 +13,189 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Inference session lifecycle and model-input envelope.""" +"""Inference session lifecycle, model-input envelope, and schema.""" +from abc import ABC, abstractmethod from collections.abc import Mapping from dataclasses import dataclass, field -from typing import Any, TypedDict - -from typing_extensions import Unpack +from typing import Any from flashdreams.infra.pipeline import ( StreamInferencePipeline, + StreamInferencePipelineCache, StreamInferencePipelineConfig, ) from flashdreams.runtime._utils import freeze_mapping -from flashdreams.runtime.inputs import InputPhase, validate_phase +from flashdreams.runtime.inputs import InputField, TimeWindow, check_payload +from flashdreams.runtime.types import StepRequest @dataclass(frozen=True, kw_only=True, slots=True) class InferenceInput: - """Encoded inputs for one :class:`InferenceSession` call. - - Two conditioning slots: - - - ``global_conditioning``: values that condition the whole rollout, such as - the conditioning frame or prompt. Normally supplied when the session - starts. - - ``step``: values needed to generate the next chunk or frame. - - A non-empty ``global_conditioning`` on a mid-rollout input is an *update - request*, not a reset. The session should apply it when the model supports - that; resetting rollout state is a separate, explicit - :meth:`InferenceSession.reset` call. Whether a given value can be updated - mid-rollout is declared by ``InputField.update_policy``; see - ``InferenceInputSchema.unsupported_global_updates``. - """ + """Global and per-step conditioning for one inference call.""" __hash__ = None global_conditioning: Mapping[str, Any] = field(default_factory=dict) - step: Mapping[str, Any] = field(default_factory=dict) - metadata: Mapping[str, Any] = field(default_factory=dict) + per_step_conditioning: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: object.__setattr__( self, "global_conditioning", freeze_mapping(self.global_conditioning) ) - object.__setattr__(self, "step", freeze_mapping(self.step)) + object.__setattr__( + self, + "per_step_conditioning", + freeze_mapping(self.per_step_conditioning), + ) + + +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceOutput: + """Generated output and metadata for one inference step.""" + + __hash__ = None + + step_index: int + """Zero-based index of the completed inference step.""" + + output: Any = None + """Generated payload for the step.""" + + frame_count: int | None = None + """Number of generated frames when the output is frame-based.""" + + output_window: TimeWindow | None = None + """Session time window represented by the generated output.""" + + metadata: Mapping[str, Any] = field(default_factory=dict) + """Output metadata supplied by the session or model adapter.""" + + metrics: Mapping[str, float | int] = field(default_factory=dict) + """Per-step numeric measurements.""" + + def __post_init__(self) -> None: + if self.step_index < 0: + raise ValueError("InferenceOutput.step_index must be >= 0.") + if self.frame_count is not None and self.frame_count < 0: + raise ValueError("InferenceOutput.frame_count must be >= 0.") object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) + object.__setattr__(self, "metrics", freeze_mapping(self.metrics)) - @property - def requests_global_update(self) -> bool: - """Return whether this input asks the session to update conditioning.""" - return bool(self.global_conditioning) - def with_step(self, step: Mapping[str, Any]) -> "InferenceInput": - """Return a copy with replaced per-step payload. +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceInputSchema: + """Required and optional fields in each inference input payload.""" - The global slot is carried through unchanged, so a mid-rollout input - built this way keeps whatever update request it already had. Use - :meth:`without_global_update` for the common steady-state case. - """ - return InferenceInput( - global_conditioning=self.global_conditioning, - step=step, - metadata=self.metadata, - ) + global_fields: tuple[InputField, ...] = () + """Model inputs required before starting the initial generation/session.""" - def with_global_update( - self, global_conditioning: Mapping[str, Any] - ) -> "InferenceInput": - """Return a copy requesting a mid-rollout conditioning update.""" - return InferenceInput( - global_conditioning=global_conditioning, - step=self.step, - metadata=self.metadata, - ) + per_step_fields: tuple[InputField, ...] = () + """Per-step model inputs required after the session starts.""" - def without_global_update(self) -> "InferenceInput": - """Return a copy that requests no conditioning update.""" - return InferenceInput(step=self.step, metadata=self.metadata) + def check_global_payload(self, inputs: InferenceInput) -> None: + """Check that required global fields are present in ``inputs``.""" + check_payload(self.global_fields, inputs.global_conditioning) - def for_phase(self, phase: InputPhase) -> Mapping[str, Any]: - """Return the payload mapping for ``phase``.""" - return ( - self.global_conditioning if validate_phase(phase) == "global" else self.step - ) + def check_per_step_payload(self, inputs: InferenceInput) -> None: + """Check that required per-step fields are present in ``inputs``.""" + check_payload(self.per_step_fields, inputs.per_step_conditioning) -class InferenceSessionConfig(TypedDict): +@dataclass(frozen=True, kw_only=True, slots=True) +class InferenceSessionConfig: """Configuration for constructing an inference session.""" + __hash__ = None + pipeline: StreamInferencePipelineConfig """Pipeline configuration to instantiate.""" -class InferenceSession: +class InferenceSession(ABC): """Stateful inference pipeline session.""" - def __init__(self, **kwargs: Unpack[InferenceSessionConfig]) -> None: + _pipeline_cache: StreamInferencePipelineCache[Any, Any, Any] | None + """Pipeline cache for the active rollout; ``None`` before its first step.""" + + _step_index: int + """Zero-based index assigned to the next generated output.""" + + def __init__(self, config: InferenceSessionConfig) -> None: """Initialize the inference pipeline. Args: - **kwargs: Session construction keyword arguments. + config: Session configuration. """ + self.config = config # Initialize the inference pipeline from the provided configuration. - self.pipeline: StreamInferencePipeline = kwargs["pipeline"].setup() + self.pipeline: StreamInferencePipeline = self.config.pipeline.setup() + self._pipeline_cache = None + self._step_index = 0 def __del__(self) -> None: """Release session resources.""" if hasattr(self, "pipeline"): del self.pipeline - def reset(self) -> None: - """Reset the inference session.""" + def next_step_request(self) -> StepRequest: + """Return input requirements for the next pipeline step.""" + return StepRequest(step_index=self._step_index) - def step(self, inference_input: InferenceInput) -> None: + @abstractmethod + def reset(self) -> None: + """Reset the pipeline and discard the active rollout state.""" + pipeline_reset = getattr(self.pipeline, "reset", None) + if callable(pipeline_reset): + pipeline_reset() + self._pipeline_cache = None + self._step_index = 0 + + @abstractmethod + def step(self, inference_input: InferenceInput) -> InferenceOutput: """Run one inference step. Args: inference_input: Model-ready inputs for the step. + + Returns: + Generated output for the step. + + Raises: + ValueError: Global conditioning is supplied after the rollout starts. """ + request = self.next_step_request() + input_schema = request.inference_input_schema + if self._pipeline_cache is None: + if input_schema is not None: + input_schema.check_global_payload(inference_input) + self._pipeline_cache = self.pipeline.initialize_cache( + **inference_input.global_conditioning + ) + elif inference_input.global_conditioning: + raise ValueError( + "InferenceInput.global_conditioning can only be supplied on the " + "first step after reset()." + ) + + if input_schema is not None: + input_schema.check_per_step_payload(inference_input) + pipeline_input = inference_input.per_step_conditioning or None + output = self.pipeline.generate( + autoregressive_index=request.step_index, + cache=self._pipeline_cache, + input=pipeline_input, + ) + self.pipeline.finalize( + autoregressive_index=request.step_index, + cache=self._pipeline_cache, + ) + inference_output = InferenceOutput( + step_index=request.step_index, + output=output, + output_window=request.user_input_window, + metadata=request.metadata, + metrics={}, # No metrics for now, inject to pipeline later. + ) + self._step_index = request.step_index + 1 + return inference_output diff --git a/flashdreams/flashdreams/runtime/inputs.py b/flashdreams/flashdreams/runtime/inputs.py index 66a4432c..edd97bc4 100644 --- a/flashdreams/flashdreams/runtime/inputs.py +++ b/flashdreams/flashdreams/runtime/inputs.py @@ -1,20 +1,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""User, canonical, and model-input schemas for the experimental runtime API.""" +"""User and canonical input envelopes and schemas for the runtime API.""" from __future__ import annotations import math from collections.abc import Iterable, Mapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import Any, Literal, cast from flashdreams.runtime._utils import freeze_mapping -if TYPE_CHECKING: - from flashdreams.runtime.inference_session import InferenceInput - InputPhase = Literal["global", "step"] INPUT_PHASES: tuple[InputPhase, ...] = ("global", "step") @@ -197,7 +194,11 @@ def validate_event(self, event: "UserInputEvent") -> None: def missing_snapshot(self, inputs: "UserInputs") -> tuple[str, ...]: """Return required snapshot fields absent from ``inputs``.""" - return _missing_required(self.snapshot_fields, inputs.snapshot) + return tuple( + input_field.name + for input_field in self.snapshot_fields + if input_field.required and input_field.name not in inputs.snapshot + ) def require_snapshot(self, inputs: "UserInputs") -> None: """Raise if required snapshot fields are absent.""" @@ -206,99 +207,6 @@ def require_snapshot(self, inputs: "UserInputs") -> None: raise ValueError(f"Missing required user snapshot field(s): {missing}") -@dataclass(frozen=True, kw_only=True, slots=True) -class InferenceInputSchema: - """Minimal metadata for model-facing initial and per-step inputs.""" - - global_fields: tuple[InputField, ...] = () - """Model inputs required before starting the initial generation/session.""" - - step_fields: tuple[InputField, ...] = () - """Per-step model inputs required after the session starts.""" - - description: str = "" - - def fields_for(self, phase: InputPhase) -> tuple[InputField, ...]: - """Return every declared field for ``phase``.""" - return ( - self.global_fields - if validate_phase(phase) == "global" - else self.step_fields - ) - - def required_fields( - self, - phase: InputPhase | None = None, - ) -> tuple[tuple[InputPhase, InputField], ...]: - """Return required fields as ``(phase, field)``, optionally filtered.""" - return self._select(phase, required=True) - - def optional_fields( - self, - phase: InputPhase | None = None, - ) -> tuple[tuple[InputPhase, InputField], ...]: - """Return optional fields as ``(phase, field)``, optionally filtered.""" - return self._select(phase, required=False) - - def field_for(self, *, name: str, phase: InputPhase) -> InputField | None: - """Return one declared field, if present.""" - for input_field in self.fields_for(phase): - if input_field.name == name: - return input_field - return None - - def _select( - self, - phase: InputPhase | None, - *, - required: bool, - ) -> tuple[tuple[InputPhase, InputField], ...]: - phases = INPUT_PHASES if phase is None else (validate_phase(phase),) - return tuple( - (each_phase, input_field) - for each_phase in phases - for input_field in self.fields_for(each_phase) - if input_field.required is required - ) - - def unsupported_global_updates(self, inputs: InferenceInput) -> tuple[str, ...]: - """Return requested conditioning updates this model cannot apply. - - A field whose ``update_policy`` is :data:`SESSION_START_ONLY` can be - supplied when the session starts but not changed mid-rollout. Any other - policy, including ``None``, is treated as permissive here; the adapter - still owns whether the swap actually succeeds. - """ - return tuple( - name - for name in inputs.global_conditioning - if (declared := self.field_for(name=name, phase="global")) is not None - and declared.update_policy == SESSION_START_ONLY - ) - - def missing_global(self, inputs: InferenceInput) -> tuple[str, ...]: - """Return required initial fields absent from ``inputs``.""" - return _missing_required(self.global_fields, inputs.global_conditioning) - - def missing_step(self, inputs: InferenceInput) -> tuple[str, ...]: - """Return required per-step fields absent from ``inputs``.""" - return _missing_required(self.step_fields, inputs.step) - - def require_global(self, inputs: InferenceInput) -> None: - """Raise if required initial fields are absent.""" - missing = self.missing_global(inputs) - if missing: - raise ValueError( - f"Missing required global conditioning input(s): {missing}" - ) - - def require_step(self, inputs: InferenceInput) -> None: - """Raise if required per-step fields are absent.""" - missing = self.missing_step(inputs) - if missing: - raise ValueError(f"Missing required step model input(s): {missing}") - - @dataclass(frozen=True, kw_only=True, slots=True) class UserInputEvent: """User-facing input event timestamped in seconds since session start. @@ -444,11 +352,12 @@ def __post_init__(self) -> None: object.__setattr__(self, "metadata", freeze_mapping(self.metadata)) -def _missing_required( - fields: tuple[InputField, ...], payload: Mapping[str, Any] -) -> tuple[str, ...]: - return tuple( +def check_payload(fields: tuple[InputField, ...], payload: Mapping[str, Any]) -> None: + """Raise if ``payload`` omits any required field.""" + missing = tuple( input_field.name for input_field in fields if input_field.required and input_field.name not in payload ) + if missing: + raise ValueError(f"Missing required input(s): {missing}") diff --git a/flashdreams/flashdreams/runtime/interfaces.py b/flashdreams/flashdreams/runtime/interfaces.py index 0ff276e4..65a14e9b 100644 --- a/flashdreams/flashdreams/runtime/interfaces.py +++ b/flashdreams/flashdreams/runtime/interfaces.py @@ -8,10 +8,14 @@ from typing import Protocol, runtime_checkable from flashdreams.runtime.config import InferenceConfig -from flashdreams.runtime.inference_session import InferenceInput -from flashdreams.runtime.inputs import CanonicalInputSchema, InferenceInputSchema +from flashdreams.runtime.inference_session import ( + InferenceInput, + InferenceInputSchema, + InferenceOutput, +) +from flashdreams.runtime.inputs import CanonicalInputSchema from flashdreams.runtime.mapping import InputMapping -from flashdreams.runtime.types import StepRequest, StepResult +from flashdreams.runtime.types import StepRequest @runtime_checkable @@ -22,7 +26,7 @@ def next_step_request(self) -> StepRequest | None: """Describe the next step's inputs, or return ``None`` when complete.""" ... - def step(self, inputs: InferenceInput) -> StepResult: + def step(self, inputs: InferenceInput) -> InferenceOutput: """Run one sequential inference step.""" ... diff --git a/flashdreams/flashdreams/runtime/mapping.py b/flashdreams/flashdreams/runtime/mapping.py index 4527c3fd..79b26904 100644 --- a/flashdreams/flashdreams/runtime/mapping.py +++ b/flashdreams/flashdreams/runtime/mapping.py @@ -10,13 +10,12 @@ from typing import Any, Protocol, runtime_checkable from flashdreams.runtime._utils import freeze_mapping -from flashdreams.runtime.inference_session import InferenceInput +from flashdreams.runtime.inference_session import InferenceInput, InferenceInputSchema from flashdreams.runtime.inputs import ( INPUT_PHASES, CanonicalInputs, CanonicalInputSchema, CanonicalModality, - InferenceInputSchema, InputField, InputPhase, ) @@ -269,7 +268,15 @@ def _build_compatibility( unavailable.append(mapping_schema) usable = combine_mapping_schemas(feedable, name=reported_schema.name) - required = inference_input_schema.required_fields() + declared_fields: tuple[tuple[InputPhase, InputField], ...] = tuple( + (phase, input_field) + for phase, fields in ( + ("global", inference_input_schema.global_fields), + ("step", inference_input_schema.per_step_fields), + ) + for input_field in fields + ) + required = tuple(declared for declared in declared_fields if declared[1].required) missing_required = tuple( (phase, input_field) for phase, input_field in required @@ -282,7 +289,8 @@ def _build_compatibility( ) available_optional = tuple( (phase, input_field) - for phase, input_field in inference_input_schema.optional_fields() + for phase, input_field in declared_fields + if not input_field.required if usable.can_produce(phase, input_field) ) @@ -361,10 +369,14 @@ def undeclared_inference_inputs( ``map_global_inputs``/``map_step_inputs`` actually return. Mapping tests can use this to keep the declared compatibility surface honest. """ + payloads: tuple[tuple[InputPhase, Mapping[str, Any]], ...] = ( + ("global", inputs.global_conditioning), + ("step", inputs.per_step_conditioning), + ) return tuple( (phase, key) - for phase in INPUT_PHASES - for key in inputs.for_phase(phase) + for phase, payload in payloads + for key in payload if not any( declared.name == key for declared in mapping_schema.produces_for(phase) ) diff --git a/flashdreams/flashdreams/runtime/output.py b/flashdreams/flashdreams/runtime/output.py index aac341ee..675b04d6 100644 --- a/flashdreams/flashdreams/runtime/output.py +++ b/flashdreams/flashdreams/runtime/output.py @@ -10,7 +10,7 @@ from typing import Any, Protocol, runtime_checkable from flashdreams.runtime._utils import freeze_mapping -from flashdreams.runtime.types import StepResult +from flashdreams.runtime.inference_session import InferenceOutput @dataclass(frozen=True, kw_only=True, slots=True) @@ -39,7 +39,7 @@ def open(self) -> None: """Prepare the target for a new run.""" ... - def write(self, result: StepResult) -> None: + def write(self, result: InferenceOutput) -> None: """Consume one generated step result.""" ... @@ -54,7 +54,7 @@ class NullOutputTarget: store_results: bool = False output_count: int = field(default=0, init=False) - results: list[StepResult] = field(default_factory=list, init=False) + results: list[InferenceOutput] = field(default_factory=list, init=False) _opened: bool = field(default=False, init=False, repr=False) @property @@ -66,7 +66,7 @@ def open(self) -> None: self.output_count = 0 self.results.clear() - def write(self, result: StepResult) -> None: + def write(self, result: InferenceOutput) -> None: if not self._opened: raise RuntimeError("Cannot write to a closed output target.") self.output_count += 1 diff --git a/flashdreams/flashdreams/runtime/types.py b/flashdreams/flashdreams/runtime/types.py index 46775302..e430f479 100644 --- a/flashdreams/flashdreams/runtime/types.py +++ b/flashdreams/flashdreams/runtime/types.py @@ -7,10 +7,13 @@ from collections.abc import Mapping from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any from flashdreams.runtime._utils import freeze_mapping -from flashdreams.runtime.inputs import InferenceInputSchema, TimeWindow +from flashdreams.runtime.inputs import TimeWindow + +if TYPE_CHECKING: + from flashdreams.runtime.inference_session import InferenceInputSchema @dataclass(frozen=True, kw_only=True, slots=True) diff --git a/flashdreams/tests/test_inference_runtime_api.py b/flashdreams/tests/test_inference_runtime_api.py index edfafa63..f504cf85 100644 --- a/flashdreams/tests/test_inference_runtime_api.py +++ b/flashdreams/tests/test_inference_runtime_api.py @@ -15,6 +15,7 @@ InferenceConfig, InferenceInput, InferenceInputSchema, + InferenceOutput, InferenceRuntime, InferenceSession, InMemoryMetricsRecorder, @@ -35,6 +36,9 @@ UserInputs, UserInputSchema, ) +from flashdreams.runtime.inference_session import ( + InferenceSession as PipelineInferenceSession, +) pytestmark = pytest.mark.ci_cpu @@ -88,8 +92,8 @@ def test_inference_config_rejects_empty_model_id() -> None: ), (lambda: UserInputEvent(timestamp_s=0.0, event_type=" "), "event_type"), (lambda: StepRequest(step_index=-1), "step_index"), - (lambda: StepResult(step_index=-1), "step_index"), - (lambda: StepResult(step_index=0, frame_count=-1), "frame_count"), + (lambda: InferenceOutput(step_index=-1), "step_index"), + (lambda: InferenceOutput(step_index=0, frame_count=-1), "frame_count"), (lambda: RuntimeMetricSample(name=" ", value=1.0), "name"), (lambda: RuntimeMetricSample(name="sample", value=float("nan")), "finite"), (lambda: OutputArtifact(kind=" ", uri="artifact://demo"), "kind"), @@ -106,23 +110,55 @@ def test_runtime_metric_sample_rejects_bool_values() -> None: RuntimeMetricSample(name="sample", value=True) -def test_inference_input_schema_validates_initial_and_step_payloads() -> None: +def test_inference_input_schema_checks_both_payloads() -> None: schema = InferenceInputSchema( global_fields=( InputField(name="prompt"), InputField(name="global_conditioning_frame"), ), - step_fields=(InputField(name="camera_poses"),), + per_step_fields=(InputField(name="camera_poses"),), ) inputs = InferenceInput( - global_conditioning={"prompt": "drive", "global_conditioning_frame": object()} + global_conditioning={ + "prompt": "drive", + "global_conditioning_frame": object(), + }, + per_step_conditioning={"camera_poses": object()}, ) - schema.require_global(inputs) - assert schema.missing_step(inputs) == ("camera_poses",) + schema.check_global_payload(inputs) + schema.check_per_step_payload(inputs) + with pytest.raises(ValueError, match="prompt"): + schema.check_global_payload(InferenceInput()) with pytest.raises(ValueError, match="camera_poses"): - schema.require_step(inputs) + schema.check_per_step_payload(InferenceInput()) + + +def test_inference_input_and_schema_only_declare_two_fields() -> None: + assert tuple(InferenceInput.__dataclass_fields__) == ( + "global_conditioning", + "per_step_conditioning", + ) + assert tuple(InferenceInputSchema.__dataclass_fields__) == ( + "global_fields", + "per_step_fields", + ) + + +def test_inference_output_matches_step_result_fields() -> None: + assert tuple(field.name for field in fields(InferenceOutput)) == tuple( + field.name for field in fields(StepResult) + ) + + +def test_inference_session_uses_step_requests_instead_of_a_schema_property() -> None: + assert "inference_input_schema" not in PipelineInferenceSession.__dict__ + assert callable(PipelineInferenceSession.next_step_request) + + +def test_pipeline_inference_session_requires_reset_and_step_implementations() -> None: + assert PipelineInferenceSession.__abstractmethods__ == frozenset({"reset", "step"}) def test_user_inputs_filter_timestamped_event_windows() -> None: @@ -185,7 +221,8 @@ def test_user_input_schema_validates_required_snapshot_fields() -> None: def test_identity_input_mapping_leaves_inference_input_unchanged() -> None: mapping = IdentityInputMapping() inference_input = InferenceInput( - global_conditioning={"prompt": "fixed"}, step={"hdmap": object()} + global_conditioning={"prompt": "fixed"}, + per_step_conditioning={"hdmap": object()}, ) request = StepRequest(step_index=0) @@ -208,7 +245,7 @@ def test_identity_input_mapping_leaves_inference_input_unchanged() -> None: def test_null_output_target_counts_and_optionally_stores_results() -> None: target = NullOutputTarget(store_results=True) - result = StepResult(step_index=0, output=b"frame") + result = InferenceOutput(step_index=0, output=b"frame") assert target.closed with pytest.raises(RuntimeError, match="closed output target"): @@ -224,22 +261,22 @@ def test_null_output_target_counts_and_optionally_stores_results() -> None: assert target.output_count == 1 assert target.results == [result] with pytest.raises(RuntimeError, match="closed output target"): - target.write(StepResult(step_index=1)) + target.write(InferenceOutput(step_index=1)) def test_null_output_target_open_resets_per_run_state() -> None: target = NullOutputTarget(store_results=True) target.open() - target.write(StepResult(step_index=0, output=b"first")) + target.write(InferenceOutput(step_index=0, output=b"first")) target.close() target.open() assert target.output_count == 0 assert target.results == [] - target.write(StepResult(step_index=0, output=b"second")) + target.write(InferenceOutput(step_index=0, output=b"second")) assert target.output_count == 1 - assert target.results == [StepResult(step_index=0, output=b"second")] + assert target.results == [InferenceOutput(step_index=0, output=b"second")] def test_in_memory_metrics_recorder_uses_seconds_for_timing() -> None: @@ -390,11 +427,9 @@ def _drive_two_step_session( or TimeWindow(start_s=0.0, end_s=_SESSION_HORIZON_S), source_schema=source_schema, ), - # The global slot stays empty in steady state. A mapping that - # sees ``canonical_inputs.has_global_change`` fills it via - # ``with_global_update`` to request a mid-rollout swap. + # Per-step calls do not need to repeat global conditioning. inference_input=InferenceInput( - step={"chunk_index": request.step_index}, + per_step_conditioning={"chunk_index": request.step_index}, ), request=request, ) @@ -418,7 +453,7 @@ class _FakeAdapter: model_id = "fake-model" inference_input_schema = InferenceInputSchema( global_fields=(InputField(name="prompt"),), - step_fields=(InputField(name="chunk_index"),), + per_step_fields=(InputField(name="chunk_index"),), ) canonical_input_schema = CanonicalInputSchema() @@ -440,7 +475,7 @@ def __init__(self, *, inference_input_schema: InferenceInputSchema) -> None: self.closed = False def start_session(self, inputs: InferenceInput) -> InferenceSession: - self._inference_input_schema.require_global(inputs) + self._inference_input_schema.check_global_payload(inputs) return _FakeSession(inference_input_schema=self._inference_input_schema) def close(self) -> None: @@ -471,9 +506,9 @@ def next_step_request(self) -> StepRequest | None: ), ) - def step(self, inputs: InferenceInput) -> StepResult: - self._inference_input_schema.require_step(inputs) - result = StepResult( + def step(self, inputs: InferenceInput) -> InferenceOutput: + self._inference_input_schema.check_per_step_payload(inputs) + result = InferenceOutput( step_index=self.step_index, output=f"chunk-{self.step_index}", frame_count=3, diff --git a/flashdreams/tests/test_inference_session.py b/flashdreams/tests/test_inference_session.py new file mode 100644 index 00000000..261b5384 --- /dev/null +++ b/flashdreams/tests/test_inference_session.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for inference-session pipeline orchestration.""" + +from typing import Any, cast + +import pytest +import torch + +from flashdreams.infra.pipeline import ( + StreamInferencePipeline, + StreamInferencePipelineConfig, +) +from flashdreams.runtime.inference_session import ( + InferenceInput, + InferenceInputSchema, + InferenceOutput, + InferenceSession, + InferenceSessionConfig, +) +from flashdreams.runtime.inputs import TimeWindow +from flashdreams.runtime.types import StepRequest + +pytestmark = pytest.mark.ci_cpu + + +class FakeStreamInferencePipeline(StreamInferencePipeline[Any, Any, Any]): + """Record session orchestration without constructing model components.""" + + def __init__(self, output: Any) -> None: + torch.nn.Module.__init__(self) + self.output = output + self.cache = object() + self.reset_calls = 0 + self.initialize_cache_calls: list[dict[str, Any]] = [] + self.generate_calls: list[dict[str, Any]] = [] + self.finalize_calls: list[dict[str, Any]] = [] + + def reset(self) -> None: + self.reset_calls += 1 + + def initialize_cache(self, **global_conditioning: Any) -> object: + self.initialize_cache_calls.append(global_conditioning) + self.cache = object() + return self.cache + + def generate( + self, autoregressive_index: int, cache: object, input: Any = None + ) -> Any: + self.generate_calls.append( + { + "autoregressive_index": autoregressive_index, + "cache": cache, + "input": input, + } + ) + return self.output + + def finalize(self, autoregressive_index: int, cache: object) -> dict[str, float]: + self.finalize_calls.append( + {"autoregressive_index": autoregressive_index, "cache": cache} + ) + return {"total_ms": 4.0} + + +class _FakePipelineConfig: + def __init__(self, pipeline: FakeStreamInferencePipeline) -> None: + self.pipeline = pipeline + self.setup_calls = 0 + + def setup(self) -> FakeStreamInferencePipeline: + self.setup_calls += 1 + return self.pipeline + + +class _ConcreteInferenceSession(InferenceSession): + def next_step_request(self) -> StepRequest: + return StepRequest( + step_index=self._step_index, + inference_input_schema=InferenceInputSchema(), + user_input_window=TimeWindow( + start_s=float(self._step_index), + end_s=float(self._step_index + 1), + ), + metadata={"request": "fake"}, + ) + + def reset(self) -> None: + super().reset() + + def step(self, inference_input: InferenceInput) -> InferenceOutput: + return super().step(inference_input) + + +def _create_session( + pipeline: FakeStreamInferencePipeline, +) -> tuple[_ConcreteInferenceSession, _FakePipelineConfig]: + pipeline_config = _FakePipelineConfig(pipeline) + config = InferenceSessionConfig( + pipeline=cast(StreamInferencePipelineConfig, pipeline_config) + ) + return _ConcreteInferenceSession(config), pipeline_config + + +def test_constructor_initializes_the_configured_pipeline() -> None: + pipeline = FakeStreamInferencePipeline(output=object()) + + session, pipeline_config = _create_session(pipeline) + + assert session.pipeline is pipeline + assert pipeline_config.setup_calls == 1 + + +def test_reset_resets_the_pipeline_and_rollout_state() -> None: + pipeline = FakeStreamInferencePipeline(output=object()) + session, _ = _create_session(pipeline) + session.step( + InferenceInput( + global_conditioning={"prompt": "first"}, + per_step_conditioning={"control": 1}, + ) + ) + + session.reset() + result = session.step( + InferenceInput( + global_conditioning={"prompt": "second"}, + per_step_conditioning={"control": 2}, + ) + ) + + assert pipeline.reset_calls == 1 + assert pipeline.initialize_cache_calls == [ + {"prompt": "first"}, + {"prompt": "second"}, + ] + assert result.step_index == 0 + + +def test_step_converts_inference_input_and_wraps_pipeline_output() -> None: + generated = object() + pipeline = FakeStreamInferencePipeline(output=generated) + session, _ = _create_session(pipeline) + inference_input = InferenceInput( + global_conditioning={"prompt": "drive"}, + per_step_conditioning={"steering": 0.25}, + ) + + result = session.step(inference_input) + + assert pipeline.initialize_cache_calls == [{"prompt": "drive"}] + assert pipeline.generate_calls == [ + { + "autoregressive_index": 0, + "cache": pipeline.cache, + "input": {"steering": 0.25}, + } + ] + assert pipeline.finalize_calls == [ + {"autoregressive_index": 0, "cache": pipeline.cache} + ] + assert result == InferenceOutput( + step_index=0, + output=generated, + output_window=TimeWindow(start_s=0.0, end_s=1.0), + metadata={"request": "fake"}, + metrics={"total_ms": 4.0}, + ) + + +def test_step_reuses_the_pipeline_cache_and_advances_the_index() -> None: + pipeline = FakeStreamInferencePipeline(output=object()) + session, _ = _create_session(pipeline) + session.step(InferenceInput(global_conditioning={"prompt": "drive"})) + + result = session.step(InferenceInput(per_step_conditioning={"steering": -0.5})) + + assert pipeline.initialize_cache_calls == [{"prompt": "drive"}] + assert pipeline.generate_calls[-1] == { + "autoregressive_index": 1, + "cache": pipeline.cache, + "input": {"steering": -0.5}, + } + assert result.step_index == 1 + assert session.next_step_request().step_index == 2 diff --git a/flashdreams/tests/test_runtime_canonical.py b/flashdreams/tests/test_runtime_canonical.py index 1ad48d39..6a6df6b8 100644 --- a/flashdreams/tests/test_runtime_canonical.py +++ b/flashdreams/tests/test_runtime_canonical.py @@ -64,7 +64,7 @@ consumes=(DRIVER_COMMAND,), produces_step=(InputField(name="steering"),), ) -STEERING_MODEL = InferenceInputSchema(step_fields=(InputField(name="steering"),)) +STEERING_MODEL = InferenceInputSchema(per_step_fields=(InputField(name="steering"),)) WINDOW = TimeWindow(start_s=0.0, end_s=1.0) NEXT_WINDOW = TimeWindow(start_s=1.0, end_s=2.0) @@ -217,13 +217,14 @@ def test_canonical_inputs_carry_live_control_only() -> None: def test_application_supplies_global_conditioning_directly() -> None: - """A prompt swap reaches the session without touching canonicalization.""" - update = InferenceInput(step={"steering": 0.0}).with_global_update( - {"prompt": "heavy rain"} + """Global conditioning reaches the session without canonicalization.""" + inputs = InferenceInput( + global_conditioning={"prompt": "heavy rain"}, + per_step_conditioning={"steering": 0.0}, ) - assert update.requests_global_update - assert update.global_conditioning["prompt"] == "heavy rain" + assert inputs.global_conditioning["prompt"] == "heavy rain" + assert inputs.per_step_conditioning["steering"] == 0.0 # --- device independence ------------------------------------------------ diff --git a/flashdreams/tests/test_runtime_input_mapping.py b/flashdreams/tests/test_runtime_input_mapping.py index 00cd9758..3ba230f6 100644 --- a/flashdreams/tests/test_runtime_input_mapping.py +++ b/flashdreams/tests/test_runtime_input_mapping.py @@ -17,7 +17,6 @@ from flashdreams.runtime import ( DRIVER_COMMAND, - SESSION_START_ONLY, CanonicalInputs, CanonicalInputSchema, CanonicalModality, @@ -87,7 +86,7 @@ global_fields=( InputField(name="prompt", semantic_type="text", lifecycle="cache_init"), ), - step_fields=( + per_step_fields=( InputField(name="steering", lifecycle="step_input"), InputField(name="camera_delta", required=False, lifecycle="step_input"), ), @@ -193,42 +192,24 @@ def test_event_validation_rejects_undeclared_event_type() -> None: # --- model input schemas ------------------------------------------------ -def test_model_declares_required_and_optional_fields_per_phase() -> None: - required = DRIVING_MODEL.required_fields() - optional = DRIVING_MODEL.optional_fields() - - assert {(phase, f.name) for phase, f in required} == { - ("global", "prompt"), - ("step", "steering"), - } - assert {(phase, f.name) for phase, f in optional} == {("step", "camera_delta")} - - -def test_required_fields_can_be_filtered_by_phase() -> None: - step_only = DRIVING_MODEL.required_fields("step") - - assert [f.name for _, f in step_only] == ["steering"] - - -def test_field_lookup_is_phase_scoped() -> None: - assert DRIVING_MODEL.field_for(name="prompt", phase="global") is not None - assert DRIVING_MODEL.field_for(name="prompt", phase="step") is None - - -def test_invalid_phase_is_rejected() -> None: - bad_phase: Any = "final" - - with pytest.raises(ValueError, match="phase must be"): - DRIVING_MODEL.fields_for(bad_phase) +def test_model_declares_fields_for_each_payload() -> None: + assert [field.name for field in DRIVING_MODEL.global_fields] == ["prompt"] + assert [field.name for field in DRIVING_MODEL.per_step_fields] == [ + "steering", + "camera_delta", + ] + assert DRIVING_MODEL.per_step_fields[0].required + assert not DRIVING_MODEL.per_step_fields[1].required -def test_inference_input_expose_payload_per_phase() -> None: +def test_inference_input_exposes_both_conditioning_payloads() -> None: inputs = InferenceInput( - global_conditioning={"prompt": "drive"}, step={"steering": 0.25} + global_conditioning={"prompt": "drive"}, + per_step_conditioning={"steering": 0.25}, ) - assert inputs.for_phase("global")["prompt"] == "drive" - assert inputs.for_phase("step")["steering"] == 0.25 + assert inputs.global_conditioning["prompt"] == "drive" + assert inputs.per_step_conditioning["steering"] == 0.25 def test_lifecycle_and_update_policy_are_queryable_metadata() -> None: @@ -434,7 +415,7 @@ def test_combine_rejects_non_schema_entries() -> None: def test_undeclared_inference_input_catches_schema_drift() -> None: produced = InferenceInput( - global_conditioning={"prompt": "drive"}, step={"steering": 0.0} + global_conditioning={"prompt": "drive"}, per_step_conditioning={"steering": 0.0} ) undeclared = undeclared_inference_inputs(produced, PROMPT_MAPPING) @@ -445,7 +426,7 @@ def test_undeclared_inference_input_catches_schema_drift() -> None: def test_declared_outputs_report_no_drift() -> None: combined = combine_mapping_schemas((PROMPT_MAPPING, STEERING_MAPPING)) produced = InferenceInput( - global_conditioning={"prompt": "drive"}, step={"steering": 0.0} + global_conditioning={"prompt": "drive"}, per_step_conditioning={"steering": 0.0} ) assert undeclared_inference_inputs(produced, combined) == () @@ -458,7 +439,7 @@ def test_identity_mapping_needs_no_declared_surface() -> None: """Fixed-input runs stay possible without any schema declaration.""" mapping = IdentityInputMapping() fixed = InferenceInput( - global_conditioning={"prompt": "fixed"}, step={"steering": 0.0} + global_conditioning={"prompt": "fixed"}, per_step_conditioning={"steering": 0.0} ) mapped = mapping.map_step_inputs( @@ -467,7 +448,7 @@ def test_identity_mapping_needs_no_declared_surface() -> None: request=StepRequest(step_index=0), ) - assert mapped.step["steering"] == 0.0 + assert mapped.per_step_conditioning["steering"] == 0.0 def test_empty_mapping_set_cannot_satisfy_a_required_field() -> None: @@ -489,85 +470,3 @@ def test_model_with_no_requirements_is_always_drivable() -> None: ) assert compatibility.can_drive - - -# --- global conditioning updates vs reset ------------------------------- - - -def test_empty_global_slot_requests_no_update() -> None: - steady_state = InferenceInput(step={"steering": 0.25}) - - assert not steady_state.requests_global_update - - -def test_non_empty_global_slot_mid_rollout_is_an_update_request() -> None: - """Changing weather mid-run updates conditioning; it is not a reset.""" - updated = InferenceInput(step={"steering": 0.0}).with_global_update( - {"prompt": "heavy rain"} - ) - - assert updated.requests_global_update - assert updated.global_conditioning["prompt"] == "heavy rain" - assert updated.step["steering"] == 0.0 - - -def test_with_step_carries_the_global_slot_through() -> None: - started = InferenceInput(global_conditioning={"prompt": "drive"}) - - stepped = started.with_step({"steering": 0.5}) - - assert stepped.global_conditioning["prompt"] == "drive" - - -def test_without_global_update_clears_the_request() -> None: - started = InferenceInput( - global_conditioning={"prompt": "drive"}, step={"steering": 0.5} - ) - - steady_state = started.without_global_update() - - assert not steady_state.requests_global_update - assert steady_state.step["steering"] == 0.5 - - -def test_model_can_declare_conditioning_it_cannot_swap_mid_rollout() -> None: - schema = InferenceInputSchema( - global_fields=( - InputField(name="prompt", update_policy="step_boundary"), - InputField(name="scene_id", update_policy=SESSION_START_ONLY), - ) - ) - update = InferenceInput( - global_conditioning={"prompt": "heavy rain", "scene_id": "town_02"} - ) - - assert schema.unsupported_global_updates(update) == ("scene_id",) - - -def test_permissive_when_no_update_policy_is_declared() -> None: - schema = InferenceInputSchema(global_fields=(InputField(name="prompt"),)) - update = InferenceInput(global_conditioning={"prompt": "heavy rain"}) - - assert schema.unsupported_global_updates(update) == () - - -def test_undeclared_global_values_are_left_to_the_adapter() -> None: - schema = InferenceInputSchema(global_fields=(InputField(name="prompt"),)) - update = InferenceInput(global_conditioning={"mystery": 1}) - - assert schema.unsupported_global_updates(update) == () - - -def test_steady_state_steps_do_not_request_a_global_update() -> None: - """Carrying session-start conditioning forward would look like an update.""" - started = InferenceInput(global_conditioning={"prompt": "drive"}) - - steady_state = InferenceInput(step={"chunk_index": 1}) - - assert started.requests_global_update - assert not steady_state.requests_global_update - assert ( - not started.with_step({"chunk_index": 1}) - .without_global_update() - .requests_global_update - ) From 3de1996b788f7e6d9d5ddff2b471933185add11a Mon Sep 17 00:00:00 2001 From: Fangjun Zhou Date: Wed, 22 Jul 2026 16:39:21 -0700 Subject: [PATCH 8/8] Add local nvim config for pyright lsp --- .nvim.lua | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .nvim.lua diff --git a/.nvim.lua b/.nvim.lua new file mode 100644 index 00000000..ec0ca694 --- /dev/null +++ b/.nvim.lua @@ -0,0 +1,3 @@ +vim.lsp.config("pyright", { + root_markers = { ".git" }, +})