-
Notifications
You must be signed in to change notification settings - Fork 46
Add experimental inference runtime API envelope #403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
580a182
da683d8
5131a2d
d402552
63f8b38
2ce669d
390a523
b891eaf
c9da5d3
5f83769
c7c6cc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # 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, | ||
| StepRequest, | ||
| StepResult, | ||
| ) | ||
| from flashdreams.runtime.mapping import IdentityInputMapping, InputMapping | ||
| from flashdreams.runtime.metrics import ( | ||
| InMemoryMetricsRecorder, | ||
| MetricsRecorder, | ||
| NullMetricsRecorder, | ||
| RuntimeMetricSample, | ||
| ) | ||
| from flashdreams.runtime.output import NullOutputTarget, OutputArtifact, OutputTarget | ||
|
|
||
| __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", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||
| # 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 dataclasses import dataclass, field | ||||||||||||||
| from pathlib import Path | ||||||||||||||
| from typing import Any, Literal, Mapping | ||||||||||||||
|
|
||||||||||||||
| ExecutionBackend = Literal["local", "local-distributed", "external", "hosted"] | ||||||||||||||
| """Execution backend families the v0 envelope leaves room for.""" | ||||||||||||||
|
|
||||||||||||||
| Precision = Literal["auto", "fp32", "fp16", "bf16"] | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We should use enum.Enum for all enums, this should make LSP happier.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah there may be an LSP benefit. I’d keep literal for the T1 task and revisit enum when we actually implement this part of the API. |
||||||||||||||
| """Coarse runtime precision choices.""" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||||||||||||||
| class InferenceConfig: | ||||||||||||||
| """Model/runtime execution settings. | ||||||||||||||
|
|
||||||||||||||
| Prompts, user controls, browser settings, output paths, and benchmark | ||||||||||||||
| directories intentionally live outside this object. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| model_id: str | ||||||||||||||
| """Stable model or adapter identity.""" | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about "Stable identity for the model adapter or runtime integration." |
||||||||||||||
|
|
||||||||||||||
| preset_id: str | None = None | ||||||||||||||
| """Optional preset identity under :attr:`model_id`.""" | ||||||||||||||
|
Comment on lines
+37
to
+38
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this optional? To identify a model given a YAML we need an identity string to refer to
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The model_id is defined earlier, which is non-optional, so this would be an optional extra thing, like if you're using the lingbot model, it could have some optional preset things that you could use instead, like a lingbot+wan or lingbot+taev. Unless you think that should not be optional? |
||||||||||||||
|
|
||||||||||||||
| checkpoint: str | Path | None = None | ||||||||||||||
| """Optional checkpoint or model-asset selector understood by the adapter.""" | ||||||||||||||
|
Comment on lines
+40
to
+41
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this optional? this would mean we hard-code default checkpoint locations somewhere.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking here that for some models, the checkpoint may just be implicit and the ModelAdapter would be able to handle it just based on the model_id, or that could be overridden by this checkpoint str. But maybe that's wrong and we should just always require this? What do you think? |
||||||||||||||
|
|
||||||||||||||
| backend: ExecutionBackend = "local" | ||||||||||||||
| """Runtime backend family.""" | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about "Execution placement and backend family for inference compute." instead? |
||||||||||||||
|
|
||||||||||||||
| device: str | None = None | ||||||||||||||
| """Optional device selector such as ``cuda`` or ``cuda:0``.""" | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Why does not specifying a device make sense to include?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. device=None lets the adapter/backend choose placement, which might matter for hosted/external runs and existing local configs. Adapters that require explicit placement can reject None in a validate step. |
||||||||||||||
|
|
||||||||||||||
| precision: Precision = "auto" | ||||||||||||||
| """Preferred compute precision.""" | ||||||||||||||
|
|
||||||||||||||
| compile: bool | None = None | ||||||||||||||
| """Whether model compilation is requested, disabled, or left to the adapter.""" | ||||||||||||||
|
jarcherNV marked this conversation as resolved.
Outdated
|
||||||||||||||
|
|
||||||||||||||
| cuda_graph: bool | None = None | ||||||||||||||
| """Whether CUDA graph capture is requested, disabled, or left to the adapter.""" | ||||||||||||||
|
jarcherNV marked this conversation as resolved.
Outdated
|
||||||||||||||
|
|
||||||||||||||
| attention_backend: str | None = None | ||||||||||||||
| """Optional attention implementation selector.""" | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What're available One of the suggestions I have is we remove all these implementation details selector out of See Triton autotune system for reference: https://triton-lang.org/main/python-api/generated/triton.autotune.html
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that probably makes sense. The fields like attention_backend and cache_policy are not really universal inference config and if we keep adding stuff like that then InferenceConfig could probably become bloated pretty quickly. I'll see if I can split this stuff out into a ModelConfig thing.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the suggested wording change, what about "Optional attention implementation selector; None leaves the choice to the adapter." |
||||||||||||||
|
|
||||||||||||||
| cache_policy: str | None = None | ||||||||||||||
| """Optional cache policy selector.""" | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are some available options here? We should make a Enum instead of str.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah a free string is probably ambiguous if this is a shared field. I'd prefer not to define the canonical cache policies now though, it's probably better to leave that for the T9 task. |
||||||||||||||
|
|
||||||||||||||
| runtime_options: Mapping[str, Any] = field(default_factory=dict) | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use TypedDict over dict for all dictionary? Again, this should make LSP happier.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For runtime_options/resource_hints, a shared TypedDict would be too generic to help much because the keys are adapter/backend-specific. It's probably better to use adapter-owned typed configs once those shapes are known when we implement them. |
||||||||||||||
| """Adapter/backend-specific runtime options.""" | ||||||||||||||
|
|
||||||||||||||
| resource_hints: Mapping[str, Any] = field(default_factory=dict) | ||||||||||||||
| """Cheap resource hints for launchers, schedulers, or hosted backends.""" | ||||||||||||||
|
jarcherNV marked this conversation as resolved.
Outdated
|
||||||||||||||
|
|
||||||||||||||
| def __post_init__(self) -> None: | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We likely need more validation on parameter values and checks if types assigned were correct & if elements like maybe we stub these checks?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that sounds reasonable, I'll see what checks I can add. |
||||||||||||||
| if not self.model_id.strip(): | ||||||||||||||
| raise ValueError("InferenceConfig.model_id must be non-empty.") | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we split user input and model input into two source files?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that might make sense. We could probably do that in the T2/T3 tasks, I'll leave that up to whoever works on those tasks to decide. |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,144 @@ | ||||||||||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should at some point doc-comment these members in the file to better explain purpose (and add a requirement to CI so that our API does not fall apart over time)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah we should do that at some point before we merge all this stuff to main. I guess once the API is more settled we can throw that in. |
||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||
|
|
||||||||||
| """User- and model-input envelopes for the experimental runtime API.""" | ||||||||||
|
|
||||||||||
| from __future__ import annotations | ||||||||||
|
|
||||||||||
| import math | ||||||||||
| from dataclasses import dataclass, field | ||||||||||
| from typing import Any, Iterable, Mapping | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||||||||||
| class TimeWindow: | ||||||||||
| """Half-open time window in canonical seconds.""" | ||||||||||
|
|
||||||||||
| 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 controls an app, transport, or trace can provide.""" | ||||||||||
|
|
||||||||||
| event_kinds: frozenset[str] = field(default_factory=frozenset) | ||||||||||
| snapshot_fields: tuple[InputField, ...] = () | ||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||||||||
| description: str = "" | ||||||||||
|
|
||||||||||
| def supports_event_kinds(self, kinds: Iterable[str]) -> bool: | ||||||||||
| """Return whether every requested event kind is declared supported.""" | ||||||||||
| requested = frozenset(kinds) | ||||||||||
| if not requested: | ||||||||||
| return True | ||||||||||
| return requested.issubset(self.event_kinds) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||||||||||
| class ModelInputSchema: | ||||||||||
| """Minimal metadata for model-facing initial and per-step inputs.""" | ||||||||||
|
|
||||||||||
| initial_fields: tuple[InputField, ...] = () | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about "These are model inputs required before starting the initial generation/session." |
||||||||||
| step_fields: tuple[InputField, ...] = () | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about phrasing it as "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: | ||||||||||
| """Timestamped user-facing input event.""" | ||||||||||
|
|
||||||||||
| timestamp_s: float | ||||||||||
| kind: str | ||||||||||
| payload: Mapping[str, Any] = field(default_factory=dict) | ||||||||||
| source: str | None = None | ||||||||||
| event_id: str | None = None | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why string and not also- how do we have no id?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we mean event_type here? otherwise sounds hash should just be the id
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, but we already have event.kind
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think part of the confusion is naming. Maybe we just rename What do you guys think? |
||||||||||
|
|
||||||||||
| 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.kind.strip(): | ||||||||||
| raise ValueError("UserInputEvent.kind must be non-empty.") | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||||||||||
| class UserInputs: | ||||||||||
| """Transport-neutral user inputs for live, replayed, synthetic, or no-op runs.""" | ||||||||||
|
|
||||||||||
| events: tuple[UserInputEvent, ...] = () | ||||||||||
| snapshot: Mapping[str, Any] = field(default_factory=dict) | ||||||||||
| metadata: Mapping[str, Any] = field(default_factory=dict) | ||||||||||
|
|
||||||||||
| 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: | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As commented in flashdreams/flashdreams/runtime/interfaces.py, I'd like a declarative way of defining model inputs. The user should be able to build a Model that expose the input scheme directly. Instead of using a dedicated ModelInput object.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah a declarative Model API could expose its schema directly. I still think we need a transport-neutral ModelInputs payload underneath for mapping, replay, tests, and benchmarks. The later declarative layer can wrap or produce ModelInputs rather than removing that boundary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One new issue I found when reviewing this: How is the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Design-wise, I agree the current T1 schema is only lightweight field-presence validation. I’ll leave the decision on how to validate up to whoever implements the T2/T3 tasks. |
||||||||||
| """Model-facing input payloads split by initial and per-step use.""" | ||||||||||
|
|
||||||||||
| 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 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(field.name for field in fields if field.required and field.name not in payload) | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if the current "pipeline" way of interface definition is a good idea or not. This requires users to compose the application in a sequential way. They need to choose the session, an adapter, then a model. I think a better to structure the application is providing a model interface and a session interface. The model interface specifies model input schema. The session interface specifies user input schemas. The user will implement their Session declaratively. The input mapping happens inside a Session implementation.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that might be a useful design direction, we should probably compare it before committing T2-T4. My intent for this PR is the lower-level T1 envelope rather than the final user-facing authoring API. I think the current primitives can still sit underneath a declarative model/session API: a model can expose ModelInputSchema, a session can expose UserInputSchema, and the session can own or wrap an InputMapping. I’d prefer not to do a full pivot in this PR until we have the alternative proposal and can check it against LingBot, OmniDreams, the-internal-demo-that-shall-not-be-named and Reactor requirements. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Protocols for model adapters, runtimes, and sessions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from typing import TYPE_CHECKING, Any, Mapping, Protocol, runtime_checkable | ||
|
|
||
| from flashdreams.runtime.config import InferenceConfig | ||
| from flashdreams.runtime.inputs import ( | ||
| ModelInputs, | ||
| ModelInputSchema, | ||
| TimeWindow, | ||
| UserInputSchema, | ||
| ) | ||
|
|
||
| if TYPE_CHECKING: | ||
| from flashdreams.runtime.mapping import InputMapping | ||
|
|
||
|
|
||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||
| class StepRequest: | ||
| """Model-session request for the next step's inputs.""" | ||
|
|
||
| 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.") | ||
|
|
||
|
|
||
| @dataclass(frozen=True, kw_only=True, slots=True) | ||
| class StepResult: | ||
| """Generated output and metadata for one inference step.""" | ||
|
|
||
| step_index: int | ||
| output: Any = 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.") | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class InferenceSession(Protocol): | ||
| """One rollout or stream with isolated cache/state.""" | ||
|
|
||
| def next_step_request(self) -> StepRequest: | ||
| """Describe the model inputs needed for the next call to :meth:`step`.""" | ||
| ... | ||
|
|
||
| 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.""" | ||
| ... | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class ModelAdapter(Protocol): | ||
| """Model-specific boundary that connects FlashDreams to a model runtime.""" | ||
|
|
||
| @property | ||
| def model_id(self) -> str: | ||
| """Stable model or adapter identity.""" | ||
| ... | ||
|
|
||
| @property | ||
| def model_input_schema(self) -> ModelInputSchema: | ||
| """Initial and per-step model input requirements.""" | ||
| ... | ||
|
|
||
| @property | ||
| def user_input_schema(self) -> UserInputSchema | None: | ||
| """User input capabilities this adapter can map directly, if any.""" | ||
| ... | ||
|
|
||
| def default_input_mapping(self) -> "InputMapping | None": | ||
| """Return the adapter's default user-to-model input 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.""" | ||
| ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should define what each "ENUM" literal means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ‘Where and how inference compute is run’ rather than ‘host’, since local backends are not hosted services.